Met het .NET framework 4.0 in het vooruitschiet is er een nieuwe loot aan de ASP.NET technologie boom: het ASP.NET MVC Framework (MVC = Model-View-Controller). En met dit framework ontstaat er weer discussie over een oud design pattern, maar daarmee wordt een wellicht belangrijker design pattern over het hoofd gezien: het Model-View-Presenter pattern. Laat ik eens uitleggen waarom MVP een "Most Valuable Pattern" is.
Inleiding
Over design patterns is veel geschreven, maar ik zie dat deze in de praktijk nog onvoldoende worden toegepast. Waarom dat zo is vraag ik mij geregeld af. Toen bij een SDE van enige tijd geleden de spreker vroeg wie het MVP pattern kende, stak slechts een handje vol mensen hun hand op. Ergo: het is tijd om wat kennis met jullie te delen en toe te lichten waarom het Model-View-Presenter design pattern (hierna MVP genoemd) een "Most Valuable Pattern" is.
Het Model-View-Presenter pattern is een Most Valuable Pattern
Design patterns 101
Wat zijn design patterns? Ik definieer een design pattern als een sjabloon dat een standaard oplossing beschrijft van een standaard programmeerprobleem. Begin daarom bij het toepassen van design patterns ook met het benoemen van het probleem.
Een bekend voorbeeld is het probleem dat er maar één instantie van een bepaalde klasse in de applicatie mag voorkomen. Het antwoord is het singleton pattern (http://en.wikipedia.org/wiki/Singleton_pattern). Het singleton pattern beschrijft hoe je realiseert dat je van een klasse slechts één instantie kunt maken.
Maar design patterns zijn vooral sjablonen: hoe je een pattern precies implementeert, wordt niet dwingend voorgeschreven maar men geeft alleen advies over hoe je het zou kunnen doen.
Design patterns hebben als doel standaard oplossingen voor te schrijven voor standaard programmeerproblemen. Het voordeel van standaard oplossingen is dat deze al in de praktijk zijn uitgetest. Het tweede doel van design patterns is vooral te kunnen communiceren over de architectuur van een applicatie. Om bij dit voorbeeld te blijven kan een software-architect zeggen: van deze klasse mag in de applicatie maar één instantie voorkomen en om dit te bereiken dient van het singleton-pattern gebruik te worden gemaakt.
Architectuur 101
Design patterns worden gebruikt om applicatie-architecturen vorm te geven. Welke architectuur echter ook wordt gebruikt, elke architectuur streeft uiteindelijke naar één doel: “loose coupling and high cohesion”, ofwel “verdergaande ontkoppeling en toenemende samenhang” in goed Nederlands. En dàt is nu waar het MVP design pattern om gaat.
Architectuur streeft naar “loose coupling and high cohesion”
De geschiedenis
In de tijd dat applicaties van traditionele client-server architecturen naar 3- of meer lagen applicaties evolueerden, ontstond in toenemende vraag behoefte aan het antwoord op de vraag: hoe scheid ik de (G)UI code van de business layer code? Het "Separation of Concerns" principe, zoals beschreven door Edsger Wiebe Dijkstra (zie http://en.wikipedia.org/wiki/Separation_of_concerns) was hierop een antwoord. Het Model-View-Controller (MVC) design pattern was een design pattern dat op dit principe werd ontwikkeld.
Het MVC design pattern is al in 1979 beschreven voor SmallTalk, en in de volgende 30 jaar is er natuurlijk veel veranderd. In de loop van de tijd zijn er verschillende variaties op dit thema ontstaan. Het ASP.NET MVC Framework is geen MVC-pattern in de originele zin; dertig jaar geleden bestond web-development nog helemaal niet. Dit framework maakt gebruik van het Model2 design pattern (http://en.wikipedia.org/wiki/Model2); een MVC web-variant.
In het begin van 1990, toen OO al iets verder was uitgebouwd, is uit een joint venture van Apple, HP en IBM om een nieuw OS te ontwikkelen (genaamd “Taligent”) het MVP pattern ontstaan, wat later door de Java-wereld werd overgenomen.
Het MVP-pattern werd pas echt beroemd toen Martin Fowler zich ermee bemoeide. Nadat Fowler enige tijd had nagedacht over het MVP-pattern kwam hij tot de conclusie dat het MVP-pattern opgesplitst zou moeten worden in twee afzonderlijke patterns: Supervising Controller (http://www.martinfowler.com/eaaDev/SupervisingPresenter.html) en Passive View (http://www.martinfowler.com/eaaDev/PassiveScreen.html). De term MVP was inmiddels echter zo ingeburgerd dat het MVP design pattern als zodanig is blijven bestaan. Een WPF variatie op het MVP design pattern is het Presentation Model: http://www.martinfowler.com/eaaDev/PresentationModel.html of Model-View-ViewModel pattern (zie http://en.wikipedia.org/wiki/Model_View_ViewModel).
Weet je nu niet meer wanneer je welk design pattern kunt gebruiken, raadpleeg dan de onderstaande matrix:
| |
ASP.NET |
WinForms |
WPF |
| MVC web / Model2
|
• |
|
|
| Model View Presenter
|
• |
• |
• |
| Presentation Model
|
|
|
• |
Tabel 1: Wanneer gebruik je welk design pattern?
Hoe werkt MVC?
In het MVC-pattern beschrijft het “Model” de data in de business layer en de “View” de representatie ervan in de user interface. De “Controller” verzorgt de communicatie tussen de twee andere lagen, veelal door middel van Observer- of Publish/Subscribe-patterns. Een vervanging voor deze twee patterns in .NET zijn "events" (event EventHandler<EventArgs>). Zie figuur 1.

Fig. 1: Eenvoudig diagram dat de relatie beschrijft tussen Model, View en Controller.
Noot: De bloklijnen geven een directe associatie weer en de stippellijnen een indirecte associatie (observer pattern / events).
Geen Loose Coupling is het nadeel van het MVC-pattern
Eén van de grootste nadelen van het MVC pattern is dat zowel de Model, de View en de Controller concrete klassen, concrete implementaties zijn. Een wijziging in één van de klassen leidt direct tot een hercompilatie van alle drie de klassen en dus van de hele applicatie. Er is dus geen sprake van “loose coupling”.
Hoe werkt MVP: de theorie
Het MVP-pattern is voornamelijk een presentation layer design pattern. Een presentation layer in een applicatie bestaat uit twee lagen:
- een view/GUI
- presentatie logica

De technologie die gebruikt wordt voor de view/GUI bepaalt vaak wat voor soort context er beschikbaar is in de client (rich-client / web-client). En dit heeft vaak weer veel impact op hoe de presentatielogica wordt ontwikkeld. Maar het is tegenwoordig niet ongewoon om in één applicatie zowel een web-client als een rich-client te moeten ondersteunen. En juist dan zou je de presentatielogica willen hergebruiken. Bij een goede MVP-implementatie is dit dan ook zeer wel mogelijk.
Een applicatie met zowel een Web als Rich-Client interface is zeer gebruikelijk
In object-oriëntatie is het werken met interfaces *de* manier om loose coupling te verwezenlijken, en dit is o.a. het verschil tussen het MVC-pattern en het MVP-pattern. In een MVP-implementatie praat de presenter tegen een interface, waarbij het interface geïmplementeerd wordt door de view.
Het MVP-pattern bestaat dus uit:
- Een "Model": een concrete klasse die de data voorstelt. Deze komt meestal uit de Business Layer. Een webservice/WCF proxy klasse voldoet ook;
- Een "View": een concrete klasse die de GUI voorstelt. Dit kan een Windows Forms klasse, een aspx-pagina, een WPF-Forms klasse zijn. Voor WPF en/of Silverlight is de variatie "Presentation Model" beschikbaar;
- Een "View Interface": een interface die de data (in de vorm van properties) beschrijft waar de GUI iets mee moet doen. Deze interface moet geïmplementeerd worden door de "View";
- Een "Presenter": een concrete klasse die de data uit de "View Interface" (die weer wordt geïmplementeerd door de "View") leest en schrijft en deze aan het "Model" (de business layer) geeft.
In object-oriëntatie is het werken met interfaces *de* manier om loose coupling te verwezenlijken
Dit betekent dat je per weer te geven object werkt met drie klassen en één interface:
- De business layer "Model" klasse: dit object vormt een onderdeel van het domeinmodel (of een proxy daarvan) waarvoor je een applicatie maakt en bevat alle gegevens (properties) en gedrag (methoden en events) die dit object nodig heeft in het domeinmodel;
- Een View klasse. Dit is een WinForms-, WPF-, of ASPX-pagina klasse die de GUI moet gaan voorstellen voor de gebruiker. In een MVP implementatie bepaalt de view alleen maar hoe iets getoond moet worden: of een eigenschap van een buisness object nu wordt weergegeven in een textbox, een label, combobox, radio buttons of om het even wat voor control(s), dat is de verantwoordelijkheid van deze klasse. Ergo: deze klasse is alleen maar verantwoordelijk voor de rendering van de te tonen objecten;
- De interface: deze schrijft dwingend voor wat er getoond moet worden d.m.v. property definities (op het gedrag kom ik later terug). De interface moet worden geïmplementeerd in de view klasse (punt 2);
- De presenter klasse: deze kent de Model klasse (via een referentie naar de betreffende assembly of d.m.v. gegenereerde proxy klassen) en kent alleen de interface. De presenter krijgt de data van een business layer klasse en schrijft deze data naar de properties in de interfacedefinitie. De presenter heeft dus geen weet van wat voor type GUI (webclient of rich client) hij bedient. De presenter is alleen verantwoordelijk voor wanneer iets getoond moet worden.
GUI klassen kennen vaak iets van een run-time context waar je in de niet-GUI lagen geen weet van wil hebben; denk hierbij bijvoorbeeld aan de HttpContext die je indirect meekrijgt met een ASPX-pagina of het feit dat een textbox control in WinForms statefull is. Doordat de presenter alleen met de view communiceert via een interface, ontstaat er een verdere ontkoppeling tussen de GUI en de manier waarop de data wordt verwerkt. De presenter heeft dus geen weet van in wat voor soort context hij zit.
Omdat iedere klasse/interface zijn eigen verantwoordelijkheid heeft, wordt het “Single Responsibility Principle” als vanzelf toegepast (zie ook: Separation of Concern vs Single Responsibility Principle op http://weblogs.asp.net/arturtrosin/archive/2009/01/26/separation-of-concern-vs-single-responsibility-principle-soc-vs-srp.aspx).
Het verschil tussen een Supervising Controller (SVC) en een Passive View (PV) is dat bij een Supervising Controller implementatie (meestal) complexe objecten in de interface worden gedefinieerd en in de View (GUI klasse) gebruik gemaakt kan worden van geavanceerdere technieken als databinding, terwijl bij een passive view implementatie in de interface definitie de eigenschappen van complexe business layer objecten worden gedefinieerd als een reeks van simple type properties (string, int, bool, DateTime, etc.).
Dit alles lijkt misschien nog wat abstract, dus laten we dit eens in een codevoorbeeld uitwerken. De code voor dit artikel is beschikbaar via de website en bevat een ASP.NET en een WinForms voorbeeld in een PV- en een SVC implementatie.
Hoe werkt MVP: de praktijk, deel 1
Het voorbeeld business layer model bestaat, om het eenvoudig te houden, uit één object: Person, met als eigenschappen "Name" (string) en "Age" (int) en methoden "Save" ,"Delete", "Get(int id)" en "GetAll". De oplettende lezer vermoed reeds het Active Record pattern. Commentaar is omwille van bondigheid achterwege gelaten (zie listing 1).
public class Person
{
public int ID { get;set;}
public string Name { get;set;}
public int Age { get;set;}
public void Save()
{ Debug.WriteLine(
"Saving this person to a database..."); }
public void Delete()
{ Debug.WriteLine(
"Deleting this person from a database..."); }
public static Person Create()
{ return new Person {ID = (-1),
Name = string.Empty, Age = (-1)} }
public static Person Get(int id)
{
Person person = new Person
{ ID = id,
Name = string.Format("James Bond {0:000}", id),
Age = (50 + id)
};
return person;
}
public static List<Person> GetAll()
{
List<Person> result = new List<Person>(9);
for(int i = 1; i < 10; i++)
{
result.Add(Get(i));
}
return result;
}
}
Lisintg 1 (C#): Het buisness layer object Person
Tot zo ver niets spannends. Dit object leeft op/in de server en komt op wat voor manier dan ook op de client terecht. De user interface, zowel webforms als winforms, is ook niet spannend: zie figuur 2.

Fig. 2: De user interface om personen weer te geven
De interfacedefinitie bepaalt alleen dat er een integer met de naam "ID" in een GUI klasse gelezen en geschreven kan worden, evenals een tekst property "Name" en een integer met de naam "Age". Er kan alleen een list van Person-objecten ‘geset’ worden (zie listing 2).
Hoe deze properties getoond moeten worden, is niet de verantwoordelijkheid van het interface. In de Windows-client wordt b.v. de ID property getoond met een numeric up/down control i.p.v. een textbox.
public interface IPersonView
{
int ID { get; set; }
string Name { get; set; }
int Age { get; set; }
List<Person> Persons { set; }
}
Listing 2 (C#): het interface van Person
De GUI klasse, de view, moet het interface implementeren. De eerste opzet staat in listing 3.
public partial class PersonView : Form, IPersonView
{
public PersonView()
{ InitializeComponent(); }
public int ID
{
get { return Convert.ToInt32(IDTextBox.Text); }
set { IDTextBox.Text = value.ToString(); }
}
public int Age
{
get { return Convert.ToInt32(AgeUpDown.Value); }
set { AgeUpDown.Value = Convert.ToDecimal(value); }
}
string IPersonView.Name
{
get { return NameTextBox.Text; }
set { NameTextBox.Text = value; }
}
public List<Person> Perons
{ set { PersonsBindingSource.DataSource = value; } }
}
Listing 3 (C#): de implementatie van het interface in de view
Het implementeren van de properties uit de interface lijkt erg voor de hand te liggen, maar toch wil ik twee opmerkingen maken:
- Valideer altijd, altijd, altijd de user input ("All user input is evil"). Gebruik dus nooit Convert.ToInt32() direct in een property get zoals in dit code voorbeeld;
- Een Form-object kent al een property Name. Daarom wordt de name-property van Person expliciet geïmplementeerd door string IPersonView.Name.
De presenter doet dienst als intermediair tussen de view en de model. Kijk eerst even naar de code in listing 4.
public class PersonPresenter
{
private IPersonView View;
public PersonPresenter(IPersonView view)
{
if(view == null)
{ throw new ArgumentNullException("view"); }
View = view;
}
public void Delete()
{
Person person = Person.Get(View.ID);
person.Delete();
}
public void Save()
{
Person person = new Person
{ Name = View.Name, ID = View.ID, Age = View.Age };
person.Save();
}
public void New()
{
Person person = Person.Create();
View.ID = person.ID;
View.Name = person.Name;
View.Age = person.Age;
}
public void Get()
{
Person person = Person.Get(View.ID);
View.Name = person.Name;
View.Age = person.Age;
}
}
Listing 4 (C#): de implementatie van de presenter
De presenter kent een variabele die een object-instantie bevat die het interface implementeert. Via de constructor van de presenter wordt deze object-instantie "gevuld". De presenter weet dus niet wat voor type object dit is; hij weet alleen dat dit object het interface implementeert.
Het gedag van de presenter wordt bepaald door de beschikbare methoden die de business-objecten (het model) aanroepen en de properties vanuit de interfacedefinitie lezen.
Maar de grote vraag blijft nu ongetwijfeld: hoe werkt dit nu allemaal samen? De magie van het MVP-pattern wordt gevormd in drie regels (1, 3 en 8) in listing 5. Er moet één variabele van het type presenter aan de view-class worden toegevoegd die in de constructor wordt geïnstantieerd.
1 public partial class PersonView : Form, IPersonView
2 {
3 private PersonPresenter presenter;
4
5 public PersonView()
6 {
7 InitializeComponent();
8 presenter = new PersonPresenter(this);
9 }
// Andere properties methodes en events
}
Listing 5 (C#): de magie van het MVP pattern
De view-class bevat nu een private variabele van het type van de presenter (regel 3). In de constructor van de view-class wordt er een instantie van de presenter gecreëerd (regel 8). De constructor van de presenter vereist als parameter een object dat IPersonView implementeert. Dat doet de view-class (zie regel 1). Daarom wordt in de constructor bij de creëren van de presenter een instantie van zichzelf meegegeven. Door het gebruik van een interface wordt alle overbodige context in de presenter eruit gefilterd.
Het gedrag wordt nu heeft eenvoudig beschikbaar in de event-handlers van de knoppen op de view (zie listing 6). Kun je aan dit codevoorbeeld zien uit welk type client deze komt?
private void NewButton_Click(object sender, EventArgs e)
{ presenter.New(); }
private void SaveButton_Click(object sender, EventArgs e)
{ presenter.Save (); }
private void DeleteButton_Click(object sender, EventArgs e)
{ presenter.Delete(); }
Listing 6 (C#): De implementatie van het gedrag in de interface.
Hoe werkt MVP: de praktijk, deel 2
Op zich is het voorbeeld zoals hiervoor beschreven een valide implementatie van het MVP-pattern, maar er is een logisch probleem met deze implementatie: de view heeft nu expliciete kennis van de presenter en het gedrag dat deze aanbiedt. De view is nu ook verantwoordelijk voor het aanroepen van dit gedrag. In een MVC-implementatie communiceert de controller d.m.v. events met het model en de view. Voor een logisch nettere implementatie van MVP zou je dit ook moeten doen.
Je doet dit door het beschikbare gedrag als events vast te leggen in het interface. Hiermee dwing je in een view niet alleen de beschikbare data (d.m.v. property-definities) af, maar ook het gedrag (zie listing 7).
public interface IPersonView
{
event EventHandler<EventArgs> Get;
event EventHandler<EventArgs> Save;
event EventHandler<EventArgs> Delete;
event EventHandler<EventArgs> New;
Person CurrentPerson { get; set; }
List<Person> Persons { set; }
}
Listing 7: een beter interface van Person
In de GUI-klasse die dit interface zal implementeren, moeten de events "Get", "Save", "Delete" en "New" gedefinieerd zijn. De interface bepaalt ook hier alleen dat er een bepaald gedrag zal zijn, niet hoe dit gedrag zal zijn (zie listing 8).
public partial class PersonView : Form, IPersonView
{
public event EventHandler<EventArgs> Get;
public event EventHandler<EventArgs> Save;
public event EventHandler<EventArgs> Delete;
public event EventHandler<EventArgs> New;
private PersonPresenter presenter;
public PersonView()
{
InitializeComponent();
presenter = new PersonPresenter(this);
}
private void NewButton_Click(object sender, EventArgs e)
{ if(New != null) { New(sender, e); } }
private void SaveButton_Click(object sender, EventArgs e)
{ if(Save != null) { Save(sender, e); } }
private void DeleteButton_Click(
object sender, EventArgs e)
{ if(Delete != null) { Delete(sender, e); }
// Andere properties methodes en events
}
Listing 8: een view met alleen signaleringen
Het mooie aan dit voorbeeld is, dat de view nu geen kennis heeft van de presentatie-logica. Het enige dat de view doet, is een event afvuren als er iets gebeurt. Het gedrag (de event-definities) is in het interface afgedwongen en de view zegt: als er iets gebeurt, dan zal ik dat zeggen. Wat er vervolgens moet gebeuren en hoe, dat is aan de presenter en het model. De presenter abonneert zich op de events die zijn gedefinieerd in het interface (nogmaals: de presenter kent de view niet, alleen het interface) en implementeert de event-handlers op deze events, zoals in listing 9:
public class PersonSvcPresenter
{
private readonly IPersonSvcView svcView;
public PersonSvcPresenter(IPersonSvcView view)
{
if(view == null)
{ throw new ArgumentNullException("view"); }
svcView = view;
svcView.Get += OnGetView;
svcView.New += OnNewView;
svcView.Save += OnSaveView;
svcView.Delete += OnDeleteView;
}
private void OnDeleteView(object sender, EventArgs e)
{
// Do your presentation logic level validations here
Person person = svcView.CurrentPerson;
person.Delete();
}
private void OnSaveView(object sender, EventArgs e)
{
// Do your presentation logic level validations here
Person person = svcView.CurrentPerson;
person.Save();
}
private void OnNewView(object sender, EventArgs e)
{
Person person = Person.Create();
svcView.CurrentPerson = person;
}
private void OnGetView(object sender, EventArgs e)
{
svcView.Perons = Person.GetAll();
}
}
Listing 9: een nettere presenter
De implementatie in dit tweede deel leidt tot een verdere ontkoppeling van de GUI met zijn presentatie-logica, wat vanuit architectuur oogpunt wenselijk is. Maar ik wil er een paar opmerking over maken:
- Als je net met het MVP-pattern aan de gang gaat, lijkt het erg ondoorzichtig. Dat is echter van tijdelijk aard. Als je dit pattern een paar keer hebt toegepast, gaat het allemaal vanzelf;
- Als je Resharper gebruikt (of wellicht een andere tool), dan klaagt Resharper dat de presenter in de view alleen maar wordt geïnstantieerd, maar verder niet wordt gebruikt. Negeer die klacht.
De coding guidelines van IDesign.Net (Juval Lowy) schrijven voor dat in een interface-definitie geen event-declaraties zouden moeten voorkomen. Waarom dat is, weet ik niet (ik zou het op prijs stellen als iemand het mij kon vertellen). Een event-definitie in een interface is wel gebruikelijk in het MVP-pattern.
In sommige coding guidelines geldt een write-only property als een "design flaw". Als je het daarmee eens bent, definieer dan in de interface een aparte set-methode i.p.v. een property set.
Tips & Ticks
Hieronder staat een aantal aanwijzingen en overdenkingen voor goed gebruik.
- Naamgeving: geef je GUI-classes de naam van het object dat ze moeten weergeven en suffix deze met "View", b.v. class PersonView : Form;
Voor het interface geldt dat deze heten als de GUI-class met een "I" prefix, b.v. interface IPersonView;
De presenter heet "MyObject"Presenter, b.v. class PersonPresenter.
- De presenter wordt samen met de interface-definitie waar de presenter op acteert, in een aparte assembly geïmplementeerd;
- Gedrag wordt gedefinieerd d.m.v. events van meestal dit type: event EventHandler<EventArgs>. De data die je mogelijkerwijs nodig hebt bij het verwerken van een event kun je het best definiëren in een property in je interface;
- Die properties die read-only in de GUI zijn, worden gedefinieerd als een set property in de interface (b.v. teksten van labels). Write-only properties in de GUI worden gedefinieerd als een get property in de interface (b.v. een wachtwoord);
- Het MVP-pattern leent zich erg goed om complexe objecten in custom user controls te implementeren;
- Wanneer de GUI-class meerdere complexe objecten moet tonen (omdat je een tabpage-control gebruikt om verschillende objecten te tonen) kan je GUI-class meerdere interfaces implementeren. Multiple inheritance wordt niet ondersteund in .NET, maar meerdere interfaces kunnen wel in één class worden geïmplementeerd (maar wellicht zou ik dan toch voor custom user controls kiezen);
- Als je niet iedere keer de volgende code in je event handlers wil schrijven:
if(Save != null) { Save(sender, e); }
maar in één keer
Save.Raise(sender, e);
lees dan de blog entry "Eventing made easy: MyEvent.Raise(this, EventArgs.Empty);" van Pieter-Joost van der Sande op: http://born2code.net/blog/1-general/15-eventing-made-easy-myeventraisethis-eventargsempty.html

Conclusie
Hoewel het MVP design pattern erg ingewikkeld kan lijken als je er voor de eerste keer naar kijkt en de complexiteit van de code iets toeneemt, acht ik het MVP design pattern een "Most Valuable Pattern" omdat dit pattern kan worden gebruikt in zowel WPF, WinForms als ASP.NET. Met het MVP pattern krijg je bijna automatisch het "Separation of Concerns" principe en het "Single Responsibility Principle" cadeau. De presentatie-logica is beter herbruikbaar. Tevens is de presentatie-logica beter testbaar via frameworks als Rhino.Mocks (http://www.ayende.com/projects/rhino-mocks.aspx), omdat de presenter op een interface acteert. Maar bovenal bereik je met het MVP-pattern een verdergaande ontkoppeling en een toenemende samenhang (loose coupling and high cohesion). Dat wil jij toch ook bereiken in je code?
Op de website staat de code uitgewerkt met commentaar in een SVC- en een PV-implementatie ,zodat je er zelf eens mee kunt spelen.
Om zelf te lezen:
Op de onderstaande site wordt m.b.v. webcasts uitgelegd hoe MVP werkt:
Bijbehorende sources zijn hier te downloaden.