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. Hinweis: Labor 5 - MVVM-Labor ist sehr abwechslungsreich und zeigt neben vielen anderen Themen ein Beispiel für das MVVM-Muster im Kontext einer komplexeren Anwendung. Die vorliegende Hausaufgabe ist fokussierter, in kleineren Schritten aufgebaut: In unserem Fall wird die Lösung dieser Hausaufgabe helfen, um die zugehörige Teile von Lab 5 - MVVM zu verstehen.
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 Sie die 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 für die Einreichung¶
Auf das Moodle soll ein ZIP-Archiv hochgeladen werden, das die folgenden Anforderungen entspricht:
- Die Aufgaben sind aufeinander basiert, deshalb ist es genügend den resultierenden Quellcode am Ende der letzten Aufgabe hochzuladen (Visual Studio Solution Verzeichnis). Der Name des Verzeichnisses soll "MVVM_NEPTUN" sein (wo NEPTUN Ihre Neptun-Code ist).
- Wir erwarten keine schriftliche Begründung oder Beschreibung, aber die komplexe Codeteile sollen mit Kommentaren versehen werden
- Das ZIP-Archiv darf die Ausgangsdaten (.exe) und die temporären Dateien nicht enthalten. Um diese Bestände zu löschen, Visual Studio soll geöffnet werden und in dem Solution Explorer Rechtsklick an dem „Clean Solution” Menüelement. Das manuelle Löschen von den "obj" und "bin" Verzeichnissen kann auch nötig sein.
- In den Aufgaben werden Sie aufgefordert, einen Screenshot von einem Teil Ihrer Lösung zu machen, da dies beweist, dass Sie Ihre Lösung selbst erstellt haben. Der erwartete Inhalt der Screenshots ist immer in der Aufgabe angegeben. Die Screenshots sollten als Teil der Lösung eingegeben, also innerhalb dem ZIP-Archiv auf das Moodle hochgeladen werden. Wenn Sie Inhalte im Screenshot haben, die Sie nicht hochladen möchten, können Sie diese aus dem Screenshot ausblenden.
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üfen Sie im Code, ob dies tatsächlich der Fall ist!
Kopfzeile des Hauptfensters¶
Die Überschrift des Hauptfensters sollte "MVVM" sein, angehängt mit Ihrem Neptun-Code: (z.B."MVVM - ABCDEF" im Falle des Neptun-Codes "ABCDEF"), ist es wichtig, dass dies der Text ist! Setzen Sie dazu die Eigenschaft Title
Ihres 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 Sie sich den Code (PersonListPage.xaml
und PersonListPage.xaml.cs
) genau angesehen und die Anwendung ausgeführt haben - sagen Sie 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). Beachten Sie 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 Sie die Anwendung ausführen, überprüfen Sie, ob die Änderungen, die Sie auf NewPerson.Age
durch Drücken der Tasten "+" und "-" vornehmen, tatsächlich in der TextBox
, die das Alter anzeigt, wiedergegeben werden.
In der Klasse Person
können Sie sehen, dass die Implementierung von INotifyPropertyChanged
und der dazugehörige Code recht umfangreich ist. Schauen Sie sich 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ügen Sie 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 Sie zum Abschnitt "NuGet Package Manager". Folgen Sie den vier hier angegebenen Schritten (mit dem Unterschied, dass Sie auf das Paket "CommunityToolkit.Mvvm" statt auf "Newtonsoft.Json" verweisen müssen).
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
. Klicken Sie im Solution Explorer auf den Projektknoten "HelloXaml", öffnen Sie die Projektdatei .csproj
und überprüfen Sie, ob diese Zeile enthalten ist (die Version kann unterschiedlich sein):
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
Sie können unsere NuGet-Referenz überprüfen, ohne die Datei csproj
zu öffnen: Öffnen Sie im Solution Explorer den Knoten "HelloXaml"/"Dependencies"/"Packages": Wenn alles in Ordnung ist, sehen Sie 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.
- Kommentieren Sie die ganze Klasse
Person
aus. - Fügen Sie 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 Ihnen 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
ObservableObject
aus dem Toolkit: Dieser Vorgänger implementiert die SchnittstelleINotifyPropertyChanged
, so dass wir sie nicht mehr benötigen. - Ersetzen Sie die Eigenschaften
Name
undAge
mit Mitgliedsvariablenname
undage
, die auch die AttributeObservableProperty
besitzen.
Wir sind fertig.
Überprüfung der Lösung
```` csharp 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
ObservableObject
bereits 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
ObservableProperty
in 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 EreignisPropertyChanged
auslö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 klicken Sie mit der rechten Maustaste auf den Klassennamen
Person
und wählen 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: Sie sehen, dass der Code-Generator einen relativ ausführlichen Code generiert hat, aber was für uns wichtig ist, ist, dass die EigenschaftenName
undAge
hier 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 Sie müssen auch die manuelle Lösung kennen, damit Sie verstehen können, was hinter den Kulissen geschieht).
EINGABE
Machen Sie einen Screenshot mit dem Namen f1b.png
wie folgt:
- Starten Sie die App. Verkleinern Sie sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- Im "Hintergrund" sollte Visual Studio mit "Person.cs" geö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: Arbeiten Sie das entsprechende Vorlesungsmaterial durch (am Ende des WinUI-Abschnitts):
- Verstehen Sie 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 Ihnen 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
Person
im 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 aktuellePersonListPage
wird in zwei Teile aufgeteilt:PersonListPage.xaml
und seiner Code-Behind-Datei wird die Ansicht.- Wir führen ein ViewModel für
PersonListPage
mit dem NamenPersonListPageViewModel
ein.- Sehr wichtig: Die gesamte Anzeigelogik wird von
PersonListPage
Code-Behind-Datei insPersonListPageViewModel
bewegt. Der Sinn des Musters ist, dass die View nur eine reine Beschreibung der Oberfläche enthält, die Anzeigelogik befindet sich im ViewModel.
- Sehr wichtig: Die gesamte Anzeigelogik wird von
- 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
PersonListPage
einePersonListPageViewModel
Eigenschaft haben muss. - Dies ist sehr wichtig, da wir in unserer
PersonListPage
Xaml-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: Ändern Sie die bestehende Logik so, dass sie dem MVVM-Muster folgt und den oben genannten Grundsätzen entspricht. Legen Sie die Klasse PersonListPageViewModel
in einem neu erstellten Ordner ViewModels
ab. Versuchen Sie, die Lösung anhand der obigen Hilfe selbst zu bearbeiten! Dazu geben wir einen vorherigen Hinweis, da das schwieriger herauszufinden ist: Sie können auch Ereignishandler für Ereignisse durch Datenverbindung angeben: 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.cs
Code-Behind-Datei sollte fast alles (außerthis.InitializeComponent()
Aufruf im Konstruktor) in die neu eingeführtePersonListPageViewModel
verschoben werden, da es sich um UI-Logik handelt. PersonListPageViewModel
sollte eine öffentliche Klasse sein.- In der
PersonListPage
Code-Behind-Datei müssen Sie eine automatisch implementierte Eigenschaft namens ViewModel vom TypPersonListPageViewModel
mit nur Getter einfügen und diese auf ein neues Objekt initialisieren. Mit anderen Worten, die Ansicht erstellt und enthält das ViewModel! - In
PersonListPage.xaml
müssen die beiden Datenverbindungen der zweiTextBox
entsprechend korrigiert werden (NewPerson.Name
undNewPerson.Age
sind jetzt eine Ebene tiefer verfügbar, über die ViewModel-Eigenschaft der Code-Behind-Datei). - In
PersonListPage.xaml
mü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_Click
usw.). - 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
Click
in ViewModel lautenAddButton_Click
,IncreaseButton_Click
undDecreaseButton_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
,IncreaseAge
undDecreaseAge
. Benennen Sie die Funktionen entsprechend um! Natürlich müssen Sie diese noch an dieClick
Ereignisse 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 Sie auch eine Operation ohne Parameter angeben können, und das funktioniert auch problemlos. Entfernen Sie daher die oben genannten unnötigen Parameter aus den drei Funktionen unseres ViewModel. Dies führt zu einer schlankeren Lösung.
Prüfen Sie, 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 finden Sie 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)en Sie Unit-Tests für sie schreiben.
Je komplexer eine Anwendung ist, desto mehr sind diese wahr.
EINGABE
Machen Sie einen Screenshot mit dem Namen f2.png
wie folgt:
- Starten Sie die App. Verkleinern Sie sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- Im "Hintergrund" sollte Visual Studio mit
PersonListPageViewModel.cs
geöffnet sein.
Aufgabe 3 - Deaktivieren/Aktivieren von Controllern¶
In diesem Stadium verhält sich die Anwendung etwas komisch: Sie können die Taste "-" verwenden, um ein Alter in den negativen Bereich zu verschieben, oder die Taste "+", um es über 150 zu verschieben, oder die Taste "+Add", um eine Person mit sinnlosen Attributen hinzuzufügen. 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 deaktivieren/aktivieren Sie die Taset "-" entsprechend. Die Taste sollte nur aktiviert werden, wenn das Alter der Person größer als 0 ist.
Versuchen Sie, es zuerst selbst zu lösen, zumindest um die Grundlagen zu schaffen! Denken Sie unbedingt über eine Lösung mit Datenverbindung, nur diese ist akzeptabel! Wenn Sie nicht weiterkommen kann, Ihre Lösung nicht funktionieren "will", überdenken Sie, was der Grund dafür sein könnte, und konstruiren Sie Ihre 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 Sie einen Haltepunkt in IsDecrementEnabled
setzen und die Anwendung auf diese Weise starten, werden Sie feststellen, dass der Wert der Eigenschaft nur einmal vom gebundenen Steuerelement abgefragt wird, wenn die Anwendung startet: Danach können Sie auf die Taste "-" mehrmals klicken, aber es wird nicht mehr als einmal abgefragt. Probieren Sie es aus!
Überdenken Sie, was die Ursache dafür ist, und lesen Sie 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 implementieren Sie die entsprechende Änderungsmeldung in der Klasse PersonListPageViewModel
:
- Implementieren Sie die
INotifyPropertyChanged
Schnittstelle auf MVVM Toolkit "Grundlagen"! - Die Eigenschaft
IsDecrementEnabled
kann 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). - Versuchen Sie, Folgendes in der ViewModel-Klasse selbst zu implementieren (die Klasse
Person
bleibt unverändert): Wenn sichNewPerson.Age
ändert, wird die vom Vorgänger geerbte EigenschaftOnPropertyChanged
aufgerufen, um die Änderung der EigenschaftIsDecrementEnabled
anzuzeigen. Hinweis: Die KlassePerson
hat bereits ein EreignisPropertyChanged
, da sie selbst die SchnittstelleINotifyPropertyChanged
implementiert, können Sie dieses Ereignis abonnieren! Wegen der Einfachheit haben wir nichts dagegen, wenn wir eine Änderung anIsDecrementEnabled
melden, 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: Geben Sie eine Ereignishandler-Funktion mit einem Lambda-Ausdruck an).
Testen Sie Ihre Lösung! Wenn Sie richtig gearbeitet haben, sollte die Taste auch dann deaktiviert sein, wenn Sie manuell einen negativen Alterswert in die Textbox eingeben (und dann aus der Textbox herausklicken). Denken Sie darüber nach, warum das so ist!
Erarbeiten Sie 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, verwenden Sie 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? Ändern Sie Ihre 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
Machen Sie einen Screenshot mit dem Namen f3.png
wie folgt:
- Starten Sie die App. Verkleinern Sie 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.cs
geö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 finden Sie 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. Versuchen Sie zuerst, das meiste davon selbst zu implementieren, basierend auf dem zugehörigen WinUI-Vorlesungen. Das Ausführen des Befehls ist einfacher, aber Sie müssen etwas Arbeit investieren, um den Befehl zu deaktivieren und zu aktivieren. Die wichtigsten Schritte:
- Fügen Sie eine öffentliche
RelayCommand
Eigenschaft mit nur Getter zum ViewModel hinzu, z.B.DecreaseAgeCommand
. Anders als in den Vorlesungsmaterial brauchen wir in unserem FallRelayCommand
keinen allgemeinen Parameter zu geben, da unsere Befehlsbehandlungsfunktion (DecreaseAge
) keinen Parameter hat. - Geben Sie der neu eingeführten Eigenschaft im ViewModel-Konstruktor einen Wert. Geben Sie die Parameter des
RelayCommand
Konstruktors entsprechend an. - In
PersonListPage.xaml
muss die Taste "-" nicht mehrClick
undIsEnabled
binden, sie werden gelöscht. Binden Sie stattdessen die EigenschaftCommand
der Taste an die EigenschaftDecreaseAgeCommand
, die im vorherigen Schritt im ViewModel eingeführt wurde.
Wenn Sie es ausprobieren, funktioniert die Ausführund des Befehls, aber das Deaktivieren/Aktivieren nicht: Wenn Sie es gut beobachten, 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.
Ändern Sie die Behandlung der "+"-Taste auf ähnliche Weise auf Befehlsbasis! Ändern Sie nicht die Behandlung der Taste "+Add"!
EINGABE
Machen Sie einen Screenshot mit dem Namen f4.png
wie folgt:
- Starten Sie die App. Verkleinern Sie 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.cs
geö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änzen Sie die Klasse
PersonListPageViewModel
mit dem Schlüsselwortpartial
. - Entfernen Sie die Eigenschaft
DecreaseAgeCommand
und ihre Instanziierung aus dem Konstruktor. - Ergänzen Sie
DecreaseAge
mit diesem Attribut:[RelayCommand(CanExecute = nameof(IsDecrementEnabled))]
.- Als Ergebnis führt der Codegenerator eine Eigenschaft
RelayCommand
in 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
CanExecute
kann 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 OperationIsDecrementEnabled
nachträglich umbenennt, würde die aktuelle "IsDecrementEnabled" nicht auf die richtige Operation verweisen. Die Verwendung des Ausdrucksnameof
vermeidet dieses Problem. Die Angabe vonCanExecute
ist im Allgemeinen optional (geben Sie es nicht an, wenn Sie den Befehl niemals deaktivieren wollen).
- Als Ergebnis führt der Codegenerator eine Eigenschaft
Testen Sie die Lösung (Verkleinerung des Alters), sie sollte genauso funktionieren wie zuvor.
EINGABE
Machen Sie einen Screenshot mit dem Namen f5.png
wie folgt:
- Starten Sie die App. Verkleinern Sie sie gegebenenfalls, damit sie nicht zu viel Platz auf dem Bildschirm einnimmt,
- im "Hintergrund" sollte Visual Studio mit
PersonListPageViewModel.cs
geöffnet sein.