MVP Pattern
A UI architecture pattern in which a presenter controls a screen through a view interface, so presentation rules can be tested without a UI.
· Last updated: 2026-10-26 · 20 min read
Overview
Suppose a product-search screen has a search-button handler that reads the input field, fills an error label when the query is shorter than two characters, and otherwise searches the catalog and fills a results list box. It works, but checking the rule "do not search for a one-character query" requires opening the form and clicking the button. The rule and control manipulation share one method, leaving no way to run the rule without the form.
MVP (Model-View-Presenter) divides this code into three roles. The model provides business functionality without knowing the screen; here, product search belongs to ICatalog. The view is the screen itself: it receives user input, forwards it to the presenter and renders the screen. The presenter validates input, calls the model and decides what the view should display. Variations differ in whether the view observes the model and updates itself, or the presenter supplies every display value. The examples here take the latter approach, Passive View, discussed below.
The presenter references only ISearchView, a view interface, rather than a concrete form class. This interface defines the operations the presenter needs from the screen: reading the query, displaying results and displaying errors. A fake view can implement the same contract by recording calls, allowing the presenter to be tested in place of a real form. That is why these examples use both console views and fake views.
MVC Pattern also separates the model and screen, but it handles input at a different scale. Smalltalk MVC represents an individual GUI object, such as a checkbox or input field, through model, view and controller roles. A model-change notification causes the view to redraw.1 Fowler explains that in MVP, a widget is not split into view and controller; the presenter usually belongs to a form rather than a widget, and decides how to respond to user events.2
Structure
The diagram collects the roles used throughout the examples. InMemoryCatalog and ConsoleSearchView appear in the first example; FakeSearchView appears in the extended example, whose test catalog, ScriptedCatalog, is omitted. SearchPresenter references only ISearchView and ICatalog, so it works unchanged with either a console view or a fake view.
The view forwards a click as a SearchRequested event without validating the query. Validation, model calls and decisions about what to display all happen in the presenter.
History
In a 1996 paper, Taligent's Mike Potel introduced MVP as a programming model for C++ and Java that generalized Smalltalk MVC. Taligent was then a wholly owned IBM subsidiary. He divided program design into three data-management questions, model, selection and command, and three user-interface questions, view, interactor and presenter. The presenter raised the classical Smalltalk controller to the application level, interpreting user events and connecting them to commands that manipulate the model.3
Andy Bower and Blair McGlashan, the creators of Dolphin Smalltalk, described their adoption of MVP in the ESUG 2000 tutorial paper Twisting the Triad. They started their UI framework in summer 1995 with a widget-based design, then spent roughly two months toward the end of that year almost completing a replacement based on MVC. Two things did not fit: MVC connected the application model and views only indirectly, and native Windows controls already supplied most controller behavior. After a colleague introduced Taligent's MVP, they read the paper and adopted its direct connection between presenter and view. To keep the framework simple, they left Taligent's interactor out of its core.4
Michael Feathers's 2002 article The Humble Dialog Box took this separation further for testing. A class holding the logic, which Feathers called a smart object, is created before the dialog class and communicates with the screen through a view interface consisting only of pure virtual functions. Tests use a mock implementation of that interface, while the dialog class retains only delegation and value assignment. Feathers wrote that the logic object pushing data into the view ran opposite to the tendency of most UI frameworks.5 Fowler described this as using a presenter much more deeply than original MVP, with the dialog forming a Passive View manipulated by the presenter.6
Martin Fowler published a Model View Presenter pattern draft in 2004. In July 2006, he split it into Supervising Controller and Passive View and retired the original entry.7 In GUI Architectures that same month, he wrote that MVP first appeared at IBM, became better known through Taligent in the 1990s, and was most often cited through Potel's paper.2
For .NET, Dino Esposito described writing view interfaces and presenter classes for Windows Forms and ASP.NET Web Forms in MSDN Magazine in 2010. He identified abstracting the view's details behind an interface as a main innovation of MVP, explaining that presenters knowing only that interface are easier to reuse and test.8
Timeline
Year | Event | Source |
|---|---|---|
Late 1970s | Smalltalk MVC was developed at Xerox PARC | |
1995 | After building a widget-based UI framework, the Dolphin Smalltalk team began replacing it with MVC late that year, taking about two months | |
1996 | Mike Potel, "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java" | |
2000 | Andy Bower and Blair McGlashan, "Twisting the Triad" (ESUG 2000 tutorial paper) | |
2002 | Michael Feathers, "The Humble Dialog Box" | |
2004 | Martin Fowler published the Model View Presenter pattern draft on July 19 | |
2006 | Fowler retired MVP after splitting it into Supervising Controller and Passive View on July 11, then published GUI Architectures on July 18 | |
2010 | Dino Esposito introduced MVP implementations for Web Forms and Windows Forms in MSDN Magazine |
- Year
Late 1970s
- Event
Smalltalk MVC was developed at Xerox PARC
- Source
- Year
1995
- Event
After building a widget-based UI framework, the Dolphin Smalltalk team began replacing it with MVC late that year, taking about two months
- Source
- Year
1996
- Event
Mike Potel, "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java"
- Source
- Year
2000
- Event
Andy Bower and Blair McGlashan, "Twisting the Triad" (ESUG 2000 tutorial paper)
- Source
- Year
2002
- Event
Michael Feathers, "The Humble Dialog Box"
- Source
- Year
2004
- Event
Martin Fowler published the Model View Presenter pattern draft on July 19
- Year
2006
- Event
Fowler retired MVP after splitting it into Supervising Controller and Passive View on July 11, then published GUI Architectures on July 18
- Year
2010
- Event
Dino Esposito introduced MVP implementations for Web Forms and Windows Forms in MSDN Magazine
- Source
Passive View and Supervising Controller
Fowler's two variations differ in how much responsibility remains in the view. In Supervising Controller, the presenter, called a controller in Fowler's terminology, handles all user input. The view manages simple synchronization with the model through declarative mechanisms such as data binding, in which a framework automatically propagates changes between control values and model data. The presenter manipulates the view directly only for presentation logic that binding cannot readily express.9
Passive View, by comparison, leaves all decisions to the presenter, including the values pushed into widgets. The view does not update itself from the model, so all presentation logic resides in the presenter, and neither the view nor the model depends on the other. Fowler identifies testing as the main motivation: substituting a test double, an object standing in for a real one during a test, allows the presenter to be checked.10 The Supervising Controller entry compares this with Passive View: more code is needed, but the entire presentation behavior can be tested.9
These examples use Passive View. The view simply prints the values supplied to ShowResults and ShowError, while the presenter also chooses error text and handles empty results. Passive View was not the original form of MVP, though. In Potel's and Dolphin's original MVP, views observed the model and redrew themselves,11 and Fowler wrote that Passive View emerged through the search for testability and was absent from the original MVP descriptions.2
Why use it?
Because the presenter knows only the view interface, an ordinary unit test can use a fake view to check presentation rules such as "do not call the model for a short query" without a UI. The same presenter can also be attached to views built with different UI technologies; Esposito described reuse across Windows Forms, Web Forms and WPF.
Each screen gains a view interface, view implementation and presenter, increasing the amount of code. Esposito also wrote that MVP increases application-code complexity, and that large applications can accommodate this more easily than small programs. For a screen with a few fields and hardly any rules, direct event handlers can be easier to read.
Fake-view tests validate only the presenter. Whether real widgets receive the correct values and whether the layout remains intact still requires UI tests or direct inspection, so the view implementation is kept thin, limited to delegation and value assignment. Frameworks built around two-way data binding, such as WPF, tend to offer tools suited to MVVM Pattern, which exposes state for the view to bind to.12
Example
Product search makes a useful example because input validation, querying and displaying results follow within one operation, exposing the decisions that belong to the presenter. A console view, presenter and in-memory catalog run two searches. The view forwards input as an event and prints the results or errors chosen by the presenter.
The two queries were chosen to take different paths. ke reaches the catalog and finds keyboard and keycap; k stops at the minimum-length check. Follow the first call through to its search results, then check that the presenter chooses the second call's error before calling the catalog.
using System;
using System.Collections.Generic;
using System.Linq;
var view = new ConsoleSearchView();
var presenter = new SearchPresenter(view, new InMemoryCatalog());
view.Type("ke");
view.ClickSearch();
view.Type("k");
view.ClickSearch();
abstract record SearchResult;
sealed record Found(IReadOnlyList<string> Items) : SearchResult;
sealed record Failed(string Reason) : SearchResult;
// Model: search functionality with no knowledge of the screen.
interface ICatalog { SearchResult Search(string query); }
sealed class InMemoryCatalog : ICatalog
{
private readonly string[] products = ["keyboard", "mouse", "monitor", "keycap"];
public SearchResult Search(string query) =>
new Found(products.Where(p => p.Contains(query, StringComparison.OrdinalIgnoreCase)).ToList());
}
// View interface: everything the presenter knows about the screen.
interface ISearchView
{
string Query { get; }
event Action? SearchRequested;
void ShowResults(IReadOnlyList<string> items);
void ShowError(string message);
}
// Presenter: validates input, calls the model and decides what the view should display.
sealed class SearchPresenter
{
private readonly ISearchView view;
private readonly ICatalog catalog;
public SearchPresenter(ISearchView view, ICatalog catalog)
{
this.view = view;
this.catalog = catalog;
view.SearchRequested += OnSearchRequested;
}
private void OnSearchRequested()
{
var query = view.Query.Trim();
if (query.Length < 2)
{
view.ShowError("Enter at least 2 characters.");
return;
}
switch (catalog.Search(query))
{
case Found found: view.ShowResults(found.Items); break;
case Failed failed: view.ShowError("Search failed: " + failed.Reason); break;
default: view.ShowError("Unexpected search result."); break;
}
}
}
// Console view: forwards input as an event and prints only the supplied content.
sealed class ConsoleSearchView : ISearchView
{
public string Query { get; private set; } = "";
public event Action? SearchRequested;
public void Type(string text)
{
Query = text;
Console.WriteLine($"> search \"{text}\"");
}
public void ClickSearch() => SearchRequested?.Invoke();
public void ShowResults(IReadOnlyList<string> items) => Console.WriteLine("results: [" + string.Join(", ", items) + "]");
public void ShowError(string message) => Console.WriteLine("error: " + message);
}> search "ke"
results: [keyboard, keycap]
> search "k"
error: Enter at least 2 characters.The one-character query is rejected by the presenter's OnSearchRequested, rather than by the view. This minimal example leaves previously displayed results in place when an error appears; the extended example makes that policy explicit.
Extended example
Add empty results and failures alongside successful searches to check that the presenter decides what the screen displays. Empty search results produce guidance text, and a model failure produces an error. All error-display paths, short queries, no results and model failures, pass through ShowFailure, which first clears previous results. Replace the console view with FakeSearchView to exercise the presenter directly, as in a unit test. This fake view records calls instead of drawing a screen, and ScriptedCatalog returns predefined results in order while recording the queries it receives.
If a new search shows an error while leaving the previous results visible, users may mistake that list for the results of the query they just entered. Clearing the list before displaying an error prevents that confusion. The fake view need not draw a real list: recording the order in which the presenter requests clearing results and showing an error is enough to test this presentation policy.
using System;
using System.Collections.Generic;
using System.Linq;
var passed = 0;
var failed = 0;
// 1) A short query does not call the model: clear previous results, then show a validation error.
{
var view = new FakeSearchView();
var catalog = new ScriptedCatalog();
_ = new SearchPresenter(view, catalog);
view.Query = "k";
view.ClickSearch();
Check("short query", view, catalog,
["ShowResults()", "ShowError(Enter at least 2 characters.)"], []);
}
// 2) On model failure, clear previous results and show an error.
{
var view = new FakeSearchView();
var catalog = new ScriptedCatalog(new Found(["keyboard"]), new Failed("catalog timeout"));
_ = new SearchPresenter(view, catalog);
view.Query = "key";
view.ClickSearch();
view.ClickSearch();
Check("failure clears old results", view, catalog,
["ShowResults(keyboard)", "ShowResults()", "ShowError(Search failed: catalog timeout)"],
["key", "key"]);
}
// 3) The presenter trims the query before passing it to the model.
{
var view = new FakeSearchView();
var catalog = new ScriptedCatalog(new Found([]));
_ = new SearchPresenter(view, catalog);
view.Query = " zz ";
view.ClickSearch();
Check("trim and no match", view, catalog,
["ShowResults()", "ShowError(No products match \"zz\".)"], ["zz"]);
}
Console.WriteLine($"{passed} passed, {failed} failed");
void Check(string name, FakeSearchView view, ScriptedCatalog catalog, string[] expectedCalls, string[] expectedQueries)
{
var ok = view.Calls.SequenceEqual(expectedCalls) && catalog.Queries.SequenceEqual(expectedQueries);
if (ok) passed++; else failed++;
Console.WriteLine($"{(ok ? "PASS" : "FAIL")} {name}");
Console.WriteLine(" view : " + string.Join(" | ", view.Calls));
Console.WriteLine(" catalog: " + (catalog.Queries.Count == 0 ? "(not called)" : string.Join(", ", catalog.Queries)));
}
abstract record SearchResult;
sealed record Found(IReadOnlyList<string> Items) : SearchResult;
sealed record Failed(string Reason) : SearchResult;
interface ICatalog { SearchResult Search(string query); }
// Fake model: returns prepared results in order and records received queries.
sealed class ScriptedCatalog(params SearchResult[] script) : ICatalog
{
private int next;
public List<string> Queries { get; } = [];
public SearchResult Search(string query)
{
Queries.Add(query);
return script[next++];
}
}
interface ISearchView
{
string Query { get; }
event Action? SearchRequested;
void ShowResults(IReadOnlyList<string> items);
void ShowError(string message);
}
sealed class SearchPresenter
{
private readonly ISearchView view;
private readonly ICatalog catalog;
public SearchPresenter(ISearchView view, ICatalog catalog)
{
this.view = view;
this.catalog = catalog;
view.SearchRequested += OnSearchRequested;
}
private void OnSearchRequested()
{
var query = view.Query.Trim();
if (query.Length < 2)
{
ShowFailure("Enter at least 2 characters.");
return;
}
switch (catalog.Search(query))
{
case Found { Items.Count: 0 }:
ShowFailure($"No products match \"{query}\".");
break;
case Found found:
view.ShowResults(found.Items);
break;
case Failed failure:
ShowFailure("Search failed: " + failure.Reason);
break;
default:
ShowFailure("Unexpected search result.");
break;
}
}
// Clear the previous list first so it cannot be mistaken for this search's results alongside an error.
private void ShowFailure(string message)
{
view.ShowResults([]);
view.ShowError(message);
}
}
// Fake view: records presenter calls as strings without drawing a screen.
sealed class FakeSearchView : ISearchView
{
public string Query { get; set; } = "";
public event Action? SearchRequested;
public List<string> Calls { get; } = [];
public void ClickSearch() => SearchRequested?.Invoke();
public void ShowResults(IReadOnlyList<string> items) => Calls.Add($"ShowResults({string.Join(", ", items)})");
public void ShowError(string message) => Calls.Add($"ShowError({message})");
}PASS short query
view : ShowResults() | ShowError(Enter at least 2 characters.)
catalog: (not called)
PASS failure clears old results
view : ShowResults(keyboard) | ShowResults() | ShowError(Search failed: catalog timeout)
catalog: key, key
PASS trim and no match
view : ShowResults() | ShowError(No products match "zz".)
catalog: zz
3 passed, 0 failedThese checks run without a UI framework and cover only the conversation between presenter and view interface. They do not verify that a real form draws the list correctly after receiving ShowResults. Calls in this example are synchronous, although actual searches are usually asynchronous. With two quick searches, the earlier response can arrive last and overwrite newer results. Such screens need the presenter to compare request numbers or cancel earlier requests so only the latest response is applied. Applying responses to the view on the UI thread also requires separate handling. ScriptedCatalog is a test double that throws when called more times than the prepared results allow.
TypeScript example
The same structure in TypeScript registers callbacks through onSearch instead of events, and represents model results as a union discriminated by its kind field. The second view uses a catalog that always fails.
The catalog that always fails is a test double for checking the presenter's behavior on failure. The same presenter must request clearing previous results and displaying an error when it receives a failure instead of successful results. Both cases show that the view prints supplied content without interpreting the failure and choosing its own display.
type SearchResult =
| { kind: "found"; items: readonly string[] }
| { kind: "failed"; reason: string };
// Model: search functionality with no knowledge of the screen.
interface Catalog { search(query: string): SearchResult; }
class InMemoryCatalog implements Catalog {
private readonly products = ["keyboard", "mouse", "monitor", "keycap"];
search(query: string): SearchResult {
const q = query.toLowerCase();
return { kind: "found", items: this.products.filter((p) => p.includes(q)) };
}
}
class DownCatalog implements Catalog {
search(_query: string): SearchResult {
return { kind: "failed", reason: "catalog timeout" };
}
}
// View interface: everything the presenter knows about the screen.
interface SearchView {
readonly query: string;
onSearch(handler: () => void): void;
showResults(items: readonly string[]): void;
showError(message: string): void;
}
// Presenter: validates input, calls the model and decides what the view should display.
class SearchPresenter {
constructor(private readonly view: SearchView, private readonly catalog: Catalog) {
view.onSearch(() => this.search());
}
private search(): void {
const query = this.view.query.trim();
if (query.length < 2) {
this.showFailure("Enter at least 2 characters.");
return;
}
const result = this.catalog.search(query);
if (result.kind === "failed") {
this.showFailure("Search failed: " + result.reason);
return;
}
if (result.items.length === 0) {
this.showFailure(`No products match "${query}".`);
return;
}
this.view.showResults(result.items);
}
// Clear the previous list first so it cannot be mistaken for this search's results alongside an error.
private showFailure(message: string): void {
this.view.showResults([]);
this.view.showError(message);
}
}
// Console view: forwards input as an event and prints only the supplied content.
class ConsoleSearchView implements SearchView {
query = "";
private handler: () => void = () => {};
onSearch(handler: () => void): void { this.handler = handler; }
type(text: string): void {
this.query = text;
console.log(`> search "${text}"`);
}
clickSearch(): void { this.handler(); }
showResults(items: readonly string[]): void { console.log("results: [" + items.join(", ") + "]"); }
showError(message: string): void { console.log("error: " + message); }
}
const view = new ConsoleSearchView();
new SearchPresenter(view, new InMemoryCatalog());
view.type("k");
view.clickSearch();
view.type(" ke ");
view.clickSearch();
view.type("zz");
view.clickSearch();
const offline = new ConsoleSearchView();
new SearchPresenter(offline, new DownCatalog());
offline.type("ke");
offline.clickSearch();> search "k"
results: []
error: Enter at least 2 characters.
> search " ke "
results: [keyboard, keycap]
> search "zz"
results: []
error: No products match "zz".
> search "ke"
results: []
error: Search failed: catalog timeoutThe same SearchPresenter decides what to display for both successful and failing catalogs. In either case, the view class prints the supplied content without making its own decisions.
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
- Mike Potel. "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java". Taligent, Inc. (1996). PDF. Smalltalk Programming Model section. ↩
- Martin Fowler. "GUI Architectures". martinfowler.com (July 18, 2006). Original article. Model-View-Presenter (MVP) section. ↩
- Mike Potel. "MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java". Taligent, Inc. (1996). PDF. Three User Interface Questions section. ↩
- Andy Bower, Blair McGlashan. "Twisting the Triad: The evolution of the Dolphin Smalltalk MVP application framework". ESUG 2000 Tutorial Paper (2000). Internet Archive copy. Model-View-Controller and Twisting the triad sections; footnote 5. ↩
- Michael Feathers. "The Humble Dialog Box". Object Mentor (2002). Internet Archive copy. ↩
- Martin Fowler. "GUI Architectures". martinfowler.com (July 18, 2006). Original article. Humble View section. ↩
- Martin Fowler. "Retirement note for Model View Presenter Pattern". martinfowler.com (July 11, 2006). Original article. Significant Revisions entry. ↩
- Dino Esposito. "Cutting Edge: Better Web Forms with the MVP Pattern". MSDN Magazine 25(9) (September 2010). Microsoft Learn archive. MVP at a Glance section. ↩
- Martin Fowler. "Supervising Controller". martinfowler.com (June 19, 2006). Original article. ↩
- Martin Fowler. "Passive View". martinfowler.com (July 18, 2006). Original article. ↩
- Andy Bower, Blair McGlashan. "Twisting the Triad: The evolution of the Dolphin Smalltalk MVP application framework". ESUG 2000 Tutorial Paper (2000). Internet Archive copy. The View section. ↩
- Dino Esposito. "Cutting Edge: Better Web Forms with the MVP Pattern". MSDN Magazine 25(9) (September 2010). Microsoft Learn archive. MVP at a Glance, MVP and MVVM, and Postback sections. ↩