Model View ViewModel (MVVM)

Tipp

Diese Inhalte sind ein Auszug aus dem eBook „Enterprise Application Patterns Using .NET MAUI“, verfügbar unter .NET Docs oder als kostenlos herunterladbare PDF-Datei, die offline gelesen werden kann.

Miniaturansicht: Deckblatt des E-Books „Enterprise Application Patterns Using .NET MAUI“

Die Entwicklung mit .NET MAUI umfasst in der Regel das Erstellen einer Benutzeroberfläche in XAML und das anschließende Hinzufügen von CodeBehind für Vorgänge auf der Benutzeroberfläche. Komplexe Wartungsprobleme können auftreten, wenn Apps geändert werden und größer und umfangreicher werden. Diese Probleme schließen die enge Kopplung zwischen den UI-Steuerelementen und der Geschäftslogik ein, die die Kosten für UI-Änderungen erhöht, sowie die Schwierigkeit der Durchführung von Komponententests für solchen Code.

Das MVVM-Muster hilft dabei, die Geschäfts- und Präsentationslogik einer Anwendung klar von ihrer Benutzeroberfläche (user interface, UI) zu trennen. Eine klare Trennung zwischen Anwendungslogik und Benutzeroberfläche trägt zur Behebung zahlreicher Entwicklungsprobleme bei und erleichtert das Testen, Verwalten und Weiterentwickeln von Anwendungen. Des Weiteren kann sie die Wiederverwendungsmöglichkeiten von Code erheblich verbessern und die Zusammenarbeit von Entwicklern und Benutzeroberflächendesignern bei der Entwicklung ihrer jeweiligen App-Komponenten vereinfachen.

Das MVVM-Muster

Das MVVM-Muster umfasst drei Kernkomponenten: das Modell (Model), die Ansicht (View) und das Ansichtsmodell (ViewModel). Diese dienen jeweils einem bestimmten Zweck. Das folgende Diagramm veranschaulicht die Beziehungen zwischen den drei Komponenten:

Das MVVM-Muster

Es ist nicht nur wichtig, zu verstehen, wofür die einzelnen Komponenten zuständig sind, sondern auch, wie sie miteinander interagieren. Ganz allgemein ist die Ansicht über das Ansichtsmodell informiert, und das Ansichtsmodell ist über das Modell informiert, das Modell ist allerdings nicht über das Ansichtsmodell und das Ansichtsmodell nicht über die Ansicht informiert. Das Ansichtsmodell isoliert also die Ansicht vom Modell und ermöglicht die Entwicklung des Modells unabhängig von der Ansicht.

Die Verwendung des MVVM-Musters bietet folgende Vorteile:

  • Wenn eine vorhandene Modellimplementierung bereits vorhandene Geschäftslogik kapselt, kann es schwierig oder riskant sein, sie zu ändern. In diesem Szenario fungiert das Ansichtsmodell als Adapter für die Modellklassen und verhindert erhebliche Änderungen am Modellcode.
  • Entwickler können Komponententests für das Ansichtsmodell und das Modell erstellen, ohne die Ansicht zu verwenden. Durch die Komponententests für das Ansichtsmodell können genau die gleichen Funktionen ausgeführt werden, die auch von der Ansicht verwendet werden.
  • Die App-Benutzeroberfläche kann neu gestaltet werden, ohne das Ansichtsmodell und den Modellcode anzupassen – vorausgesetzt, die Ansicht ist vollständig in XAML oder C# implementiert. Daher sollte eine neue Version der Ansicht mit dem vorhandenen Ansichtsmodell funktionieren.
  • Designer und Entwickler können während der Entwicklung unabhängig und gleichzeitig an ihren Komponenten arbeiten. Designer können sich auf die Ansicht konzentrieren, während Entwickler am Ansichtsmodell und an Modellkomponenten arbeiten können.

Der Schlüssel zur effektiven Verwendung von MVVM liegt darin, zu verstehen, wie App-Code in die richtigen Klassen integriert wird und wie die Klassen miteinander interagieren. In den folgenden Abschnitten werden die Aufgaben der einzelnen Klassen im MVVM-Muster erläutert:

Sicht

Die Ansicht dient zum Definieren der Struktur, des Layouts und der Darstellung dessen, was dem Benutzer auf dem Bildschirm angezeigt wird. Im Idealfall wird jede Ansicht in XAML definiert und verfügt über ein eingeschränktes CodeBehind, das keine Geschäftslogik enthält. Manchmal enthält das CodeBehind jedoch Benutzeroberflächenlogik, um visuelles Verhalten zu implementieren, das in XAML nur schwer auszudrücken ist (beispielsweise Animationen).

In einer .NET MAUI-Anwendung ist eine Ansicht in der Regel eine von ContentPage oder ContentView abgeleitete Klasse. Ansichten können jedoch auch durch eine Datenvorlage dargestellt werden, die die UI-Elemente angibt, mit denen ein Objekt visuell dargestellt werden soll, wenn es angezeigt wird. Eine Datenvorlage als Ansicht verfügt über kein CodeBehind und ist für die Bindung an einen bestimmten Ansichtsmodelltyp konzipiert.

Tipp

Vermeiden Sie das Aktivieren und Deaktivieren von UI-Elementen im CodeBehind.

Achten Sie darauf, dass die Ansichtsmodelle zum Definieren logischer Zustandsänderungen verwendet werden, die sich auf einige Aspekte der Ansichtsanzeige auswirken, um beispielsweise anzugeben, ob ein Befehl verfügbar ist oder ob ein Vorgang aussteht. Verwenden Sie daher zum Aktivieren und Deaktivieren von UI-Elementen eine Bindung an Anzeigemodelleigenschaften, anstatt sie im CodeBehind zu aktivieren und zu deaktivieren.

Es gibt mehrere Möglichkeiten, Code für das Ansichtsmodell als Reaktion auf Interaktionen in der Ansicht (beispielsweise Klicken auf eine Schaltfläche oder Auswählen eines Elements) auszuführen. Wenn ein Steuerelement Befehle unterstützt, kann die Command-Eigenschaft des Steuerelements an eine ICommand-Eigenschaft im Ansichtsmodell gebunden werden. Wenn der Befehl des Steuerelements aufgerufen wird, wird der Code im Ansichtsmodell ausgeführt. Neben Befehlen kann auch Verhalten an ein Objekt in der Ansicht angefügt werden und auf das Aufrufen eines Befehls oder auf das Auslösen des Ereignisses lauschen. Als Reaktion darauf kann das Verhalten dann einen ICommand-Befehl für das Ansichtsmodell oder eine Methode für das Ansichtsmodell aufrufen.

ViewModel

Das Ansichtsmodell implementiert Eigenschaften und Befehle, an die die Ansicht Daten binden kann, und informiert die Ansicht mithilfe von Änderungsbenachrichtigungsereignissen über alle Zustandsänderungen. Die durch das Ansichtsmodell bereitgestellten Eigenschaften und Befehle definieren zwar die auf der Benutzeroberfläche angebotenen Funktionen, aber die Ansicht steuert, wie diese Funktionen angezeigt werden.

Tipp

Verwenden Sie asynchrone Vorgänge, um die Reaktionsfähigkeit der Benutzeroberfläche nicht zu beeinträchtigen.

Bei multiplattformfähigen Apps sollte der UI-Thread nicht blockiert werden, um die vom Benutzer wahrgenommene Leistung zu verbessern. Nutzen Sie daher im Ansichtsmodell asynchrone Methoden für E/A-Vorgänge, und lösen Sie Ereignisse aus, um Ansichten asynchron über Eigenschaftsänderungen zu informieren.

Das Ansichtsmodell ist auch für die Koordination der Interaktionen der Ansicht mit allen erforderlichen Modellklassen zuständig. Zwischen dem Ansichtsmodell und den Modellklassen besteht in der Regel eine 1:n-Beziehung. Das Ansichtsmodell kann Modellklassen direkt für die Ansicht verfügbar machen, sodass Steuerelemente in der Ansicht Daten direkt an sie binden können. In diesem Fall müssen die Modellklassen so konzipiert sein, dass sie Datenbindung und Änderungsbenachrichtigungsereignisse unterstützen.

Jedes Ansichtsmodell stellt Daten aus einem Modell in einem Format bereit, das von der Ansicht problemlos genutzt werden kann. Hierzu führt das Ansichtsmodell manchmal Datenkonvertierungen durch. Es empfiehlt sich, diese Datenkonvertierung im Ansichtsmodell zu platzieren, da sie Eigenschaften bereitstellt, die von der Ansicht zu Bindungszwecken genutzt werden können. So kann das Ansichtsmodell beispielsweise die Werte von zwei Eigenschaften kombinieren, um die Anzeige durch die Ansicht zu erleichtern.

Tipp

Zentralisieren Sie Datenkonvertierungen auf einer Konvertierungsebene.

Es ist auch möglich, Konverter als separate Datenkonvertierungsebene zwischen Ansichtsmodell und Ansicht zu verwenden. Dies kann beispielsweise erforderlich sein, wenn Daten eine spezielle Formatierung erfordern, die durch das Ansichtsmodell nicht bereitgestellt wird.

Damit das Ansichtsmodell an der bidirektionalen Datenbindung mit der Ansicht teilnehmen kann, müssen seine Eigenschaften das Ereignis PropertyChanged auslösen. Ansichtsmodelle erfüllen diese Anforderung durch Implementieren der Schnittstelle INotifyPropertyChanged sowie durch Auslösen des Ereignisses PropertyChanged, wenn eine Eigenschaft geändert wird.

Für Sammlungen wird die ansichtsfreundliche Sammlung ObservableCollection<T> bereitgestellt. Diese Sammlung implementiert Benachrichtigungen zu geänderten Sammlungen, sodass die Schnittstelle INotifyCollectionChanged bei Sammlungen nicht implementiert werden muss.

Modell

Modellklassen sind nicht visuelle Klassen, die die Daten der App kapseln. Das Modell kann somit als Darstellung des Domänenmodells der App betrachtet werden, das in der Regel ein Datenmodell zusammen mit Geschäfts- und Validierungslogik enthält. Beispiele für Modellobjekte sind Datenübertragungsobjekte (Data Transfer Objects, DTOs) und POCOs (Plain Old CLR Objects) sowie generierte Entitäts- und Proxyobjekte.

Modellklassen werden in der Regel zusammen mit Diensten oder Repositorys verwendet, die den Datenzugriff und die Zwischenspeicherung kapseln.

Verbinden von Ansichtsmodellen mit Ansichten

Ansichtsmodelle können mithilfe der Datenbindungsfunktionen von .NET MAUI mit Ansichten verbunden werden. Es gibt viele Ansätze, die verwendet werden können, um Ansichten und Ansichtsmodelle zu erstellen und zur Laufzeit zuzuordnen. Diese Ansätze lassen sich in zwei Kategorien unterteilen: Erstellung mit Schwerpunkt auf der Ansicht und Erstellung mit Schwerpunkt auf dem Ansichtsmodell. Die Entscheidung für die Erstellung mit Schwerpunkt auf der Ansicht oder für die Erstellung mit Schwerpunkt auf dem Ansichtsmodell hängt vom bevorzugten Ansatz und von der Komplexität ab. Alle Ansätze haben jedoch zum Ziel, der BindingContext-Eigenschaft der Ansicht ein Ansichtsmodell zuzuweisen.

Bei der Erstellung mit Schwerpunkt auf der Ansicht besteht die App konzeptionell aus Ansichten, die eine Verbindung mit den Ansichtsmodellen herstellen, von denen sie abhängen. Der Hauptvorteil dieses Ansatzes besteht in der mühelosen Erstellung lose gekoppelter, für Komponententests geeignete Apps, da die Ansichtsmodelle nicht von den Ansichten selbst abhängig sind. Außerdem lässt sich die Struktur der App ganz einfach anhand der visuellen Struktur nachvollziehen, anstatt die Codeausführung nachverfolgen zu müssen, um zu verstehen, wie Klassen erstellt und zugeordnet werden. Darüber hinaus ist die Erstellung mit Schwerpunkt auf der Ansicht auf das Navigationssystem von Microsoft Maui abgestimmt, das für die Seitenerstellung bei der Navigation zuständig ist. Dies macht eine Erstellung mit Schwerpunkt auf dem Ansichtsmodell komplex und führt dazu, dass sie nicht korrekt auf die Plattform ausgerichtet ist.

Bei der Erstellung mit Schwerpunkt auf dem Ansichtsmodell besteht die App konzeptionell aus Ansichtsmodellen, und ein Dienst ist für die Suche nach der Ansicht für ein Ansichtsmodell zuständig. Die Erstellung mit Schwerpunkt auf dem Ansichtsmodell wird von einigen Entwicklern als natürlicher empfunden, da die Erstellung der Ansicht abstrahiert werden kann, sodass sich die Entwickler auf die logische UI-fremde Struktur der App konzentrieren können. Darüber hinaus ermöglicht sie die Erstellung von Ansichtsmodellen durch andere Ansichtsmodelle. Dieser Ansatz ist allerdings häufig komplex, und es kann schwierig werden, die Erstellung und Zuordnung der verschiedenen App-Komponenten nachzuvollziehen.

Tipp

Achten Sie darauf, dass Ansichtsmodelle und Ansichten unabhängig bleiben.

Die Bindung von Ansichten an eine Eigenschaft in einer Datenquelle sollte die Hauptabhängigkeit der Ansicht vom entsprechenden Ansichtsmodell sein. Verweisen Sie insbesondere nicht auf Ansichtstypen wie „Button“ und „ListView“ aus Ansichtsmodellen. Wenn Sie den hier beschriebenen Prinzipien folgen, können Ansichtsmodelle isoliert getestet werden, was den Umfang beschränkt und so die Wahrscheinlichkeit von Softwarefehlern verringert.

In den folgenden Abschnitten werden die wichtigsten Ansätze zum Verbinden von Ansichtsmodellen mit Ansichten erläutert.

Deklaratives Erstellen eines Ansichtsmodells

Der einfachste Ansatz besteht darin, die Ansicht ihr entsprechendes Ansichtsmodell deklarativ in XAML instanziieren zu lassen. Bei der Erstellung der Ansicht wird auch das entsprechende Ansichtsmodellobjekt erstellt. Dieser Ansatz wird im folgenden Codebeispiel veranschaulicht:

XAML
<ContentPage xmlns:local="clr-namespace:eShop">
    <ContentPage.BindingContext>
        <local:LoginViewModel />
    </ContentPage.BindingContext>
    <!-- Omitted for brevity... -->
</ContentPage>

Wenn ContentPage erstellt wird, wird automatisch eine Instanz von LoginViewModel erstellt und als BindingContext der Ansicht festgelegt.

Der Vorteil dieser deklarativen Erstellung und Zuweisung des Ansichtsmodells durch die Ansicht ist ihre Unkompliziertheit. Der Nachteil ist, dass im Ansichtsmodell ein Standardkonstruktor (ohne Parameter) benötigt wird.

Programmgesteuertes Erstellen eines Ansichtsmodells

Eine Ansicht kann Code in der CodeBehind-Datei enthalten, der dazu führt, dass das Ansichtsmodell der zugehörigen Eigenschaft BindingContext zugewiesen wird. Dies wird häufig im Konstruktor der Ansicht erreicht, wie im folgenden Codebeispiel zu sehen:

C#
public LoginView()
{
    InitializeComponent();
    BindingContext = new LoginViewModel(navigationService);
}

Die programmgesteuerte Erstellung und Zuweisung des Ansichtsmodells im CodeBehind der Ansicht hat den Vorteil, dass sie unkompliziert ist. Der Hauptnachteil dieses Ansatzes besteht darin, dass die Ansicht dem Ansichtsmodell gegenüber alle erforderlichen Abhängigkeiten angeben muss. Mithilfe eines Containers für die Abhängigkeitsinjektion kann eine lose Kopplung zwischen der Ansicht und dem Ansichtsmodell aufrechterhalten werden. Weitere Informationen finden Sie unter Dependency injection (Abhängigkeitsinjektion).

Aktualisieren von Ansichten als Reaktion auf Änderungen im zugrunde liegenden (Ansichts-)Modell

Alle Ansichtsmodell- und Modellklassen, auf die eine Ansicht Zugriff hat, müssen die Schnittstelle INotifyPropertyChanged implementieren. Durch die Implementierung dieser Schnittstelle in einer Ansichtsmodell- oder Modellklasse kann die Klasse Änderungsbenachrichtigungen für alle datengebundenen Steuerelemente in der Ansicht bereitstellen, wenn sich der zugrunde liegende Eigenschaftswert ändert.

Apps müssen für die korrekte Verwendung von Eigenschaftsänderungsbenachrichtigungen konzipiert sein. Hierzu müssen sie folgende Anforderungen erfüllen:

  • Es muss immer ein Ereignis vom Typ PropertyChanged ausgelöst werden, wenn sich der Wert einer öffentlichen Eigenschaft ändert. Gehen Sie nicht davon aus, dass das Auslösen des Ereignisses PropertyChanged ignoriert werden kann, weil bekannt ist, wie die XAML-Bindung erfolgt.
  • Es muss immer ein Ereignis vom Typ PropertyChanged für alle berechneten Eigenschaften ausgelöst werden, deren Werte von anderen Eigenschaften im (Ansichts-)Modell verwendet werden.
  • Das Ereignis PropertyChanged muss immer am Ende der Methode ausgelöst werden, die eine Eigenschaft ändert, oder wenn bekannt ist, dass sich das Objekt in einem sicheren Zustand befindet. Das Auslösen des Ereignisses unterbricht den Vorgang durch synchrones Aufrufen der Handler des Ereignisses. Passiert dies während eines laufenden Vorgangs, wird das Objekt möglicherweise für Rückruffunktionen verfügbar, wenn es sich in einem unsicheren, teilweise aktualisierten Zustand befindet. Darüber hinaus können durch Ereignisse vom Typ PropertyChanged auch kaskadierende Änderungen ausgelöst werden. Um kaskadierende Änderung sicher ausführen zu können, müssen Aktualisierungen in der Regel abgeschlossen sein.
  • Es wird niemals ein Ereignis vom Typ PropertyChanged ausgelöst, wenn sich die Eigenschaft nicht ändert. Das bedeutet, dass vor dem Auslösen des Ereignisses PropertyChanged die alten und neuen Werte verglichen werden müssen.
  • Wenn Sie eine Eigenschaft initialisieren, darf das Ereignis PropertyChanged niemals ausgelöst werden, während der Konstruktor eines Ansichtsmodells aktiv ist. Datengebundene Steuerelemente in der Ansicht haben an diesem Punkt noch keine Änderungsbenachrichtigungen abonniert.
  • Innerhalb eines einzelnen synchronen Aufrufs einer öffentlichen Methode einer Klasse werden niemals mehrere Ereignisse vom Typ PropertyChanged mit dem gleichen Eigenschaftsnamenargument ausgelöst. Ein Beispiel: Angenommen, der Sicherungsspeicher der Eigenschaft NumberOfItems ist das Feld _numberOfItems. Wenn eine Methode nun während der Ausführung einer Schleife _numberOfItems fünfzigmal erhöht, darf nur eine einzelne Eigenschaftsänderungsbenachrichtigung für die Eigenschaft NumberOfItems ausgelöst werden, nachdem alle Arbeiten abgeschlossen sind. Lösen Sie bei asynchronen Methoden das Ereignis PropertyChanged für einen bestimmten Eigenschaftsnamen in jedem synchronen Segment einer asynchronen Fortsetzungskette aus.

Diese Funktionen können beispielsweise ganz einfach durch Erstellen einer Erweiterung der Klasse BindableObject bereitgestellt werden. In diesem Beispiel stellt die Klasse ExtendedBindableObject Änderungsbenachrichtigungen bereit, wie im folgenden Codebeispiel zu sehen:

C#
public abstract class ExtendedBindableObject : BindableObject
{
    public void RaisePropertyChanged<T>(Expression<Func<T>> property)
    {
        var name = GetMemberInfo(property).Name;
        OnPropertyChanged(name);
    }

    private MemberInfo GetMemberInfo(Expression expression)
    {
        // Omitted for brevity ...
    }
}

Die Klasse BindableObject von .NET MAUI implementiert die Schnittstelle INotifyPropertyChanged und stellt eine Methode vom Typ OnPropertyChanged bereit. Die Klasse ExtendedBindableObject stellt die Methode RaisePropertyChanged zum Aufrufen von Eigenschaftsänderungsbenachrichtigungen bereit und verwendet dabei die von der Klasse BindableObject bereitgestellten Funktionen.

Ansichtsmodellklassen können dann von der Klasse ExtendedBindableObject abgeleitet werden. Daher verwendet jede Ansichtsmodellklasse die Methode RaisePropertyChanged in der Klasse ExtendedBindableObject, um Eigenschaftsänderungsbenachrichtigungen bereitzustellen. Das folgende Codebeispiel zeigt, wie die multiplattformfähige eShop-App Eigenschaftsänderungsbenachrichtigungen mithilfe eines Lambdaausdrucks aufruft:

C#
public bool IsLogin
{
    get => _isLogin;
    set
    {
        _isLogin = value;
        RaisePropertyChanged(() => IsLogin);
    }
}

Die Verwendung eines Lambdaausdrucks auf diese Weise führt zu geringfügigen Leistungseinbußen, da der Lambdaausdruck für jeden Aufruf ausgewertet werden muss. Die Leistungseinbußen sind gering und wirken sich in der Regel nicht auf eine App aus. Sie können allerdings zunehmen, wenn viele Änderungsbenachrichtigungen vorhanden sind. Der Vorteil dieses Ansatzes besteht jedoch in Typsicherheit und Refactoringunterstützung zur Kompilierzeit, wenn Eigenschaften umbenannt werden.

MVVM-Frameworks

Das MVVM-Muster ist in .NET gut etabliert, und die Community hat viele Frameworks erstellt, die diese Entwicklung erleichtern. Jedes Framework bietet zwar andere Features, aber standardmäßig auch ein gemeinsames Ansichtsmodell mit einer Implementierung der Schnittstelle INotifyPropertyChanged. Zu den zusätzlichen Features von MVVM-Frameworks gehören benutzerdefinierte Befehle, Navigationshilfen, Komponenten für Abhängigkeitsinjektion/Dienstlocator sowie UI-Plattformintegration. Diese Frameworks müssen zwar nicht verwendet werden, sie können Ihre Entwicklungsarbeiten allerdings beschleunigen und standardisieren. Die multiplattformfähige eShop-App verwendet das MVVM-Toolkit der .NET Community. Berücksichtigen Sie bei der Wahl eines Frameworks die Anforderungen Ihrer Anwendung und die Stärken Ihres Teams. Die folgende Liste enthält einige der gängigsten MVVM-Frameworks für .NET MAUI:

Benutzeroberflächeninteraktion mithilfe von Befehlen und Verhalten

In multiplattformfähigen Apps werden Aktionen in der Regel als Reaktion auf eine Benutzeraktion (beispielsweise Klicken auf eine Schaltfläche) aufgerufen, die durch Erstellen eines Ereignishandlers in der CodeBehind-Datei implementiert werden kann. Im MVVM-Muster ist dagegen das Ansichtsmodell für die Implementierung der Aktion zuständig, und es sollte kein Code im CodeBehind sollte platziert werden.

Befehle sind praktisch, um Aktionen darzustellen, die an Steuerelemente auf der Benutzeroberfläche gebunden werden können. Sie kapseln den Code, der die Aktion implementiert, und tragen dazu bei, ihn von der visuellen Darstellung in der Ansicht zu entkoppeln. Dadurch können Ihre Ansichtsmodelle leichter für neue Plattformen portiert werden, da sie nicht direkt von Ereignissen abhängen, die vom Benutzeroberflächenframework der Plattform bereitgestellt werden. .NET MAUI enthält Steuerelemente, die deklarativ mit einem Befehl verbunden werden können, und diese Steuerelemente rufen den Befehl auf, wenn der Benutzer mit dem Steuerelement interagiert.

Auch Verhalten ermöglicht es, Steuerelemente deklarativ mit einem Befehl zu verbinden. Verhalten kann jedoch verwendet werden, um eine Aktion aufzurufen, die einem Bereich von Ereignissen zugeordnet ist, die von einem Steuerelement ausgelöst werden. Daher behandelt Verhalten großteils die gleichen Szenarien wie befehlsbasierte Steuerelemente, bietet aber ein höheres Maß an Flexibilität und Kontrolle. Darüber hinaus kann Verhalten auch verwendet werden, um Befehlsobjekte oder Methoden Steuerelementen zuzuordnen, die nicht speziell für die Interaktion mit Befehlen entwickelt wurden.

Implementieren von Befehlen

Ansichtsmodelle machen in der Regel öffentliche Eigenschaften (für die Bindung aus der Ansicht) verfügbar, die die Schnittstelle ICommand implementieren. Viele .NET MAUI-Steuerelemente und -Gesten bieten eine Eigenschaft vom Typ Command, die per Datenbindung mit einem vom Ansichtsmodell bereitgestellten Objekt vom Typ ICommand verbunden werden kann. Das Schaltflächen-Steuerelement ist eines der am häufigsten verwendeten Steuerelemente und stellt eine Befehlseigenschaft bereit, die ausgeführt wird, wenn auf die Schaltfläche geklickt wird.

Hinweis

Es ist zwar möglich, die eigentliche Implementierung der von Ihrem Ansichtsmodell verwendeten Schnittstelle ICommand verfügbar zu machen (beispielsweise Command<T> oder RelayCommand), es empfiehlt sich jedoch, Ihre Befehle öffentlich als ICommand verfügbar zu machen. Dadurch können Sie die Implementierung später problemlos austauschen, wenn eine Änderung erforderlich sein sollte.

Die Schnittstelle ICommand definiert eine Methode vom Typ Execute, die den eigentlichen Vorgang kapselt, eine Methode vom Typ CanExecute, die angibt, ob der Befehl aufgerufen werden kann, und ein Ereignis vom Typ CanExecuteChanged, das auftritt, wenn Änderungen vorgenommen werden, die beeinflussen, ob der Befehl ausgeführt werden soll. In den meisten Fällen wird nur die Methode Execute für Befehle angegeben. Eine ausführlichere Übersicht über ICommand finden Sie in der Befehlsdokumentation für .NET MAUI.

Zusammen mit .NET MAUI werden die Klassen Command und Command<T> bereitgestellt, die die Schnittstelle ICommand implementieren, wobei T der Typ der Argumente für Execute und CanExecute ist. Command und Command<T> sind grundlegende Implementierungen, die die minimalen Funktionen bereitstellen, die für die Schnittstelle ICommand erforderlich sind.

Hinweis

Viele MVVM-Frameworks bieten Implementierungen der Schnittstelle ICommand mit mehr Features.

Der Konstruktor Command oder Command<T> erfordert ein Aktionsrückrufobjekt, das aufgerufen wird, wenn die Methode ICommand.Execute aufgerufen wird. Die Methode CanExecute ist ein optionaler Konstruktorparameter und eine Funktion, die einen booleschen Wert zurückgibt.

Die multiplattformfähige eShop-App verwendet RelayCommand und AsyncRelayCommand. Der Hauptvorteil für moderne Anwendungen besteht darin, dass AsyncRelayCommand bessere Funktionen für asynchrone Vorgänge bietet.

Der folgende Code zeigt, wie eine Command-Instanz, die einen Registrierungsbefehl darstellt, durch Angabe eines Delegaten für die Ansichtsmodellmethode zum Registrieren erstellt wird:

C#
public ICommand RegisterCommand { get; }

Der Befehl wird für die Ansicht über eine Eigenschaft verfügbar gemacht, die einen Verweis auf einen Befehl (ICommand) zurückgibt. Wenn die Methode Execute für das Objekt Command aufgerufen wird, leitet sie den Aufruf einfach über den im Konstruktor Command angegebenen Delegaten an die Methode im Ansichtsmodell weiter. Eine asynchrone Methode kann von einem Befehl aufgerufen werden, indem beim Angeben des Execute-Delegaten des Befehls die Schlüsselwörter „async“ und „await“ verwendet werden. Dadurch wird angegeben, dass es sich bei dem Rückruf um eine Aufgabe (Task) handelt und dass auf den Rückruf gewartet werden muss. Der folgende Code zeigt beispielsweise, wie eine ICommand-Instanz, die einen Anmeldebefehl darstellt, durch Angabe eines Delegaten für die Ansichtsmodellmethode SignInAsync erstellt wird:

C#
public ICommand SignInCommand { get; }
...
SignInCommand = new AsyncRelayCommand(async () => await SignInAsync());

Parameter können an die Aktionen Execute und CanExecute übergeben werden, indem die Klasse AsyncRelayCommand<T> zum Instanziieren des Befehls verwendet wird. Der folgende Code zeigt beispielsweise, wie mithilfe einer AsyncRelayCommand<T>-Instanz angegeben wird, dass die Methode NavigateAsync ein Zeichenfolgenargument erfordert:

C#
public ICommand NavigateCommand { get; }

...
NavigateCommand = new AsyncRelayCommand<string>(NavigateAsync);

Sowohl in der Klasse RelayCommand als auch in der Klasse RelayCommand<T> ist der Delegat für die Methode CanExecute in den einzelnen Konstruktoren optional. Wenn kein Delegat angegeben wird, gibt Command für CanExecute „true“ zurück. Das Ansichtsmodell kann jedoch eine Statusänderung für den Befehl CanExecute angeben, indem die Methode ChangeCanExecute für das Objekt Command aufgerufen wird. Dadurch wird das Ereignis CanExecuteChanged ausgelöst. Alle Steuerelemente der Benutzeroberfläche, die an den Befehl gebunden sind, aktualisieren daraufhin ihren Aktivierungsstatus, um die Verfügbarkeit des datengebundenen Befehls widerzuspiegeln.

Aufrufen von Befehlen über eine Ansicht

Das folgende Codebeispiel zeigt, wie ein Raster (Grid) in der Anmeldeansicht (LoginView) mithilfe einer TapGestureRecognizer-Instanz an den Registrierungsbefehl (RegisterCommand) in der Klasse LoginViewModel gebunden wird:

XML
<Grid Grid.Column="1" HorizontalOptions="Center">
    <Label Text="REGISTER" TextColor="Gray"/>
    <Grid.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding RegisterCommand}" NumberOfTapsRequired="1" />
    </Grid.GestureRecognizers>
</Grid>

Ein Befehlsparameter kann auch optional mithilfe der Eigenschaft CommandParameter definiert werden. Die Art des erwarteten Arguments wird in den Zielmethoden Execute und CanExecute angegeben. TapGestureRecognizer ruft automatisch den Zielbefehl auf, wenn der Benutzer mit dem angefügten Steuerelement interagiert. CommandParameter wird (sofern angegeben) als Argument an den Ausführungsdelegaten des Befehls übergeben.

Implementieren von Verhalten

Verhalten ermöglicht das Hinzufügen von Funktionen zu Steuerelementen der Benutzeroberfläche, ohne Unterklassen verwenden zu müssen. Stattdessen wird die Funktion in einer Verhaltensklasse implementiert und an das Steuerelement angefügt, als wäre sie ein Teil des Steuerelements selbst. Durch Verhalten können Sie Code, den Sie üblicherweise in eine CodeBehind-Datei schreiben müssten, da er direkt mit der API des Steuerelements interagiert, so implementieren, dass er präzise an das Steuerelement angefügt werden und zur Wiederverwendung in mehreren Ansichten oder Apps gepackt werden kann. Im Kontext von MVVM ist Verhalten nützlich, um Steuerelemente mit Befehlen zu verbinden.

Ein Verhalten, das über angefügte Eigenschaften an ein Steuerelement angefügt wird, wird als angefügtes Verhalten bezeichnet. Das Verhalten kann dann die verfügbar gemachte API des Elements verwenden, an das es angefügt ist, um diesem Steuerelement (oder anderen Steuerelementen) in der visuellen Struktur der Ansicht Funktionen hinzuzufügen.

Ein .NET MAUI-Verhalten ist eine Klasse, die von der Klasse Behavior oder Behavior<T> abgeleitet wird. „T“ ist hierbei der Typ des Steuerelements, für das das Verhalten gelten soll. Diese Klassen stellen Methoden vom Typ OnAttachedTo und OnDetachingFrom bereit, die überschrieben werden müssen, um Logik bereitzustellen, die ausgeführt wird, wenn das Verhalten an Steuerelemente angefügt bzw. von ihnen getrennt wird.

In der multiplattformfähigen eShop-App wird die Klasse BindableBehavior<T> von der Klasse Behavior<T> abgeleitet. Der Zweck der Klasse BindableBehavior<T> besteht darin, eine Basisklasse für .NET MAUI-Verhalten bereitzustellen, für das der Bindungskontext (BindingContext) des Verhaltens auf das angefügte Steuerelement festgelegt sein muss.

Die Klasse BindableBehavior<T> stellt eine überschreibbare Methode vom Typ OnAttachedTo bereit, die den Bindungskontext (BindingContext) des Verhaltens festlegt, sowie eine überschreibbare Methode vom Typ OnDetachingFrom, die den Bindungskontext (BindingContext) bereinigt.

Die multiplattformfähige eShop-App enthält eine Klasse vom Typ EventToCommandBehavior aus dem Community-Toolkit für MAUI. EventToCommandBehavior führt einen Befehl als Reaktion auf ein Ereignis aus. Diese Klasse wird von der Klasse BaseBehavior<View> abgeleitet, sodass das Verhalten an eine durch eine Command-Eigenschaft angegebene ICommand-Instanz gebunden werden und diese ausführen kann, wenn das Verhalten genutzt wird. Das folgende Codebeispiel zeigt die EventToCommandBehavior-Klasse:

C#
/// <summary>
/// The <see cref="EventToCommandBehavior"/> is a behavior that allows the user to invoke a <see cref="ICommand"/> through an event. It is designed to associate Commands to events exposed by controls that were not designed to support Commands. It allows you to map any arbitrary event on a control to a Command.
/// </summary>
public class EventToCommandBehavior : BaseBehavior<VisualElement>
{
    // Omitted for brevity...

    /// <inheritdoc/>
    protected override void OnAttachedTo(VisualElement bindable)
    {
        base.OnAttachedTo(bindable);
        RegisterEvent();
    }

    /// <inheritdoc/>
    protected override void OnDetachingFrom(VisualElement bindable)
    {
        UnregisterEvent();
        base.OnDetachingFrom(bindable);
    }

    static void OnEventNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
        => ((EventToCommandBehavior)bindable).RegisterEvent();

    void RegisterEvent()
    {
        UnregisterEvent();

        var eventName = EventName;
        if (View is null || string.IsNullOrWhiteSpace(eventName))
        {
            return;
        }

        eventInfo = View.GetType()?.GetRuntimeEvent(eventName) ??
            throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't resolve the event.", nameof(EventName));

        ArgumentNullException.ThrowIfNull(eventInfo.EventHandlerType);
        ArgumentNullException.ThrowIfNull(eventHandlerMethodInfo);

        eventHandler = eventHandlerMethodInfo.CreateDelegate(eventInfo.EventHandlerType, this) ??
            throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't create event handler.", nameof(EventName));

        eventInfo.AddEventHandler(View, eventHandler);
    }

    void UnregisterEvent()
    {
        if (eventInfo is not null && eventHandler is not null)
        {
            eventInfo.RemoveEventHandler(View, eventHandler);
        }

        eventInfo = null;
        eventHandler = null;
    }

    /// <summary>
    /// Virtual method that executes when a Command is invoked
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    [Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
    protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null)
    {
        var parameter = CommandParameter
            ?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null);

        var command = Command;
        if (command?.CanExecute(parameter) ?? false)
        {
            command.Execute(parameter);
        }
    }
}

Die Methoden OnAttachedTo und OnDetachingFrom werden verwendet, um einen Ereignishandler für das in der Eigenschaft EventName definierte Ereignis zu registrieren bzw. seine Registrierung aufzuheben. Wenn das Ereignis ausgelöst wird, wird die Methode OnTriggerHandled aufgerufen, wodurch wiederum der Befehl ausgeführt wird.

Die Verwendung von EventToCommandBehavior zum Ausführen eines Befehls, wenn ein Ereignis ausgelöst wird, hat den Vorteil, dass Befehle mit Steuerelementen verknüpft werden können, die nicht für die Interaktion mit Befehlen konzipiert wurden. Außerdem wird der Code für die Ereignisbehandlung dadurch in Ansichtsmodelle verlagert, wo Komponententests für ihn ausgeführt werden können.

Aufrufen von Verhalten über eine Ansicht

EventToCommandBehavior ist besonders nützlich, um einen Befehl an ein Steuerelement anzufügen, das keine Befehle unterstützt. Die Anmeldeansicht (LoginView) verwendet beispielsweise EventToCommandBehavior, um den Validierungsbefehl (ValidateCommand) auszuführen, wenn der Benutzer den Wert seines Kennworts ändert, wie im folgenden Code gezeigt:

XAML
<Entry
    IsPassword="True"
    Text="{Binding Password.Value, Mode=TwoWay}">
    <!-- Omitted for brevity... -->
    <Entry.Behaviors>
        <mct:EventToCommandBehavior
            EventName="TextChanged"
            Command="{Binding ValidateCommand}" />
    </Entry.Behaviors>
    <!-- Omitted for brevity... -->
</Entry>

Zur Laufzeit reagiert EventToCommandBehavior auf die Interaktion mit der Eingabe (Entry). Wenn ein Benutzer etwas in das Feld Entry eingibt, wird das Ereignis TextChanged ausgelöst, wodurch der Validierungsbefehl (ValidateCommand) in LoginViewModel ausgeführt wird. Standardmäßig werden die Ereignisargumente für das Ereignis an den Befehl übergeben. Bei Bedarf kann die Eigenschaft EventArgsConverter verwendet werden, um die vom Ereignis bereitgestellten Ereignisargumente (EventArgs) in einen Wert zu konvertieren, den der Befehl als Eingabe erwartet.

Weitere Informationen zu Verhalten finden Sie im .NET MAUI Developer Center unter Verhalten.

Zusammenfassung

Das MVVM-Muster (Model View ViewModel) hilft dabei, die Geschäfts- und Präsentationslogik einer Anwendung klar von ihrer Benutzeroberfläche (user interface, UI) zu trennen. Eine klare Trennung zwischen Anwendungslogik und Benutzeroberfläche trägt zur Behebung zahlreicher Entwicklungsprobleme bei und erleichtert das Testen, Verwalten und Weiterentwickeln von Anwendungen. Des Weiteren kann sie die Wiederverwendungsmöglichkeiten von Code erheblich verbessern und die Zusammenarbeit von Entwicklern und Benutzeroberflächendesignern bei der Entwicklung ihrer jeweiligen App-Komponenten vereinfachen.

Mithilfe des MVVM-Musters werden die Benutzeroberfläche der App und die zugrunde liegende Präsentations- und Geschäftslogik in drei separate Klassen unterteilt: die Ansicht (kapselt die Benutzeroberfläche und die Benutzeroberflächenlogik), das Ansichtsmodell (kapselt die Präsentationslogik und den Zustand) und das Modell (kapselt die Geschäftslogik und die Daten der App).