Eigene WPF Commands in MVVM VB.NET & C# – der 2024 Guide

Eigene Commands in VB.NET WPF MVVM mit PasswordBox, Tricks und Co. - Guide
Eigene Commands in VB.NET WPF MVVM mit PasswordBox, Tricks und Co. – Guide

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 :)!

💡 Hinweis: Nutze gerne das Inhaltsverzeichnis, um an passende Stellen zu springen, da es sich hierbei um ein ausführliches Thema handelt, Welches man nicht „mal eben“ besprechen kann. Falls Du z. B. nur eine wiederverwendbare Command-Basisklasse benötigst, findest Du Diese weiter unten. Vielleicht möchtest Du auch einen Blick auf die Downloads-Sektion werfen?

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 „LoginCommandverwenden 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 ViewModelKonstruktor.
  • Übergib manuelle oder gebundene Parameter, indem Du an die CommandParameterEigenschaft 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

Projekt- und Ordnerstruktur – VB.NET WPF Commands in MVVM
Projekt- und Ordnerstruktur – VB.NET WPF Commands in MVVM

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

Trennung von Designern und Entwicklern
Trennung von Rollen im MVVM-Entwurfsmuster

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!?

MVVM WPF XAML Wireframing Prozess – Quelle: moqups Wireframing Tool
MVVM WPF XAML Wireframing Prozess – Quelle: moqups.com Wireframing Tool

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

Ein erstes, eigenes WPF Command in MVVM
Ein erstes, eigenes WPF Command in MVVM

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..
Login-Fenster mit WPF Commands in MVVM
Login-Fenster mit WPF Commands in MVVM

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.

💡 Hinweis: Wir werden im weiteren Verlauf noch an der PasswordBox und Co. arbeiten, sowie wichtige Sicherheitsaspekte besprechen. Dies hier ist daher noch nicht die finale Variante! Ebenso fehlt hier auch noch ein wichtiger Punkt bei der Benutzer-Textbox!

<!-- 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

Ein ViewModel zur Oberfläche
Ein 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:

BindingExpression Fehler – User konnte nicht für die Datenbindung auf dem ViewModel gefunden werden
BindingExpression Fehler – User konnte nicht für die Datenbindung auf dem ViewModel gefunden werden

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:

  1. Die User-Eigenschaft
  2. Die Password-Eigenschaft
  3. 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

WPF Command nach 3 Klicks sperren
WPF Command nach 3 Klicks sperren

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

Das DelegateCommand – unsere Basisklasse für WPF Commands
Das DelegateCommand – unsere Basisklasse für WPF Commands

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:

  1. Was soll bei Ausführung getan werden?
  2. Kann es ausgeführt werden?
  3. 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?

WPF Command-Parameter übergeben
WPF Command-Parameter übergeben

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.

💡 Hinweis: Viele Online-Code-Schnipsel verwenden die Übergabe des Parameters auch als Möglichkeit, die PasswordBox in das Command zu übergeben und so das Passwort bei Bedarf abzurufen. Es ist laut Diskussion im Netz zwar in erster Linie „sicherer“ und sicherlich eine mögliche Herangehensweise, allerdings finde ich dies wenig praktikabel.

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert