Eigene WPF Commands in MVVM VB.NET & C# – der 2024 Guide
Inhaltsverzeichnis
- 1 Nutze WPF Commands, vermeide „Button1_Click“ & Co.
- 2 Kurzfassung – .NET WPF Commands in MVVM
- 3 Beitrag als Video ansehen
- 4 Projekt- und Ordnerstruktur
- 5 Die Rollenverteilung in .NET WPF MVVM
- 6 Ein kleines Login-Fenster-Beispiel für WPF Commands
- 7 Das ViewModel zur Oberfläche
- 8 Der erste Command-Versuch
- 9 Eine wiederverwendbare DelegateCommand-Klasse
- 10 Wie übergibt man Command-Parameter?
- 11 Downloads
Nutze WPF Commands, vermeide „Button1_Click“ & Co.
Im heutigen Beitrag schauen wir uns das Thema „eigene WPF Commands“, bzw. die passende Schnittstelle „ICommand“ an. Damit werden wir der besonders aus Winforms-Zeiten (Windows Forms) bekannten Vermischung von Design und Quellcode entfliehen. Dazu schauen wir uns erst die Basis der „ICommand“-Schnittstelle an und erstellen auch anschließend eine wiederverwendbare Klasse – wir haben schließlich keinen Bock, alles 10x zu schreiben! Um alles zu verstehen, geht es hier und da natürlich noch einmal Richtung Details, aber gut, so viel Zeit muss sein, wenn man nicht nur „Copy & Paste“-Master sein möchte :)!
In diesem Beitrag werden wir synchrone Commands besprechen, die asynchrone Variante werden wir im Detail, in einem der nächsten Beiträge angehen. Ebenso wird das Thema „Login“ nur als Beispiel thematisiert und nicht vollständig realisiert. Allgemein möchte ich erreichen, dass Du hier rausgehen und „Nice, ich habe Commands verstanden“ sagen kannst :)!
Kurzfassung – .NET WPF Commands in MVVM
Wenn Du es eilig hast, hier die Kurzfassung über .NET MVVM Commands in WPF:
- Ersetze z. B. die typischen Click-Handler von Buttons durch passende Datenbindungen an sogenannte Commands. Beachte jedoch, dass Commands nicht nur von Buttons, sondern z. B. auch von Kontextmenüs unterstützt werden!
- Dazu musst Du Deinem View in erster Linie einen Datenkontext geben, damit es weiß, woher es die „Dinge“ durch Datenbindung bekommen kann.
- Sage in Deinem Button-XAML dann via Command-Eigenschafts-Bindung, dass Du das „LoginCommand“ verwenden möchtest – voilà!
- Implementiere im nächsten Schritt die „ICommand„-Schnittstelle und erstelle danach in Deinem Datenkontext eine passende, bindbare Eigenschaft, z. B. „LoginCommand“ zur Verfügung.
- Instanziiere die Command-basierte Eigenschaft nun z. B. in Deinem ViewModel–Konstruktor.
- Übergib manuelle oder gebundene Parameter, indem Du an die CommandParameter–Eigenschaft des (z. B.) Buttons bindest.
Damit Du die Implementierung allerdings nicht hunderte Male und für z. B. jeden Knopf wiederholen musst, würde ich Dir eine Abstraktion durch eine Basisklasse vorschlagen. Eine derartige Klasse (für VB.NET & C#) findest Du natürlich auch hier im Beitrag. Scrolle dazu einfach zu „eine wiederverwendbare Command-Basisklasse„, oder direkt zu den Downloads.
Beitrag als Video ansehen
Wenn Du diesen Beitrag lieber im Videoformat genießen möchtest, kannst Du natürlich auch das folgende Video (vielleicht auch ergänzend) ansehen. Besonders die Hinweise bezüglich der „PasswordBox“ sind sehr wichtig!
Projekt- und Ordnerstruktur
Bevor wir mit dem eigentlichen Thema „WPF Commands in MVVM“ loslegen können, legen wir uns als Erstes unsere Projektstruktur zurecht. Eine vernünftige Ordner- und Organisationsstruktur ist natürlich nicht nur nützlich, wenn man allein arbeitet, um z. B. immer in den gewohnten Ablauf hineinzukommen. Es hilft uns zusätzlich auch dabei, falls wir mal etwas outsourcen, bzw. jemanden um Hilfe bitten müssen – und das kommt häufig schneller vor, als man denkt! Wer will sich außerdem in seinem eigenen Code immer den Ast absuchen müssen, nur weil es jedes Mal anders aussieht? Keiner!
Natürlich könnte ich jetzt hier auch alles wieder von vorn erzählen, möchte es aber im Sinne des Entwicklers (Don’t repeat yourself – wiederhole dich nicht) vermeiden. Schaue Dir den Basis-Projektaufbau daher gerne in meinem Beitrag „Wie setzt man ein VB.NET, bzw. WPF MVVM-Projekt auf“ an.
Die Rollenverteilung in .NET WPF MVVM
Da dieser Beitrag die Verwendung von Commands im Fokus hat, verstehe bitte, dass ich nicht zu tief in das „Model-View-ViewModel“-Entwurfsmuster selbst abtauchen kann. Allerdings ist für die Verwendung von WPF Commands natürlich ein gewisses Grundverständnis der MVVM-Architektur erforderlich. Besuche für eine erweiterte Erklärung gerne meinen Beitrag zum MVVM-Entwurfsmuster, auch wenn Dieser C#-geprägt ist, sind alle Erkenntnisse natürlich auch bei VB.NET analog.
Grundsätzlich geht es bei dieser Strukturierung von modernen .NET Applikationen (oder auch Applikationen im Allgemeinen) anhand des MVVM-Musters darum, eine saubere Rollentrennung zu erreichen. Insbesondere spricht man hier von der Trennung von Benutzeroberfläche und Geschäftslogik – woran meist verschiedene Personen wie Designer und Entwickler arbeiten. Dies bietet nicht nur die Möglichkeit, sich fast ganz auf seine Rolle konzentrieren zu können, sondern auch ganz andere praktische Vorteile. So erreicht man durch die einfachere Austauschbarkeit von „Views“ und „ViewModels“ etwa eine erhöhte Test- und Wartbarkeit seiner Software im Allgemeinen.
Das View – die Aufgabe des Designers
Je nach Herangehensweise (also View-, oder ViewModel-First) geht z. B. der Designer hin und entwirft nach etwaigen Wireframing-Prozessen die gewünschte Oberfläche mit Hilfe der XAML-Auszeichnungssprache. Mit dem Kunden wurden also ggf. vorherige Absprachen (mit Skizzen – Wireframes) getroffen, Welche das ungefähre Layout und Design zumindest grob bestimmen sollen. Dies kann dann z. B. ohne eine Zeile Code geschrieben zu haben wie gleich folgend aussehen und eine gemeinsame Basis / einen gemeinsamen Konsens bilden. Zugegebenermaßen ist das folgende Beispiel schon eher auf einem „Non-Plus-Ultra“-Level, da man – je nach Unternehmensgröße – eher ein „mach‘ Mal“ geliefert bekommt. Aber gut, träumen darf man als Entwickler noch, oder!?
Wenn unsere Wünsche daher also wahr werden, haben die Designer die Möglichkeit, die skizzierten Wünsche des Kunden relativ unabhängig vom Entwickler (-Team) in XAML umsetzen zu können. Ganz im Stil von WPF und Co. verwendet man hier Datenbindungen, also XAML Binding-Anweisungen, wie z. B.:
<TextBox Text="{Binding WelcomeMessage}" />
Den Designern kann und soll es an dieser Stelle egal sein, wie genau die „WelcomeMessage“ erschaffen und verändert wird, solange Sie daran binden können – ergo sich an die WPF-Mechanismen gehalten wird. Viele verkaufen den utopischen Traum des „Solo-Separat-Entwickelns“ zu gern, allerdings muss natürlich trotzdem Austausch zwischen Entwicklern und Designern stattfinden. Ein „wir arbeiten vollkommen getrennt voneinander“ ist also nicht zu 100% möglich, ABER eben viel viel einfacher. Die Designer können ja schließlich z. B. auch nicht raten, ob unsere „IsSelected“-Eigenschaft nun eben sinnigerweise so, oder „Selected“ heißt. Je öfter man zusammenarbeitet, desto leichter erschließen sich solche Muster natürlich in Zukunft und man kann immer unabhängiger, gar blind zusammenarbeiten.
Der Entwickler, die ViewModels, Geschäftslogik und Co.
Nachdem wir als Entwickler – im Optimalfall – also nun unsere Wireframes bekommen haben, können nun auch wir ziemlich isoliert / separat tätig werden. Wir entwerfen die passenden ViewModel-Klassen zu den gezeigten Oberflächen, Welche „INotifyPropertyChanged“ und Co. verwenden, um durch die Datenbindungen zu kommunizieren. Schaue Dir hierzu gerne meinen Beitrag zum Thema „INotifyPropertyChanged“ an. Bei Bedarf erschaffen wir Test-ViewModels, Welche wir durch die neue Herangehensweise, ohne Probleme und Aufwand austauschen und testen können.
Wie erwähnt möchte ich den Exkurs in Richtung „Model-View-ViewModel-Entwurfsmuster“ nicht zu weit treiben, daher gehe ich hier nicht weiter auf z. B. Models ein. Ich denke sowieso, dass das nicht zielführend wäre, denn zu viel Input ist ja auch schlecht, oder? „Kleine“ Häppchen machen dann schon eher den Appetit :)!
Ein kleines Login-Fenster-Beispiel für WPF Commands
Nach dem kleinen Exkurs von oben, schauen wir uns nun die weitere Reise in Richtung Commands an. Dazu bauen wir uns im nächsten Schritt eine kleine Login-Oberfläche (Achtung, billig..), Welche wie gleich folgend aussehen könnte. Hier werden wir auch direkt aus einer Art Designer-Perspektive agieren, also wir arbeiten in erster Linie „nur“ am XAML-Code selbst.
Wir tun hier einfach mal so, als hätten wir ein passendes Wireframe vom Kunden vorliegen.. Was macht also ggf. in erster Linie eine Login-Oberfläche aus? Naja, wir brauchen:
- Eine Textbox für die Eingabe des Nutzers / E-Mail-Adresse
- Ein Label / Textblock für diese Textbox, sonst weiß der Nutzer nicht was das soll
- Eine PasswordBox für die Eingabe des Passworts (beachte hier die Hinweise)
- Auch hierfür einen Textblock
- Einen Button, wie soll der Benutzer sonst „Login-Versuch durchführen“ triggern
- ggf. weiteren Kram, wie „Passwort vergessen“ – lassen wir hier aber aus..
Bitte bedenke auch an dieser Stelle – wie oben angekündigt –, dass wir hier kein vollständiges Login-Beispiel realisieren können, da der Beitrag auch so schon lang genug wird 😉! Einen Login werde ich ggf. mal in einem anderen Beitrag separat behandeln und dort dann ausführen.
Datenbindungen des Login-Fensters
Nachdem wir im obigen Schritt die grundsätzlich benötigten Steuerelemente angemerkt haben, brauchen wir – für MVVM selbstverständlich – auch passende Datenbindungen. Ansonsten wüssten weder die Textboxen, woher Sie Ihren Text bekommen, oder gar „hinschicken“ sollen. Ebenso hätte der Anmelden-Knopf keine Ahnung, was beim Klick passieren soll (Spoiler: Ein Command ausführen).
Der Designer wird an dieser Stelle hier irgendwie vorher (mit Kunden und Entwicklern) besprochen haben, dass es im ViewModel so etwas namens „User“ geben muss. Dabei handelt es sich genauer genommen um eine String-Eigenschaft, welche den dementsprechenden Text für die Textbox – also den Benutzer – beinhalten muss. Ebenso muss diese Eigenschaft bei Code-technischen Änderungen nach außen kommunizieren, dass Sie sich geändert hat. Dies geht wie vermutlich bekannt über „INotifyPropertyChanged“, was hier allerdings nicht weiter Thema sein wird. Hier nochmal erneut der Hinweis auf meinen „INotifyPropertyChanged“-Beitrag mit „PropertyChanged“-Basisklasse.
Schaue Dir nun einmal den XAML-Code zum Fenster an, beachte, dass Du die obigen „Projekt- und Ordnerstruktur“-Schritte korrekt abgeschlossen hast. Ansonsten wirst Du hier vermutlich Probleme mit Namespace und Co. bekommen. Passe ebenfalls auf, dass Du den Projektnamen umbenennen musst, in meinem Beispiel hieß das Projekt „WpfCommandsVbTutorial“.
Datenkontext (DataContext) setzen
Im ersten Schritt ergänzen wir die folgenden zwei Punkte in den XAML-Code des MainWindows (MainViews).
1. Wir mappen via „xmlns:<name>“ einen Namespace anhand eines Aliases:
<!-- ganz oben, im Abschnitt des Window-Tags --> xmlns:vm="clr-namespace:WpfCommandsVbTutorial.ViewModels"
2. Wir geben dem jeweiligen View einen Datenkontext (DataContext-Eigenschaft des Windows) über XAML. Wie Du vielleicht weißt, wäre dies auch via „Code behind“-Datei möglich, ich habe mich hier für die XAML Variante entschieden:
<!-- im "Content" des Windows --> <Window.DataContext> <vm:MainViewModel /> </Window.DataContext>
Du kannst an dieser Stelle übrigens die Styling-relevanten Dinge ignorieren, also praktisch alles, was in „StackPanel.Resources“ z. B. steht. Dies ist nur zur Anschauung erstellt worden und Du solltest Deinen Fokus größtenteils auf die oben aufgelisteten Steuerelemente legen.
<!-- in C#, there's a project prefix before Views --> <Window x:Class="Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfCommandsVbTutorial" mc:Ignorable="d" xmlns:vm="clr-namespace:WpfCommandsVbTutorial.ViewModels" Title="MainWindow" Background="DarkOrange" Height="450" Width="800" WindowStartupLocation="CenterScreen"> <Window.DataContext> <vm:MainViewModel /> </Window.DataContext> <Grid> <StackPanel Width="200" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel.Resources> <Style TargetType="TextBlock"> <Setter Property="Foreground" Value="WhiteSmoke" /> <Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontSize" Value="16" /> <Setter Property="Margin" Value="0 0 0 4" /> </Style> <Style TargetType="TextBox"> <Setter Property="FontSize" Value="16" /> <Setter Property="Margin" Value="0 0 0 4" /> <Setter Property="Effect"> <Setter.Value> <DropShadowEffect ShadowDepth="2" Direction="330" Color="Black" Opacity="0.5" BlurRadius="1"/> </Setter.Value> </Setter> </Style> <Style TargetType="PasswordBox"> <Setter Property="FontSize" Value="16" /> <Setter Property="Margin" Value="0 0 0 8" /> <Setter Property="Effect"> <Setter.Value> <DropShadowEffect ShadowDepth="2" Direction="330" Color="Black" Opacity="0.5" BlurRadius="1"/> </Setter.Value> </Setter> </Style> </StackPanel.Resources> <TextBlock Text="Benutzer" /> <TextBox Text="{Binding User}" /> <TextBlock Text="Passwort" /> <!-- die Passwordbox kann nicht so einfach gebunden werden und da dies nicht Bestandteil dieses Beitrages ist, werde ich dies auf einen anderen Beitrag verlagern. Konzentriere Dich bitte auf die Aspekte von WPF Commands :) --> <PasswordBox /> <Button Command="{Binding LoginCommand}" Content="Anmelden" FontSize="16" /> </StackPanel> </Grid> </Window>
Das ViewModel zur Oberfläche
Bevor wir mit den WPF Commands im Detail fortfahren können, müssen wir noch etwas bauen, was Diese letztendlich beinhaltet. Hierbei rede ich natürlich von einer ViewModel-Klasse, Welche alles Notwendige für die Datenbindungen bereitstellt. Neben dem oben im XAML erwähnten „LoginCommand“, bezieht sich dies natürlich auch auf die Eigenschaften wie „User“ und „Password“.
Wenn wir dies auslassen würden, könnte die grafische Oberfläche Ihre dort empfangenen Änderungen nie via Datenbindungen an die dahinter liegenden ViewModel-Eigenschaften weitergeben. Somit hätten wir auch in unserer Geschäftslogik (innerhalb des ViewModels) nie die Chance, damit tatsächlich arbeiten zu können. Wie Du Dir also vorstellen kannst, wäre eine Anmeldung ohne z. B. typische Benutzer- und Passwort-Kombination ziemlich schwierig.
Fehlermeldungen bei fehlender / falscher Datenbindung
Falls Du an dieser Stelle also eine der typischen Binding-Fehlermeldungen bekommen solltest, kannst Du Dir nun vorstellen woran das liegt. Du hast vermutlich vergessen, den DataContext mit einer passenden ViewModel-Instanz zu befüllen – wie wir es im gleich folgenden Schritt tätigen werden. Typische Fehlermeldung sehen ungefähr so aus, also hier wurde z. B. die „User“-Eigenschaft nicht auf der „MainViewModel“-Instanz gefunden:
Falls Du also eine derartige Fehlermeldung bekommst, stelle sicher, dass die Eigenschaft „User“ in Deinem Viewmodel existiert. Es kann hier aber auch vorkommen, dass z. B. gar kein Fehler kommt und dennoch nichts passiert. Dies ist z. B. der Fall, wenn Du vergisst, den DataContext überhaupt zu setzen! Denke daran, dass wir das hier im View selbst, also via XAML-Code weiter oben getan haben!
Das (erste) MainViewModel erstellen
Erstelle für die weitere Vorgehensweise in Richtung WPF Commands nun einmal bitte eine Klasse namens „MainViewModel“ in Deinem „ViewModels“-Ordner. Unsere ViewModel-Klasse wird hierbei von der „PropertyChangedBase“-Basisklasse erben, damit wir die dort zurechtgeschriebene Funktionalität weiterverwenden können. Somit sparen wir es uns, die „INotifyPropertyChanged“-Schnittstelle immer und immer wieder manuell implementieren zu müssen. Kopiere Dir die Basisklasse ruhig von diesem Beitrag hier.
Nun das MainViewModel, wundere Dich bitte nicht, dass dort schon anderer Kram drin steht, aber ich möchte Dir diesen Batzen gleich nicht noch 3x um die Ohren hauen. Wir nehmen dies daher als Basis und arbeiten uns von dort aus weiter durch den Code. Es fehlen hier auch noch die ein odere andere Funktion, sowie eine Klasse für die Commands, Welche wir natürlich noch erstellen.
Imports WpfCommandsVbTutorial.Utils Namespace ViewModels Public Class MainViewModel Inherits PropertyChangedBase Private _user As String Public Property User As String Get Return _user End Get Set(value As String) If _user = value Then Return End If _user = value NotifyOfPropertyChange() End Set End Property Private _password As String Public Property Password As String Get Return _password End Get Set(value As String) If _password = value Then Return End If _password = value NotifyOfPropertyChange() End Set End Property Public Property LoginCommand As LoginCommand Sub New() LoginCommand = New LoginCommand() End Sub End Class End Namespace
using Utils; namespace ViewModels; public class MainViewModel : PropertyChangedBase { private string _user; public string User { get => _user; set { if (_user == value) return; _user = value; NotifyOfPropertyChange(); } } private string _password; public string Password { get => _password; set { if (_password == value) return; _password = value; NotifyOfPropertyChange(); } } public LoginCommand LoginCommand { get; set; } public MainViewModel() { LoginCommand = new LoginCommand(); } }
Die üblichen Mechanismen
Zuerst einmal siehst Du im obigen MainViewModel-Code die 3 wichtigsten Eigenschaften für unser Login-Fenster:
- Die User-Eigenschaft
- Die Password-Eigenschaft
- Die LoginCommand-Eigenschaft
Bis auf das „LoginCommand“ sind alle Eigenschaften sogenannte „voll implementierte“-Eigenschaften, Sie haben also einen von uns manuell implementierten Getter und Setter. Dies geschieht deshalb, weil wir im Setter sagen müssen: „Hey, es hat sich Code-seitig z. B. der User geändert“. Das passiert einerseits, wenn wir einen gespeicherten Zugang von der Festplatten laden, aber auch, wenn der Benutzer des Programms eine Eingabe in der Textbox macht – besonders hierzu aber gleich noch etwas! Andererseits können wir dem Command das dann auch noch explizit mitteilen, denn warum sollte man sich anmelden können, wenn, wenn z. B. gar kein Benutzername eingegeben wurde? Dazu kommen wir aber noch..
Etwas anders sieht es wie schon gesagt beim „LoginCommand“ aus, denn Dieses wird nach der Instanziierung nicht mehr geändert. Hier reicht also eine sogenannte automatisch implementierte Eigenschaft aus.
Der erste Command-Versuch
An dieser Stelle werden wir den ersten Versuch Richtung eigener WPF Commands starten. Wir werden dafür das sogenannte „ICommand“-Interface (manuell) implementieren – später vereinfacht, keine Sorge! Erstelle also nun bitte eine weitere Klasse (ohne Datei) namens „LoginCmd“, Diese kannst Du für den Anfang einfach innerhalb des Wurzelverzeichnisses Deines Projektes ablegen. Dies werden wir selbstverständlich später ändern, brauchen wir aber am Anfang zur reinen Demonstration.
Schreibe für VB.NET anschließend – wie für Schnittstellen-Implementierungen üblich – einfach „Implements INotifyPropertyChanged“ unter die beginnende Klassendefinition: „Public Class LoginCommand“. Für C# kannst Du ein „: INotifyPropertyChanged“ hinter den Klassennamen schreiben. In beiden Fällen kannst Du dann die Imports durch die Visual Studio-Hilfe durchführen. Alternativ kannst Du auch Strg+“.“ drücken und die dann erscheinenden Vorschläge durchführen.
Imports WpfCommandsVbTutorial.Utils Namespace ViewModels Public Class MainViewModel Inherits PropertyChangedBase ' restlicher MainViewModel-Code Public Property LoginCommand As LoginCommand Sub New() LoginCommand = New LoginCommand() End Sub Public Class LoginCmd Implements ICommand Public Sub Execute(parameter As Object) Implements ICommand.Execute Throw New NotImplementedException() End Sub Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute Throw New NotImplementedException() End Function Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged End Class End Class End Namespace
using System.Windows.Input; using System; using Utils; namespace ViewModels; public class MainViewModel : PropertyChangedBase { // restlicher MainViewModel-Code public LoginCmd LoginCommand { get; set; } public MainViewModel() { LoginCommand = new LoginCmd(); } public class LoginCmd : ICommand { public void Execute(object? parameter) { throw new NotImplementedException(); } public bool CanExecute(object? parameter) { throw new NotImplementedException(); } public event EventHandler? CanExecuteChanged; } }
Was soll bei Ausführung des WPF Commands passieren?
Um dem Command sagen zu können: „Hey, wenn Du ausgeführt wirst, soll xy passieren“, müssen wir nun eine bestimmte Methode implementieren. Dabei handelt es sich um die „Execute“-Methode, der „ICommand“-Schnittstelle. Dort drin können wir beispielsweise einmal einen Anmeldevorgang simulieren, mehr wird denke ich zu kompliziert sein, also etwa mit Services zu kommunizieren, usw. Das werde ich aber mit Sicherheit noch in einem anderen Beitrag vertiefen!
Die „Execute“-Methode sieht dann grundsätzlich erstmal wie folgt aus:
Public Class LoginCommand Implements ICommand Public Sub Execute(parameter As Object) Implements ICommand.Execute ' hier kommt rein, was beim Klick passieren soll Debug.WriteLine("Melde mich an") End Sub ' restlicher Code.. End Class
public class LoginCommand : ICommand { public void Execute(object? parameter) { // hier kommt rein, was beim Klick passieren soll Debug.WriteLine("Melde mich an"); } // restlicher Code.. }
Kann das WPF Command überhaupt ausgeführt werden?
Nachdem wir die eine „Execute“-Methode implementiert und somit beschrieben haben, was beim Ausführen des Commands passieren soll, geht’s nun mit dem „kann ich das ausführen“ weiter. Es wäre ja z. B. blöd, wenn der Benutzer der Software den „Anmelden“-Knopf spammen könnte. Stattdessen sollte man den Button deaktivieren, während z. B. eine Art Sperre stattfindet. Dazu müssen wir eine Funktion namens „CanExecute“ implementieren, Welche – wie der Name schon sagt – bestimmt, ob das jeweilige Command gerade ausgeführt werden kann.
Der Code dazu könnte wie folgt aussehen, also, dass z. B. das Command nach 3 fehlerhaften Login-Versuchen gesperrt ist.
Public Class LoginCommand Implements ICommand Private _failedAttempts As Integer Public Sub Execute(parameter As Object) Implements ICommand.Execute ' simulate failed login attempt _failedAttempts += 1 End Sub Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute Return _failedAttempts < 3 End Function Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged End Class
using System; using System.Windows.Input; public class LoginCommand : ICommand { private int _failedAttempts; public void Execute(object? parameter) { // simulate failed login attempt _failedAttempts++; } public bool CanExecute(object? parameter) { return _failedAttempts < 3; } public event EventHandler? CanExecuteChanged; }
Zum Schluss bleibt hier nun noch ein letztes Problem: Das Command wird so gesehen niemals „aktualisiert“. Es wird aktuell leider nicht nach außen bekanntgemacht: „Hey, grafische Oberfläche, evaluiere bitte einmal neu, ob ich ausgeführt werden kann, denn hier hat sich was geändert!“. Somit bleibt das Command aktuell immer ausführbar. Wenn Du stattdessen z. B. initial ein Kriterium hättest – wie z. B. eine Rolle an Nutzern– die den Button nicht drücken dürfen, dann könnte es auch sein, dass der Button permanent grau hinterlegt ist. Keine Sorge, darum kümmern wir uns nun im letzten Schritt!
Neu evaluieren, ob ausgeführt werden kann
Um der grafischen Oberfläche genau dieses Signal a la „Hey, re-evaluiere hier bitte einmal die CanExecute-Funktion“ zu geben, müssen wir nur eins tun: Das Ereignis auslösen, Welches bewusst von der „ICommand“-Schnittstelle dafür vorgegeben wurde, namens „CanExecuteChanged“. Aktuell ist dies noch relativ einfach, da wir alles was wir dafür benötigen, innerhalb der Command-Instanz selbst haben.
Was ich meine, ist die Anzahl an fehlgeschlagenen Anmelde-Versuchen, Welche wir in der Zeile 3 deklariert haben. Wir müssten also eigentlich immer das „CanExecuteChanged“-Ereignis auslösen, wenn sich diese Variable ändert. Gleich kommen wir auch noch dazu, wie wir unter Umständen agieren könnten, wenn wir z. B. auf eine „IsLoading“-Eigenschaft eines ViewModels keinen Zugriff haben.
Das könnte z. B. so aussehen:
Public Class LoginCommand Implements ICommand Private _failedAttempts As Integer Public Sub Execute(parameter As Object) Implements ICommand.Execute ' simulate failed login attempt _failedAttempts += 1 RaiseEvent CanExecuteChanged(Me, EventArgs.Empty) End Sub Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute Return _failedAttempts < 3 End Function Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged End Class
using System.Windows.Input; using System; public class LoginCommand : ICommand { private int _failedAttempts; public void Execute(object? parameter) { // simulate failed login attempt _failedAttempts++; CanExecuteChanged?.Invoke(this, EventArgs.Empty); } public bool CanExecute(object? parameter) { return _failedAttempts < 3; } public event EventHandler? CanExecuteChanged; }
Der ganze Aufwand für ein einziges Command?
Ich gebe zu, dass dies alles, vor allem als Anfänger natürlich erstmal überwältigend klingen kann, besonders für bisher ein einziges Command, aber keine Sorge – „I got you“. Statt diesen Aufwand immer wieder von vorn zu betreiben, werden wir uns – als „faule“ Entwickler – selbstverständlich einige Hilfsmittel schreiben, Welche uns die Arbeit vereinfachen. Im ersten Schritt wird das die gleich folgende, wiederverwendbare Klasse Namens z. B. „DelegateCommand“ sein.
Eine wiederverwendbare DelegateCommand-Klasse
Wie Du vermutlich im letzten Schritt festgestellt hast, ist das Ganze mit den WPF Commands nicht so „mal eben“ wie mit den Click-Handlern erledigt. Lasse Dich aber bitte hier dennoch nicht dazu verführen – typisch Mensch, typisch Gewohnheitstier – dennoch wieder die alten „Button1_Click“ und Co. Dinger zu verwenden. Wir können uns wie hier drüber erwähnt, auch viele Hilfsmittel erschaffen, mit Denen wir uns das Handling deutlich vereinfachen können.
Im Endeffekt ändert sich von der Konzeption des Commands selbst, nicht viel, wir brauchen immer noch folgende Aspekte:
- Was soll bei Ausführung getan werden?
- Kann es ausgeführt werden?
- Wann ändert sich die Info, ob’s ausgeführt werden kann?
Allerdings möchten wir nun nicht jedes Mal dafür eine eigene Klasse erstellen, sondern alles ein wenig mehr zentralisieren. Das können wir ganz einfach, indem wir Obiges logisch betrachten: Punkt 1 ist einfach nur ein „Packen“ Arbeit, Welchen wir in die Zukunft delegieren (vielleicht rückt der Begriff „DelegateCommand“ nun etwas näher 😉), nämlich bei z. B. einem Klick. Punkt 2 ist praktisch das Gleiche in bunt, nur, dass wir eine „Ja/Nein“-Info zurückbekommen: Also einen Boolean. Punkt 3 kann je nach Fall komplizierter werden, aber auch das lässt sich regeln.
Methoden / Funktionalität dynamisch übergeben
Wenn man nun zu den obigen Erkenntnissen gekommen ist, fragt man sich im nächsten Schritt das Offensichtliche: „Okay, aber wie verlagere / gruppiere ich denn überhaupt Arbeit?“. Das Stichwort, bzw. die Stichworte Deiner Begierde nennen sich „Action„, bzw. „Func„. Diese übernehmen exakt das, was wir bruachen, Sie kapseln / gruppieren unsere Anweisungen und machen Diese praktisch ansprech- und ausführbar – auch später.
Der erste Versuch – delegierte Arbeit
Schreiben wir also nun unseren aus den Erkenntnissen resultierenden, ersten Versuch. Hierbei geht es erstmal nur darum, die „Was soll getan werden“-Anweisungen als Gruppe von Anweisungen zu verlagern / zu delegieren. Das geht auch im Endeffekt ganz einfach, schaue Dir dazu diese neue Variante unserer Command-Implementierung an.
Wir lassen hierbei der instaziierenden Partei die Möglichkeit, den gruppierten Stapel an Arbeit von außen mitzugeben. Beachte auch, dass wir sogar die Möglichkeit haben, einen Parameter mitzuliefern. Dadurch sind wir – indem wir z. B. eine weitere Klasse erstellen – auch nicht auf einen Übergabeparameter limitiert, könnten also praktisch eine ganze „Config“ mitliefern. Natürlich muss dies passend in der Execute-Methode gecastet werden..
Namespace Utils Public Class DelegateCommand Implements ICommand Private _action As Action(Of Object) Sub New(action As Action(Of Object)) _action = action End Sub Public Sub Execute(parameter As Object) Implements ICommand.Execute ' führt unsere von außen mitgegebene Aktion aus und übergibt auch den Parameter _action(parameter) End Sub Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute Return True End Function Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged End Class End Namespace
using System.Windows.Input; using System; namespace Utils; public class DelegateCommand : ICommand { private Action<object?> _action; public DelegateCommand(Action<object?> action) { _action = action; } public void Execute(object? parameter) { // führt unsere von außen mitgegebene Aktion aus und übergibt auch den Parameter _action?.Invoke(parameter); } public bool CanExecute(object? parameter) { return true; } public event EventHandler? CanExecuteChanged; }
Delegierte Arbeit im MainViewModel verwenden
Zurück in unserem (komprimierten) MainViewModel, könnte die Erstellung des Commands nun wie gleich folgend und vollkommen dynamisch aussehen. Beachte auch, dass wir durch Polymorphie oben ein „ICommand“ deklarieren, aber ein diese Schnittstelle implementierendes Objekt zuweisen können (im Konstruktor).
Imports WpfCommandsVbTutorial.Utils Namespace ViewModels Public Class MainViewModel Inherits PropertyChangedBase ' restliche Eigenschaften.. Public Property LoginCommand As ICommand Sub New() LoginCommand = New DelegateCommand(AddressOf Login) End Sub ' unsere gruppierten Anweisungen - zusammengefasst Private Sub Login(parameter As Object) ' tu dies ' tu jenes ' tu das End Sub End Class End Namespace
using System.Windows.Input; using Utils; namespace ViewModels; public class MainViewModel : PropertyChangedBase { // restliche Eigenschaften.. public ICommand LoginCommand { get; set; } public MainViewModel() { LoginCommand = new DelegateCommand(Login); } // unsere gruppierten Anweisungen - zusammengefasst private void Login(object? parameter) { // tu dies // tu jenes // tu das } }
Was fehlt ist, ob’s geht!
Was nun noch als vorletzter Punkt fehlt, ist eine dynamische Feststellung, ob das Command überhaupt ausgeführt werden kann, denn das haben wir aktuell völlig vernachlässigt. Zum Glück können wir uns auch hier ganz schnell helfen, denn dafür gab es neben der „Action“ auch etwas namens „Func“. Damit können wir eine Funktion dynamisch übergeben und dessen Rückgabewert nach unserem Belieben auswerten.
Hierzu habe ich einen neuen Konstruktor erstellt, Welcher uns dann die optionale Möglichkeit bietet, sagen zu können: „Hey, so stellst Du fest, ob das Command ausgeführt werden kann“. Wenn dies also nicht mitgegeben wird, könnte man praktisch davon ausgehen, dass das Command immer ausgeführt werden kann.
Die finale DelegateCommand-Basisklasse
Namespace Utils Public Class DelegateCommand Implements ICommand Private _action As Action(Of Object) Private _canExecute As Func(Of Object, Boolean) Sub New(action As Action(Of Object)) _action = action End Sub Sub New(action As Action(Of Object), canExecute As Func(Of Object, Boolean)) _action = action _canExecute = canExecute End Sub Public Sub Execute(parameter As Object) Implements ICommand.Execute ' führt unsere von außen mitgegebene Aktion aus und übergibt auch den Parameter _action(parameter) End Sub Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute If _canExecute Is Nothing Then Return True End If Return _canExecute(parameter) End Function Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged End Class End Namespace
using System.Windows.Input; using System; namespace Utils { public class DelegateCommand : ICommand { private Action<object?> _action; private Func<object?, bool>? _canExecute; public DelegateCommand(Action<object?> action) { _action = action; } public DelegateCommand(Action<object?> action, Func<object?, bool> canExecute) { _action = action; _canExecute = canExecute; } public void Execute(object? parameter) { _action(parameter); } public bool CanExecute(object? parameter) { if (_canExecute == null) return true; return _canExecute(parameter); } public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } public event EventHandler? CanExecuteChanged; } }
Nun kann unser MainViewModel wie folgt ergänzt werden:
Imports WpfCommandsVbTutorial.Utils Namespace ViewModels Public Class MainViewModel Inherits PropertyChangedBase ' other properties.. Private Property LoginAttempts As Integer Public Property LoginCommand As ICommand Sub New() LoginCommand = New DelegateCommand(AddressOf Login, AddressOf CanLogin) End Sub ' our grouped instructions - summed up Private Sub Login(parameter As Object) ' do this ' do that ' do something else LoginAttempts += 1 End Sub Private Function CanLogin(parameter As Object) As Boolean ' determine, if the login process should be executed and return it Return LoginAttempts < 3 End Function End Class End Namespace
using System.Windows.Input; using Utils; namespace ViewModels; public class MainViewModel : PropertyChangedBase { // restliche Eigenschaften.. private int LoginAttempts { get; set; } public ICommand LoginCommand { get; set; } public MainViewModel() { LoginCommand = new DelegateCommand(Login, CanLogin); } // our grouped instructions - summed up private void Login(object? parameter) { // do this // do that // do something else LoginAttempts++; } private bool CanLogin(object? parameter) { // determine, if the login process should be executed and return it return LoginAttempts < 3; } }
Und was, wenn es sich ändert?
Nun kommen wir (sorry, endlich) zum finalen Punkt: Wir müssen auch hier die Möglichkeit bieten, sagen zu können: „Hey Oberfläche, bitte neu evaluieren, ob das Command ausgeführt werden kann“. Dafür gibt es z. B. die „CommandManager.InvalidateRequerySuggested“-Methode, Welche ich aber nicht ganz toll finde. Alle Commands re-evaluieren, nur weil sich ggf. Eines geändert hat? Nee..
Wir werden stattdessen unser eigenes kleines Helferlein dafür einbauen, ich meine, dafür haben wir ja sowieso schon unsere Basisklasse. Wir ergänzen also eine Methode, Welche es uns von außen erlaubt, das „CanExecuteChanged“-Ereignis des Commands auszulösen. Für mich fühlt sich auch das nicht ganz super an, aber es deckt genau unsere Bedürfnisse:
Namespace Utils Public Class DelegateCommand Implements ICommand Private _action As Action(Of Object) Private _canExecute As Func(Of Object, Boolean) Sub New(action As Action(Of Object)) _action = action End Sub Sub New(action As Action(Of Object), canExecute As Func(Of Object, Boolean)) _action = action _canExecute = canExecute End Sub Public Sub Execute(parameter As Object) Implements ICommand.Execute _action(parameter) End Sub Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute If _canExecute Is Nothing Then Return True End If Return _canExecute(parameter) End Function Public Sub RaiseCanExecuteChanged() RaiseEvent CanExecuteChanged(Me, EventArgs.Empty) End Sub Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged End Class End Namespace
using System.Windows.Input; using System; namespace Utils; public class DelegateCommand : ICommand { private Action<object?> _action; private Func<object?, bool>? _canExecute; public DelegateCommand(Action<object?> action) { _action = action; } public DelegateCommand(Action<object?> action, Func<object?, bool>? canExecute) { _action = action; _canExecute = canExecute; } public void Execute(object? parameter) { _action(parameter); } public bool CanExecute(object? parameter) { if (_canExecute == null) return true; return _canExecute(parameter); } public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } public event EventHandler? CanExecuteChanged; }
Zum Schluss nun unsere letzte MainViewModel-Anpassung (bezogen auf die fehlgeschlagene Logins Geschichte):
Imports WpfCommandsVbTutorial.Utils Namespace ViewModels Public Class MainViewModel Inherits PropertyChangedBase ' restliche Eigenschaften Public Property LoginCommand As CommandBase Private _loginAttempts As Integer Private Property LoginAttempts As Integer Get Return _loginAttempts End Get Set(value As Integer) _loginAttempts = value LoginCommand.RaiseCanExecuteChanged() End Set End Property Sub New() LoginCommand = New CommandBase(AddressOf Login, AddressOf CanLogin) End Sub Private Sub Login(parameter As Object) ' fehlgeschlagenen Login simulieren.. LoginAttempts += 1 End Sub Private Function CanLogin(parameter As Object) As Boolean Return _loginAttempts < 3 End Function End Class End Namespace
using Utils; namespace ViewModels; public class MainViewModel : PropertyChangedBase { // restliche Eigenschaften public DelegateCommand LoginCommand { get; set; } private int _loginAttempts; private int LoginAttempts { get => _loginAttempts; set { _loginAttempts = value; LoginCommand.RaiseCanExecuteChanged(); } } public MainViewModel() { LoginCommand = new DelegateCommand(Login, CanLogin); } private void Login(object? parameter) { // simulate failed login LoginAttempts++; } private bool CanLogin(object? parameter) { return _loginAttempts < 3; } }
Wie übergibt man Command-Parameter?
Bisher konnten wir schon viele Eindrücke in das WPF Command gewinnen, eine Sache ist jedoch noch ein wenig auf der Strecke geblieben, sorry. Es geht um das Übergeben von Parametern, da wir ja auch z. B. extra eine „Action“ / „Func“ mit generischem Typenparameter vom Typ „Object“ deklariert haben. Bisher verwenden wir diese Möglichkeit der Parameterübergabe noch nicht.
Jeder (und ich meine jeder) Entwickler muss hier selbst entscheiden, Welche Sicherheitsmaßnahmen für sich, Kunden und Co. angemessen sind! Dies soll die „Considerations“ selbstverständlich trotzdem nicht kleinreden und man sollte sich an „best practices“ halten..
Warum, bzw. wann übergibt man Command-Parameter?
Bevor man sich allerdings tiefer in diesen Dschungel wagt, macht diese kleine Überlegung ggf. auch Sinn: „Wann braucht man Command-Parameter überhaupt!?“. Grundsätzlich können wir ja auf die Eigenschaften unseres Viewmodels zugreifen, wenn wir lokale Methoden des ViewModels mit der DelegateCommand-Klasse von oben verwenden. Das stimmt soweit auch, aber wie wir schon im Absatz hier drüber gesehen haben, kann es Situationen geben, wo wir auf die Dinge ggf. keinen Zugriff haben, dann könnte so eine Übergabe helfen.
Parameter manuell via XAML übergeben
Lasse uns daher einmal im nächsten Schritt schauen, wie wir diese Möglichkeit der Parameter nun allgemein verwenden könnten. Ich denke hier zuerst an die einfachste Variante, also eine einfache „manuelle“ Übergabe. Dies können wir sehr einfach im XAML-Code erledigen, indem wir einfach einen passenden Wert, an die „CommandParameter“-Eigenschaft mitliefern:
<Button Command="{Binding LoginCommand}" CommandParameter="D" />
WPF Command-Parameter via DataBinding übergeben
Auch ganz interessant ist die eigentlich offensichtlichste Sache, also die Übergabe via normalem Databinding. Hierzu kann ich einfach eine normale „Binding-Expression“, also einen Bindungs-Ausdruck schreiben. Dieser wird dann zu passender Zeit evaluiert und als CommandParameter übergeben:
<Button Command="{Binding LoginCommand}" CommandParameter="{Binding SomeBoundPropertyToPassIn}" />
Mehrere Parameter via Klasse oder MultiBinding übergeben
Hier kommt noch ein wichtiger Punkt, da viele Anfänger daran scheitern. Häufig stellt man folgende Überlegung an: „Okay, so übergebe ich einen einzigen Parameter an das WPF Command, aber wie kann ich Mehrere auf einmal übergeben?“.
Aber keine Sorge, die Antwort ist relativ einfach, die erste Variante wäre via eigener Klasse. Lege dazu einfach eine passende Klasse mit den Eigenschaften / Werten, die Du übertragen möchtest, an:
Public Class SpecialCmdParams Public Property SomePropToPassOne As String Public Property SomePropToPassTwo As Integer ' ... End Class
public class SpecialCmdParams { public string SomePropToPassOne { get; set; } public int SomePropToPassTwo { get; set; } // ... }
Instanziiere diese Klasse nun an passender Stelle, z. B. im ViewModel-Konstruktor und befülle die Eigenschaften durch passende Bindungen an z. B. Textboxen, whatever. Im nächsten Schritt kannst Du die Instanz der Klasse natürlich einfach via Datebindung (wie hier drüber, siehe „SomeBoundPropertyToPassIn“) übergeben. Gleich schauen wir uns noch an, wie man dies dann abrufen kann.
Parameter via Multibinding übergeben
Wer keine Lust hat jedes Mal eine passende Klasse zu erstellen, kann sich die Übergabe mehrerer Command-Parameter auch durch ein sogenanntes „Multibinding“ vereinfachen. Wie der Name schon sagt, bindet man hier nicht an ein „Ding“, sondern kann gleich mehrere Datenbindungen quasi in Einer verwenden.
Das Blöde ist hier nur, dass man einen „Converter“ benötigt, wie soll sonst aus „mehrere Dinger“ -> „ein Ding“ werden? Man muss hier auch noch bedenken, dass man eventuell den Weg zurück braucht, also aus einem Ding wieder Mehrere zu machen. Ein derartiges, passendes Muster finden wir bei der „IMultiValueConverter“-Schnittstelle.
Implementieren wir dieses Interface also einmal beispielhaft, um mehrere Zahlen zu übergeben (auch wenn dies bei einem Login keinen Sinn macht), es geht wie gesagt um „Commands“ selbst :)! Wir übergeben hier einfach mal stumpf die Größe des Login-Fensters. Dies könnten wir natürlich auch durch eine normale Datenbindung realisieren, aber es geht wie gesagt um ein Beispiel!
Beachte, dass die Implementierung hier nur beispielhaft und z. B. nicht konfigurierbar ist. Zum Thema „ValueConverter“ im Allgemeinen werde ich ggf. noch einen separaten Beitrag schreiben. Erstelle also nun einmal folgende Klasse, im (z. B.) „Utils“-Ordner
Imports System.Globalization Namespace Utils Public Class DoubleNumberConverter Implements IMultiValueConverter Public Function Convert(values() As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IMultiValueConverter.Convert Return String.Join("|", values) End Function Public Function ConvertBack(value As Object, targetTypes() As Type, parameter As Object, culture As CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack Return value.ToString.Split("|") End Function End Class End Namespace
using System.Globalization; namespace Utils { public class DoubleNumberConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { return string.Join("|", values); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { return value.ToString().Split("|"); } } }
Damit können wir dann ein Multibinding realisieren, Welches dann über übliche Binding-Ausdrücke Daten mitliefert – aber eben Mehrere! Vergiss hier jedoch nicht, eine Instanz des „DoubleNumberConverters“ entweder in der App.xaml / Application.xaml, oder in Deinem jeweiligen Control (UserControl, Window, etc..) zu erstellen. Ansonsten kannst Du den Konverter nicht verwenden:
<!-- The Window here is named "MyWindow" for example purposes --> <!-- further above --> <Window.Resources> <ResourceDictionary> <u:DoubleNumberConverter x:Key="MyDoubleNumberConverter" /> </ResourceDictionary> </Window.Resources> <!-- usage --> <Button Command="{Binding LoginCommand"> <Button.CommandParameter> <MultiBinding Converter="{StaticResource MyDoubleNumberConverter}"> <Binding Path="ActualWidth" ElementName="MyWindow"/> <Binding Path="ActualHeight" ElementName="MyWindow"/> </MultiBinding> </Button.CommandParameter> </Button>
Übergebene Parameter abgreifen
Nachdem wir die Beispiele zum Übergeben der Parameter gesehen haben, schauen wir uns nun an, wie wir Diese auch wieder abgreifen können. Je nachdem Welches der obigen Beispiele Du verwendest, sieht die Vorgehensweise eigentlich immer gleich aus (auch bei CanExecute):
' rest of the ViewModel-Class (like MainViewModel) ' the "parameter" contains everything, but combined in one thing Public Sub Login(parameter As Object) ' if it looked like this in the XAML: ' CommandParameter="D" Dim passedLetter = Convert.ToString(parameter) ' now there's the letter D inside passedLetter '' if it was a custom class Dim yourParam = CType(parameter, SpecialCmdParams) '' if it was the DoubleNumberConverter - you now have : Dim numbersAsStrings = Convert.ToString(parameter).Split("|") ' do conversion, whatever End Sub ' rest of the ViewModel-Class
public void Login(object parameter) { // if it looked like this in the XAML: // CommandParameter="D" var passedLetter = Convert.ToString(parameter); // now there's the letter D inside passedLetter // // if it was a custom class var yourParam = (SpecialCmdParams)parameter; // // if it was the DoubleNumberConverter - you now have : var numbersAsStrings = Convert.ToString(parameter).Split("|"); }
Downloads
Lade Dir hier gerne das Beispielprojekt (oder einzelne Bestandteile) in der Sprache Deiner Wahl herunter, da der Beitrag natürlich anhand des Themas ein wenig riesig ist. So kannst Du Dich spielerisch an die einzelnen Punkte wagen und begleitend mit dem Beitrag arbeiten. Die einzelnen Dinge beinhalten jeweils die Dateien für beide Sprachen.