MVVM en Blazor
Inleiding
Design patterns zijn sjablonen / raamwerken die gebruikt kunnen worden bij het ontwikkelen van software. De MVVM (Model View ViewModel) design pattern is binnen Blazor erg populair en in deze post laten we zien hoe we een Blazor applicatie kunnen bouwen conform de MVVM design pattern. De voorbeeldcode kun je hier (GitHub) vinden.
Design Patterns
We komen elke weer dezelfde “problemen” tegen als we software ontwikkelen voor “de” computer. We moeten iets van een gebruikersscherm maken zodat de gebruiker via zo’n scherm kan aangeven wat hij/zij wil.
We zullen ook een programma moeten maken dat om kan gaan met data. De data moet ergens opgeslagen worden en wat we opslaan, dat moeten we ook weer ophalen waarbij het opgehaalde getoond moet worden op een manier zoals de gebruiker dat graag wil.
Voor deze veel voorkomende software-ontwikkel “problemen” zijn design patterns ontwikkeld zijnde een sjabloon / raamwerk dat gebruikt kan worden bij het ontwikkelen van software.
Bekende design patterns zijn MVC (Model View Controller) en MVVM (Model View ViewModel). Design patterns zijn niet gebonden aan een bepaalde programmeertaal of ontwikkelplatform en zo is MVVM binnen Blazor erg populair.
MVVM
De applicatie in deze post gaan we voor een deel herstructureren (refactoren) naar een MVVM design pattern en het één en ander zal er als volgt uit komen te zien met Views, ViewModels en Models:
Het is geen grote “code behind” meer waarbij alle code in de View is gepropt en direct is gekoppeld aan de model definities (een constructie dat we veel zien bij .aspx Web Forms en Ms Access applicaties). We hebben in plaats daarvan allerlei losse onderdelen die we naar behoefte aan elkaar kunnen koppelen.
Alle onderdelen verwijzen naar een bepaalde interface en welke implementatie van de interface precies gebruikt gaat worden? Dat hoeven we gelukkig niet bij elke onderdeel van de applicatie op te geven. We kunnen dankzij de host/dependency injection-constructie gewoon op één plek bij de host aangeven welke implementatie voor die interface uiteindelijk gebruikt gaat worden in de applicatie.
View
De View is wat de gebruiker ziet van de applicatie. De gebruiker geeft via de view aan wat hij/zij wil en het gewenste wordt getoond in de view en op een manier zoals de gebruiker dat wil.
In dit voorbeeld kan de gebruiker met component VoegToe.razor aangeven dat hij/zij een eigenaar van een auto wil toevoegen en wat de vastgelegd moet worden van die eigenaar:
In component TotaalAantal.razor worden de gegevens van de autobezitters getoond (op een bepaalde manier en zoals gewenst door de gebruiker):
De Views zijn gekoppeld aan een ViewModel waarbij de desbetreffende .razor-View-bestanden weinig tot geen code-behind hebben omdat dat is verlegd naar de ViewModel. De View is primair gericht op het kunnen presenteren van gegevens naar de gebruiker en de nadruk binnen een view ligt dan ook op HTML, CSS en Bootstrap (of een ander CSS framework).
We zien een eerste voordeel van MVVM. De View kan ontkoppeld worden van de rest van de applicatie en je kunt bij wijze van spreken het ontwikkelen van de view helemaal overlaten aan een View Designer. De View Designer kan dan helemaal los gaan en voor de View Designer zijn alleen de properties en de methoden van de ViewModel van belang. De View Designer hoeft zich verder niet bezig te houden met wat de ViewModel verder met die properties doet.
Het is geen probleem als het scherm opnieuw ontworpen moet worden. De View Designer maakt in dat geval een versie 2 van de view en die nieuwe versie wordt vervolgens geactiveerd door aan te geven in de host dat een nieuwe versie gebruikt moet worden.
ViewModel
De ViewModel is de grote middleman in de applicatie. De ViewModel staat in verbinding met de View en de ViewModel doet ook de meeste validaties op datgene wat via de View binnenkomt. Als de gegevens in orde zijn dan zorgt de ViewModel ervoor dat de gegevens doorgespeeld worden naar een Model. De ViewModel houdt zich verder niet bezig met wat de Model met die gegevens doet.
Model
In dit voorbeeld hebben we twee Model-implementaties. Er is een implementatie dat een database als opslagmedium gebruikt en we hebben een “mock” implementatie dat het intern geheugen als opslagmedium gebruikt. In de host geven we wederom aan welke Model implementatie binnen de applicatie van toepassing is (gegevens van/naar een database of van/naar het intern geheugen).
Er wordt in het geval van de implementatie waarbij een database betrokken is, gebruik gemaakt van een Web API. De Web API / Controller staat ergens op een server en het kan ook een externe Web API zijn als de server zich ergens in “Verweggistan” bevindt.
De intern geheugen “Mock” implementatie in dit voorbeeld maakt geen gebruik van een Web API en dat is in dit geval ook niet nodig. De Model implementatie mag bepalen wat precies gebruikt gaat worden om het één en ander voor elkaar te krijgen en het maakt de andere onderdelen van de applicatie niet uit. Als ze maar hun gegevens krijgen of kwijt kunnen.
Model Definitie
De applicatie doet (meestal) dingen met gegevens en we maken een modeldefinitie voor datgene wat we willen vastleggen. We houden het in dit voorbeeld simpel en we registreren van een autobezitter diens naam, de regio en een omschrijving dat betrekking heeft op de auto.
De modeldefinitie leggen we vast in model definitie EIGENAAR in de .shared-project omdat de model definitie door meerdere onderdelen binnen de applicatie wordt gebruikt.
Waarom MVVM?
Ontkoppeling van de user interface van de rest van de applicatie
We hadden er al iets over gezegd bij de bespreking van de View. De View kan naar hartelust aangepast worden zonder dat dat invloed heeft op de rest van de applicatie. Voor de View Designer die de view maakt zijn alleen de properties en de methoden van de ViewModel van belang. En wat de ViewModel nog meer doet, dat is verder niet relevant voor de persoon die zich bezighoudt met het ontwikkelen van de View.
Herbruikbaarheid van onderdelen van de applicatie
De applicatie bestaat met MVVM uit allerlei losse onderdelen die hergebruikt kunnen worden indien de applicatie “op de schop moet”. Stel dat de applicatie overgezet moet worden naar een compleet andere ontwikkelplatform dat ook nog eens draait op een ander operating system. Een (gunstig) scenario voor wat betreft het doen overzetten van de applicatie zou er als volgt kunnen uitzien:
- De Views kunnen hergebruikt worden want dat is “gewoon” HTML, CSS en Bootstrap waar elke Browser mee overweg moet kunnen.
- De Web API kan ook hergebruikt worden want dat staat ergens op een server en het interesseert de Web API verder niet door wie of wat de Web API wordt aangeroepen zolang de Web API maar gegevens ontvangt en weer kwijt kan.
- Ook de model definities blijven verder buiten schot want we blijven waarschijnlijk hetzelfde registreren.
- Maakt het nieuwe framework ook gebruik C#? Met een beetje geluk hoeven we ook niks aan te passen aan de Models en hun implementaties.
M.a.w. alleen de ViewModel blijft over als datgene wat aangepast moet worden waarbij de rest hergebruikt kan worden.
Unit testing
Het zijn allemaal afzonderlijke onderdelen waarbij de onderdelen afzonderlijk getest kunnen worden door verschillende Unit Testing tools.
Host
We hoeven dankzij de host/dependency injection-constructie maar op één plek bij de host aan te geven welke implementatie voor die interface uiteindelijk gebruikt gaat worden in de applicatie. Dit is beschikbaar binnen de voorbeeldapplicatie, we hebben implementaties voor de ViewModels en de Model:
Interface | Implementatie |
IVoegToeViewModel | VoegToeViewModel |
ITotaalAantalViewModel | TotaalAantalViewModel |
IModel | MemoryModel |
IModel | DbModel |
In dit voorbeeld gebruiken we voor de IModel interface de MemoryModel-implementatie. De onderdelen binnen de applicatie verwijzen allemaal naar de interfaces en we hoeven alleen bij de host op te geven welke implementaties van toepassing zijn binnen de applicatie:
View
De View is primair gericht op het kunnen presenteren van gegevens naar de gebruiker en de nadruk binnen een view ligt dan ook op HTML, CSS en Bootstrap (of een ander CSS framework).
We zien naast de gebruikelijke HTML ook de zogenaamde “Razor markup syntax” welke te herkennen is aan de @-symbool. Via de @inject-directive en de overige Razor @markup-directives wordt vanuit de View een link gelegd naar de View Model en de properties en methoden van de View Model.
We zien dat we bij de @inject-directive de naam is opgeven van een View Model interface. Een interface kan meerdere implementaties hebben en bij de host is ingesteld welke implementatie precies gebruikt gaat worden. In een View hoeven we ons niet druk te maken over de te gebruiken implementatie.
De views bevatten nu minder C# code (alles voorbij de @code-directive), maar we hebben toch nog wat code behind nodig want we willen dat het aantal eigenaren wordt getoond bij het opstarten van de view. We moeten daarvoor de viewmodel triggeren en dat doen we met wat @code behind in de view.
De data in de View gaat uiteindelijk naar de ViewModel en we moeten voor de validatie wat tags opnemen (zie daarvoor de EditForm post). We zien in de code behind van de View ook geen validatie code. De validatie wordt verder door de ViewModel gedaan.
De View Designer hoeft uiteindelijk alleen maar te weten welke properties de ViewModel heeft en welke methodes van de ViewModel aangeroepen mogen worden.
We zien dat in view-bestand VoegToe.razor de methoden .VoegToe() en .GetTotaalAantalEigenaren() van de IVoegToeViewModel-interface worden aangeroepen.
In view-bestand TotaalAantal.razor zien we dat methode .GetLijstEigenaren() van de ITotaalAantalViewModel-interface wordt aangeroepen.
ViewModel
In onze voorbeeldapplicatie hebben we twee ViewModels en de ViewModels beginnen met een Interface. We hebben de IVoegToeViewModel en de ITotaalAantalViewModel.
Interface
Interface IVoegToeViewModel bestaat uit vijf properties (Omschrijving, Regio, Voornaam, Achternaam en TotaalAantalEigenaren) en twee methodes. De View Designer moet weten wat zij van de ViewModel kan gebruiken voor de View:
Interface ITotaalAantalViewModel bestaat uit property TotAantalEigenaren en methode .GetLijstEigenaren() dat een List retourneert met EIGENAAR-objecten. Met deze twee zaken kan de View Designer de gegevens tonen van de autobezitters in een View.
Klasse / Implementatie
De interfaces moeten geïmplementeerd worden en we hebben klasse VoegToeViewModel welke een implementatie is van interface IVoegToeViewModel. We zagen dat bij de host is opgegeven dat voor de applicatie de VoegToeViewModel-implementatie gebruikt moet worden.
De velden van een View zijn gekoppeld zijn aan de properties van een ViewModel. Bij de properties van de ViewModel geven we diverse validatie attributen op waarmee de ViewModel validaties kan uitvoeren op datgene wat via de View binnenkomt.
De ViewModel zorgt na de validatie van de gegevens van de View ervoor dat de gegevens van de View doorgespeeld worden naar een Model. De ViewModel houdt zich verder niet bezig met wat de Model verder doet.
Een soortgelijk verhaal gaat op als een ViewModel gegevens wil hebben. De ViewModel wil alleen weten dat er een Model is waar gegevens uit komen zodat die doorgespeeld kunnen worden naar de View. Het interesseert de ViewModel verder niet hoe de Model zijn zaakjes voor elkaar krijgt.
We zien in beide ViewModels dat we bij de constructor een IModel injecteren die we toekennen aan een backing variable _serviceModel:
readonly private IModel _serviceModel;
public TotaalAantalViewModel(IModel serviceModel)
{
// Constructor - injecteer de model
_serviceModel = serviceModel;
}
public VoegToeViewModel(IModel serviceModel)
{
// Constructor - injecteer de model
_serviceModel = serviceModel;
}
Wat voor de View geldt m.b.t. de ViewModel, dat geldt ook voor de ViewModel m.b.t de Model. De ViewModel hoeft alleen maar te weten welke properties de Model heeft en welke methodes van de Model aangeroepen mogen worden. De Model heeft in dit voorbeeld de volgende drie methoden die vanuit de ViewModel aangeroepen worden:
.GetLijstEigenaren()
public int TotAantalEigenaren { get; set; }
public async Task<IEnumerable<EIGENAAR>> GetLijstEigenaren()
{
//--------------------
// directe aanroep
// return await _serviceModel.GetLijstEigenaren();
//--------------------
TotAantalEigenaren =
await _serviceModel.GetTotaalAantalEigenaren();
//--------------------
// met een task
return await Task.Run (() =>
{
return _serviceModel.GetLijstEigenaren();
});
}
.GetTotaalAantalEigenaren()
public async Task GetTotaalAantalEigenaren()
{
TotaalAantalEigenaren =
await _serviceModel.GetTotaalAantalEigenaren();
}
.VoegToe()
De ViewModel gebruikt verder een implicit operator om de ViewModel “gemapt” te krijgen naar de Model.
public static implicit operator
EIGENAAR(VoegToeViewModel serviceViewModel)
{
// Map de ViewModel naar de Model //
return new EIGENAAR
{
Omschrijving = serviceViewModel.Omschrijving,
Regio = serviceViewModel.Regio,
Voornaam = serviceViewModel.Voornaam,
Achternaam = serviceViewModel.Achternaam,
};
}
public async Task VoegToe()
{
// Door de implicit operator wordt de
// ViewModel gemapped naar de Model (EIGENAAR)
// Toevoegen
await _serviceModel.VoegToe(eigenaar);
// Schonen velden
Omschrijving = "";
Regio = "";
Voornaam = "";
Achternaam = "";
// Feedback
TotaalAantalEigenaren =
await _serviceModel.GetTotaalAantalEigenaren();
}
Model
De Model staat voor de data die opgeslagen is in een data store (een database of gewoon in het intern geheugen). Daarbij ligt de nadruk op het opslaan en het opvragen van de gegevens. Het is niet de bedoeling dat de Model bewerkingen gaat uitvoeren op de data om de gebruiker ter wille te zijn. Dat soort dingen mag de ViewModel of de View doen.
Interface
De ViewModel moet weten wat zij van de Model kan gebruiken en dat is gedefinieerd in de Interface. Interface IModel bestaat uit de methodes .GetLijstEigenaren(), .GetTotaalAantalEigenaren() en .VoegToe().
Er is een implementatie dat een database als opslagmedium gebruikt en we hebben een “mock” implementatie dat het intern geheugen als opslagmedium gebruikt. In de host geven we wederom aan welke Model implementatie binnen de applicatie van toepassing is (gegevens van/naar een database of van/naar het intern geheugen).
MemoryModel
De MemoryModel “mock” implementatie haalt gegevens op uit een List<> in het geheugen en voegt gegevens toe aan die List<> in het geheugen. Aan de methodes is de volgende invulling gegeven:
DbModel
De DbModel implementatie haalt gegevens op uit een tabel in een SQL Server database en voegt gegevens toe aan diezelfde tabel in de SQL Server database. Aan de methodes wordt de volgende invulling gegeven:
Er wordt in het geval van de implementatie waarbij een database betrokken is, gebruik gemaakt van Web API / Controller ServiceDb. De Web API / Controller staat ergens op een server en het mag dus ook een externe Web API wezen zijn als de server zich ergens in “Verweggistan” bevindt.
Web API
En ten slotte Web API ServiceDB. De Web API wordt door de DbModel implementatie gebruikt en deze Web API heeft o.a. een methode .GetLijstEigenaren dat een lijst in Json formaat retourneert:
Zie verder het gedeelte van deze post waarin wordt beschreven hoe verbinding gemaakt kan worden met een SQL Server database.
Slot
In deze post heb ik laten zien hoe je een Blazor applicatie kunt herstructureren naar een MVVM design pattern. MVVM oogt omslachtig, maar het zal de onderhoudbaarheid van je applicatie zeker ten goede komen. Vooral als je applicatie gaat groeien en steeds meer functionaliteiten moet bevatten.
Je zal snel de voordelen zien als je een structuur als onderstaand consistent aanhoudt. Views kunnen aangepast worden zonder dat dat meteen hoeft te leiden tot “breaking changes”. De onderdelen kunnen afzonderlijk getest worden indien je gebruik maakt van Unit Testing tools en verschillende onderdelen van de applicatie kunnen hergebruikt worden in andere applicaties of zelfs in een ander framework.
Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over Blazor heb geschreven? Hit the Blazor button…