Implementatie van het MVVM patroon in een WPF applicatie

Dit artikel en bijbehorende Git repository tonen aan hoe een implementatie van het MVVM patroon een basis kan vormen voor een WPF applicatie. Het ontwerp, de structuur en de realisatie worden toegelicht.
Men is hierbij niet (verplicht) afhankelijk van externe libraries.



Aangezien het patroon een basis vormt voor de verdere ontwikkeling van een applicatie is het belangrijk om, alvorens verder te gaan, aan te halen dat er uitgebreide MVVM WPF frameworks bestaan welke functioneel omvangrijker zijn dan deze implementeringswijze. Dit StackOverflow bericht geeft een overzicht van dergelijke frameworks.

Desalniettemin vormt deze onafhankelijke implementatie door zijn karakteristieken, zoals verder omschreven, een goede basis voor verdere ontwikkeling.


💾 Github repository



De projecten binnen de solution maken gebruik van .NET framework 5.0


📦 NuGet packages

Standaard heeft de solution volgende afhankelijkheden:

Geen van deze packages ismits verwijderen van referenties en wijzigen van properties zodat zij PropertyChanged events emittennoodzakelijk voor de correcte werking van de implementatie.

Er is voor PropertyChanged.Fody gekozen om de code met betrekking tot properties in de ViewModels overzichtelijker te maken en voor MaterialDesign om de Views te voorzien van een moderne thematisering.


📺 Interactie

Animatie van interactie met de de applicatie, functioneel mogelijk gemaakt door de MVVM implementatie. 

Illustreert top-level beheer met tabblad buttons, gebruik van beheerfuncties vanuit een View die zelf ook onder het beheer valt en enkele simpele Bindings op ViewModel properties.



💡 Initiatie

Tijdens het vak Programmeren Gevorderd & specialisatie is MVVM zeer kort aan bod gekomen, de interesse was hiermee gewekt en aangezien er voor de graduaatsproef een applicatie gemaakt diende te worden welke verschillende contexten zou gaan behandelen (overzichten, toevoegen, wijzigen, filteren) is er gekozen om het MVVM patroon toe te passen.

Na het in 2 applicaties toegepast te hebben heb ik besloten om een toegewijde repository aan te maken en dit Nederlandstalig artikel te schrijven (de Engelstalige bronvermeldingen, welke gebruik maken van oudere .NET frameworks, zijn terug te vinden op het einde van het artikel).


📝 Reflectie

De implementatie die besproken wordt is minimalistisch, de focus ligt op het mogelijk maken van de MVVM structuur. De beheerfunctionaliteiten die er zijn, zijn flexibel toepasbaar qua hierarchie en er is mogelijkheid tot uitbreiding.

Indien men een complexe applicatie wenst te ontwikkelen of functioneel niet het wiel terug wenst uit te vinden wanneer men functionele flexibiliteit wenst, kan men overwegen om een MVVM framework te gebruiken.


🌐 Beheer

Het beheer van Views en ViewModels wordt mogelijk gemaakt door een beherend paar, welke ook een View- en ViewModel is.

Er zijn enkele manieren om de beheerfunctionaliteiten te gebruiken:
  • Door de gebruiker
    • Door interactie met de View van het beherende paar
      • Bijvoorbeeld door een menu, tabbladen of buttons
    • Door interactie met een View welke beheerd wordt
      • Bijvoorbeeld door een submenu, tabbladen of buttons

  • Door het beherende paar, initiatie door het ViewModel
    • Indien aan bepaalde voorwaarden voldaan is

  • Door paren die beheerd worden, initiatie door View of ViewModel
    • Indien aan bepaalde voorwaarden voldaan is

In het volgende hoofdstuk wordt aangehaald hoe meerdere niveaus van beheer mogelijk zijn.


🖼️ Ontwerp

Onderstaande diagram geeft weer hoe de logica en state afgezonderd worden in het ViewModel en hoe het raakvlak met de View en het Model tot stand komt. 

De View maakt gebruik van Bindings op Observable properties, interpreteert deze en wijzigt vervolgens de weergave.


In het geval van het beherende paar, welke zelf een View- en ViewModel zijn, is er sprake van een ObservableCollection met ViewModels die beheerd worden en een property die het actieve ViewModel aangeeft.

Het actieve ViewModel wordt geobserveerd en de corresponderende View weergegeven binnen een gebied van de beherende View.

Aangezien het beherende paar ook een View- en ViewModel is kunnen er meerdere niveaus van beheer bestaan, en dus Views genest weergegeven worden binnen de applicatie.

Men dient dit echter af te wegen tegenover een veralgemeende aanpak met een centraliserende beheerder, welke iets uitgebreider is qua functionaliteit.

Indien men nodeloos (voor slechts enkele View(Model)s afgezonderd te beheren) meerdere niveaus aanmaakt wordt men afhankelijk van deze hierarchie en dienen alle verwijzingen naar de properties en commands bij wijziging aangepast te worden.

Ter anecdote, bij het ontwikkelen van de graduaatsproef was er sprake van een twintigtal Views en ViewModels waarvan ~5 ondergebracht konden worden onder een nieuw niveau van beheer, er werd echter beslist om het beheer gecentraliseerd te houden wat verder geen obstakel vormde.


🏗️ MVVM Core

Volgende bestanden vormen de basis elementen waar het beherend ViewModel gebruik van maakt.

IViewModel legt de minimum vereisten van een ViewModel vast en dient anderzijds als marker interface om omgang van ViewModels te vereenvoudigen in een beherend ViewModel.

Men kan nieuwe soorten ViewModels differentieren door een nieuwe interface toe te voegen. Een voorbeeld hiervan zou een IWijzigViewModel kunnen zijn met als minimum functionaliteit BereidModelVoor, die de properties van een initiele waarde voorziet aan de hand van de meegegeven data.

Presenteerder is een abstracte klasse die de INotifyPropertyChanged interface implementeert, naast IViewModel erven de ViewModels hier van over.
  • Indien men Fody gebruikt (default)
  • Indien men Fody niet wenst te gebruiken
    • Door overerving kan in de property setter gebruik gemaakt worden van de Update functie. Dit laat toe om met 1 lijn code, de aanroeping, de waarde van het veld aan te passen en het PropertyChanged event te emitten.

RelayCommand implementeert ICommand en wordt gebruikt in de ViewModels om functionaliteiten beschikbaar te stellen aan diens View.

Een Binding middels de Command en CommandParameter element props in de View's XAML verwacht namelijk een ICommand implementatie.

Bij initialisatie wordt een delegate verwacht en een predicaat waaraan voldaan dient te worden alvorens het commando mag uitgevoerd worden.

Een voorbeeld hiervan, bij het beherend paar, is WijzigActiefViewModel welke toelaat om van het actieve ViewModel en dus de weergegeven View te veranderen.


💢 Aanverwante ViewModels

Er kan van een ViewModel overgeërfd worden om (een deel van) de functionaliteiten op te nemen. 

Een voorbeeld hiervan is een ProductToevoegenViewModel welke toegewijd is aan het toevoegen van een product, het ProductWijzigenViewModel kan hiervan overerven aangezien het gros van de View dezelfde vorm heeft.
Mits er selectief private functies omgezet worden naar protected kan een deel van de functionaliteiten beschikbaar gesteld worden.

Daarnaast kan men, indien er meerdere overervers zijn, de gemeenschappelijke functionaliteiten onderbrengen in een abstracte klasse waar van overgeërfd wordt. Toegepast zou dit op vlak van beheer, indien er meerdere beheerders aangesteld worden, een abstracte klasse kunnen zijn die de algemene beheerfunctionaliteiten bevat. 


ℹ️ MVVM met beheer, hoe is het toegepast?

Nu er een beeld is gevormd van de structuur en hoe het beheer werkt, zal dit hoofdstuk aanhalen hoe het patroon en het beheer daadwerkelijk tot stand komen binnen het project.

De solution is tevens voorzien van in-line commentaar.

Start van de applicatie

  • App codebehind
  • App staat ingesteld als het startup object van het project, het instantieert het beherend paar ApplicatieOverzicht en zorgt voor diens weergave
Beherende View - ApplicatieOverzicht
  • Deze Window heeft als DataContext diens ViewModel, ApplicatieOverzichtViewModel
  • De verbanden tussen de View(Model)s die beheert worden, worden opgenomen in de Window.Resources
  • Een ContentControl met Binding op het ActieveViewModel laat toe om van weergegeven View te veranderen
  • De tabblad buttons laten toe om middels het WijzigHuidigViewModel Command van ActieveViewModel te veranderen
Bijbehorend beherend ViewModel
In de constructor worden de beheerde ViewModels geinstantieerd.
  • Properties
  • ActieveViewModel
  • Bepaalt welke View weergegeven wordt
  • BeheerdeViewModels
  • Bevat de namen van de Views als key en een instantie van diens ViewModel als value
  • Commands
  • WijzigHuidigViewModel
  • zie functie WijzigActiefViewModel
  • ResetViewModelCommand
  • zie functie ResetViewModel
  • Functies
  • WijzigActiefViewModel
  • Wijzigt het ActieveViewModel, probeert indien nodig een instantie van het ViewModel aan te maken
  • ResetViewModel
  • Zal de instantie in BeheerdeViewModels vervangen door een nieuwe instantie
  • MaakNieuwViewModel
  • Maakt een nieuwe instantie aan de hand van het type van de meegegeven instantie
  • Indien het ViewModel beschikt over een BeginStartupRoutine Command wordt deze uitgevoerd
  • Aangezien diens View mogelijk reeds weergegeven wordt en het Loaded event in dat geval niet meer zal plaatsvinden
View(Model)s die beheerd worden
  • ViewModel constructors en BeginStartupRoutine
  • Aangezien de beheerder alle ViewModels tegelijk aanmaakt dient men bedachtzaam met ViewModel constructors om te gaan
  • Men gebruikt de state pas indien de View gerendered wordt
  • Men roept mogelijk resource-intensive functies op, zoals het vergaren van veel data voor een overzicht 
  • BeginStartupRoutine & StartupRoutine
  • Laat toe om bij het renderen van de View het commando en dus diens functie aan te roepen welke acties verricht
  • Bij first-render bijvoorbeeld om data te vergaren en een initial state te hebben
  • Bij overige renders, indien toegelaten, om de state te "resetten"
  • Introductie
  • Bevat uitleg over de applicatie
  • Toont aan hoe het ViewModel ingesteld wordt als DataContext, hetzelfde geld voor de overige Views
  • Producten
  • Bevat uitleg over de View
  • Toont aan hoe het Loaded event behandeld wordt door UserControl_Loaded in de codebehind
  •  Deze handler voert BeginStartupRoutine van het ViewModel uit
  • Toont aan hoe men met EersteRenderPlaatsgevonden bij een volgende rerender nog over dezelfde state beschikt (testen kan door datagrid aan te passen en van tabblad te veranderen)
  • Bevat een DataGrid met Binding op een ObservableCollection, Producten
  • Wijzigingen hebben effect op de collectie in het ViewModel



      • Account
        • Bevat uitleg over de View
        • Toont aan hoe het Loaded event behandeld wordt door UserControl_Loaded in de codebehind
          • Deze handler voert BeginStartupRoutine van het ViewModel uit
            • Door geen conditie op te nemen in StartupRoutine zal bij iedere rerender de routine opnieuw uitgevoerd worden
        • Toont aan hoe wijzigingen die aangebracht worden direct invloed hebben op het ViewModel (zie groene tekst)


      • Instellingen
        • Bevat uitleg over de View
        • Geen noemenswaardig ViewModel, ter illustratie wel ingesteld als DataContext
        • Toont aan hoe vanuit de beheerde View de beheersfunctionaliteiten gebruikt kunnen worden
          (button met Binding op ViewModel commando van een Ancestor)
          • Geeft PrivacyInstellingView weer welke hetzelfde toepast om terugkeren naar InstellingenView mogelijk te maken




      Bronnen


      Comments

      Popular posts from this blog