Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 13 Min.

MVVM im Handumdrehen

Die Bibliothek MvvmGen erzeugt Code für ViewModels und reduziert so den Aufwand.
© dotnetpro
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 OnProperty­Changed() 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> =&gt; _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
T4-Vorlagen sind sicherlich sehr weit verbreitet, da sie in Visual Studio schon eingebaut sind. Im Projektmappen-Explorer lässt sich eine .tt-Datei mit einem Rechtsklick ausführen, um damit Code zu erzeugen.All diese Ansätze haben eines gemeinsam: Sie erhalten eine oder mehrere .NET-Assemblies als Input und erzeugen darauf aufbauend den Code. Das führt zu einem Workflow, der mindestens zwei Kompilierungen benötigt, um den Code verwenden zu können. Der Workflow sieht üblicherweise wie folgt aus:
  • 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.
Für einige Anwendungsfälle reicht dieser Workflow natürlich aus und ist auch gut. Aber beim Entwickeln eines ViewModel, wenn im Grund genommen für jedes hinzugefügte Feld eine Eigenschaft erstellt werden soll, kann ein ständiges Kompilieren und manuelles Hinzufügen von generiertem Code den Spaß am Programmieren doch etwas schmälern; zudem kostet es sehr viel Zeit. Doch seit .NET 5 gibt es ­glücklicherweise eine weitere Variante, um Code zu erzeugen, deren Grundprinzipien sich von allen bisherigen Ansätzen unterscheiden.

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.
Das bedeutet, ein C#-Codegenerator kann den C#-Code eines Projekts analysieren und während des Kompilierens zusätzlichen C#-Code hinzufügen. Somit ist kein zusätzliches Kompilieren notwendig, um den erzeugten Code einzubeziehen, da alles Nötige während des schon erfolgten Kompilierens passiert.Bild 1 verdeutlicht diesen Vorgang im C#-Compiler. Nach dem Starten der Kompilierung gibt es in der Abbildung den Source Generator Schritt. Hier durchsucht der Compiler die Projektreferenzen und schaut nach, ob das aktuell zu kompilierende Projekt auf Codegeneratoren verweist. Codegeneratoren sind speziell implementierte Klassen, die in .NET-Standard-2.0-Bibliotheken untergebracht sind. Werden ein oder auch mehrere Codegeneratoren gefunden, ist der Ablauf für jeden Generator gleich: Er kann den bestehenden Code des Projekts, in dem er referenziert wird, analysieren. Dabei stehen die C#-Syntax-APIs zur Verfügung, die auch zum Entwickeln eines Analyzers für C#-Code verwendet werden. Basierend auf der Analyse des bestehenden Codes lässt sich dann zusätzlicher Code erstellen.
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#-Code­generatoren. 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.
Das Herzstück von MvvmGen ist ein C#-Codegenerator, der die ViewModels erzeugt. Er sorgt dafür, dass der Entwickler sich beim Programmieren von ViewModels auf das Wesentliche konzentrieren kann. All der sich wiederholende Code bei einem ViewModel – auch Boilerplate genannt – wird von Mvvm­Gen erzeugt, während der Entwickler seinen eigenen Code in Visual Studio eingibt.MvvmGen ist mit Klassen wie ViewModelBase ein vollständiges MVVM-Framework. Es lässt sich somit auch ohne den C#-Codegenerator verwenden (auch wenn dies wenig Sinn ergibt), da dies dann einfach viel mehr Schreibaufwand für den Entwickler bedeutet. Tatsächlich ist jedoch der im obigen Codebeispiel dieses Artikels dargestellte Code zu 100 Prozent gültiger MvvmGen-Code.

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
In Bild 2 sind diese drei Teile dargestellt, aus denen die Mvvm­Gen-Bibliothek besteht.
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
&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; MvvmGen.ViewModels; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;namespace&lt;/span&gt; &lt;span class="hljs-title"&gt;DotnetPro.EmployeeBrowser.ViewModel&lt;/span&gt;; &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;EmployeeViewModel&lt;/span&gt; : &lt;span class="hljs-title"&gt;ViewModelBase&lt;/span&gt; &lt;br/&gt;{ &lt;br/&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;? _firstName; &lt;br/&gt;&lt;br/&gt;  &lt;span class="hljs-keyword"&gt;public&lt;/span&gt; &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;? FirstName &lt;br/&gt;  { &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;get&lt;/span&gt; =&amp;gt; _firstName; &lt;br/&gt;    &lt;span class="hljs-keyword"&gt;set&lt;/span&gt; &lt;br/&gt;    { &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (_firstName != &lt;span class="hljs-keyword"&gt;value&lt;/span&gt;) &lt;br/&gt;      { &lt;br/&gt;        _firstName = &lt;span class="hljs-keyword"&gt;value&lt;/span&gt;; &lt;br/&gt;        OnPropertyChanged(&lt;span class="hljs-string"&gt;"FirstName"&lt;/span&gt;); &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;&lt;br/&gt;} 
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
// &amp;lt;&lt;span class="hljs-built_in"&gt;auto&lt;/span&gt;-generated&amp;gt; &lt;br/&gt;//  This code was generated &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; you &lt;span class="hljs-built_in"&gt;by&lt;/span&gt; &lt;br/&gt;//  MvvmGen, a tool created &lt;span class="hljs-built_in"&gt;by&lt;/span&gt; Thomas Claudius Huber &lt;br/&gt;//  Generator version: &lt;span class="hljs-number"&gt;1.1&lt;/span&gt;&lt;span class="hljs-number"&gt;.5&lt;/span&gt; &lt;br/&gt;// &amp;lt;/&lt;span class="hljs-built_in"&gt;auto&lt;/span&gt;-generated&amp;gt; &lt;br/&gt;&lt;span class="hljs-built_in"&gt;using&lt;/span&gt; MvvmGen.Commands; &lt;br/&gt;&lt;span class="hljs-built_in"&gt;using&lt;/span&gt; MvvmGen.Events; &lt;br/&gt;&lt;span class="hljs-built_in"&gt;using&lt;/span&gt; MvvmGen.ViewModels; &lt;br/&gt;&lt;br/&gt;namespace DotnetPro.EmployeeBrowser.ViewModel &lt;br/&gt;{ &lt;br/&gt;  partial class EmployeeViewModel :&lt;br/&gt;      global::MvvmGen.ViautomewModels.ViewModelBase &lt;br/&gt;  { &lt;br/&gt;    public EmployeeViewModel() &lt;br/&gt;    { &lt;br/&gt;      this.OnInitialize(); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    partial void OnInitialize(); &lt;br/&gt;&lt;br/&gt;    public string? FirstName &lt;br/&gt;    { &lt;br/&gt;      get =&amp;gt; _firstName; &lt;br/&gt;      &lt;span class="hljs-built_in"&gt;set&lt;/span&gt; &lt;br/&gt;      { &lt;br/&gt;        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (_firstName != value) &lt;br/&gt;        { &lt;br/&gt;          _firstName = value; &lt;br/&gt;          OnPropertyChanged(&lt;span class="hljs-string"&gt;"FirstName"&lt;/span&gt;); &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;} 
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 First­Name 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 =&gt; <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
// &amp;lt;&lt;span class="hljs-built_in"&gt;auto&lt;/span&gt;-generated&amp;gt; &lt;br/&gt;//  This code was generated &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; you &lt;span class="hljs-built_in"&gt;by&lt;/span&gt; &lt;br/&gt;//  MvvmGen, a tool created &lt;span class="hljs-built_in"&gt;by&lt;/span&gt; Thomas Claudius Huber &lt;br/&gt;//  Generator version: &lt;span class="hljs-number"&gt;1.1&lt;/span&gt;&lt;span class="hljs-number"&gt;.5&lt;/span&gt; &lt;br/&gt;// &amp;lt;/&lt;span class="hljs-built_in"&gt;auto&lt;/span&gt;-generated&amp;gt; &lt;br/&gt;&lt;span class="hljs-built_in"&gt;using&lt;/span&gt; MvvmGen.Commands; &lt;br/&gt;&lt;span class="hljs-built_in"&gt;using&lt;/span&gt; MvvmGen.Events; &lt;br/&gt;&lt;span class="hljs-built_in"&gt;using&lt;/span&gt; MvvmGen.ViewModels; &lt;br/&gt;&lt;br/&gt;namespace DotnetPro.EmployeeBrowser.ViewModel &lt;br/&gt;{ &lt;br/&gt;  partial class EmployeeViewModel :&lt;br/&gt;      global::MvvmGen.ViewModels.ViewModelBase &lt;br/&gt;  { &lt;br/&gt;    public EmployeeViewModel() &lt;br/&gt;    { &lt;br/&gt;      this.OnInitialize(); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    partial void OnInitialize(); &lt;br/&gt;&lt;br/&gt;    public string? FirstName &lt;br/&gt;    { &lt;br/&gt;      get =&amp;gt; _firstName; &lt;br/&gt;      &lt;span class="hljs-built_in"&gt;set&lt;/span&gt; &lt;br/&gt;      { &lt;br/&gt;        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (_firstName != value) &lt;br/&gt;        { &lt;br/&gt;          _firstName = value; &lt;br/&gt;          OnPropertyChanged(&lt;span class="hljs-string"&gt;"FirstName"&lt;/span&gt;); &lt;br/&gt;          OnPropertyChanged(&lt;span class="hljs-string"&gt;"FullName"&lt;/span&gt;); &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public string? LastName &lt;br/&gt;    { &lt;br/&gt;      get =&amp;gt; _lastName; &lt;br/&gt;      &lt;span class="hljs-built_in"&gt;set&lt;/span&gt; &lt;br/&gt;      { &lt;br/&gt;        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (_lastName != value) &lt;br/&gt;        { &lt;br/&gt;          _lastName = value; &lt;br/&gt;          OnPropertyChanged(&lt;span class="hljs-string"&gt;"LastName"&lt;/span&gt;); &lt;br/&gt;          OnPropertyChanged(&lt;span class="hljs-string"&gt;"FullName"&lt;/span&gt;); &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;} 

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 :&lt;br/&gt;    global::MvvmGen.ViewModels.ViewModelBase &lt;br/&gt;{ &lt;br/&gt;  public EmployeeViewModel() &lt;br/&gt;  { &lt;br/&gt;    this.InitializeCommands(); &lt;br/&gt;    this.OnInitialize(); &lt;br/&gt;  } &lt;br/&gt;&lt;br/&gt;  partial void OnInitialize(); &lt;br/&gt;&lt;br/&gt;  private void InitializeCommands() &lt;br/&gt;  { &lt;br/&gt;    SaveCommand = new DelegateCommand(&lt;br/&gt;      _ =&amp;gt; Save()); &lt;br/&gt;  } &lt;br/&gt;&lt;br/&gt;  public DelegateCommand SaveCommand&lt;br/&gt;  { get; private set; } &lt;br/&gt;&lt;br/&gt;  ...&lt;br/&gt;} 

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() =&gt;
  !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 View­ModelBase ü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; &lt;br/&gt;&lt;br/&gt;partial class EmployeeViewModel :&lt;br/&gt;    global::MvvmGen.ViewModels.ViewModelBase &lt;br/&gt;{ &lt;br/&gt;  ... &lt;br/&gt;  private void InitializeCommands() &lt;br/&gt;  { &lt;br/&gt;    SaveCommand = new DelegateCommand(&lt;br/&gt;      _ =&amp;gt; Save(), _ =&amp;gt; CanSave()); &lt;br/&gt;  } &lt;br/&gt;&lt;br/&gt;  public DelegateCommand SaveCommand&lt;br/&gt;  { get; private set; } &lt;br/&gt;&lt;br/&gt;  ... &lt;br/&gt;&lt;br/&gt;  protected override void InvalidateCommands(&lt;br/&gt;      string? propertyName) &lt;br/&gt;  { &lt;br/&gt;    base.InvalidateCommands(propertyName); &lt;br/&gt;    if (propertyName == "FirstName") &lt;br/&gt;    { &lt;br/&gt;      SaveCommand.RaiseCanExecuteChanged(); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;} 

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 IEmployeeData­Provider 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; &lt;br/&gt;using MvvmGen; &lt;br/&gt;&lt;br/&gt;namespace DotnetPro.EmployeeBrowser.ViewModel; &lt;br/&gt;&lt;br/&gt;[Inject(typeof(IEmployeeDataProvider))] &lt;br/&gt;[ViewModel] &lt;br/&gt;public partial class EmployeeViewModel &lt;br/&gt;{ &lt;br/&gt;  [Property] private string? _firstName; &lt;br/&gt;  [Property] private string? _lastName; &lt;br/&gt;&lt;br/&gt;  [PropertyInvalidate(nameof(FirstName), nameof(&lt;br/&gt;    LastName))] &lt;br/&gt;  public string? FullName =&amp;gt;&lt;br/&gt;    $"{FirstName} {LastName}"; &lt;br/&gt;&lt;br/&gt;  [Command(CanExecuteMethod = nameof(CanSave))] &lt;br/&gt;  private void Save() &lt;br/&gt;  { &lt;br/&gt;    EmployeeDataProvider.Save(&lt;br/&gt;      FirstName, LastName); &lt;br/&gt;  } &lt;br/&gt;&lt;br/&gt;  [CommandInvalidate(nameof(FirstName))] &lt;br/&gt;  private bool CanSave() =&amp;gt;&lt;br/&gt;    !string.IsNullOrWhiteSpace(FirstName);&lt;br/&gt;} 
Der Codegenerator erzeugt aufgrund dieses Attributs einen Kons­truktor 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
// &amp;lt;auto-generated&amp;gt; &lt;br/&gt;//  This code was generated for you by &lt;br/&gt;//  ? MvvmGen, a tool created by &lt;br/&gt;//  Thomas Claudius Huber &lt;br/&gt;//  Generator version: 1.1.5 &lt;br/&gt;// &amp;lt;/auto-generated&amp;gt; &lt;br/&gt;using MvvmGen.Commands; &lt;br/&gt;using MvvmGen.Events; &lt;br/&gt;using MvvmGen.ViewModels; &lt;br/&gt;&lt;br/&gt;namespace DotnetPro.EmployeeBrowser.ViewModel &lt;br/&gt;{ &lt;br/&gt;  partial class EmployeeViewModel :&lt;br/&gt;      global::MvvmGen.ViewModels.ViewModelBase &lt;br/&gt;  { &lt;br/&gt;&lt;br/&gt;    public EmployeeViewModel(DotnetPro.&lt;br/&gt;        EmployeeBrowser.Data.&lt;br/&gt;        IEmployeeDataProvider employeeDataProvider) &lt;br/&gt;    { &lt;br/&gt;      this.EmployeeDataProvider =&lt;br/&gt;        employeeDataProvider; &lt;br/&gt;      this.InitializeCommands(); &lt;br/&gt;      this.OnInitialize(); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    partial void OnInitialize(); &lt;br/&gt;&lt;br/&gt;    private void InitializeCommands() &lt;br/&gt;    { &lt;br/&gt;      SaveCommand = new DelegateCommand(&lt;br/&gt;        _ =&amp;gt;Save(), _ =&amp;gt; CanSave()); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public DelegateCommand SaveCommand&lt;br/&gt;    { get; private set; } &lt;br/&gt;&lt;br/&gt;    public string? FirstName &lt;br/&gt;    { &lt;br/&gt;      get =&amp;gt; _firstName; &lt;br/&gt;      set &lt;br/&gt;      { &lt;br/&gt;        if (_firstName != value) &lt;br/&gt;        { &lt;br/&gt;          _firstName = value; &lt;br/&gt;          OnPropertyChanged("FirstName"); &lt;br/&gt;          OnPropertyChanged("FullName"); &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public string? LastName &lt;br/&gt;    { &lt;br/&gt;      get =&amp;gt; _lastName; &lt;br/&gt;      set &lt;br/&gt;      { &lt;br/&gt;        if (_lastName != value) &lt;br/&gt;        { &lt;br/&gt;          _lastName = value; &lt;br/&gt;          OnPropertyChanged("LastName"); &lt;br/&gt;          OnPropertyChanged("FullName"); &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    protected DotnetPro.EmployeeBrowser.Data.&lt;br/&gt;      IEmployeeDataProvider EmployeeDataProvider &lt;br/&gt;      { get; private set; } &lt;br/&gt;&lt;br/&gt;    protected override void InvalidateCommands(&lt;br/&gt;        string? propertyName) &lt;br/&gt;    { &lt;br/&gt;      base.InvalidateCommands(propertyName); &lt;br/&gt;      if (propertyName == "FirstName") &lt;br/&gt;      { &lt;br/&gt;        SaveCommand.RaiseCanExecuteChanged(); &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;  } &lt;br/&gt;} 
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 View­Model 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 Save­Command des ViewModels verbunden.
Listing 8: Die MainWindow.xaml-Datei
&amp;lt;Window &lt;br/&gt;  x:Class="DotnetPro.EmployeeBrowser.MainWindow" &lt;br/&gt;  xmlns="http://schemas.microsoft.com/winfx/2006/&lt;br/&gt;        xaml/presentation" &lt;br/&gt;  xmlns:x="http://schemas.microsoft.com/winfx/&lt;br/&gt;          2006/xaml" &lt;br/&gt;    Title="{Binding FullName}" Height="450"&lt;br/&gt;    Width="800" FontSize="30"&amp;gt; &lt;br/&gt;  &amp;lt;StackPanel&amp;gt; &lt;br/&gt;    &amp;lt;TextBlock Margin="10 10 10 5"&amp;gt;Firstname&lt;br/&gt;    &amp;lt;/TextBlock&amp;gt; &lt;br/&gt;    &amp;lt;TextBox Margin="10 0 10 10" &lt;br/&gt;      Text="{Binding FirstName,Mode=TwoWay,&lt;br/&gt;            UpdateSourceTrigger=PropertyChanged}"/&amp;gt; &lt;br/&gt;&lt;br/&gt;    &amp;lt;TextBlock Margin="10 10 10 5"&amp;gt;LastName&lt;br/&gt;    &amp;lt;/TextBlock&amp;gt; &lt;br/&gt;    &amp;lt;TextBox Margin="10 0 10 10" &lt;br/&gt;      Text="{Binding LastName,Mode=TwoWay,&lt;br/&gt;            UpdateSourceTrigger=PropertyChanged}"/&amp;gt; &lt;br/&gt;&lt;br/&gt;    &amp;lt;Button Margin="10 30" Content="Save" Width="75" &lt;br/&gt;      HorizontalAlignment="Left" &lt;br/&gt;    Command="{Binding SaveCommand}"/&amp;gt; &lt;br/&gt;  &amp;lt;/StackPanel&amp;gt; &lt;br/&gt;&amp;lt;/Window&amp;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 View­Model 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].
Projektdateien herunterladen

Fussnoten

  1. Phillip Carter, Introducing C# Source Generators, http://www.dotnetpro.de/SL2305MvvmGen1
  2. MvvmGen bei GitHub, http://www.dotnetpro.de/SL2305MvvmGen2
  3. MvvmGen-Beispiele bei GitHub, http://www.dotnetpro.de/SL2305MvvmGen3
  4. MvvmGen-Dokumentation bei GitHub, http://www.dotnetpro.de/SL2305MvvmGen4

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
IoT neu eingebunden - Integration und Verwaltung von IoT-Geräten mit Azure IoT Operations
Wie sich das neue Azure IoT Operations von bestehenden Azure-Diensten unterscheidet, welche Technologien dabei zum Einsatz kommen und wann sich der Umstieg lohnt.
16 Minuten
15. Jun 2025
Microsoft./NET

Das könnte Dich auch interessieren

MAUI-Tools - Steuerelemente der Community
Nützliche Hilfen für besondere Aufgaben.
12 Minuten
14. Okt 2024
WPF/VB: Fenster auf dem richtigen Monitor starten - Tipp der Woche
Das Notebook steht links, der große Monitor rechts daneben. Die Anwendung soll auf dem großen Monitor starten – außer wenn der Anwender unterwegs arbeitet und kein zweiter Monitor angeschlossen ist.
2 Minuten
24. Mär 2022
Von .NET Core 3.0 zu .NET 5 - Microsoft Build
Aus .NET, .NET Core und Mono soll mit .NET 5 wieder eine gemeinsame Plattform für alle Anwendungen entstehen.
2 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige