17. Apr 2023
Lesedauer 13 Min.
MVVM im Handumdrehen
Eine WPF-Anwendung mit MvvmGen erzeugen
Die Bibliothek MvvmGen erzeugt Code für ViewModels und reduziert so den Aufwand.

Wer mit einem XAML-basierten Framework wie WPF, WinUI oder .NET MAUI eine Applikation entwickelt, der stößt früher oder später auf das Entwurfsmuster Model-View-ViewModel, kurz MVVM genannt. Mit diesem Entwurfsmuster – englisch Pattern genannt – werden grafische Programmoberflächen in XAML erstellt und via Datenbindung an eine ViewModel-Instanz gebunden.Ein ViewModel ist eine Klasse, die der Oberfläche die benötigten Daten in Form von Eigenschaften zur Verfügung stellt. Die ViewModel-Klasse selbst ist unabhängig von der Oberfläche der Anwendung und genau daraus ergeben sich die Vorteile des MVVM-Musters: Durch die klare Trennung zwischen Oberfläche (XAML) und UI-Logik (ViewModel) sind insbesondere komplexe Programmoberflächen einfacher umzusetzen und auch einfacher zu pflegen.Ein weiterer, nicht minder wichtiger Vorteil: Da das ViewModel unabhängig vom User Interface ist, lässt es sich ohne Weiteres in Unit-Tests verwenden und testen.Das klingt alles sehr gut – zu gut, als dass da nicht noch ein Haken wäre. Doch wo ist dieser Haken?
Eigenschaften in ViewModels
Beim Entwickeln von ViewModels kommt nicht nur Freude auf. Beispielsweise muss jede Eigenschaft programmiert werden, da im Setter das PropertyChanged-Ereignis auszulösen ist. Der Setter der Eigenschaft FirstName im folgenden Beispiel ruft dazu eine Hilfsmethode mit dem Namen OnPropertyChanged() auf:<span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span>? _firstName;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? FirstName
{
<span class="hljs-keyword">get</span> => _firstName;
<span class="hljs-keyword">set</span>
{
<span class="hljs-keyword">if</span> (_firstName != <span class="hljs-keyword">value</span>)
{
_firstName = <span class="hljs-keyword">value</span>;
OnPropertyChanged(<span class="hljs-string">"FirstName"</span>);
}
}
}
Das Auslösen des PropertyChanged-Events ist notwendig, um die Datenbindung im User Interface über eine Änderung der Eigenschaft zu benachrichtigen. Dadurch kann der Mechanismus der Datenbindung die Oberfläche entsprechend aktualisieren.Bei einer genaueren Betrachtung des Codes fällt auf, dass das Feld _firstName alle Informationen enthält, die notwendig sind, um die vollständig ausprogrammierte Eigenschaft FirstName automatisch zu generieren: Das sind der Typ string und der Name FirstName. Ließe sich also die Eigenschaft aus dem Feld erzeugen, so würde das den Schreibaufwand beim Programmieren eines ViewModel massiv reduzieren. Doch welche Möglichkeiten zum Generieren von Code gibt es für .NET?
Code erzeugen mit .NET
In der Vergangenheit kamen verschiedene Ansätze auf, um mit .NET weiteren Quellcode zu erstellen. Prinzipiell gibt es drei verschiedene Möglichkeiten:- T4-Vorlagen (.tt-Dateien)
- Produkte von Drittherstellern, zum Beispiel CodeSmith
- eigene Entwicklung, etwa mit C# oder PowerShell
- Projekt kompilieren, um die Assembly zu erhalten.
- Code generieren; die Assembly aus dem vorherigen Schritt dient dabei als Input.
- Den generierten Code zum Projekt hinzufügen.
- Das Projekt erneut kompilieren, um den erzeugten Code zu verwenden.
Generatoren für C#-Quellcode
.NET 5 führte die sogenannten C#-Codegeneratoren [1] ein. Sie bieten eine neue Möglichkeit, um C#-Code zu erstellen. Das Besondere an diesen Generatoren ist, dass sie direkt im C#-Compiler (auch Roslyn genannt) eingebunden sind. Dadurch unterscheiden sich C#-Codegeneratoren von bisherigen Techniken zum Erzeugen von Code in zwei wesentlichen Punkten:- Sie verwenden keine Assembly als Input, sondern den C#-Quellcode des jeweiligen Projekts, in dem die Generatoren verwendet werden.
- Sie werden während des Kompilierens ausgeführt. Somit ist der erstellte Code direkt in der kompilierten Assembly enthalten.

C#-Codegeneratorenals Teil des .NET Compilers(Bild 1)
Autor
Wichtig ist hier die Betonung auf „zusätzlich“. Bestehender Code lässt sich durch einen C#-Codegenerator nicht verändern, es ist nur möglich, zusätzlichen Code zu ergänzen. Der generierte Code wird als Teil des Kompiliervorgangs hinzugefügt und das Kompilieren fortgesetzt, bis am Ende die Assembly erstellt ist, die nun sowohl den Code des Projekts als auch den zusätzlich erzeugten Code enthält. Somit sind nicht mehrere Kompiliervorgänge notwendig, um den zusätzlichen Code einzubinden.Es ist sogar so, dass dieser Kompiliervorgang während des Tippens in Visual Studio läuft. Das heißt, dass ein C#-Codegenerator im Hintergrund Code erzeugen kann und der generierte Code dann sofort zur Verfügung steht und sich in den eigenen Codedateien verwenden lässt. C#-Codegeneratoren eröffnen somit ganz neue Möglichkeiten, um Code zu erzeugen und sind eine ideale Variante, um ViewModel-Klassen zu erstellen. Genau hier setzt die MvvmGen-Bibliothek an.
Die MvvmGen-Bibliothek
Die Bibliothek MvvmGen ist ein Open-Source-Projekt, um XAML-Apps mit dem MVVM-Muster zu entwickeln [2]. Dabei macht MvvmGen intensiven Gebrauch von C#-Codegeneratoren. Die Bibliothek enthält alle zentralen Klassen, die zum Entwickeln einer XAML-Anwendung mit dem MVVM-Pattern benötigt werden:- Eine ViewModelBase-Klasse, die das INotifyPropertyChanged-Interface implementiert. Diese Schnittstelle enthält das für die Datenbindung wichtige Ereignis PropertyChanged, das im Setter einer Eigenschaft auszulösen ist. Die Klasse enthält zudem eine Hilfsmethode OnPropertyChanged(), die sich zum Auslösen des Events aus Subklassen verwenden lässt.
- Eine Klasse DelegateCommand, die das Interface ICommand implementiert. Dieses wird zum Ausführen von Logik im ViewModel verwendet und DelegateCommand ist eine notwendige Implementierung dieser Schnittstelle.
- Eine EventAggregator-Klasse, die sich zur Kommunikation zwischen verschiedenen ViewModels einsetzen lässt.
Ein ViewModel erstellen
Alles, was zum Einsatz von MvvmGen nötig ist, ist eine Referenz auf das NuGet-Paket MvvmGen. Darin enthalten sind- die MvvmGen-Framework-Klassen wie ViewModelBase
- der C#-Codegenerator namens ViewModelGenerator
- Attribute, auf die der C#-Codegenerator achtet, um Code zu generieren

Die Bestandteileder Bibliothek MvvmGen(Bild 2)
Autor
Sobald das MvvmGen-NuGet-Paket referenziert ist, lässt sich die Bibliothek verwenden. In Listing 1 ist das der Fall, wobei hier der C#-Codegenerator noch nicht zum Einsatz kommt. Zu sehen ist eine EmployeeViewModel-Klasse, welche die Eigenschaft FirstName enthält. In ihrem Setter wird die Methode OnPropertyChanged() der aus MvvmGen stammenden ViewModelBase-Basisklasse aufgerufen. Dadurch wird das für die Datenbindung wichtige PropertyChanged-Ereignis ausgelöst. Zum Verwenden der Klasse ViewModelBase ist in der Datei eine using-Direktive für den Namensraum MvvmGen.ViewModels enthalten.
Listing 1: Ein ViewModel mit MvvmGen
<span class="hljs-keyword">using</span> MvvmGen.ViewModels; <br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">DotnetPro.EmployeeBrowser.ViewModel</span>; <br/><br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">EmployeeViewModel</span> : <span class="hljs-title">ViewModelBase</span> <br/>{ <br/><br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span>? _firstName; <br/><br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? FirstName <br/> { <br/> <span class="hljs-keyword">get</span> =&gt; _firstName; <br/> <span class="hljs-keyword">set</span> <br/> { <br/> <span class="hljs-keyword">if</span> (_firstName != <span class="hljs-keyword">value</span>) <br/> { <br/> _firstName = <span class="hljs-keyword">value</span>; <br/> OnPropertyChanged(<span class="hljs-string">"FirstName"</span>); <br/> } <br/> } <br/> } <br/><br/>}
In dem Listing wurde das komplette ViewModel noch von Hand geschrieben. Zwar wurde die MvvmGen-Klasse ViewModelBase hier schon verwendet, aber ansonsten ist alles selbst programmiert. Doch genau darin liegt die Stärke von MvvmGen: Große Teile des Codes aus Listing 1 lassen sich generieren.
Generieren statt tippen
Die Bibliothek bietet im Namensraum MvvmGen Attribute, die als Markierungen für den C#-Codegenerator dienen. Sieht der Generator ein solches Attribut, wird er zusätzlichen Code erzeugen.Um auf diese Weise ein ViewModel zu erstellen, wird eine Klasse mit dem [ViewModel]-Attribut dekoriert und mit dem Schlüsselwort partial versehen:<span class="hljs-keyword">using</span> MvvmGen;
<span class="hljs-keyword">namespace</span> <span class="hljs-title">DotnetPro.EmployeeBrowser.ViewModel</span>;
[ViewModel]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">EmployeeViewModel</span>
{
[Property] <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span>? _firstName;
}
Letzteres ist notwendig, damit die Klassendefinition über mehrere Dateien verteilt werden kann. Der Codegenerator wird aufgrund des [ViewModel]-Attributs eine weitere Datei mit der Klasse EmployeeViewModel erstellen, die den erzeugten Code enthält.In dem Beispiel ist auch das Attribut [Property] zu sehen, mit dem das Feld _firstName auszeichnet ist. Dadurch wird die Eigenschaft FirstName für dieses Feld generiert. Das bedeutet, dass der wenige Code aus dem Beispiel dem im vorherigen Abschnitt dargestellten ViewModel aus Listing 1 entspricht.Doch was wird hier eigentlich genau erzeugt? Der Code des ViewModels, der zu diesem Beispiel erzeugt wird, ist in Listing 2 zu sehen. Wichtig ist zu verstehen, dass die Codegenerierung bereits während der Eingabe des eigenen Codes erfolgt. Das bedeutet, der Code aus Listing 2 ist unmittelbar verfügbar, sobald man den Code der partiellen Klasse geschrieben hat; Visual Studio – oder genauer der C#-Codegenerator von MvvmGen – erzeugt ihn bei jeder Änderung im Hintergrund. Das Projekt muss dazu nicht neu kompiliert werden, der Code ist stattdessen sofort verfügbar und verwendbar.
Listing 2: Von MvvmGen erstellter Code
// &lt;<span class="hljs-built_in">auto</span>-generated&gt; <br/>// This code was generated <span class="hljs-keyword">for</span> you <span class="hljs-built_in">by</span> <br/>// MvvmGen, a tool created <span class="hljs-built_in">by</span> Thomas Claudius Huber <br/>// Generator version: <span class="hljs-number">1.1</span><span class="hljs-number">.5</span> <br/>// &lt;/<span class="hljs-built_in">auto</span>-generated&gt; <br/><span class="hljs-built_in">using</span> MvvmGen.Commands; <br/><span class="hljs-built_in">using</span> MvvmGen.Events; <br/><span class="hljs-built_in">using</span> MvvmGen.ViewModels; <br/><br/>namespace DotnetPro.EmployeeBrowser.ViewModel <br/>{ <br/> partial class EmployeeViewModel :<br/> global::MvvmGen.ViautomewModels.ViewModelBase <br/> { <br/> public EmployeeViewModel() <br/> { <br/> this.OnInitialize(); <br/> } <br/><br/> partial void OnInitialize(); <br/><br/> public string? FirstName <br/> { <br/> get =&gt; _firstName; <br/> <span class="hljs-built_in">set</span> <br/> { <br/> <span class="hljs-keyword">if</span> (_firstName != value) <br/> { <br/> _firstName = value; <br/> OnPropertyChanged(<span class="hljs-string">"FirstName"</span>); <br/> } <br/> } <br/> } <br/> } <br/>}
Wie in dem Listing zu sehen ist, erstellt der MvvmGen-Generator eine weitere partielle Klasse EmployeeViewModel. Diese erbt von der MvvmGen-Basisklasse ViewModelBase. Diese Vererbung könnte im obigen Beispiel auch explizit angegeben werden. Das ist jedoch nicht nötig. Ist ViewModelBase in einem ViewModel wie oben nicht als Basisklasse angegeben, fügt der Codegenerator sie automatisch zum erzeugten Code hinzu.In Listing 2 ist außerdem die generierte Eigenschaft FirstName zu sehen, die im Setter die OnPropertyChanged()-Methode der Basisklasse aufruft. Dadurch wird das für die Datenbindung wichtige Ereignis PropertyChanged ausgelöst.
Den generierten Code einsehen
Bereits beim Entwickeln ist es möglich, einen Blick auf den erzeugten Code zu werfen. Dazu sind im Projektmappen-Explorer im jeweiligen Projekt die Abhängigkeiten zu erweitern, siehe Bild 3. Unter dem Eintrag Analyzers befindet sich das Element MvvmGen.SourceGenerators und darin MvvmGen.ViewModelGenerator. Das ist der C#-Codegenerator von MvvmGen. Unter diesem Eintrag befinden sich die von MvvmGen generierten Dateien. Wie das Bild zeigt, bestehen diese aus dem voll qualifizierten Klassennamen und der Dateiendung .g.cs, wobei das „g“ hier für „generiert“ steht.
Der von MvvmGenerzeugte Code lässt sich schon während des Entwickelns in Visual Studio begutachten(Bild 3)
Autor
Die erzeugten Dateien lassen sich mit einem Doppelklick öffnen und betrachten. Sie werden dabei automatisch aktualisiert, wenn die entsprechenden ViewModel-Klassen verändert werden.
Weitere Eigenschaften erstellen
Mit den Attributen von MvvmGen lassen sich im ViewModel nun ganz einfach weitere Properties erstellen. Im folgenden Code werden die beiden Eigenschaften FirstName und LastName erzeugt. Darüber hinaus hat das ViewModel eine Read-only-Eigenschaft namens FullName:[ViewModel]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">EmployeeViewModel</span>
{f
[Property] <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span>? _firstName;
[Property] <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span>? _lastName;
[PropertyInvalidate(<span class="hljs-keyword">nameof</span>(FirstName),
<span class="hljs-keyword">nameof</span>(LastName))]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? FullName => <span class="hljs-string">$"<span class="hljs-subst">{FirstName}</span> <span class="hljs-subst">{LastName}</span>"</span>;
}
Die FullName-Property in dem Beispiel verwendet einen interpolierten String, um die Werte der erzeugten Eigenschaften FirstName und LastName zurückzugeben. Das heißt, dass sich die Eigenschaft FullName ändert, wann immer sich die Werte von FirstName oder LastName ändern. Aus MVVM-Sicht bedeutet dies, dass in den Settern der Eigenschaften FirstName und LastName auch das PropertyChanged-Event für die Eigenschaft FullName ausgelöst werden muss, damit die Datenbindung im User Interface dementsprechend über eine Änderung von FullName benachrichtigt wird. Damit dieses Ereignis an den entsprechenden Stellen ausgelöst wird, ist im Code des Beispiels die Eigenschaft FullName mit dem Attribut [PropertyInvalidate] versehen. Es erhält als Konstruktor-Argumente die Namen der Eigenschaften FirstName und LastName.Damit weiß der Codegenerator Bescheid und erstellt die Eigenschaften FirstName und LastName wie in Listing 3. Darin ist zu sehen, dass sowohl im Setter von FirstName als auch im Setter von LastName das PropertyChanged-Ereignis für FullName ausgelöst wird.
Listing 3: Die Eigenschaften FirstName und LastName
// &lt;<span class="hljs-built_in">auto</span>-generated&gt; <br/>// This code was generated <span class="hljs-keyword">for</span> you <span class="hljs-built_in">by</span> <br/>// MvvmGen, a tool created <span class="hljs-built_in">by</span> Thomas Claudius Huber <br/>// Generator version: <span class="hljs-number">1.1</span><span class="hljs-number">.5</span> <br/>// &lt;/<span class="hljs-built_in">auto</span>-generated&gt; <br/><span class="hljs-built_in">using</span> MvvmGen.Commands; <br/><span class="hljs-built_in">using</span> MvvmGen.Events; <br/><span class="hljs-built_in">using</span> MvvmGen.ViewModels; <br/><br/>namespace DotnetPro.EmployeeBrowser.ViewModel <br/>{ <br/> partial class EmployeeViewModel :<br/> global::MvvmGen.ViewModels.ViewModelBase <br/> { <br/> public EmployeeViewModel() <br/> { <br/> this.OnInitialize(); <br/> } <br/><br/> partial void OnInitialize(); <br/><br/> public string? FirstName <br/> { <br/> get =&gt; _firstName; <br/> <span class="hljs-built_in">set</span> <br/> { <br/> <span class="hljs-keyword">if</span> (_firstName != value) <br/> { <br/> _firstName = value; <br/> OnPropertyChanged(<span class="hljs-string">"FirstName"</span>); <br/> OnPropertyChanged(<span class="hljs-string">"FullName"</span>); <br/> } <br/> } <br/> } <br/><br/> public string? LastName <br/> { <br/> get =&gt; _lastName; <br/> <span class="hljs-built_in">set</span> <br/> { <br/> <span class="hljs-keyword">if</span> (_lastName != value) <br/> { <br/> _lastName = value; <br/> OnPropertyChanged(<span class="hljs-string">"LastName"</span>); <br/> OnPropertyChanged(<span class="hljs-string">"FullName"</span>); <br/> } <br/> } <br/> } <br/> } <br/>}
Mit Commands arbeiten
Um Logik im ViewModel auszuführen, wird mit dem MVVM-Muster das aus dem Namensraum System.ComponentModel stammende Interface ICommand verwendet. Üblicherweise enthält das ViewModel Eigenschaften vom Typ ICommand; in der Nutzeroberfläche werden dann beispielsweise Button-Elemente an diese Eigenschaften gebunden, um die Logik des Commands auszuführen. Die MvvmGen-Bibliothek enthält mit der Klasse DelegateCommand eine Implementierung der ICommand-Schnittstelle. Das Besondere ist jedoch wieder der Codegenerator. Anstatt die DelegateCommand-Klasse direkt im eigenen Code zu verwenden und manuell eine Eigenschaft dieses Typs zu erstellen und zu initialisieren, übernimmt der Generator diesen schreibaufwendigen Teil.Um ein Command zu erstellen, wird im ViewModel eine Methode mit dem [Command]-Attribut ausgestattet:[Command]
private void Save()
{
Console.WriteLine($"Saved: {FirstName} {LastName}");
}
Durch dieses Attribut wird der C#-Codegenerator in der erzeugten Datei eine Eigenschaft namens SaveCommand erstellen und mit einer DelegateCommand-Instanz initialisieren. Listing 4 zeigt den entstandenen Code. Darin ist die Eigenschaft SaveCommand zu sehen, die in der Methode InitializeCommands() initialisiert wird. Der Konstruktor der DelegateCommand-Klasse enthält einen Lambda-Ausdruck als Argument, der auf die Save()-Methode zeigt, die wiederum – siehe oben – mit dem [Command]-Attribut versehen wurde. Dadurch wird diese Save()-Methode beim Ausführen des Commands aufgerufen.
Listing 4: Die erzeugte Eigenschaft SaveCommand
partial class EmployeeViewModel :<br/> global::MvvmGen.ViewModels.ViewModelBase <br/>{ <br/> public EmployeeViewModel() <br/> { <br/> this.InitializeCommands(); <br/> this.OnInitialize(); <br/> } <br/><br/> partial void OnInitialize(); <br/><br/> private void InitializeCommands() <br/> { <br/> SaveCommand = new DelegateCommand(<br/> _ =&gt; Save()); <br/> } <br/><br/> public DelegateCommand SaveCommand<br/> { get; private set; } <br/><br/> ...<br/>}
Die CanExecute-Logik eines Commands nutzen
Üblicherweise lässt sich ein Command nur unter bestimmten Umständen ausführen. So könnte eine Anforderung sein, dass das im vorherigen Abschnitt erstellte SaveCommand nur dann auszuführen ist, wenn die Eigenschaft FirstName des ViewModels nicht leer ist. Um das zu erreichen, wird eine CanSave()-Methode erstellt, die einen Bool-Wert zurückgibt:[Command(CanExecuteMethod = nameof(CanSave))]
private void Save()
{
Debug.WriteLine($"Saved: {FirstName} {LastName}");
}
[CommandInvalidate(nameof(FirstName))]
private bool CanSave() =>
!string.IsNullOrWhiteSpace(FirstName);
Dieser boolesche Wert ist true, wenn FirstName nicht leer ist. Um das zu prüfen, wird IsNullOrWhiteSpace() der String-Klasse verwendet. Damit das Command diese CanSave()- Methode nutzt, wird ihr der Name der Eigenschaft CanExecuteMethod des [Command]-Attributs zugewiesen.Darüber hinaus ist zu sehen, dass CanSave() mit dem Attribut [CommandInvalidate] ausgestattet ist. Es sorgt dafür, dass bei einer Änderung von FirstName das CanExecuteChanged-Event des SaveCommand ausgelöst wird, wodurch beispielsweise eine im UI daran gebundene Schaltfläche benachrichtigt wird, damit diese ihre eigene IsEnabled-Eigenschaft aktualisieren kann.Listing 5 zeigt den erzeugten Code für SaveCommand. Dem Konstruktor der DelegateCommand-Klasse wird jetzt neben dem Lambda-Ausdruck mit der Save()-Methode auch ein Lambda-Ausdruck mit der Methode CanSave() übergeben. Darüber hinaus wird InvalidateCommands() der Klasse ViewModelBase überschrieben; der Aufruf dieser Methode erfolgt bei jeder Änderung einer Eigenschaft. Handelt es sich um eine Änderung der Eigenschaft FirstName, wird die RaiseCanExecuteChanged()-Methode von SaveCommand aufgerufen, um das CanExecuteChanged-Ereignis des Command-Objekts auszulösen. Dadurch können sich gebundene Elemente des UI aktualisieren und entsprechend aktivieren oder deaktivieren.
Listing 5: Der erzeugte Code für CanExecute
using MvvmGen.Commands; <br/><br/>partial class EmployeeViewModel :<br/> global::MvvmGen.ViewModels.ViewModelBase <br/>{ <br/> ... <br/> private void InitializeCommands() <br/> { <br/> SaveCommand = new DelegateCommand(<br/> _ =&gt; Save(), _ =&gt; CanSave()); <br/> } <br/><br/> public DelegateCommand SaveCommand<br/> { get; private set; } <br/><br/> ... <br/><br/> protected override void InvalidateCommands(<br/> string? propertyName) <br/> { <br/> base.InvalidateCommands(propertyName); <br/> if (propertyName == "FirstName") <br/> { <br/> SaveCommand.RaiseCanExecuteChanged(); <br/> } <br/> } <br/>}
Abhängigkeiten injizieren
Beim Entwickeln eines ViewModel werden üblicherweise Abhängigkeiten an den Konstruktor übergeben, damit es lediglich lose gekoppelt ist und sich in Unit-Tests ohne Aufwand überprüfen lässt. Ein Beispiel für eine solche Abhängigkeit könnte ein wie hier dargestellter IEmployeeDataProvider sein:using System.Diagnostics;
namespace DotnetPro.EmployeeBrowser.Data;
public interface
IEmployeeDataProvider
{
void Save(string? firstName,
string? lastName);
}
public class EmployeeDataProvider
: IEmployeeDataProvider
{
public void Save(string?
firstName, string? lastName)
{
Debug.WriteLine($"Saved: {firstName} {lastName}");
}
}
Dieses Interface besitzt eine Methode Save() zum Speichern von Vorname und Nachname. Es sind natürlich andere Parameter denkbar, wie etwa eine Employee-Instanz, die in einer Datenbank gespeichert werden soll. Das Prinzip des Daten-Providers bleibt jedoch das gleiche: Über ein Interface erfolgt eine lose Kopplung zum ViewModel.In dem Beispiel ist mit der Klasse EmployeeDataProvider auch eine Implementierung der Schnittstelle IEmployeeDataProvider zu sehen, die lediglich den Vornamen und den Nachnamen in die Debug-Ausgabe schreibt, die sich beim Debuggen mit Visual Studio im Ausgabe-Fenster begutachten lässt.In Listing 6 ist die vollständige Klasse EmployeeViewModel zu sehen. Sie ist jetzt nicht nur mit dem [ViewModel]-Attribut, sondern auch mit [Inject] ausgezeichnet. Es gibt an, dass das IEmployeeDataProvider-Interface eine Abhängigkeit des ViewModel ist, die injiziert werden soll.
Listing 6: Das Inject-Attribut im Einsatz
using DotnetPro.EmployeeBrowser.Data; <br/>using MvvmGen; <br/><br/>namespace DotnetPro.EmployeeBrowser.ViewModel; <br/><br/>[Inject(typeof(IEmployeeDataProvider))] <br/>[ViewModel] <br/>public partial class EmployeeViewModel <br/>{ <br/> [Property] private string? _firstName; <br/> [Property] private string? _lastName; <br/><br/> [PropertyInvalidate(nameof(FirstName), nameof(<br/> LastName))] <br/> public string? FullName =&gt;<br/> $"{FirstName} {LastName}"; <br/><br/> [Command(CanExecuteMethod = nameof(CanSave))] <br/> private void Save() <br/> { <br/> EmployeeDataProvider.Save(<br/> FirstName, LastName); <br/> } <br/><br/> [CommandInvalidate(nameof(FirstName))] <br/> private bool CanSave() =&gt;<br/> !string.IsNullOrWhiteSpace(FirstName);<br/>}
Der Codegenerator erzeugt aufgrund dieses Attributs einen Konstruktor mit einem Parameter vom Typ IEmployeeDataProvider, siehe Listing 7. Dieser Parameter wird zum Initialisieren der ebenfalls erzeugten Eigenschaft EmployeeDataProvider verwendet, die ebenfalls vom Typ IEmployeeDataProvider ist. In Listing 6 verwendet die Methode Save() diese erzeugte Eigenschaft EmployeeDataProvider. Die in der Eigenschaft gespeicherte IEmployeeDataProvider-Instanz ruft Save() auf, um die Werte der Eigenschaften FirstName und LastName zu sichern.
Listing 7: Der generierte Code für das ViewModel aus Listing 6
// &lt;auto-generated&gt; <br/>// This code was generated for you by <br/>// ? MvvmGen, a tool created by <br/>// Thomas Claudius Huber <br/>// Generator version: 1.1.5 <br/>// &lt;/auto-generated&gt; <br/>using MvvmGen.Commands; <br/>using MvvmGen.Events; <br/>using MvvmGen.ViewModels; <br/><br/>namespace DotnetPro.EmployeeBrowser.ViewModel <br/>{ <br/> partial class EmployeeViewModel :<br/> global::MvvmGen.ViewModels.ViewModelBase <br/> { <br/><br/> public EmployeeViewModel(DotnetPro.<br/> EmployeeBrowser.Data.<br/> IEmployeeDataProvider employeeDataProvider) <br/> { <br/> this.EmployeeDataProvider =<br/> employeeDataProvider; <br/> this.InitializeCommands(); <br/> this.OnInitialize(); <br/> } <br/><br/> partial void OnInitialize(); <br/><br/> private void InitializeCommands() <br/> { <br/> SaveCommand = new DelegateCommand(<br/> _ =&gt;Save(), _ =&gt; CanSave()); <br/> } <br/><br/> public DelegateCommand SaveCommand<br/> { get; private set; } <br/><br/> public string? FirstName <br/> { <br/> get =&gt; _firstName; <br/> set <br/> { <br/> if (_firstName != value) <br/> { <br/> _firstName = value; <br/> OnPropertyChanged("FirstName"); <br/> OnPropertyChanged("FullName"); <br/> } <br/> } <br/> } <br/><br/> public string? LastName <br/> { <br/> get =&gt; _lastName; <br/> set <br/> { <br/> if (_lastName != value) <br/> { <br/> _lastName = value; <br/> OnPropertyChanged("LastName"); <br/> OnPropertyChanged("FullName"); <br/> } <br/> } <br/> } <br/><br/> protected DotnetPro.EmployeeBrowser.Data.<br/> IEmployeeDataProvider EmployeeDataProvider <br/> { get; private set; } <br/><br/> protected override void InvalidateCommands(<br/> string? propertyName) <br/> { <br/> base.InvalidateCommands(propertyName); <br/> if (propertyName == "FirstName") <br/> { <br/> SaveCommand.RaiseCanExecuteChanged(); <br/> } <br/> } <br/><br/> } <br/>}
Die Mächtigkeit der Codeerzeugung von MvvmGen wird nochmals sehr deutlich, wenn man den Umfang des generierten Codes im Verhältnis zum manuell geschriebenen Code betrachtet. Listing 6 zeigt den Code, den man tatsächlich selbst in Visual Studio schreibt; Listing 7 enthält den Code, den MvvmGen erstellt. Somit ist nun ein kleines ViewModel mit MvvmGen erstellt und es lässt sich wie jedes andere ViewModel in der eigenen WPF-, WinUI- oder auch .NET MAUI-Anwendung einsetzen.
Das ViewModel verwenden
Das erstellte EmployeeViewModel soll nun in einer WPF-Anwendung eingesetzt werden. Dazu wird in der Code-behind-Datei des MainWindow eine Instanz von EmployeeViewModel der Eigenschaft DataContext des MainWindow zugewiesen:public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new EmployeeViewModel(
new EmployeeDataProvider());
}
}
In der MainWindow.xaml-Datei in Listing 8 lassen sich nun verschiedene UI-Elemente an die Eigenschaften der in DataContext enthaltenen EmployeeViewModel-Instanz binden. Wie in dem Listing zu erkennen ist, ist die Title-Eigenschaft des Fensters mit der Eigenschaft FullName des ViewModel verknüpft. Darüber hinaus sind die Text-Eigenschaften von zwei TextBox-Elementen an die Eigenschaften FirstName und LastName des ViewModel gebunden. Ebenso ist die Command-Eigenschaft eines Button mit der Eigenschaft SaveCommand des ViewModels verbunden.
Listing 8: Die MainWindow.xaml-Datei
&lt;Window <br/> x:Class="DotnetPro.EmployeeBrowser.MainWindow" <br/> xmlns="http://schemas.microsoft.com/winfx/2006/<br/> xaml/presentation" <br/> xmlns:x="http://schemas.microsoft.com/winfx/<br/> 2006/xaml" <br/> Title="{Binding FullName}" Height="450"<br/> Width="800" FontSize="30"&gt; <br/> &lt;StackPanel&gt; <br/> &lt;TextBlock Margin="10 10 10 5"&gt;Firstname<br/> &lt;/TextBlock&gt; <br/> &lt;TextBox Margin="10 0 10 10" <br/> Text="{Binding FirstName,Mode=TwoWay,<br/> UpdateSourceTrigger=PropertyChanged}"/&gt; <br/><br/> &lt;TextBlock Margin="10 10 10 5"&gt;LastName<br/> &lt;/TextBlock&gt; <br/> &lt;TextBox Margin="10 0 10 10" <br/> Text="{Binding LastName,Mode=TwoWay,<br/> UpdateSourceTrigger=PropertyChanged}"/&gt; <br/><br/> &lt;Button Margin="10 30" Content="Save" Width="75" <br/> HorizontalAlignment="Left" <br/> Command="{Binding SaveCommand}"/&gt; <br/> &lt;/StackPanel&gt; <br/>&lt;/Window&gt;
Bild 4 zeigt die WPF-Anwendung. Der Titel des Fensters enthält den vollen Namen, der die in den beiden TextBox-Elementen eingegebenen Werte „Thomas“ und „Huber“ enthält. Sobald die TextBox mit dem Vornamen geleert wird, wird die Save-Schaltfläche automatisch deaktiviert, weil sich das gebundene SaveCommand dann nicht mehr ausführen lässt. Dies entspricht der CanExecute-Logik, wie sie im ViewModel programmiert wurde.

Die WPF-App,die an das ViewModel gebunden wurde(Bild 4)
Autor
Zusammenfassung
C#-Codegeneratoren sind ein mächtiges Werkzeug, um C#-Code zu erzeugen. Die in diesem Artikel vorgestellte Bibliothek MvvmGen baut auf C#-Codegeneratoren auf, um das Entwickeln von ViewModels deutlich zu vereinfachen. Die Attribute bei MvvmGen wurden dabei so gewählt, dass eine ViewModel-Klasse sich selbst erklären soll. Das heißt, der Entwickler muss den erzeugten Code nicht unbedingt betrachten, um das ViewModel zu verstehen.Zur Einführung in MvvmGen hat der Artikel ein einfaches Beispiel gewählt. Dabei wurde der ebenfalls in MvvmGen enthaltenen Event-Aggregator, der sich zur Kommunikation zwischen verschiedenen ViewModels einsetzen lässt, nicht verwendet.In den offiziellen MvvmGen-Beispielen [3] befindet sich eine etwas komplexere Anwendung namens EmployeeManager, die den Event-Aggregator und weitere Merkmale von MvvmGen zeigt. Auch ein Blick in die Dokumentation kann hilfreich sein, um einen Einstieg in die MvvmGen-Bibliothek zu erhalten [4]. Wer sich für das Programmieren eines C# Source Generators interessiert, kann auch einen Blick in den Quellcode von MvvmGen werfen [2].Fussnoten
- Phillip Carter, Introducing C# Source Generators, http://www.dotnetpro.de/SL2305MvvmGen1
- MvvmGen bei GitHub, http://www.dotnetpro.de/SL2305MvvmGen2
- MvvmGen-Beispiele bei GitHub, http://www.dotnetpro.de/SL2305MvvmGen3
- MvvmGen-Dokumentation bei GitHub, http://www.dotnetpro.de/SL2305MvvmGen4