Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 17 Min.

MVVM aus .NET-MAUI-Sicht

Ein Toolkit für das beliebteste Palindrom in der Welt der .NET-MAUI-Entwicklung. Warum eine Trennung manchmal nicht nur gut ist, sondern auch Spaß machen kann.
© dotnetpro
Model. View. ViewModel. Nur der Vollständigkeit halber gibt es zu Anfang dieser Einführung eine kurze Erläuterung zum allgemeinen Kontext, gefolgt von einer speziellen Einordnung in der Welt von .NET MAUI.

tl;dr

Mithilfe des Model-View-ViewModel-Patterns lassen sich Geschäftslogik und UI sauber voneinander trennen. Das ermöglicht automatisches Testen, die Trennung von Aufgabenpaketen und – die richtige Toolchain vorausgesetzt – steigert signifikant die Lesbarkeit des Codes (die Wartbarkeit auch). Apropos „richtige Toolchain“: Zum Ende des Artikels hin gibt es noch einen wertvollen Tipp, der die App-Entwicklung mit .NET MAUI und MVVM noch einmal deutlich beschleunigt. Nicht verpassen!

Historie für die, die mehr wissen wollen

Das MVVM-Pattern lässt sich in seiner initialen Form und Idee zurückverfolgen bis zu John Gossman, der es im Zusammenhang mit WPF zuerst auf seinem Blog beschrieben hat [1]. Kurz gesagt handelt es sich dabei um eine Adaption des zu der Zeit bereits beliebten MVC-Patterns. Der Grund dafür lag darin, dass das UI in WPF grundsätzlich in einer deklarativen Sprache beschrieben wurde – nämlich XAML –, während die Datenmodelle und die Geschäftslogik in einer Hochsprache wie beispielsweise C# programmiert wurden. Damit kam es erstmals auch zu einer sinnvollen Trennung der Arbeit in Aufgaben, die an Designer vergeben werden konnten, und solche, die weiterhin den Softwareentwicklern oblagen. Eine Datenbindung zwischen UI, Modellen und Geschäftslogik wurde notwendig, und somit wurde das MVVM-Pattern zum zentralen Dreh- und Angelpunkt für die Entwicklung entsprechender Applikationen.Das User Interface (UI) wird dabei in Form aller visuellen Elemente repräsentiert, mit denen die Benutzer unter anderem auch interagieren können: Buttons, Labels, Switches und dergleichen. Das alles fassen wir mit dem Begriff View zusammen. Bei dem Model handelt es sich in der Regel um eine POCO-Klasse (Plain Old CLR Object). Enthalten ist kaum mehr als eine Sammlung von Properties. In diese Klassen werden normalerweise die Daten zum Beispiel aus Datenbanken oder REST-Services geladen. Sie sind auch sehr spezifisch. Während es also beispielsweise eine Klasse für einen Mitarbeiter geben kann und eine weitere für dessen aktuelle Projekte, kann es auf der Seite des UI wiederum eine View geben, die Daten aus beiden Klassen aggregiert darstellt.Um das zu bewerkstelligen, gibt es das ViewModel. John spricht hier auch von einem „Modell einer View“. So herum ist auch leichter zu verstehen, was damit gemeint ist, und es ist leichter nachzuvollziehen, warum in einem ViewModel unter anderem auch Modelle transformiert werden (müssen): Sie werden an die Anforderungen einer View angepasst.Zusammengefasst lässt sich damit also sagen, dass das MVVM-­Pattern eine Möglichkeit darstellt, das User Interface von Geschäftslogik und Daten sauber zu entkoppeln. Das bietet nicht nur den bereits genannten Vorteil, auf unterschiedlichen Ebenen die Aufgaben möglicherweise durch unterschiedliche Experten umsetzen zu lassen (in der Praxis passiert das übrigens relativ selten), sondern auch, die Geschäftslogik einfacher (automatisiert) zu testen: Da das ViewModel keine Referenz zur View hat, lässt es sich in Unit-Tests programmatisch ansprechen, ohne dass ein Benutzer mit der Applikation interagieren muss. En passant erlangt man so übrigens auch die Möglichkeit, die Geschäftslogik vollständig aus dem Projekt heraus in eine eigenständige Bibliothek zu verschieben. Das kann insbesondere von Vorteil sein, sobald es sich bei der Applikation um etwas handelt, für das mehrere Clients für gänzlich unter­schiedliche Plattformen bereitgestellt werden oder bei dem eine Lastverteilung einzelne Komponenten auf mehreren Systemen redundant laufen lässt.

Vorteile der Source-Generatoren im Vergleich zu AOP

Dass durch diese Vorgehensweise Quellcode generiert und ­direkt in das Projekt integriert wird, kann als immenser Vorteil gegenüber klassischer aspektorientierter Programmierung ­gesehen werden. Denn auf diese Weise ist jederzeit ersichtlich, was für Code genau generiert wird, und es bedarf keines zusätzlichen Studiums der Dokumentation.

But why?

Bevor es nun gleich losgeht mit einer Einführung in das Micro­soft CommunityToolkit – und weil wir hier ja unter uns sind und offen miteinander sprechen können: Wie bereits erwähnt, sind es doch meist die Entwickler, die die Applikationen designen. Automatisiert getestet wird auch verblüffend wenig, und nicht jeder plant die Entwicklung des nächsten Amazon, Ebay, Facebook und Co. Von daher wäre an dieser Stelle die Frage vollkommen legitim, wozu man überhaupt mit so etwas wie einem MVVM-Pattern arbeiten sollte.Denn eines darf an dieser Stelle festgehalten werden: Grundsätzlich wird die Sache zunächst einmal komplizierter, sobald Abstraktionsebenen eingeführt werden. Dass ein View­Model nichts von einer View weiß, klingt zwar gut. Dennoch müssen Views ja über Änderungen in den Daten in Kenntnis gesetzt werden. Und andersherum möchten Benutzer auch, dass durch sie veränderte Daten persistiert werden. Die View hat aber keine direkte Möglichkeit, auf die Modelle zuzugreifen. Das MVVM-Pattern muss also auf der jeweiligen Plattform auf eine solche Art implementiert werden, dass diese Herausforderungen gelöst werden.Die Microsoft-.NET-Plattform hält dafür das INotifyPropertyChanged-Interface bereit. Kurz gesagt ermöglicht es diese Schnittstelle, dass bei Änderungen von Properties all diejenigen, die daran interessiert sein könnten, informiert werden. Diese müssen ihr Interesse nur zuvor bekunden. Im Code sieht das Ganze normalerweise in etwa so aus wie in Listing 1.
Listing 1: Standard-Implementierung der INotifyPropertyChanged-Schnittstelle
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span> : <span class="hljs-title">INotifyPropertyChanged</span> <br/>{ <br/>  <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span> firstName = <span class="hljs-keyword">string</span>.Empty; <br/>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> FirstName <br/>  { <br/>    <span class="hljs-keyword">get</span> => 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/>        NotifyPropertyChanged(); <br/>      } <br/>    } <br/>  } <br/>  <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">NotifyPropertyChanged</span>(<span class="hljs-params">[</span></span><br/><span class="hljs-function"><span class="hljs-params">      CallerMemberName] <span class="hljs-keyword">string</span> propertyName = <span class="hljs-string">""</span></span>) </span><br/><span class="hljs-function">  </span>{ <br/>    <span class="hljs-keyword">if</span> (PropertyChanged != <span class="hljs-literal">null</span>) <br/>    { <br/>      PropertyChanged(<span class="hljs-keyword">this</span>, <br/>        <span class="hljs-keyword">new</span> PropertyChangedEventArgs(propertyName)); <br/>    } <br/>  } <br/>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">event</span> PropertyChangedEventHandler <br/>    PropertyChanged; <br/>}  
Da diese Implementierung nicht nur aufwendig ist, sondern auch noch dafür sorgt, dass der Quellcode immer unübersichtlicher wird, haben findige Softwareentwickler im Lauf der Zeit nach immer besseren Möglichkeiten gesucht, diese Mechanik zu vereinfachen. Doch dazu gleich mehr.Darüber hinaus gibt es allerdings nicht nur Daten, die ihren Weg in die UIs (und wieder zurück) finden wollen, sondern auch noch Interaktionsmöglichkeiten für den Benutzer. Buttons wollen gedrückt werden, in Listen wird gescrollt, und manchmal erwarten Benutzer auch eine optimierte Darstellung von Werten. Farben wie Grün oder Rot sind oft leichter verständlich als ein True oder ein False (bei genauerer Betrachtung nicht immer, aber sicher ist klar, was gemeint ist).In der Vergangenheit wurden Interaktionen und Verhaltensweisen einer Applikation oft im sogenannten Code-behind implementiert. In Windows-Forms-Anwendungen handelt es sich dabei meist um die zu den Steuerelementen ­gehörigen Eventhandler. Bei der Verwendung des MVVM-Patterns ist das allerdings aus den genannten Gründen normalerweise zu vermeiden. Und was das INotifyPropertyChanged-Interface für die Daten ist, ist das ICommand-Interface für alles andere.

Und was ist mit klassischen Events?

Commands können einfach an Controls gebunden werden und sind somit in einem MVVM-Szenario unverzichtbar. Was ist aber, wenn ein Control nicht für das Binding an Commands vorgesehen wurde?

CommunityToolkit to the rescue

Und jetzt, without any further ado, kommen wir endlich auf das CommunityToolkit zu sprechen!Wenn im Internet vom CommunityToolkit die Rede ist, darf zunächst einmal differenziert werden zwischen dem .NET CommunityToolkit und dem .NET MAUI CommunityToolkit. Während Ersteres mit allgemeinen Ergänzungen, Helfern und mehr daherkommt, die plattformunabhängig angeboten und genutzt werden können, richtet sich das .NET MAUI CommunityToolkit natürlich spezifisch an Entwickler für die .NET-MAUI-Plattform. In diesem Artikel interessieren wir uns allerdings für Ersteres, denn Microsoft hat es geschafft, das Thema MVVM unabhängig von der zugrunde liegenden Plattform so (weiter-) zu entwickeln, dass es nicht nur in .NET-MAUI-Applikationen genutzt werden kann. Dennoch gibt es ein paar Besonderheiten.Die Installation gelingt zunächst denkbar einfach über die Package Manager Console direkt in Visual Studio:
Install-Package CommunityToolkit.Mvvm 
Die Geschichte dieser Erweiterung geht auf das beliebte Mvvm­Light-Framework zurück. Wer ein wenig Archäologie betreiben will, informiert sich über MvvmLight, Microsoft.Toolkit.Mvvm, MVVM Toolkit und jetzt eben CommunityTool­kit.Mvvm. Der Einfachheit halber wird ab hier nur noch vom MVVM Toolkit gesprochen.Praktischerweise zielt die Implementierung auf .NET Standard und ist somit verfügbar für die Universal Windows Platform (UWP), Windows Forms, WPF, Xamarin und einige mehr, wobei es keine Rolle spielt, ob es sich bei der zugrunde liegen­den Laufzeitumgebung um das .NET Framework, .NET Core, .NET Native oder Mono handelt. Und da auch .NET 6 als Zielplattform verfügbar ist, freut sich die .NET-MAUI-Community ebenfalls über die Möglichkeit, damit zu arbeiten.

Object LifeTime Configuration

Transiente Objekte werden eher für Services oder ViewModels genutzt, die einen tendenziell geringen Memory Footprint haben und idealerweise keinen festzuhaltenden Status mit sich führen. Denn sie werden für alle Verwendungen neu instanziert, verbrauchen dadurch mehr Arbeitsspeicher und können sich negativ auf die Performance der App auswirken.

Einrichtung

Mehr als das NuGet-Paket zu installieren ist kaum zu tun. Es müssen beispielsweise keine Handler in der Methode Create­MauiApp registriert werden. Wohl aber ergibt es Sinn, notwendige Namespaces in die Datei GlobalUsings.cs aufzunehmen. Und schon steht der Nutzung nichts mehr im Wege:
global using CommunityToolkit.Mvvm.ComponentModel; 
Das, was die Nutzung des MVVM Toolkits so elegant macht, ist die Verwendung von Source-Generatoren. Noch vor nicht allzu langer Zeit war auch in der .NET-Community nicht selten von der sogenannten aspektorientierten Programmierung die Rede (AOP). Über den Ansatz der objektorientierten Programmierung hinaus akzeptiert die AOP, dass es manchmal Mechanismen gibt, die unabhängig vom Sinn und Nutzen ­einer Klasse übergreifend Einfluss auf das Verhalten haben und – streng objektorientiert umgesetzt – doch sehr dazu beitragen, dass der Code immer unleserlicher wird.Das obige Beispiel zeigt es recht deutlich: Die Implementierung des INotifyPropertyChanged-Interfaces erzeugt einen immer gleich bleibenden Boilerplate-Code, auf den gut und gerne verzichtet werden könnte. Genauso beispielsweise, wie es verankert in der Sprache inzwischen Auto Properties gibt. AOP nutzt im Zusammenhang mit Cross Cutting Concerns sogenannte Decorations, um Klassen, Methoden und mehr mit den Anforderungen an zusätzliches Verhalten zu attribuieren. Damit das funktioniert, muss allerdings eine Drittanbieter-Komponente während des Build-Prozesses den Quellcode auf das Vorhandensein solcher Attribute hin analysieren, um dann den Quellcode entsprechend beim Bauen zu modifizieren. Das geschieht oft auf der Ebene der Intermediate Language mittels IL-Injection.

Roslyn

Während das .NET Framework über lange Zeit hinweg keine (einfache) Möglichkeit enthielt, AOP zu integrieren und die Community auf Produkte von Drittanbietern mehr oder weniger angewiesen war (Beispiel: PostSharp), hat sich das Feld an Möglichkeiten spätestens mit der Migration auf die Roslyn-Compiler-Plattform erheblich verändert. Mit Roslyn ist es unter anderem möglich, sich in den Build-Prozess einzuschalten, Quellcode zu analysieren und dabei neuen Quellcode programmatisch zu erzeugen. Der wird dabei wiederum direkt im weiteren Build-Prozess eingebunden. Da Roslyn praktisch permanent im Hintergrund agiert, passiert das auch schon während der Arbeit in der Entwicklungsumgebung.Auf dieser Grundlage arbeiten auch die Source-Generatoren des MVVM Toolkits. Mehr Informationen dazu gibt es unter [2]. Das bedeutet, dass während der Entwicklung Klassen und Methoden einfach attribuiert werden können und ohne große Verzögerung der erforderliche Code im Hintergrund erzeugt und eingebunden wird. Damit das funktioniert, sind im Grunde genommen kaum mehr als eine kleine Umstellung und die Umgewöhnung an ein paar Konventionen nötig.

Ab jetzt wird observiert

Damit das MVVM Toolkit den erforderlichen Code generieren kann, um dafür zu sorgen, dass bei Veränderungen einer Property alle passend informiert werden, wird das obige Beispiel wie im Listing 2 zu sehen umgebaut.
Listing 2: Standard-Implementierung eines einfachen ViewModels mittels MVVM Toolkit
public partial class MainViewModel : ObservableObject <br/>{ <br/>  [ObservableProperty] <br/>  string firstName = string.Empty;  <br/>}  
Dabei sind drei Dinge auffällig (neben der Tatsache, dass sich der Code dadurch dramatisch verschlankt, einfacher lesbar ist und der Fokus wieder auf dem Inhalt liegt und nicht mehr auf dem Prozess):
  • Die Klasse erbt nun von ObservableObject. Das ist notwendig, da diese Basisklasse die allseits bekannte Schnittstelle INotifyPropertyChanged implementiert (und noch ein bisschen mehr).
  • Die Property ist verschwunden und wurde ersetzt durch ein privates Backing Field, das mit ObservableProperty attribuiert wurde.
  • Die Klasse ist nun (zwingend) partial. Warum, wird gleich klar. 
Durch die Attribuierung mittels ObservableProperty springt sofort der Background Compiler an, analysiert den Quellcode und erzeugt einige Zeilen Code, die in Visual Studio im Solution Explorer zu finden sind (siehe Bild 1).
Source-Generatorenim Solution Explorer(Bild 1) © Autor
Im generierten Code ist dann auch zu erkennen, warum es zur neuen Konvention gehört, statt einer Property nur noch ein Backing Field zu implementieren, und warum es erforderlich ist, die Klasse als partial zu markieren: Der Generator erweitert die Klasse ganz einfach um passende Properties, deren Benennungen sich nach dem Namen des Backing Fields richten. Innerhalb des generierten Codes werden dabei Methoden aufgerufen, die ebenfalls verfügbar sein müssen und ihren Ursprung in der Basisklasse ObservableObject haben (siehe Listing 3).
Listing 3: Vom MVVM Toolkit generierter ViewModel-Code
partial class MainViewModel <br/>{ <br/>  /// <inheritdoc cref="firstName"/> <br/>  [global::System.CodeDom.Compiler.GeneratedCode( <br/>    "CommunityToolkit.Mvvm.SourceGenerators<br/>    .ObservablePropertyGenerator", "8.1.0.0")] <br/>  [global::System.Diagnostics.CodeAnalysis<br/>    .ExcludeFromCodeCoverage] <br/>  public string FirstName <br/>  { <br/>    get => firstName; <br/>    set <br/>    { <br/>      if (!global::System.Collections.Generic<br/>          .EqualityComparer<string>.Default.<br/>          Equals(firstName, value)) <br/>      { <br/>        OnFirstNameChanging(value); <br/>        OnPropertyChanging( <br/>          global::CommunityToolkit.Mvvm.ComponentModel<br/>          .__Internals.__KnownINotifyPropertyChanging<br/>          Args.FirstName); <br/>        firstName = value; <br/>        OnFirstNameChanged(value); <br/>        OnPropertyChanged( <br/>          global::CommunityToolkit.Mvvm.ComponentModel<br/>          .__Internals.__KnownINotifyPropertyChanged<br/>          Args.FirstName); <br/>      } <br/>    } <br/>  } <br/>  /// <summary>Executes the logic for when <see <br/>  /// cref="FirstName"/> is changing.</summary> <br/>  [global::System.CodeDom.Compiler.GeneratedCode( <br/>    "CommunityToolkit.Mvvm.SourceGenerators<br/>    .ObservablePropertyGenerator", "8.1.0.0")] <br/>  partial void OnFirstNameChanging(string value); <br/>  /// <summary>Executes the logic for when <see <br/>  /// cref="FirstName"/> just changed.</summary> <br/>  [global::System.CodeDom.Compiler.GeneratedCode( <br/>    "CommunityToolkit.Mvvm.SourceGenerators<br/>    .ObservablePropertyGenerator", "8.1.0.0")] <br/>  partial void OnFirstNameChanged(string value); <br/>}  
Die Implementierung macht auch den Prozess transparent. Es ist nicht nur so, dass vor einer Veränderung der Property geprüft wird, ob sich der Wert überhaupt verändert hat; über die eigentliche Notification hinaus gibt es sogar noch einige Extension Points frei Haus: Es ist möglich, sich in den Veränderungsprozess einzuschalten, um zusätzlichen Code direkt vor der Veränderung oder auch danach auszuführen. Der Generator erzeugt dafür partielle Methodenstubs für die Ereignisse OnChanging und OnChanged.Das kann beispielsweise dann sinnvoll sein, wenn Settings verändert werden und zusätzlich zu den Properties beispielsweise auch Preferences gesetzt werden sollen.

RelayCommand

.NET-MAUI-Controls wie etwa Buttons unterschiedlicher Art verfügen wie gewohnt über Eventhandler wie beispielsweise Clicked. Darüber lässt sich die Reaktion auf das Tippen auf einen Button wie auch in Windows Forms oft direkt im Code-behind der XAML-Datei implementieren.Obwohl weiter oben in diesem Artikel bereits erläutert wurde, dass das praktisch ein Antipattern ist (insofern mit dem MVVM Toolkit gearbeitet wird), darf an dieser Stelle da­rauf hingewiesen werden, dass es Ausnahmefälle gibt, in denen es schlicht an Alterna­tiven mangelt.Es gibt Situationen, in denen eine Applikation ein UI-Verhalten zeigen soll, das sich rein an die User Experience richtet und in diesem Sinne keine Geschäftslogik abbildet. Ein Beispiel dafür wären Animationen. Mitunter existiert also schon technisch keine (dafür vorgesehene) Möglichkeit, über Data Binding derartige Mechanismen über ein ViewModel zu implementieren.Handelt es sich indes um tatsächliche Geschäftslogik, gestaltet sich die Umsetzung mittels des MVVM Toolkits nun um einiges entspannter als noch zuvor. Ohne das MVVM Toolkit ist es erforderlich, ein Command als Property vom Typ ICommand zu konfigurieren und dann beispielsweise im Constructor der Klasse zu initialisieren. Ein Beispiel dafür ist in Listing 4 zu sehen.
Listing 4: Standard-Implementierung des Command-Pattern in .NET MAUI
public class SettingsViewModel : <br/>    INotifyPropertyChanged <br/>{ <br/>  public SettingsViewModel() <br/>  { <br/>    ResetDatabaseCommand = new <br/>      Command( execute: async () => <br/>      { <br/>        await ResetDatabase(); <br/>      }); <br/>  } <br/>  public async Task ResetDatabase() <br/>  {      <br/>    // Todo: Do something to reset <br/>    // the database <br/>  } <br/><br/>  public ICommand ResetDatabase<br/>    Command { private set; get; }  <br/>  public event PropertyChangedEvent<br/>    Handler PropertyChanged; <br/>}  
Listing 5 zeigt, wie einfach das gleiche Ziel mittels MVVM Toolkit erreicht werden kann.
Listing 5: Standard-Implementierung eines Commands mittels MVVM Toolkit
public partial class SettingsViewModel : ObservableObject <br/>{ <br/>  [RelayCommand] <br/>  public async Task ResetDatabase() <br/>  {      <br/>    // Todo: Do something to reset the database        <br/>  } <br/>}  
Auch zeigt sich hier eine weitere wichtige Konvention: Während ein Command beim Binding in XAML auch das Suffix Command enthält, ist das beim tatsächlichen Methodennamen nicht der Fall. Das hat praktisch den gleichen Grund wie auch bei der Implementierung von Properties, die als kleingeschrie­bene private Backing Fields implementiert werden müssen, um dann als Camel Cased Property generiert zu werden: Der RelayCommandGenerator erzeugt passend zu der Methode einen Wrapper, der für die Nutzung in XAML das Suffix Command enthält und auf die private Member-Methode verweist (siehe Listing 6).
Listing 6: Vom MVVM Toolkit generierter Command-Code
partial class SettingsViewModel <br/>{ <br/>  /// <summary>The backing field for <see <br/>  /// cref="ResetDatabaseCommand"/>.</summary> <br/>  [global::System.CodeDom.Compiler.GeneratedCode( <br/>    "CommunityToolkit.Mvvm.SourceGenerators<br/>    .RelayCommandGenerator", "8.1.0.0")] <br/>  private global::CommunityToolkit.Mvvm.Input<br/>    .AsyncRelayCommand? resetDatabaseCommand; <br/><br/>  /// <summary>Gets an <see cref="global::Community<br/>  /// Toolkit.Mvvm.Input.IAsyncRelayCommand"/> <br/>  /// instance wrapping <see cref="ResetDatabase"/>.<br/>  /// </summary> <br/>  [global::System.CodeDom.Compiler.GeneratedCode( <br/>    "CommunityToolkit.Mvvm.SourceGenerators<br/>    .RelayCommandGenerator", "8.1.0.0")] <br/>  [global::System.Diagnostics.CodeAnalysis<br/>    .ExcludeFromCodeCoverage] <br/>  public global::CommunityToolkit.Mvvm.Input.<br/>    IAsyncRelayCommand ResetDatabaseCommand => <br/>    resetDatabaseCommand ??= new global::<br/>    CommunityToolkit.Mvvm.Input.AsyncRelayCommand( <br/>    new global::System.Func<global::System.Threading<br/>    .Tasks.Task>(ResetDatabase)); <br/>}  
Die Dokumentation des ICommand-Interfaces für .NET MAUI zeigt zusätzlich, dass es möglich ist, die Ausführung ­eines Kommandos zu verhindern [3]. Dabei wird einem neu instanzierten RelayCommand dessen Eigenschaft CanExe­cute gesetzt und in der Regel von entsprechend gewünschten Zuständen abhängig gemacht. Mittels MVVM Toolkit funktioniert das analog, indem dem RelayCommand direkt im Attribut der Name einer Methode mitgegeben wird, die eine entsprechende Prüfung vornimmt [4].

Spezialfälle

Ob es sich dabei nun um Spezialfälle handelt oder eher einen Standard, lässt sich schwer sagen und ist in der Realität von Fall zu Fall unterschiedlich zu betrachten. Aber was passiert eigentlich, wenn sich der Zustand einer Applikation durch die Interaktion mit dem Benutzer verändert und dadurch ein Command möglicherweise deaktiviert werden muss? Für solche und ähnliche Fälle hält das MVVM Toolkit diverse Hilfsattribute parat.Mittels NotifyCanExecuteChangedFor beispielsweise kann eine Property derart konfiguriert werden, dass bei einer Änderung automatisch eine bestimmte zum Beispiel als RelayCommand attribuierte Methode aufgerufen wird. So lassen sich – ebenfalls ähnlich wie bei verschiedenen Extension Points der Properties – auch komplexere Szenarien umsetzen.

Registrierung

Wer beim Lesen dieser Einführung bereits probiert hat, das bis hierhin Gelernte umzusetzen, wird vermutlich über die ­eine oder andere Fehlermeldung gestolpert sein. Denn bevor Views und ViewModels – ohne direkt voneinander zu wissen – miteinander arbeiten können, müssen sie über die ServiceCollection registriert werden.Und wer schon einmal Services in einer .NET-MAUI-­Anwendung registriert hat, dem ist die Methode AddSingleton sicher ein Begriff. Innerhalb der statischen Methode ­CreateMauiApp – zu finden in der Datei MauiProgram.cs – lassen sich über das Builder-Pattern Services aller Art über deren Schnittstellen und konkrete Implementierungen registrieren, damit das in .NET MAUI integrierte IoC-API (Inversion of Control) in die Lage versetzt wird, Instanzen direkt über Constructor Injection beispielsweise in ein ViewModel zu injizieren.Das klang vielleicht kompliziert, meint im Grunde genommen aber nichts anderes, als dass das .NET Framework Abhängigkeiten eher locker nimmt. Was das genau bedeutet – und man möge den misslungenen Wortwitz verzeihen – wird gleich deutlich. Das Schöne daran: Was mit Services funktioniert, klappt auch mit ViewModels und ermöglicht es so, alle Bestandteile des MVVM Toolkits auf denkbar einfache Weise „zusammenzustecken“.Ob ViewModels und Views nun allerdings mittels AddSingleton, AddTransient oder AddScoped registriert werden, ist davon abhängig, welchem Zweck sie jeweils dienen.Es erklärt sich übrigens von selbst, dass es keine gute Idee wäre, Scoped- oder transiente Objekte zum Beispiel in einen Singleton Service zu injizieren, denn das würde sie praktisch in ein Singleton überführen. Ebenso wenig sollte ein tran­sienter Service in einen Scoped-Service überführt werden, da analog zu Ersterem der transiente Service so zu einem Scoped-Service würde.Ist beispielsweise eine große Anzahl von Views und ViewModels für die Constructor Injection zu registrieren und würde es genügen, sie alle beispielsweise transient hinzuzu­fügen, lässt sich das mittels Reflection-Magie vereinfachen (siehe Listing 7).
Listing 7: Registrieren aller ViewModels und Views mit Reflection
public static MauiApp CreateMauiApp() <br/>{ <br/>  var builder = MauiApp.CreateBuilder(); <br/>  builder <br/>    .UseMauiApp<App>(); <br/>  var currentAssembly = Assembly.GetExecutingAssembly(); <br/>  foreach (var type in currentAssembly.DefinedTypes.Where( e => <br/>      e.IsSubclassOf(typeof(Page)) || <br/>      e.IsSubclassOf(typeof(ObservableObject)))) <br/>  { <br/>    builder.Services.AddTransient(type.AsType()); <br/>  } <br/>  return builder.Build(); <br/>}  

Best Practices

Da das Registrieren von Services, ViewModels und Views über MauiAppBuilder-Extensions realisiert wird, ist man maximal flexibel in der Wahl darin, wo genau das im Code umgesetzt wird. Eine Möglichkeit ist es natürlich, alle Objekte direkt in der Methode CreateMauiApp zu registrieren.Es zeigt sich allerdings in immer mehr Sample-Repositories, dass es deutlich beliebter zu sein scheint, die jeweiligen Registrierungen dort durchzuführen, wo die dazu passenden Klassen aufgenommen werden.Der Vorteil liegt darin, dass die unterschiedlichen Themen so sauber voneinander getrennt werden. Und dabei muss es sich nicht einmal um Services handeln, da die jeweilige Extension lediglich den MauiAppBuilder zurückgeben muss.In Listing 8 findet sich ein Beispiel für das Registrieren von Microsoft AppCenter, was originär nicht Bestandteil des MauiAppBuilders ist, didaktisch dort aber gut aufgehoben sein kann.
Listing 8: Registrieren eines externen Dienstes innerhalb des Builder-Patterns
public static partial class ConfigServicesExtensions <br/>{ <br/>  public static MauiAppBuilder RegisterAppCenter(<br/>      this MauiAppBuilder builder) <br/>  { <br/>    if (!string.IsNullOrWhiteSpace(<br/>        AppConstants.AppCenterStart)) <br/>    { <br/>      var startServices = new List<Type>(); <br/>      if (Preferences.Get("ConsentAnalytics", false)) <br/>        startServices.Add(typeof(Analytics)); <br/>      if (Preferences.Get("ConsentDiagnostics", <br/>        false)) startServices.Add(typeof(Crashes)); <br/><br/>      AppCenter.Start(AppConstants.AppCenterStart, <br/>        startServices.ToArray()); <br/>    } <br/>    return builder; <br/>  } <br/>}  
Die Erweiterungsmethode kann sich in einem beliebigen Verzeichnis befinden. Der Aufruf findet dann in CreateMauiApp einfach in folgender Weise statt:
builder 
  .UseMauiApp<App>() 
  .RegisterAppCenter(); 

Projektstruktur

Wer diese Einführung in das MVVM Toolkit aufmerksam verfolgt hat, wird vielleicht festgestellt haben, dass sie die Aufmerksamkeit zu lenken versucht und daher nicht wie ein Tutorial zu lesen ist. Für Letzteres wäre der Aufbau des Artikels didaktisch ungünstig, denn er zäumt das Pferd von hinten auf: Die spannenden Themen begeistern direkt zu Beginn. Nutzen aber können Sie das Gelernte erst, wenn Sie den Artikel bis zum Ende gelesen haben.Daher zum Schluss die Frage danach, wie ein Projekt, das das MVVM Toolkit einsetzt, idealerweise zu strukturieren ist.Die Antwort ist ganz einfach: Es ist im Grunde genommen egal und hängt (fast) einzig von Ihren persönlichen Vorlieben ab. Die meisten Entwickler haben sich allerdings stillschweigend darauf geeinigt, Verzeichnisse für Models, Views und ViewModels anzulegen (siehe Bild 2).
Typische Projektstruktureiner .NET-MAUI-Anwendung(Bild 2) © Autor
Es ist darüber hinaus nicht unüblich, die Dateien zwar physisch in Unterverzeichnissen zu verorten, die enthaltenen Klassen aber im Namespace des Projekts zu belassen. Aus MVVMDemo.ViewModels.MainViewModel wird dann einfach MVVMDemo.MainViewModel. Der Vorteil dabei liegt darin, dass für das Referenzieren von ViewModels, Controls, Konvertern et cetera in den jeweiligen XAML-Dateien keine separaten Namespaces mehr importiert werden müssen. Ob das für mehr Übersichtlichkeit sorgt oder nicht, muss jeder für sich selbst entscheiden und dabei natürlich auch daran denken, dass Visual Studio zumindest bei Verwendung von C# standardmäßig für neu angelegte Klassen automatisch Namespaces erzeugt, die sich an der Verzeichnisstruktur orientieren.Gleiches gilt auch für den Umgang mit den Begrifflichkeiten. Aufgrund der Bezeichnung des hier vorgestellten Patterns als Model-View-ViewModel haben sich die Begriffe Model, View und ViewModel auch für die Benennung der Verzeichnisse durchgesetzt. .NET MAUI wiederum spricht nativ von sogenannten ContentPages und ContentViews. Eine ContentPage ist dazu gedacht, den kompletten Bildschirm zu füllen, und eine ContentView ist nur ein Teilaspekt eines bildschirmfüllenden UI und kann darüber hinaus auch in mehreren Pages oder wiederum anderen Views eingesetzt respektive wiederverwendet werden. Das zeigt, dass die Typenbezeichnungen ContentPage und ContentView nicht (direkt) mit dem Begriff „View“ aus dem MVVM Pattern zusammenhängen. Ob man die Klassen also entsprechend beispielsweise MainView nennt oder es bei der Bezeichnung MainPage belässt – so wie im Default .NET-MAUI-Template vorgegeben –, ist ebenfalls einem selbst überlassen.

Fazit

Auch wenn das Thema MVVM vor allem bei Frameworks wie UWP, Xamarin, .NET MAUI und Co. inzwischen praktisch als Standard gesetzt ist, ist es wichtig zu verstehen, was der Nutzen ist und dass es Szenarien gibt, die (ohne unverhältnismäßigen Aufwand) nur über Code-behind lösbar sind.Und auch wenn Testing, Bereitstellung von Geschäftslogik über ganz unterschiedliche Plattformen hinweg oder die Trennung der Aufgaben in beispielsweise Designer und Entwickler vielfach nicht im Vordergrund stehen, zeigt sich der Nutzen von MVVM in .NET MAUI (und zugegeben: Auch weiteren Frameworks) allein schon, sobald das MVVM Toolkit ins Spiel kommt.Die Code-Beispiele zeigen eindrucksvoll, wie einfach die einzelnen Bestandteile miteinander lose gekoppelt werden können und wie diese dabei gleichzeitig so sehr an Komple­xität verlieren, dass der Code im Anschluss schlanker, übersichtlicher und deutlich leichter zu warten wird. Darüber hinaus ermöglicht es der Einsatz des MVVM Toolkits, den Blick auf das Wesentliche zu richten, statt sich mit Boiler­plate-Code die Finger wund zu tippen. Das beschleunigt nicht nur die Entwicklung, es reduziert auch die Fehleranfälligkeit.Natürlich gibt es neben dem Microsoft.CommunityToolkit noch weitere Frameworks, die einem den Einsatz von MVVM auf ihre jeweils spezifische Art erleichtern. Da sie in der Regel ­einem eigenen Ökosystem entspringen, implementieren sie beispielsweise oft eigene Dependency-Injection-Container, während das MVVM Toolkit grundsätzlich auf das bereits im .NET Framework vorhandene zugreift.Und auch hier gilt: Wie an anderen Stellen auch darf jeder selbst für sich entscheiden, ob sich dadurch zusätzliche Abhängigkeiten ergeben, die zu vermeiden eine gute Idee sein kann. Zumindest im Kontext der .NET-MAUI-Entwicklung lässt sich zweifelsfrei feststellen: Das MVVM Toolkit ist der perfekte Begleiter auf dem Weg zu professionellen, plattform­unabhängigen Apps.

Ein Schmankerl zum Schluss

Wie versprochen gibt es zum Abschluss noch einen Tipp, der die App-Entwicklung mit .NET MAUI deutlich beschleunigen kann. Und das im wahrsten Sinne des Wortes: Insbesondere bei der Erstellung einer komplett neuen Applikation empfiehlt sich die kostenlose Visual-Studio-Extension MAUI App Accelerator. Zu finden ist diese erwartungsgemäß im Visual Studio Marketplace unter [5].Matt Lacey hat damit eine völlig neue Experience for File | New Project für .NET-MAUI-Applikationen geschaffen. Nicht nur ist es einfach möglich, sich gleich zu Beginn zwischen
.NET 6 und .NET 7 zu entscheiden, sondern es lässt sich auch direkt festlegen, ob mit dem MVVM Toolkit gearbeitet werden soll oder doch Code-behind bevorzugt wird (siehe Bild 3). In jedem Fall kann aber nicht nur im Anschluss unterschieden werden zwischen einer komplett leeren App, einer solchen, die mit Tabs navigiert oder dazu ein Flyout-Menü nutzt: Es können sogar Stubs für die Struktur der Anwendung vorkonfiguriert werden (leere Pages, solche mit Listendetails, mit Zeichnungen, WebViews, Pages mit Unterstützung mehrerer Sprachen oder einfach die Default Sample Page von
.NET MAUI).
MAUIApp Accelerator(Bild 3) © Autor
Aktuell (Stand November 2022) lässt sich als zusätzliches Feature noch ein xUnit-Testing-Projekt in der Solution ergänzen. Auf der GitHub-Seite der Extension [6] sind darüber hinaus weitere Features im Gespräch – unter anderem auch die Integra­tion von Microsoft AppCenter. 

Fussnoten

  1. Archivierter Blog-Artikel von John Gossman zur ­Einführung von MVVM, http://www.dotnetpro.de/SL2302CommunityToolkit1
  2. Dokumentation zu Source Generatoren als Teil der .NET Compiler Plattform, http://www.dotnetpro.de/SL2302CommunityToolkit2
  3. Dokumentation zum Thema Commanding in .NET MAUI, http://www.dotnetpro.de/SL2302CommunityToolkit3
  4. Dokumentation zum RelayCommand im MVVM Toolkit, http://www.dotnetpro.de/SL2302CommunityToolkit4
  5. Visual Studio MAUI App Accelerator, http://www.dotnetpro.de/SL2302CommunityToolkit5
  6. MAUI App Accelerator Projektseite auf GitHub, http://www.dotnetpro.de/SL2302CommunityToolkit6
  7. .NET MAUI Community Toolkit Event to Command Behavior, http://www.dotnetpro.de/SL2302CommunityToolkit7

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
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige