MVC Pattern
An architecture pattern that separates data and rules, presentation, and input interpretation into a model, views and controllers, assigning each a different reason to change.
· Last updated: 2026-10-26 · 19 min read
Overview
A console program counts numbers using the commands + and -. Inside its input loop, receiving + causes count++, immediately followed by a call to Console.WriteLine($"value: {count}"). To add a line reporting whether the value is even, output code must be added to every branch that changes it. Reading input from a script file instead of the keyboard also requires editing that same loop, because rules for changing the value, code for displaying it and code for interpreting input are all mixed together there.
MVC (Model-View-Controller) separates these responsibilities into three roles. The model holds data and the rules for changing it; in the counter, this means the current value and Increment(). The view reads the model and presents it to the user; here it is an object that prints lines such as value: 1. The controller receives user input and translates it into model calls. In this example, receiving the line + causes it to call Increment().
Once the roles are separate, the view still needs to learn that the model changed. In classical MVC, the view registers with the model, which notifies registered dependents when its state changes. The view then reads the new value from the model and renders again. The model knows no concrete view types and only sends notifications, so adding another view leaves its code unchanged. This registration and notification structure is the Observer pattern.
Structure
A to-do list with a list view and a summary view shows how one model supports several presentations. The UML below uses types from the extended example. TodoController interprets input lines and calls TodoList, sending failures to ErrorView. Each ListView and SummaryView holds a TodoList field and subscribes to the Changed event. There is no arrow from TodoList to a view because the model does not reference view types. In Smalltalk-80, each view had a corresponding controller.1 This example receives one line at a time, so a single controller handles input for all views.
This is the sequence for the input line done 1. The controller calls only the model, never the views directly. When the model raises Changed, both views' Render() methods run in registration order, and each view reads Items again before printing.
History
Trygve Reenskaug visited Xerox PARC's Learning Research Group (LRG) from summer 1978 to summer 1979. His MVC page dates the idea to 1978 and describes it as a general solution to the problem of users controlling large, complex data sets.2
The first terms appeared in the May 12, 1979 note "Thing-Model-View-Editor". After lengthy discussions with Adele Goldberg, the names model, view and controller were settled in the December 10, 1979 note "Models-Views-Controllers". In that note, an editor remained a supporting element for modifying a view's information: it could be inserted between controller and view when needed and discarded after editing.234
Reenskaug's controller differs from the Smalltalk-80 controller. In the December note, it arranges related views on the screen and translates user commands into messages sent to views.4 He wrote that he now calls this controller a Tool, while the Smalltalk-80 controller handling mouse and keyboard input corresponds to the fourth element, Editor, in his notes. Jim Althoff and others implemented MVC in the Smalltalk-80 class library after he left PARC.2 This article uses controller in the Smalltalk-80 sense: receiving user input and translating it into model calls.
In 1988, Glenn E. Krasner and Stephen T. Pope described Smalltalk-80 MVC in JOOP. Views request data from the model and display it; controllers receive user input and send messages to the model. The model knows neither views nor controllers. Both register as dependents. When the model receives changed, its dependents receive an update message.1
Timeline
Year | Event | Source |
|---|---|---|
1978 | Reenskaug conceived MVC while visiting Xerox PARC's LRG, from summer 1978 to summer 1979. | |
1979 | The May 12 note "Thing-Model-View-Editor" explained Thing, Model, View and Editor through examples. | |
1979 | The December 10 note "Models-Views-Controllers" settled on the names model, view and controller. The editor, which lets users modify a view's information, remained a supporting element. | |
1988 | Krasner and Pope published their Smalltalk-80 MVC guide in JOOP's August/September issue. | |
1999 | Seshadri described the JSP Model 2 architecture as a server-side implementation of MVC. | |
2005 | Ruby on Rails 1.0 was released. The Rails guide describes Action Controller as the C in MVC. | |
2006 | Fowler surveyed MVC and its variations in "GUI Architectures". | |
2009 | Microsoft released ASP.NET MVC 1.0 at its MIX event. |
- Year
1978
- Event
Reenskaug conceived MVC while visiting Xerox PARC's LRG, from summer 1978 to summer 1979.
- Source
- Year
1979
- Event
The May 12 note "Thing-Model-View-Editor" explained Thing, Model, View and Editor through examples.
- Source
- Year
1979
- Event
The December 10 note "Models-Views-Controllers" settled on the names model, view and controller. The editor, which lets users modify a view's information, remained a supporting element.
- Source
- Year
1988
- Event
Krasner and Pope published their Smalltalk-80 MVC guide in JOOP's August/September issue.
- Source
- Year
1999
- Event
Seshadri described the JSP Model 2 architecture as a server-side implementation of MVC.
- Source
- Year
2005
- Event
Ruby on Rails 1.0 was released. The Rails guide describes Action Controller as the C in MVC.
- Source
- Year
2006
- Event
Fowler surveyed MVC and its variations in "GUI Architectures".
- Source
- Year
2009
- Event
Microsoft released ASP.NET MVC 1.0 at its MIX event.
- Source
MVC in web frameworks
Server-side web frameworks also use the MVC name, but a request follows a different route. In a 1999 article, Govind Seshadri described JSP Model 2, in which a servlet handles requests as the controller and a JSP page creates the presentation, as a server-side implementation of MVC.5 The ASP.NET Core documentation explains that requests are routed to a controller, which chooses the view to present to the user. Views and controllers depend on the model, while the model depends on neither.6
As in classical MVC, the model knows nothing about views, but there are no change notifications. A web view produces one response and finishes, so it does not subscribe to the model. Instead, the controller selects a view and passes data for each request. The classical explanation that "the view observes the model" therefore does not apply unchanged to web MVC.
Why use it?
The model can be tested without a screen. In the extended example, calling TodoList's Add("") directly and inspecting the title is empty result requires neither console nor controller.
Several views attached to one model display the same state. Krasner and Pope wrote that a model change caused by a controller must appear in every view of the model, including those not paired with that controller.1 In the example, adding SummaryView leaves TodoList and TodoController unchanged. Input can also change independently of the model: the example passes a string array to the controller instead of reading a keyboard.
MVC defines the boundaries of three roles, without assigning a home to every piece of code. Logic and state needed only for presentation, such as displaying negative values in red, fit poorly in either a domain model or a widget. Discussing this problem, Fowler explains that VisualWorks introduced Application Model as a home for presentation logic and state.7 MVP Pattern's presenter and MVVM Pattern's viewmodel also separate this presentation logic from the view.
In a small program, the extra classes and wiring may only make it harder to read. With one input and one output, a single loop can be clearer. Also, when change notifications are synchronous calls, as in this article's examples, every model change redraws all views. Several successive changes redraw the same screen that many times.
Example
A counter comes first because it has only one changing value, making it easy to distinguish input interpretation, value changes and output. Create one model, one view and one controller, then pass three input lines to the controller. The controller calls the model, and the view prints when it receives a model-change notification. A console replaces windowing code so the example can concentrate on how input handling connects to presentation.
The three input lines are +, +, and -. As the controller interprets each line and changes the model, the notified view prints 1, 2, and 1 in order. Although these are the same numerical changes, the controller understands the input strings, while the view understands how to present values. This short flow shows the two roles connected through a model.
using System;
var counter = new Counter();
var view = new CounterView(counter);
var controller = new CounterController(counter);
foreach (var line in new[] { "+", "+", "-" })
{
Console.WriteLine("> " + line);
controller.Handle(line);
}
// Model: holds the value and rules, and announces changes without knowing who displays them.
sealed class Counter
{
public int Value { get; private set; }
public event Action? Changed;
public void Increment() { Value++; Changed?.Invoke(); }
public void Decrement() { Value--; Changed?.Invoke(); }
}
// View: subscribes to model changes, then reads and prints its value on notification.
sealed class CounterView
{
private readonly Counter counter;
public CounterView(Counter counter)
{
this.counter = counter;
counter.Changed += Render;
}
private void Render() => Console.WriteLine("value: " + counter.Value);
}
// Controller: translates input lines into model calls without writing to the screen.
// This example sends only + and -. The extended example handles invalid input.
sealed class CounterController(Counter counter)
{
public void Handle(string line)
{
if (line == "+") counter.Increment();
if (line == "-") counter.Decrement();
}
}> +
value: 1
> +
value: 2
> -
value: 1The controller prints nothing. The value: lines come from the view after it receives the model's Changed notification. The view variable is not used afterward, but the constructor has already subscribed it.
Extended example
Now a list view and a summary view read the same to-do list. Watching both outputs update after a single model change shows why a presentation can be added without changing the model's rules. Invalid input is included too. The controller filters input-format errors, such as unknown commands and nonnumeric IDs. Rule violations, such as empty or duplicate titles, nonexistent IDs and already-completed items, are returned by the model as a Result type value rather than thrown as exceptions. Neither kind of failure is model state, so the controller passes both directly to ErrorView without going through model notifications.
Updating the list and summary separately can leave them inconsistent: an item may appear completed while the remaining count stays unchanged. Here, both views read the same to-do model. Add an item with add buy milk and complete it with done 1, then follow whether the list and summary count reflect the same change. Invalid commands distinguish this normal update path from the separate route used to display errors.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
var list = new TodoList();
var listView = new ListView(list);
var summaryView = new SummaryView(list);
var controller = new TodoController(list, new ErrorView());
string[] script =
[
"add buy milk", "add", "add buy milk", "done 7",
"done x", "done 1", "done 1", "remove 1",
];
foreach (var line in script)
{
Console.WriteLine("> " + line);
controller.Handle(line);
}
readonly record struct Result(bool Ok, string Error)
{
public static Result Success() => new(true, "");
public static Result Fail(string error) => new(false, error);
}
sealed record TodoItem(int Id, string Title, bool Done);
// Model: holds the to-do list and its rules (nonempty unique titles, completion only once).
// Raise Changed only when state actually changes.
sealed class TodoList
{
private readonly List<TodoItem> items = [];
private int nextId = 1;
public IReadOnlyList<TodoItem> Items => items;
public event Action? Changed;
public Result Add(string title)
{
if (title.Length == 0) return Result.Fail("title is empty");
if (items.Any(i => i.Title == title)) return Result.Fail($"'{title}' already exists");
items.Add(new TodoItem(nextId++, title, false));
Changed?.Invoke();
return Result.Success();
}
public Result Complete(int id)
{
var index = items.FindIndex(i => i.Id == id);
if (index < 0) return Result.Fail($"no item {id}");
if (items[index].Done) return Result.Fail($"item {id} is already done");
items[index] = items[index] with { Done = true };
Changed?.Invoke();
return Result.Success();
}
}
// View 1: redraws the entire list.
sealed class ListView
{
private readonly TodoList list;
public ListView(TodoList list)
{
this.list = list;
list.Changed += Render;
}
private void Render()
{
foreach (var item in list.Items)
Console.WriteLine($" [{(item.Done ? "x" : " ")}] {item.Id} {item.Title}");
}
}
// View 2: summarizes the same model as counts, without knowing ListView exists.
sealed class SummaryView
{
private readonly TodoList list;
public SummaryView(TodoList list)
{
this.list = list;
list.Changed += Render;
}
private void Render()
{
var done = list.Items.Count(i => i.Done);
Console.WriteLine($" open {list.Items.Count - done}, done {done}");
}
}
// Display failures (format errors and rule violations). These are not model state, so the controller calls this view directly.
sealed class ErrorView
{
public void Show(string message) => Console.WriteLine(" error: " + message);
}
// Controller: interprets input lines and calls the model.
// Format errors (unknown commands, nonnumeric IDs) are handled here; the model reports rule violations through Result.
sealed class TodoController(TodoList list, ErrorView errors)
{
public void Handle(string line)
{
var space = line.IndexOf(' ');
var command = space < 0 ? line : line[..space];
var argument = space < 0 ? "" : line[(space + 1)..].Trim();
var result = command switch
{
"add" => list.Add(argument),
"done" => int.TryParse(argument, NumberStyles.None, CultureInfo.InvariantCulture, out var id)
? list.Complete(id)
: Result.Fail($"'{argument}' is not an item number"),
_ => Result.Fail($"unknown command '{command}'"),
};
if (!result.Ok) errors.Show(result.Error);
}
}> add buy milk
[ ] 1 buy milk
open 1, done 0
> add
error: title is empty
> add buy milk
error: 'buy milk' already exists
> done 7
error: no item 7
> done x
error: 'x' is not an item number
> done 1
[x] 1 buy milk
open 0, done 1
> done 1
error: item 1 is already done
> remove 1
error: unknown command 'remove'The list is not printed again after failed input because the model left its state unchanged and did not raise Changed. This example has limits, though. Changed does not say what changed, so every view rereads and redraws the entire list. Notifications run synchronously in registration order: if one view throws, later views are not called, and the exception propagates through Complete() to the controller after the model's state has already changed. Views subscribe in their constructors and never unsubscribe; a view that must disappear before its model needs the unsubscription described in Observer pattern.
TypeScript example
The same structure in TypeScript uses an array of functions in the model instead of a C# event, with registration through onChanged(). A union type distinguishes success and failure by its ok field.
Send the same to-do commands here and check that both views change along the same flow as in C#. Replacing events with a function list preserves the roles: the controller interprets input, and each view reads and displays changes in the model.
type Result = { ok: true } | { ok: false; error: string };
const success: Result = { ok: true };
const fail = (error: string): Result => ({ ok: false, error });
type TodoItem = { readonly id: number; readonly title: string; readonly done: boolean };
// Model: holds the list and rules, calling registered functions only after a state change.
class TodoList {
#items: TodoItem[] = [];
#nextId = 1;
#listeners: (() => void)[] = [];
get items(): readonly TodoItem[] { return this.#items; }
onChanged(listener: () => void): void { this.#listeners.push(listener); }
add(title: string): Result {
if (title.length === 0) return fail("title is empty");
if (this.#items.some((i) => i.title === title)) return fail(`'${title}' already exists`);
this.#items.push({ id: this.#nextId++, title, done: false });
this.#notify();
return success;
}
complete(id: number): Result {
const index = this.#items.findIndex((i) => i.id === id);
if (index < 0) return fail(`no item ${id}`);
if (this.#items[index].done) return fail(`item ${id} is already done`);
this.#items[index] = { ...this.#items[index], done: true };
this.#notify();
return success;
}
#notify(): void {
for (const listener of this.#listeners) listener();
}
}
// View 1: redraws the entire list.
class ListView {
constructor(private readonly list: TodoList) {
list.onChanged(() => this.render());
}
private render(): void {
for (const item of this.list.items) {
console.log(` [${item.done ? "x" : " "}] ${item.id} ${item.title}`);
}
}
}
// View 2: summarizes the same model as counts, without knowing ListView exists.
class SummaryView {
constructor(private readonly list: TodoList) {
list.onChanged(() => this.render());
}
private render(): void {
const done = this.list.items.filter((i) => i.done).length;
console.log(` open ${this.list.items.length - done}, done ${done}`);
}
}
// Display failures (format errors and rule violations). These are not model state, so the controller calls this view directly.
class ErrorView {
show(message: string): void { console.log(" error: " + message); }
}
// Controller: interprets input lines and calls the model.
// Handle format errors here; the model reports rule violations through Result.
class TodoController {
constructor(private readonly list: TodoList, private readonly errors: ErrorView) {}
handle(line: string): void {
const space = line.indexOf(" ");
const command = space < 0 ? line : line.slice(0, space);
const argument = space < 0 ? "" : line.slice(space + 1).trim();
const result = this.dispatch(command, argument);
if (!result.ok) this.errors.show(result.error);
}
private dispatch(command: string, argument: string): Result {
switch (command) {
case "add":
return this.list.add(argument);
case "done":
return /^[0-9]+$/.test(argument)
? this.list.complete(Number(argument))
: fail(`'${argument}' is not an item number`);
default:
return fail(`unknown command '${command}'`);
}
}
}
const list = new TodoList();
new ListView(list);
new SummaryView(list);
const controller = new TodoController(list, new ErrorView());
const script = [
"add buy milk", "add", "add buy milk", "done 7",
"done x", "done 1", "done 1", "remove 1",
];
for (const line of script) {
console.log("> " + line);
controller.handle(line);
}> add buy milk
[ ] 1 buy milk
open 1, done 0
> add
error: title is empty
> add buy milk
error: 'buy milk' already exists
> done 7
error: no item 7
> done x
error: 'x' is not an item number
> done 1
[x] 1 buy milk
open 0, done 1
> done 1
error: item 1 is already done
> remove 1
error: unknown command 'remove'The output matches the extended C# example. Since there is no unsubscription function, registered views continue receiving notifications for the lifetime of the model.
See also
Each C# example is an independent .NET 10 console program.
The TypeScript example runs in Node.js after compilation to JavaScript targeting ES2022 or later.
Footnotes
- Glenn E. Krasner, Stephen T. Pope. "A Cookbook for Using the Model-View-Controller User Interface Paradigm in Smalltalk-80". Journal of Object-Oriented Programming, August/September 1988, pp. 26-49. PDF. ↩
- Trygve Reenskaug. MVC XEROX PARC 1978-79. Internet Archive snapshot of the author's page(February 27, 2025). ↩
- Trygve Reenskaug. "THING-MODEL-VIEW-EDITOR: an Example from a planningsystem". LRG note, May 12, 1979. Archived PDF. ↩
- Trygve Reenskaug. "MODELS - VIEWS - CONTROLLERS". Note, December 10, 1979, 2 pages. Archived PDF. ↩
- Govind Seshadri. Understanding JavaServer Pages Model 2 architecture (December 29, 1999). JavaWorld article. ↩
- Microsoft Learn. Overview of ASP.NET Core MVC. MVC pattern section. ↩
- Martin Fowler. GUI Architectures (July 18, 2006). Model View Controller and VisualWorks Application Model sections. ↩