5. HA - Verwendung der MVVM-Muster und des MVVM-Toolkits¶
Einführung¶
In dieser Hausaufgabe werden wir die während der 3. Laborübung (XAML) implementierte Anwendung für Personenregistrierung so verändern, dass sie auf dem MVVM-Muster basiert, und wir werden das MVVM-Toolkit kennenlernen.
Die Hausaufgabe baut auf dem MVVM-Thema auf, das am Ende der WinUI-Vorlesungsreihe behandelt wurde. Die praktische Grundlage für die Aufgaben bildet die 5. Laborübung – MVVM.
Durch das Durcharbeiten des zugehörigen Vorlesungsmaterials können die Aufgaben dieser eigenständigen Übung mit Hilfe der kürzeren Leitfäden, die auf die Aufgabenbeschreibung folgen (manchmal standardmäßig eingefaltet), selbständig bearbeitet werden.
Das Ziel der Hausaufgabe:
- Üben der Verwendung der MVVM-Muster
- NuGet-Referenzen verwenden
- Kennenlernen der Grundlagen des MVVM-Toolkits
- Üben von XAML-Techniken
Die erforderliche Entwicklungsumgebung wird hier beschrieben, identisch mit Hausaufgabe 3 (XAML-Grundlagen).
Das Verfahren der Eingabe¶
- Der grundlegende Ablauf ist derselbe wie zuvor. Erstelle mit GitHub Classroom ein eigenes Repository. Die Einladungs-URL findest du in Moodle (bei Hausaufgabe 4.). Klone das so erstellte Repository. Dieses enthält die erwartete Struktur der Lösung. Nach der Fertigstellung der Aufgaben committe und pushe deine Lösung.
- Schreibe deinen Neptun-Code in die Datei „neptun.txt“!
- Öffne
HelloXaml.slnaus den geklonten Dateien und arbeite in diesem. Die Aufgaben verlangen, dass du Screenshots von bestimmten Teilen deiner Lösung erstellst, um zu belegen, dass du sie selbst angefertigt hast. Der erwartete Inhalt der Screenshots wird in jeder Aufgabe genau angegeben. Die Screenshots müssen als Teil der Lösung eingegeben werden. Lege sie im Stammverzeichnis deines Repositorys ab (neben der Datei
neptun.txt). Dadurch werden die Screenshots zusammen mit dem Inhalt des Git-Repositorys auf GitHub hochgeladen. Da das Repository privat ist, können es außer den Lehrkräfte keine anderen Personen sehen. Falls Inhalte auf den Screenshots erscheinen, die du nicht hochladen möchtest, kannst du diese unkenntlich machen.Für diese Aufgabe gibt es keine inhaltliche Vorabprüfung: Nach jedem Push wird zwar eine Prüfung ausgeführt, diese kontrolliert jedoch nur, ob die Datei
neptun.txtausgefüllt ist. Die eigentliche Bewertung erfolgt nach Ablauf der Frist durch die Übungsleiter.
Bedingungen¶
Obligatorische Verwendung der MVVM-Muster!
In dieser Hausaufgabe üben wir das MVVM-Muster, daher ist das MVVM-Muster für die Lösung der Aufgaben obligatorisch erforderlich. Andernfalls wird die Bewertung der Aufgaben verweigert.
Aufgabe 0 - Überblick über den Ausgangszustand¶
Der Ausgangszustand ist im Grunde derselbe wie die Endzustand von der Laborübung 3. Entwurf der Benutzeroberfläche. Also eine solche Anwendung, die die Speicherung der Daten von Personen in einer Liste ermöglicht. Sie enthält eine kleinere Änderung im Vergleich zum Endzustand des Labors. Im Labor war die vollständige Beschreibung der Oberfläche in MainWindow.xaml (und die zugehörige Code-Behind-Datei) verfügbar. Der Unterschied zu dieser ursprünglichen Lösung besteht darin, dass sie nach PersonListPage.xaml (und in den Code dahinter) im Ordner Views verschoben wurde. PersonListPage ist keine Window, sondern eine von Page abgeleitete Klasse (siehe den Code hinter der Datei). Aber sonst hat sich nichts geändert! Wie der Name schon sagt, stellt Page eine "Seite" in der Anwendung dar: Sie kann nicht selbst angezeigt werden, sondern muss z. B. in einem Fenster platziert werden. Der Vorteil dieses Fensters ist, dass es möglich ist, zwischen den Seiten (verschiedene Page Nachkommen) zu navigieren, indem man die entsprechende Navigation verwendet. Wir werden das nicht ausnutzen, wir werden nur eine Seite haben. Der Zweck der Einführung dieser Seite war nur zu veranschaulichen, dass in der MVVM-Architektur, Ansichten können nicht nur mit Window (full window), sondern auch mit Objekten wie Page implementiert werden.
Da alles von MainWindow nach PersonListPage verschoben wurde, gibt es auf MainWindow.xaml nichts anderes als eine Kopie eines solchen PersonListPage Objekts:
<views:PersonListPage/>
Prüfe im Code, ob dies tatsächlich der Fall ist!
Kopfzeile des Hauptfensters¶
Die Überschrift des Hauptfensters sollte "MVVM" sein, angehängt mit deinem Neptun-Code: (z.B."MVVM - ABCDEF" im Falle des Neptun-Codes "ABCDEF"), ist es wichtig, dass dies der Text ist! Setze dazu die Eigenschaft
Title deines Hauptfensters auf diesen Text in der Datei MainWindow.xaml.
Aufgabe 1 - Verwendung des MVVM-Toolkits¶
In der bestehenden Anwendung implementiert die Klasse Person im Ordner Models bereits die Schnittstelle INotifyPropertyChanged (Spitzname INPC) (sie hat also ein Ereignis PropertyChanged ) und zeigt außerdem eine Eigenschaftsänderung in den Settern Name und Age an, indem sie das Ereignis PropertyChanged auslöst (siehe Person.cs für eine detaillierte Betrachtung).
Zum Aufwärmen/Wiederholen - nachdem du dir den Code (PersonListPage.xaml und PersonListPage.xaml.cs) genau angesehen und die Anwendung ausgeführt haben - sage sich, warum dies in der Anwendung erforderlich war!
Die Antwort (Wiederholung)
In der Anwendung ist die Eigenschaft Text von TextBox (dies ist die Zieleigenschaft) in PersonListPage.xaml an die Eigenschaften Age und Name des Members NewPerson mit dem Typ Person im Code-Behind-Datei gebunden (dies sind die Quellen in den beiden Datenverbindungen). Beachte im Code, dass die Quelleneigenschaften NewPerson.Name und NewPerson.Age ebenfalls im Code geändert werden: Der Controller kann nur über diese Änderungen informiert werden (und somit mit der Quelle synchron bleiben), wenn er über diese Änderungen an Name und Age informiert wird. Aus diesem Grund muss die Klasse, die die Eigenschaften Age und Name enthält, d.h. Person, die Schnittstelle INotifyPropertyChanged implementieren und das Ereignis PropertyChanged auslösen, wenn sich die Eigenschaften ändern, wobei das Ereignis entsprechend parametrisiert sein muss.
Wenn du die Anwendung ausführst, überprüfe, ob die Änderungen, die du auf NewPerson.Age durch Drücken der Tasten '+' und '-' vornimmst, tatsächlich in der TextBox, die das Alter anzeigt, wiedergegeben werden.
In der Klasse Person kannst du sehen, dass die Implementierung von INotifyPropertyChanged und der dazugehörige Code recht umfangreich ist. Schaue dir die Vorlesungsunterlagen an, um zu sehen, welche Alternativen es für die Implementierung der Schnittstelle gibt (ausgehend von der Folie "INPC Beispiel 1", etwa vier Folien zur Veranschaulichung der vier Möglichkeiten)! Die kompakteste Lösung ist das MVVM-Toolkit. Im nächsten Schritt werden wir die derzeitige umfangreichere "manuelle" INPC-Implementierung in ein MVVM-Toolkit umwandeln.
Aufgabe 1/a - Aufnahme des MVVM Toolkit NuGet Referenzes¶
Zunächst muss eine NuGet-Referenz auf das MVVM-Toolkit erstellt werden, damit es im Projekt verwendet werden kann.
Aufgabe: Füge eine NuGet-Referenz für das NuGet-Paket "CommunityToolkit.Mvvm" in das Projekt ein. Auf dieser Visual Studio-Seite wird beschrieben, wie eine NuGet-Referenz mit dem NuGet Package Manager zu einem Projekt hinzugefügt wird. Der vorhergehende Link auf der Seite führt zum Abschnitt "NuGet Package Manager". Folge den vier hier angegebenen Schritten (mit dem Unterschied, dass du auf das Paket "CommunityToolkit.Mvvm" statt auf "Newtonsoft.Json" verweisen muss).
Nachdem wir nun diese NuGet-Referenz zu unserem Projekt hinzugefügt haben, wird der nächste Build (da er einen NuGet restore Schritt enthält!) das NuGet-Paket herunterladen, die darin enthaltenen DLLs in den Ausgabeordner entpacken und sie zu einem integralen Bestandteil der Anwendung machen (ein NuGet-Paket ist eigentlich eine Zip-Datei). Es ist wichtig zu beachten, dass weder die NuGet-Zipdatei noch die darin enthaltenen DLLs in Git enthalten sind. Sie werden von der Datei .gitignore im Stammverzeichnis der Lösung herausgefiltert. Dies ist der eigentliche Kern des NuGet-Konzepts: Das Repository kann klein bleiben, da die Projektdatei nur Verweise auf NuGet-Pakete enthält, und wenn jemand eine frisch geklonte Lösung erstellt, werden die referenzierten NuGet-Pakete erst dann aus den Online-NuGet-Ressourcen heruntergeladen.
Die Kenntnis der oben genannten NuGet-Konzepte ist wichtig, sie sind ein wichtiger Teil des Lehrmaterials!
Eine NuGet-Referenz ist eigentlich nur eine Zeile in der Projektbeschreibungsdatei .csproj. Klicke im Solution Explorer auf den Projektknoten "HelloXaml", öffne die Projektdatei .csproj und überprüfe, ob diese Zeile enthalten ist (die Version kann unterschiedlich sein):
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
Überprüfe unsere NuGet-Referenz, ohne die Datei csproj zu öffnen: Öffne im Solution Explorer den Knoten "HelloXaml"/"Dependencies"/"Packages": Wenn alles in Ordnung ist, siehst du darunter einen Knoten "CommunityToolkit.Mvvm (Version)".
Aufgabe 1/b - INPC-Implementierung auf Basis des MVVM-Toolkits¶
Jetzt können wir die Klassen, Schnittstellen, Attribute usw. im MVVM Toolkit NuGet-Paket verwenden, so dass wir zur MVVM Toolkit-basierten INPC-Implementierung wechseln können.
- Kommentiere die ganze Klasse
Personaus. - Füge oberhalb des auskommentierten Teils die Klasse als neu hinzu, aber mit einer MVVM-Toolkit-basierten INPC-Implementierung.
- Die Präsentation "INPC Beispiel 4 - MVVM Toolkit" wird dir bei der Umsetzung helfen.
- Es muss sich um eine partielle Klasse handeln (d.h. Teile der Klasse können in mehreren Dateien definiert sein).
- Sie stammt von
ObservableObjectaus dem Toolkit: Dieser Vorgänger implementiert die SchnittstelleINotifyPropertyChanged, so dass wir sie nicht mehr benötigen. - Ersetze die Eigenschaften
NameundAgemit Mitgliedsvariablennameundage, die auch die AttributeObservablePropertybesitzen.
Wir sind fertig.
Überprüfung der Lösung
public partial class Person : ObservableObject
{
[ObservableProperty]
private string name;
[ObservableProperty]
private int age;
}
Dieser Code ergibt nach einer Übersetzung im Wesentlichen die gleiche Lösung wie die frühere, viel ausführlichere und jetzt auskommentierte Lösung. Das heißt (auch wenn wir es noch nicht sehen), es werden die Eigenschaften Name und Age erstellt, mit entsprechenden PropertyChanged Ereignisauslösern. Wie ist das möglich?
- Einerseits implementiert der Vorfahre
ObservableObjectbereits die SchnittstelleINotifyPropertyChanged, enthält also auch das EreignisPropertyChanged, das durch Ableitung an unsere Klasse "vererbt" wird. - Während der Kompilierung wird der MVVM-Toolkit-Codegenerator ausgeführt, der für jede Membervariable mit dem Attribut
ObservablePropertyin der Klasse eine Eigenschaft mit dem gleichen Namen, aber mit einem Großbuchstaben beginnend, erzeugt, die unter den richtigen Bedingungen und mit den richtigen Parametern das EreignisPropertyChangedauslöst. Hurra, wir müssen diesen Code nicht schreiben. - Die Frage ist, wo dieser Code generiert wird. In einem anderen "partiellen" Teil unserer Klasse. Nach einer Übersetzung in Visual Studio klicke mit der rechten Maustaste auf den Klassennamen
Personund wähle im Popup-Menü "Go to Definition". In einem unteren Fenster erhalten wir zwei Ergebnisse: das eine ist der Code, den wir oben geschrieben haben, das andere ("public class Person") springt nach einem Doppelklick zum generierten Teil des Codes: es ist sehbar, dass der Code-Generator einen relativ ausführlichen Code generiert hat, aber was für uns wichtig ist, ist, dass die EigenschaftenNameundAgehier stehen, darunter - unter anderem - die EigenschaftOnPropertyChanged.
Der Code-Generator arbeitet in der Regel in der anderen "partiellen" Hälfte unserer Klasse, um den von uns geschriebenen und den von uns generierten Code nicht zu verwechseln! Teilklassen werden am häufigsten verwendet, um handgeschriebenen Code von generiertem Code zu "trennen".
Da viel weniger Code geschrieben werden muss, verwenden wir in der Praxis die auf dem MVVM-Toolkit basierende Lösung (aber du musst auch die manuelle Lösung kennen, damit du verstehen kannst, was hinter den Kulissen geschieht).
EINGABE
Erstelle einen Screenshot mit dem Namen f1b.png wie folgt:
- Starte die App. Verkleinere sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- Im "Hintergrund" sollte Visual Studio mit
Person.csgeöffnet sein.
Aufgabe 2 - Umstellung auf eine MVVM-basierte Lösung¶
Im vorherigen Schritt haben wir zwar das MVVM-Toolkit verwendet, sind aber noch nicht zu einer MVVM-basierten Lösung gewechselt (das Toolkit wurde nur für eine einfachere Implementierung von INPC verwendet).
Im Folgenden werden wir die Architektur unserer Anwendung so anpassen, dass sie dem MVVM-Konzept folgt. Wir bauen auf dem MVVM-Toolkit auf, um die Implementierung zu erleichtern.
Aufgabe: Arbeite das entsprechende Vorlesungsmaterial durch (am Ende des WinUI-Abschnitts):
- Verstehe der grundlegenden Konzepte des MVVM-Musters.
- Der vollständige Code für die Beispiele in den Folien ist im Ordner "04-05 WinUI\DancerProfiles" ("RelaxedMVVM" und "StrictMVVM") von GitHub Repository verfügbar und kann dir helfen, die zu verstehen und die Aufgaben später zu lösen.
Was bedeutet das MVVM-Muster für unser Beispiel:
- Die Modellklasse ist die Klasse
Personim OrdnerModels, die die Daten einer Person repräsentiert (sie enthält KEINE UI-Logik und ist unabhängig von der Anzeige). - Im Moment sind alle Beschreibungen/Logiken im Zusammenhang mit der Visualisierung in
PersonListPage.Die aktuelle
PersonListPagewird in zwei Teile aufgeteilt:PersonListPage.xamlund seiner Code-Behind-Datei wird die Ansicht.- Wir führen ein ViewModel für
PersonListPagemit dem NamenPersonListPageViewModelein.Sehr wichtig: Die gesamte Anzeigelogik wird von
PersonListPageCode-Behind-Datei insPersonListPageViewModelbewegt. Der Sinn des Musters ist, dass die View nur eine reine Beschreibung der Oberfläche enthält, die Anzeigelogik befindet sich im ViewModel.
- Eine weitere Säule des Musters: Unsere View enthält einen Verweis auf ihr ViewModel (in Form einer Eigenschaft).
- In unserem Beispiel bedeutet dies, dass
PersonListPageeinePersonListPageViewModelEigenschaft haben muss. Dies ist sehr wichtig, da wir in unserer
PersonListPageXaml-Datei diese Eigenschaft verwenden können, um die Datenverbindung an Eigenschaften und Ereignishandler zu implementieren, die in das ViewModel verschoben wurden!
- In unserem Beispiel bedeutet dies, dass
PersonListPageViewModel"arbeitet" mit dem Modell und behandelt die Benutzerinteraktionen (Ereignishandler).- Da wir eher das Relaxed- als das Strict-MVVM-Muster verwenden, führen wir keinen
PersonViewModel-Wrapper noch um unserePerson-Modellklasse herum ein.
Aufgabe: Ändere die bestehende Logik so, dass sie dem MVVM-Muster folgt und den oben genannten Grundsätzen entspricht. Lege die Klasse PersonListPageViewModel in einem neu erstellten Ordner ViewModels ab. Versuche, die Lösung anhand der obigen Hilfe selbst zu bearbeiten! Dazu geben wir einen vorherigen Hinweis, da das schwieriger herauszufinden ist: Zu den Ereignissen können auch Ereignishandler durch Datenverbindung angeben werden: siehe die Folie "Bindung von Ereignissen und Funktionen" (nach der Modifikation ist dies die einzige Möglichkeit, Ereignishandler anzugeben). Es ist auch wichtig zu beachten, dass Daten nur an öffentliche Eigenschaften/Operationen gebunden werden können, so dass auch dies geändert werden muss!
Tipps/Prüfung der Lösung
- Aus der
PersonListPage.xaml.csCode-Behind-Datei sollte fast alles (außerthis.InitializeComponent()Aufruf im Konstruktor) in die neu eingeführtePersonListPageViewModelverschoben werden, da es sich um UI-Logik handelt. PersonListPageViewModelsollte eine öffentliche Klasse sein.- In der
PersonListPageCode-Behind-Datei musst du eine automatisch implementierte Eigenschaft namens ViewModel vom TypPersonListPageViewModelmit nur Getter einfügen und diese auf ein neues Objekt initialisieren. Mit anderen Worten, die Ansicht erstellt und enthält das ViewModel! - In
PersonListPage.xamlmüssen die beiden Datenverbindungen der zweiTextBoxentsprechend korrigiert werden (NewPerson.NameundNewPerson.Agesind jetzt eine Ebene tiefer verfügbar, über die ViewModel-Eigenschaft der Code-Behind-Datei). - In
PersonListPage.xamlmüssen die Ereignishandler (Click) an drei Stellen korrigiert werden. Dies ist komplizierter. Die Ereignishandler-Funktion kann nicht mehr mit der bisher verwendeten Syntax angegeben werden, da die Ereignishandler nicht mehr in der Code-Behind-Datei liegen (sie wurden in das ViewModel verschoben).- Ereignishandler können für Ereignisse durch Datenverbindung angegeben werden! Siehe Präsentationsfolie "Binden von Ereignissen und Funktionen". Das ist gut für uns, denn in der ViewModel-Eigenschaft der Code-Behind-Datei ist das
PersonListPageViewModel-Objekt, das die Ereignishandler enthält (AddButton_Click,IncreaseButton_Click,DecreaseButton_Click), und diese müssen als gebundene Eigenschaften in der Datenverbindung angegeben werden (z.B.ViewModel.AddButton_Clickusw.). - Es ist wichtig, dass die Ereignishandler-Funktionen öffentlich sind, sonst funktioniert die Datenverbindung nicht (muss von privat konvertiert werden).
- Ereignishandler können für Ereignisse durch Datenverbindung angegeben werden! Siehe Präsentationsfolie "Binden von Ereignissen und Funktionen". Das ist gut für uns, denn in der ViewModel-Eigenschaft der Code-Behind-Datei ist das
Andere wichtige Modifikationen:
- Die aktuellen Namen der Ereignishandler von
Clickin ViewModel lautenAddButton_Click,IncreaseButton_ClickundDecreaseButton_Click. Das ist nicht glücklich. Im ViewModel denken wir "semantisch" nicht im Sinne von Ereignishandlern. Stattdessen werden im Sinne von Modifizierungsoperationen denken, die den Zustand des ViewModel ändern. Also statt dem oberen Namen werden wir die folgenden, sehr viel geignetere und aussagekräftigere Namen verwenden:AddPersonToList,IncreaseAgeundDecreaseAge. Benenne die Funktionen entsprechend um! Natürlich musst du diese noch an dieClickEreignisse in der XAML-Datei binden. - Die Parameterliste für die oben genannten Funktionen lautet zunächst "
object sender, RoutedEventArgs e". Diese Parameter werden jedoch nicht für irgendetwas verwendet. Glücklicherweise ist die x:Bind-Ereignisbindung so flexibel, dass du auch eine Operation ohne Parameter angeben kannst, und das funktioniert auch problemlos. Entferne daher die oben genannten unnötigen Parameter aus den drei Funktionen unseres ViewModel. Dies führt zu einer schlankeren Lösung.
Prüfe, ob die Anwendung nach den Änderungen genauso funktioniert wie vorher!
Was haben wir durch die Umstellung unserer bisherigen Lösung auf eine MVVM-Basis gewonnen? Die Antwort findest du in den Vorlesungsmaterial! Ein paar Dinge sind hervorzuheben:
- Die verschiedenen Zuständigkeiten sind gut voneinander getrennt (nicht vermischt), so dass es leichter zu verstehen ist:
- UI-unabhängige Logik (Modell und zugehörige Klassen).
- UI-Logik (ViewModel)
- Nur UI-Erscheinung (View)
- Da die UI-Logik separat ist, könn(t)est du Unit-Tests für sie schreiben.
Je komplexer eine Anwendung ist, desto mehr sind diese wahr.
EINGABE
Erstelle einen Screenshot mit dem Namen f2.png wie folgt:
- Starte die App. Verkleinere sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- Im "Hintergrund" sollte Visual Studio mit
PersonListPageViewModel.csgeöffnet sein.
Aufgabe 3 - Deaktivieren/Aktivieren von Controllern¶
In diesem Stadium verhält sich die Anwendung etwas komisch: Mit der Taste "-" kann ein Alter auch in den negativen Bereich verschoben werden, mit der Taste "+" auf über 150, und mit der Taste "+Add" können auch Personen hinzugefügt werden, die bedeutungslose Eigenschaften aufweisen. Diese Tasten sollten deaktiviert werden, wenn die von ihnen ausgelöste Aktion keinen Sinn ergibt, und aktiviert werden, wenn sie Sinn hat.
Im nächsten Schritt deaktiviere/aktiviere die Taste "-" entsprechend. Die Taste sollte nur aktiviert werden, wenn das Alter der Person größer als 0 ist.
Versuche, es zuerst selbst zu lösen, zumindest um die Grundlagen zu schaffen! Denke unbedingt über eine Lösung mit Datenverbindung, nur diese ist akzeptabel! Wenn du nicht weiterkommen kannst, deine Lösung nicht funktionieren "will", überdenke, was der Grund dafür sein könnte, und konstruire deine Lösung wie folgt.
Es gibt mehrere mögliche Lösungen für dieses Problem. In allen gemeinsam ist, dass die Eigenschaft IsEnabled der Taste "-" in irgendeiner Weise gebunden ist. In unserer Lösung binden wir sie an eine bool-Eigenschaft, die in PersonListPageViewModel neu eingeführt wurde.
public bool IsDecrementEnabled
{
get { return NewPerson.Age > 0; }
}
IsEnabled="{x:Bind ViewModel.IsDecrementEnabled, Mode=OneWay}"
Probieren wir es aus! Leider funktioniert es nicht, die "-"-Taste wird nicht deaktiviert, wenn das Alter auf 0 oder weniger gesetzt wird (z.B. durch wiederholtes Anklicken der Taste). Wenn du einen Haltepunkt in IsDecrementEnabled setzt und die Anwendung auf diese Weise startest, wirst du feststellen, dass der Wert der Eigenschaft nur einmal vom gebundenen Steuerelement abgefragt wird, wenn die Anwendung startet: Danach kannst du auf die Taste "-" mehrmals klicken, aber es wird nicht mehr als einmal abgefragt. Probiere es aus!
Überdenke, was die Ursache dafür ist, und lese erst dann der Leitfaden weiter!
Begründung
Wie wir bereits gelernt haben, ruft die Datenverbindung den Wert der Quelleigenschaft (in diesem Fall IsDecrementEnabled) nur ab, wenn sie über INotifyPropertyChanged über eine Änderung informiert wird! Aber in unserer Lösung gibt es jedoch, selbst wenn sich die Eigenschaft Age des Objekts NewPerson ändert, keine Benachrichtigung über die Änderung der darauf basierenden Eigenschaft IsDecrementEnabled!
Im nächsten Schritt implementiere die entsprechende Änderungsmeldung in der Klasse PersonListPageViewModel:
- Implementiere die
INotifyPropertyChangedSchnittstelle auf MVVM Toolkit "Grundlagen"! - Die Eigenschaft
IsDecrementEnabledkann so bleiben, wie sie ist (get only property), sie muss nicht auf[ObservableProperty]umgeschrieben werden (aber das ist auch eine gute Lösung und für Hausaufgaben durchaus akzeptabel, sie muss nur in den nächsten Schritten etwas anders bearbeitet werden). - Versuche, Folgendes in der ViewModel-Klasse selbst zu implementieren (die Klasse
Personbleibt unverändert): Wenn sichNewPerson.Ageändert, wird die vom Vorgänger geerbte EigenschaftOnPropertyChangedaufgerufen, um die Änderung der EigenschaftIsDecrementEnabledanzuzeigen. Hinweis: Die KlassePersonhat bereits ein EreignisPropertyChanged, da sie selbst die SchnittstelleINotifyPropertyChangedimplementiert, kannst du dieses Ereignis abonnieren! Wegen der Einfachheit haben wir nichts dagegen, wenn wir eine Änderung anIsDecrementEnabledmelden, auch wenn sie sich nicht wirklich "logisch" ändert. - Die obigen Schritte können auch ohne die Implementierung einer separaten Ereignishandler-Funktion durchgeführt werden: Dies wird empfohlen, ist aber nicht zwingend erforderlich (Tipp: Gebe eine Ereignishandler-Funktion mit einem Lambda-Ausdruck an).
Teste deine Lösung! Wenn du richtig gearbeitet hast, sollte die Taste auch dann deaktiviert sein, wenn du manuell einen negativen Alterswert in die Textbox eingibst (und dann aus der Textbox herausklickst). Denke darüber nach, warum das so ist!
Erarbeite eine ähnliche Lösung für die Taste "+" und die Taste "+Add"!
- Das "akzeptable" Höchstalter sollte 150 Jahre sein.
- Der Name ist nur akzeptabel, wenn er mindestens ein Zeichen enthält, das kein Leerzeichen ist (um letzteres zu prüfen, verwende die statische Operation der String-Klasse
IsNullOrWhiteSpace). - Der Fall, dass der Benutzer eine ungültige Zahl in die Alters-Textbox eingibt (was bei dieser Lösung nicht möglich ist), muss nicht behandelt werden.
Beim Testen haben wir festgestellt, dass sich der Zustand der Taste "+Add" nicht sofort ändert, wenn wir beispielsweise den Namen in der Textbox "Name" löschen, sondern erst, wenn wir die Textbox verlassen? Warum ist das so? Ändere deine Lösung so, dass dies bei jeder Textänderung geschieht, ohne die TextBox zu verlassen. Hinweis: siehe die Folie "x:Bind wann werden die Daten aktualisiert?" in der Vorlesungsmaterial.
EINGABE
Erstelle einen Screenshot mit dem Namen f3.png wie folgt:
- Starte die App. Verkleinere sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- sollte das Alter in der Anwendung auf 0 reduziert werden,
- Im "Hintergrund" sollte Visual Studio mit
PersonListPageViewModel.csgeöffnet sein.
Aufgabe 4 - Command verwenden¶
Derzeit haben wir zwei Aufgaben für die Taste "-":
- Für
Click, die Ausführung der Ereignishandler-Funktion - Deaktivieren/Aktivieren der Taste mit der Eigenschaft
IsEnabled
Einige Controller, wie z. B. die Taste, unterstützen die Möglichkeit, beide Aufgaben, aufbauend auf dem Command-Muster, mit einem Command-Objekt zu machen. Das Konzept des Command-Entwurfsmusters kann in der Vorlesung "Design Patterns 3" ausführlicher behandelt werden (obwohl wir dort nur das grundlegende Command-Muster kennengelernt haben, das die Ausführung von Befehlen unterstützt, nicht aber das Verbieten/Erlauben). Die MVVM-spezifische Umsetzung des Command-Patterns findest du gegen Ende der WinUI-Vorlesungsreihe, beginnend mit der Folie "Command-Muster".
Das Grundprinzip ist: Anstatt die "Angaben" von Click und IsEnabled für die Taste, setzen wir die Eigenschaft Command der Taste auf ein Befehlsobjekt, das die Schnittstelle ICommand implementiert. Es liegt an diesem Befehlsobjekt, den Befehl auszuführen oder zu deaktivieren/aktivieren.
Standardmäßig sollte eine Anwendung für jeden Befehl eine eigene ICommand Implementierung haben. Dies erfordert jedoch die Einführung vieler Klassen für viele Befehle. Das MVVM-Toolkit ist hier, um zu helfen. Stellt eine Klasse RelayCommand zur Verfügung, die die Schnittstelle ICommand implementiert. Diese Klasse kann zur Ausführung beliebiger Befehle/Codes verwendet werden, so dass keine zusätzlichen Befehlsklassen eingeführt werden müssen. Wie ist das möglich? So, dass RelayCommand hat den Code für die Ausführung und deaktivieren/aktivieren in Konstruktor-Parameter, in Form von zwei Delegaten:
- Der erste Parameter gibt den Code an, der ausgeführt werden soll, wenn der Befehl ausgeführt wird.
- Der zweite Parameter (optional) ist der Code, den der Befehl aufruft, um zu prüfen, ob er sich selbst zulassen oder verbieten soll (die hier angegebene Funktion muss einen booleschen Wert zurückgeben, im wahren Fall wird der Befehl zugelassen).
Der nächste Schritt besteht darin, die Behandlung der Taste "-" auf command basierende umzustellen. Versuche, das meiste davon selbst zu implementieren, basierend auf dem zugehörigen WinUI-Vorlesungen. Das Ausführen des Befehls ist einfacher, aber du musst etwas Arbeit investieren, um den Befehl zu deaktivieren und zu aktivieren. Die wichtigsten Schritte:
- Füge eine öffentliche
RelayCommandEigenschaft mit nur Getter zum ViewModel hinzu, z.B.DecreaseAgeCommand. Anders als in den Vorlesungsmaterial brauchen wir in unserem FallRelayCommandkeinen allgemeinen Parameter zu geben, da unsere Befehlsbehandlungsfunktion (DecreaseAge) keinen Parameter hat. - Gebe der neu eingeführten Eigenschaft im ViewModel-Konstruktor einen Wert. Gebe die Parameter des
RelayCommandKonstruktors entsprechend an. - In
PersonListPage.xamlmuss die Taste "-" nicht mehrClickundIsEnabledbinden, sie werden gelöscht. Binde stattdessen die EigenschaftCommandder Taste an die EigenschaftDecreaseAgeCommand, die im vorherigen Schritt im ViewModel eingeführt wurde.
Wenn du es ausprobierst, funktioniert die Ausführund des Befehls, aber das Deaktivieren/Aktivieren nicht: Wenn du es gut beobachtest, bleibt die Taste in ihrem Aussehen immer aktiviert. Es gibt einen logischen Grund dafür, wenn man darüber nachdenkt: RelayCommand kann die Aktion im zweiten Konstruktorparameter aufrufen, um den Zustand zu überprüfen, aber es weiß nicht, dass es dies jedes Mal tun sollte, wenn NewPerson.Age sich ändert! Wir können dabei helfen. In unserem ViewModel-Konstruktor haben wir bereits das NewPerson.PropertyChanged -Ereignis abonniert: Darauf aufbauend rufen wir, wenn sich das Alter ändert (oder wenn es sich ändern könnte, es ist kein Problem, dies manchmal unnötigerweise zu tun), die Method NotifyCanExecuteChanged von DecreaseAgeCommand auf. Diese Operation hat einen sehr aussagekräftigen Namen: Sie teilt dem Befehl mit, dass sich der Zustand, auf dem der verbotene/erlaubte Zustand des Befehls aufgebaut ist, geändert hat. Auf diese Weise wird der Befehl selbst aktualisiert, genauer gesagt der Zustand der mit dem Befehl verbundenen Taste.
Ändere die Behandlung der "+"-Taste auf ähnliche Weise auf Befehlsbasis! Ändere nicht die Behandlung der Taste "+Add"!
EINGABE
Erstelle einen Screenshot mit dem Namen f4.png wie folgt:
- Starte die App. Verkleinere sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- der Name TextBox sollte in der Anwendung leer sein,
- Im "Hintergrund" sollte Visual Studio mit
PersonListPageViewModel.csgeöffnet sein.
Aufgabe 5 - Verwendung von Command mit MVVM Toolkit-basierter Codegenerierung¶
In der vorigen Aufgabe wurde die Einführung von Command-Eigenschaften und deren Instanziierung "manuell" gemacht. Das MVVM Toolkit kann dies vereinfachen: Wenn das richtige Attribut verwendet wird, können die Eigenschaft und die Instanziierung automatisch generiert werden.
Ändern wir die Behandlung von DecreaseAgeCommand (nur dieses, IncreaseAgeCommand soll unverändert bleiben! ) auf eine generierte Codebasis:
- Ergänze die Klasse
PersonListPageViewModelmit dem Schlüsselwortpartial. - Entferne die Eigenschaft
DecreaseAgeCommandund ihre Instanziierung aus dem Konstruktor. - Ergänze
DecreaseAgemit diesem Attribut:[RelayCommand(CanExecute = nameof(IsDecrementEnabled))].- Als Ergebnis führt der Codegenerator eine Eigenschaft
RelayCommandin die Klasse ein, die mit dem Namen unserer Operation (DecreaseAge) benannt ist und an die die Zeichenfolge "Command" angehängt ist. So erhalten wir die EigenschaftDecreaseAgeCommand, die wir zuvor manuell eingeführt haben. - Die Attributeigenschaft
CanExecutekann verwendet werden, um in Form einer Zeichenkette den Namen der Operation oder Eigenschaft mit booleschen Rückgabewert anzugeben, die der generierte Code verwenden wird, wenn er den Befehl verbietet/erlaubt (er ist der zweite Parameter des Konstruktors RelayCommand). Wir haben bereits eine solche Eigenschaft, die "IsDecrementEnabled" heißt. Sie wird nicht als einfache Zeichenkette angegeben, denn wenn jemand die OperationIsDecrementEnablednachträglich umbenennt, würde die aktuelle "IsDecrementEnabled" nicht auf die richtige Operation verweisen. Die Verwendung des Ausdrucksnameofvermeidet dieses Problem. Die Angabe vonCanExecuteist im Allgemeinen optional (gebe es nicht an, wenn du den Befehl niemals deaktivieren willst).
- Als Ergebnis führt der Codegenerator eine Eigenschaft
Teste die Lösung (Verkleinerung des Alters), sie sollte genauso funktionieren wie zuvor.
EINGABE
Erstelle einen Screenshot mit dem Namen f5.png wie folgt:
- Starte die App. Verkleinere sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- Im "Hintergrund" sollte Visual Studio mit
PersonListPageViewModel.csgeöffnet sein.