Model View ViewModel, kortweg MVVM. Als je met WPF of Silverlight bezig bent, ben je deze vast tegen gekomen. Op het internet zijn momenteel erg veel discussies over dit onderwerp gaande. Wat is de beste aanpak? Welk framework kun je het beste gebruiken? Hoe ga je om met dialogs? Het begint bijna op een religieuze strijd te worden, de voorstanders van de ene aanpak vliegen de tegenstanders regelmatig in de haren.
Als je geen ervaring hebt met MVVM lijkt dit allemaal een beetje overdreven. Immers, we hebben het maar over een pattern, oftewel een manier van werken die we allemaal wel kennen maar die we maar even een naam gegeven hebben. En als je zo denkt, dan heb je volgens mij gelijk.
Toch is MVVM een heel mooie pattern. Het stelt mij in ieder geval in staat om mijn WPF/Silverlight code netjes te structureren en alle logica in mijn applicatie perfect testbaar in unittests te maken. Dat laatste alleen al is volgens mij een goede reden om MVVM eens te bekijken.
Voor iedereen die al met MVVM werkt: sla deze post gerust over. Ik ga hier nog veel meer over schrijven en wellicht is dat interessanter. Voor de rest: ik ga hier uitleggen wat MVVM is en hoe je het toepast.
Frameworks
De vraag die ik het meest gesteld krijg van mensen die met MVVM beginnen is: welk framework kan ik het beste gebruiken? Het antwoord daarop is altijd: geen. Maak zelf een framework.
Nu ben ik in het algemeen geen voorstander van het opnieuw uitvinden van het wiel, maar in dit geval raad ik het toch aan. MVVM is niet ingewikkeld en de frameworks die er zijn hebben niet al te veel features (hoewel sommige wel hele slimme oplossingen hebben bedacht). Ga eerst een zelf aan de gang, zorg dat je de concepten begrijpt en dat je weet wat MVVM inhoudt. Als je dat eenmaal in de vingers hebt, kun je gaan kijken naar MVVMFoundations, MVVMLight en de andere varianten.
In deze blogpost ga ik uitleggen wat MVVM is en hoe het werkt, we beginnen gelijk met het bouwen van onze eigen MVVM framework. Zullen we hem DutchMVVM noemen? Nah..
Achtergrond
MVVM is niets nieuws. Het is een variant op de oudere, meer bekende MVP en MVC patterns die al jaren gepropageerd worden door mensen als Martin Fowler. Ze gaan allemaal uit van hetzelfde principe: scheiding van je userinterface, je business logica en je domeinmodel. Voor al deze patterns geldt het volgende plaatje:
We hebben in principe drie lagen. Iedere laag heeft zijn eigen verantwoordelijkheid. De UI laag bevat alle grafische componenten zoals textboxen, labels, buttons, listboxen en wat niet meer. De domein laag bevat je classes met daarin je business objecten, zoals een Contact class, een Order class enzovoorts. De laag daartussen, de logica laag, koppelt de andere twee aan elkaar en bevat de logica.
Ik hoor je al zeggen: maar wij hebben meer lagen! Dat is natuurlijk in de praktijk zo, maar je kunt, als het goed is, iedere applicatie opdelen op bovenstaande manier. Zelfs als je een eenvoudige ASP.Net HelloWorld maakt heb je dit principe. Je ASPX pagina is je UI, je CodeBehind bevat de logica en de data die je weergeeft (in het geval van HelloWorld alleen de string "Hello World!") is je domein model.
Het maakt de patterns niet uit hoe je domein laag eruit ziet. Je kunt deze zelf schrijven, genereren met Linq to SQL, Entity Framework gebruiken, of wat je maar wilt. Het belangrijkste is echter dat je domeinlaag geen kennis heeft van de lagen daarboven. Hetzelfde geldt voor je tussenlaag: die moet wel kennis hebben van je domeinlaag, maar mag niets weten van de UI laag. En als laatste: je UI laag heeft wel weet van je BL laag maar niet van je domeinlaag.
Als je je applicaties op die manier structureert zie je dat je je lagen later kunt aanpassen zonder grote impact op de rest van je systeem. MVVM zorgt er zelfs voor dat je je BL laag (of zoals dat daar heet: je ViewModel) en je domeinmodel los kunt testen in een testomgeving, zonder last te hebben van je User Interface.
MVVM is specifiek bedoelt voor WPF en Silverlight. Het maakt gebruik van de enorme kracht van Bindings in die platformen. Hierdoor kunnen we losse lagen schrijven die echt onafhankelijk zijn van de laag erboven.
Binding in WPF/SL
Ik ga hier niet uitgebreid in op de werking van Bindings in WPF en SL. Daar zijn genoeg andere bronnen van informatie over. Maar even een hele kleine achtergrond is wel op zijn plaats.
Binding zorgt ervoor dat de elementen op het scherm gevuld worden met gegevens die ergens anders vandaan komen. Met andere woorden: een textbox bevat een string die niet in de definitie van je scherm staat maar die een andere oorsprong heeft.
Om dat te bereiken zijn er twee elementen in WPF van belang. Als eerste is er de content van je control. Een Textbox heeft bijvoorbeeld een Text property, een Button heeft een Content Property. Dit zijn allemaal dependencyproperties en dependencyproperties kun je binden aan iets anders.
Een Binding geeft aan hoe de data opgehaald moet worden. De data zelf kan van alles zijn. Van een eenvoudige string tot aan een complete usercontrol, zolang de property die gebind wordt er mee overweg kan kun je het via binding erin stoppen.
In XAML ziet een Binding er bijvoorbeeld als volgt uit:
<TextBox Text="{Binding Path=MyText}"/>
In plaats van een hardcoded string in de TextBox te plaatsen, vertellen we WPF nu dat er ergens een property is met de naam MyText en de inhoud daarvan willen we graag in de TextBox zien.
Maar waar staat die MyText property dan? Dat is de taak van de DataContext. De DataContext geeft aan wat de bron is van de Bindings. Dus: de DataContext geeft aan waar de data staat, de binding specificeert wat er moet staan. Die twee elementen zijn genoeg om de binding te maken.
Om aan te geven dat de MyText property een property is van de huidige window, moeten we even wat kunstgrepen uithalen. Voor nu nemen we even de makkelijkste weg. In de codebehind van ons scherm plaatsen we in de constructor de volgende regel:
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
We zetten de DataContext dus op zichzelf. Als je nu in je window een property MyText hebt (van het type String), zal de inhoud van die property in de textbox geplaatst worden. En andersom: als je de textbox wijzigt, zal die nieuwe waarde in MyText terechtkomen! Er is nog veel meer te vertellen over Bindings, als je meer wilt weten raad ik je aan om deze link te volgen.
Maar: je ziet dat je door Bindings te gebruiken in staat bent om de data ergens neer te zetten en dan in de UI laag aan te geven waar deze data staat. Je hoeft dus niet meer in je logica data naar je UI te kopieren, je UI laag haalt het zelf wel op. Zie je MVVM al een beetje voor je?
MVVM
MVVM zou niet mogelijk zijn zonder de binding technieken in WPF en Silverlight. Laten we de verschillende onderdelen eens onder de loep nemen.
Het idee is dat je voor alles wat op het scherm gaat komen usercontrols maakt. Deze noemen we de views. Een view bevat alle UI elementen die je nodig hebt om je data weer te geven en/of te bewerken. Alle data in dat scherm wordt door middel van databinding gekoppeld.
Je model is simpelweg je domeinmodel. Zoals al eerder gezegd kunnen die gewone classes zijn, of iets uit NHibernate, of wat dan ook. Er hoeft in principe geen relatie te bestaan tussen je UI en je model: deze kunnen los ontwikkeld worden.
Het voordeel van deze loskoppeling is dat het nu mogelijk is dat een designer in Blend de UI aanmaakt, terwijl een developer zich bezig houdt met het domein model. Deze twee mensen kunnen los van elkaar werken.
De lijm die alles met elkaar verbindt is de ViewModel. Laten we eens een voorbeeld nemen.
Ik neem een simpel voorbeeld: een applicatie die contacten toont. Een contact is een persoon met een voornaam, een achternaam en een lijst met categorieen. Een categorie kan zijn "Prive", "Zakelijk", enzovoorts. Ons domein model ziet er als volgt uit:

Alle properties zijn strings, met uitzondering van Categories, wat een List<Category> is. In de constructor wordt deze laatste aangemaakt, de setter is private zodat we deze niet kunnen wijzigen.
De View is net zo simpel, met een kanttekening. In de specificaties staat dat boven ieder contact de volledige naam moet staan, dus de voornaam en achternaam samengevoegd.
Met de XAML:
<UserControl x:Class="dotNedContactManager.Views.ContactView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Text="{Binding FullName}" FontSize="16" FontWeight="Bold" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Voornaam:"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Achternaam:"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Categorieen:"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding FirstName}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding LastName}"/>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Categories}"/>
</Grid>
</UserControl>
Geen rocketscience, zoals ik al zei.
In ons domein hebben we geen FullName. Dat hoeft ook niet, want dat hoort niet bij het domein. Het is puur een UI ding: de gebruikers willen graag de volledige naam boven ieder contact hebben. Aangezien het composite is (dus samengesteld) zou het onzinnig zijn om de FullName bijvoorbeeld ook in de database op te slaan.
Het wordt tijd voor onze ViewModel.
Een ViewModel is een class met als enige bijzonderheid dat hij de interface INotifyPropertyChanged event implementeert. Deze interface bevat een event, PropertyChanged, die moet worden afgevuurd als een property in de class van inhoud wijzigt. Het Binding mechanisme van WPF en SL let op deze events en zal, indien nodig, de Bindings verversen (en dus de nieuwe data op het scherm tonen).
Komt 'ie:
using System;
using System.ComponentModel;
using dotNedContactManager.Model;
namespace dotNedContactManager.ViewModels
{
public class ContactViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
var eventCopy = PropertyChanged;
if (eventCopy == null)
return;
eventCopy( this, new PropertyChangedEventArgs( propertyName ) );
}
private Contact _Contact;
public ContactViewModel(Contact contact)
{
if (contact == null)
throw new ArgumentNullException( "contact", "contact is null." );
_Contact = contact;
}
public string FirstName
{
get { return _Contact.FirstName; }
set
{
_Contact.FirstName = value;
RaisePropertyChanged( "FirstName" );
RaisePropertyChanged( "FullName" );
}
}
public string LastName
{
get { return _Contact.LastName; }
set
{
_Contact.LastName = value;
RaisePropertyChanged( "LastName" );
RaisePropertyChanged( "FullName" );
}
}
public string FullName
{
get { return String.Format( "{0} {1}", _Contact.FirstName, _Contact.LastName ); }
}
}
}
We hebben in onze ViewModel een referentie gelegd naar onze domein class Contact. Zoals ik al eerder zei is dat geen probleem: een bovenliggende laag mag enige kennis hebben van de laag daar direct onder. Voor de eenvoud geef ik deze contact even mee in de constructor; dat gaan we later aanpassen. Voor nu is dat wel even makkelijk zo.
De ViewModel heeft 3 properties, te weten FirstName, LastName en FullName. De eerste twee halen hun data rechtstreeks uit de _Contact. Als de data verandert, zetten we deze ook direct in _Contact, maar we roepen ook de method RaisePropertyChanged(string propertyName) aan. Deze method vuurt het event af met de juiste propertynaam. Op die manier weet de UI laag dat er iets is verandert en dat de UI dus geupdate moet worden. Maar. aangezien de derde property FullName afhankelijk is van FirstName en LastName, moeten we ook aangeven dat deze gewijzigd is. Immers, als de voornaam wijzigt moeten ook de UI elementen die aan FullName gebonden zijn meeveranderen! FullName is verder readonly: het heeft geen zin om daar een Setter voor te schrijven.
Laten we de boel eens samenvoegen. In de MainWindow heb ik een property gemaakt van het type ContactViewModel. In de constructor van MainWindow vul ik even wat data in. Nu zit in MainWindow.xaml.cs dus kennis van de Contact class, die geven we immers door aan de ViewModel, en dat is niet zoals het hoort. Later zullen we dit eruit halen, voor nu is dit goed genoeg.
using System;
using System.Windows;
using dotNedContactManager.ViewModels;
namespace dotNedContactManager
{
public partial class MainWindow : Window
{
public ContactViewModel ContactVM { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
// Tijdelijk wat data genereren
Model.Contact newContact = new Model.Contact()
{
FirstName = "Dennis",
LastName = "Vroegop"
};
ContactVM = new ContactViewModel( newContact );
}
}
}
En de XAML:
<Window x:Class="dotNedContactManager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:dotNedContactManager.Views"
Title="MainWindow" Height="350" Width="525">
<Grid>
<views:ContactView DataContext="{Binding ContactVM}"/>
</Grid>
</Window>
Ik heb hier een namespace 'views' toegevoegd, en in het scherm een instance van de ContactView gemaakt. De DataContext van deze view is gebonden aan onze property ContactVM (en aangezien MainWindow als DataContext zichzelf heeft, wordt deze ook gevonden).
In de ContactView hebben we de bindings {Binding FirstName}, {Binding LastName} en {Binding FullName} staan. Omdat de DataContext gezet wordt op een instance van onze ContactViewModel, worden deze properties ook gevonden en uitgelezen.
Als je dit nu runt krijg je je scherm te zien. Wanneer je een van de twee naam velden wijzigt, zul je zien dat de FullName meegaat.
We hebben nu dus in onze UI laag nergens code staan die de teksten neerzet, en ook de logica voor het samenvoegen van de namen is niet terug te vinden in onze UI laag. We kunnen nu dus een unit test maken om onze ViewModel te testen:
[TestMethod]
public void FullName_Should_Reflect_FirstName_And_LastName()
{
string firstName = "A";
string lastName = "B";
string expected = "A B";
Contact newContact = new Contact()
{
FirstName = firstName,
LastName = lastName
};
ContactViewModel contactVM = new ContactViewModel( newContact );
Assert.AreEqual( expected, contactVM.FullName, "FullName doesn't work." );
}
Alle logica zit nu in de Model en ViewModel classes, we kunnen die dus perfect unittesten.
En nu?
Dit is de basis van MVVM. Er is nog veel meer over te vertellen, en dat ga ik doen ook. Maar niet nu. Speel er eens mee en de volgende keer gaan we dieper in op de Category ViewModel. Die is namelijk leuker :-)
Technorati Tags:
MVVM,
Silverlight,
WPF