Grundlagen - Commands

    • WPF

    Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von LaMiy.

      Grundlagen - Commands

      Hallo Community,

      in diesem Tutorial möchte ich euch die grundlegenden Konzepte von Commands in WPF näher bringen. Ich werde hier alle Erklärungen ausschließlich im Zusammenhang mit dem MVVM-Pattern (Model-View-ViewModel-Pattern) liefern, das Tutorial setzt daher Kenntnisse über dieses Pattern voraus (dies schließt die nötigen XAML-Kenntnisse mit ein).


      Was sind Commands und wofür brauche ich sie?

      Ein Großteil der WPF-Anwärter stammt aus dem Bereich der Vorgängertechnologie WindowsForms, welches Eventbasiert funktioniert. Es ist daher nicht verwunderlich, dass häufig die Codes von Anfängern in dem Gebiet WPF nach dem selben Prinzip funktionieren - und man kann es ihnen auch nicht verübeln, denn sie kennen es ja nicht anders.
      So findet man dann z.B. dergleichen Codes vor:

      XML-Quellcode

      1. <Button x:Name="Button1" Click="Button1_OnClick"/>

      Und der Eventhandler befindet sich logischerweise im Codebehind des Windows, ganz wie unter WindowsForms:

      C#

      C#-Quellcode

      1. public partial class MainWindow : Window
      2. {
      3. void Button1_OnClick(object sender, RoutedEventArgs e)
      4. {
      5. //some code here
      6. }
      7. }
      VB.Net

      VB.NET-Quellcode

      1. Class MainWindow
      2. Private Sub Button1_OnClick(ByVal sender As Object, ByVal e As RoutedEventArgs)
      3. 'some code here
      4. End Sub
      5. End Class

      Warum genau ist das jetzt schlecht?
      Events im Windows-Forms-Sinne sind mit dem MVVM-Pattern, welches in WPF eigentlich Pflicht ist, nicht oder nur schwer vereinbar. Grund ist, dass sich die Logik der Anwendung im ViewModel des Windows befindet, nicht im COdebehind der Window-Klasse. Man kann natürlich tricksen und die Events irgendwie an das ViewModel weiterleiten, aber das erfordert weiteren Code im Codebehind, der dort ebensowenig was zu suchen hat wie die Eventhandler. Sinn von MVVM ist es nämlich, dass im Codebehind nur Code steht, der wirklich nur das View (die Benutzeroberfläche) betrifft, Verknüfungen an das ViewModel sollten ausschließlich über Bindings erfolgen, und die legt man am einfachsten im XAML-Code an.

      Aber wie werden Events denn dann gebunden?
      Die Antwort ist: gar nicht, genau hier kommen unsere Commands in Spiel, die die Events an dieser Stelle ersetzen werden. Commands in WPF basieren auf dem ICommand-Interface und stellen eine Instanz da, die eine Benutzereingabe zu der entsprechen auzuführenden Aktion delegiert. Wichtig dabei ist, dass das Ziel des Commands vom Command allein verwaltet wird, das Steuerelement, welches das Command ausführt, muss davon nichts wissen. Dadurch können wir uns von den Handlern in der Window-Klasse verabschieden und alles ins ViewModel verlagern, wos hingehört. Zusätzlich können Commands dem View mitteilen, ob sie überhaupt ausgeführt werden können, die Steuerelemente können darauf entsprechend (z.B. mit Ausgrauung) reagieren.


      Relay-Commands

      Die simpelste Art von Commands sind die Relay-Commands. Sie tun nichts weiter als eine ihnen (über einen Delegaten) zugewiesene Aktion auszuführen.
      Leider existiert in der Standard-WPF-Klassenbibliothek keine universelle RelayCommand-Klasse, diese gibts nur mit TeamFoundation. Damit man sich aber sparen kann, für jedes Command eine neue Klasse zu implementieren, kann man sich einfach diese relativ simple Klasse selbst bauen. Die hier präsentierte Version stammt von mir und ist selbstverständlich nicht zwangsläufig das Nonplusultra.

      C#

      C#-Quellcode

      1. class RelayCommand : ICommand
      2. {
      3. readonly Action methodToExecute;
      4. readonly Func<bool> canExecuteEvaluator;
      5. public event EventHandler CanExecuteChanged
      6. {
      7. add { CommandManager.RequerySuggested += value; }
      8. remove { CommandManager.RequerySuggested -= value; }
      9. }
      10. public RelayCommand(Action methodToExecute, Func<bool> canExecuteEvaluator)
      11. {
      12. this.methodToExecute = methodToExecute;
      13. this.canExecuteEvaluator = canExecuteEvaluator;
      14. }
      15. public RelayCommand(Action methodToExecute)
      16. : this(methodToExecute, () => true)
      17. { }
      18. public bool CanExecute(object parameter)
      19. {
      20. return canExecuteEvaluator.Invoke();
      21. }
      22. public void Execute(object parameter)
      23. {
      24. methodToExecute.Invoke();
      25. }
      26. }
      VB.Net

      VB.NET-Quellcode

      1. Class RelayCommand : Implements ICommand
      2. Private ReadOnly _methodToExecute As Action
      3. Private ReadOnly _canExecuteEvaluator As Func(Of Boolean)
      4. Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
      5. AddHandler(value As EventHandler)
      6. AddHandler CommandManager.RequerySuggested, value
      7. End AddHandler
      8. RemoveHandler(value As EventHandler)
      9. RemoveHandler CommandManager.RequerySuggested, value
      10. End RemoveHandler
      11. RaiseEvent(sender As Object, e As EventArgs)
      12. End RaiseEvent
      13. End Event
      14. Public Sub New(methodToExecute As Action, canExecuteEvaluator As Func(Of Boolean))
      15. _methodToExecute = methodToExecute
      16. _canExecuteEvaluator = canExecuteEvaluator
      17. End Sub
      18. Public Sub New(methodToExecute As Action)
      19. Me.New(methodToExecute, Function() True)
      20. End Sub
      21. Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
      22. Return _canExecuteEvaluator.Invoke()
      23. End Function
      24. Public Sub Execute(parameter As Object) Implements ICommand.Execute
      25. _methodToExecute.Invoke()
      26. End Sub
      27. End Class

      Bemerkenswert an dieser Klasse ist, dass sie selbst erkennt, wann sich der Rückgabewert des canExecuteEvaluators ändert, ihr müsst also nicht erst das Event manuell auslösen. Dahinter steckt der CommandManager, dessen Magie ich selbst nicht erklären kann, aber es funktioniert.

      Mit Hilfe des RelayCommand können wir jetzt das schlechte Beispiel von weiter oben korrigieren. Wir statten das ViewModel mit dem Command für den Button als Eigenschaft aus, damit wir daran binden können. Im Konstruktor können wir dann das RelayCommand erstellen und die auzuführende Funktion auswählen sowie optional eine Ausführungsbedingung.

      C#

      C#-Quellcode

      1. class MainViewModel : NotifyPropertyChangedBase
      2. {
      3. static MainViewModel instance;
      4. public static MainViewModel Instance
      5. {
      6. get { return instance ?? (instance = new MainViewModel()); }
      7. }
      8. public ICommand ButtonCommand { get; private set; }
      9. private MainViewModel()
      10. {
      11. ButtonCommand = new RelayCommand(ButtonClicked, CanClickButton);
      12. }
      13. private bool CanClickButton()
      14. {
      15. return true;
      16. }
      17. private void ButtonClicked()
      18. {
      19. //some code here
      20. }
      21. }
      VB.Net

      VB.NET-Quellcode

      1. Class MainViewModel : Inherits NotifyPropertyChangedBase
      2. Private Shared _instance As MainViewModel
      3. Public Shared ReadOnly Property Instance As MainViewModel
      4. Get
      5. If (_instance Is Nothing) Then _instance = New MainViewModel()
      6. Return _instance
      7. End Get
      8. End Property
      9. Private _buttonCommand As ICommand
      10. Public Property ButtonCommand As ICommand
      11. Get
      12. Return _buttonCommand
      13. End Get
      14. Private Set(value As ICommand)
      15. _buttonCommand = value
      16. End Set
      17. End Property
      18. Private Sub New()
      19. ButtonCommand = New RelayCommand(AddressOf ButtonClicked, AddressOf CanClickButton)
      20. End Sub
      21. Private Function CanClickButton() As Boolean
      22. Return True
      23. End Function
      24. Private Sub ButtonClicked()
      25. 'some code here
      26. End Sub
      27. End Class

      Im XAML können wir dann die ButtonCommand-Eigenschaft an die Command-Eigenschaft des Buttons binden, sofern das ViewModel korrekt als DataContext festgelegt wurde. Die Namensgebung für den Button wird hier auch redundant, ein Zeichen, dass wir auf dem richtigen Weg sind, in WPF und MVVM benötigen die wenigsten Steuerelemente einen Namen.

      XML-Quellcode

      1. <Button Command="{Binding ButtonCommand}"/>



      Routed-Commands

      Manchmal möchte man Commands nicht an das ViewModel sondern an bestimmte Controls senden, zum Beispiel um einer Textbox mitzuteilen, sie solle ihren Inhalt in die Zwischenablage kopieren. Im Gegensatz zum RelayCommand existiert in der WPF-Klassenbibliothek bereits eine RoutedCommand-Klasse, die wir verwenden können.
      Routed-Commands sind statisch und können von mehreren Steuerelementen empfangen werden (welches Steuerelement dann tatsälich das Command ausführt, wird durch den VisualTree bestimmt, dazu später mehr). Einige Standard-Commands sind in der statischen ApplicationCommands-Klasse vordefiniert, die auch teilweise von den Standard-Stuerelementen bereits behandelt werden (z.B. reagiert die Textbox auf ApplicationCommands.Copy). Im Gegensatz zu den Relay-Commands bestimmt bei den Routed-Commands das Empfängersteuerelement das Verhalten des Commands, dies geschieht über sog. Commandbindings, die direkt im XAML des Steuerelements oder im Codebehind definiert werden können. Wenn also einem bestehenden Steuerelement ein Commandbinding hinzugefügt werden soll, muss eine abgeleitete Klasse implementiert werden, bei Usercontrols sind XAML und Codebehind ohnehin schon vorhanden.
      Der Einfachheit halber habe ich hier ein Commandbinding am MainWindow eingerichtet, da wir uns so die Erstellung eines Usercontrols oder eines abgeleiteten Controls sparen können. Die Erstellung erfolgt hier im XAML und gebunden wir das Copy-Command aus den ApplicationCommands.

      XML-Quellcode

      1. <Window.CommandBindings>
      2. <CommandBinding Command="Copy" CanExecute="CanCopy" Executed="ExecuteCopy"/>
      3. </Window.CommandBindings>

      Die Methoden CanCopy und ExecuteCopy befinden sich im Codebehind des MainWindow. Bedenkt, dass damit nur Code dort hin gehört, der das Steuerelement (also in dem Fall das MainWindow) betrifft, solltet ihr das Geühl haben, der Code gehöre ins ViewModel, ist ein RoutedCommand die falsche Wahl und ihr solltet ein RelayCommand verwenden.
      Beachtet auch, dass hier die Kurzform für das Copy-Command verwendet wurde, äquivalent dazu wäre diese Schreibweise:

      XML-Quellcode

      1. <Window.CommandBindings>
      2. <CommandBinding Command="{x:Static ApplicationCommands.Copy}" CanExecute="CanCopy" Executed="ExecuteCopy"/>
      3. </Window.CommandBindings>

      Das Codebehind des MainWindow sieht nun so aus:

      C#

      C#-Quellcode

      1. public partial class MainWindow : Window
      2. {
      3. private void CanCopy(object sender, CanExecuteRoutedEventArgs e)
      4. {
      5. e.ContinueRouting = false;
      6. e.CanExecute = true;
      7. }
      8. private void ExecuteCopy(object sender, ExecutedRoutedEventArgs e)
      9. {
      10. //some code here
      11. }
      12. }
      VB.Net

      VB.NET-Quellcode

      1. Class MainWindow
      2. Private Sub CanCopy(sender As Object, e As CanExecuteRoutedEventArgs)
      3. e.ContinueRouting = False
      4. e.CanExecute = True
      5. End Sub
      6. Private Sub ExecuteCopy(sender As Object, e As ExecutedRoutedEventArgs)
      7. 'some code here
      8. End Sub
      9. End Class

      Interessant ist der Inhalt der CanCopy-Methode. e.CanExecute entspricht hier dem Rückgabewert des canExecuteEvaluator aus dem RelayCommand, gibt also an, ob das Command ausgeführt werden kann. e.ContinueRoutung setze ich hier zur Demonstration auf false, obwohl dies eigentlich nicht nötig ist (Standardwert ist false). Wird dieser Wert jedoch auf true gesetzt, wird das CommandBinding für das Steuerelement an das entsprechende Command ignoriert, sodass andere Steuerelemente das Command empfangen können (auch dazu später mehr).

      Das Copy-Command muss jetzt auch irgendwo ausgelöst werden, damit etwas passieren kann. Ich verwende hier wieder einen normalen Button:

      XML-Quellcode

      1. <Button Command="Copy"/>

      Sobald der Button jetzt gedrückt wird, wandert das Routed-Command den Baum der Steuerelemente nach oben, bis ein Steuerelement gefunden wird, welches das Command per CommandBinding gebunden hat und e.ContinueRouting false ist. Dort wird dann das Command ausgeführt und die Executed-Funktion aufgerufen.
      Sollte kein Steuerelement gefunden worden sein und das RoutedCommand erreicht das Ende des Baumes, wird das Command ignoriert.

      Eigene Routed-Commands erstellen
      Ihr könnte selbstverständlich auch eigene Routed-Commands erstellen und auf diese reagieren.
      Das RoutedCommand muss öffentlich und statisch vorliegen, an welcher Stelle ist egal, ich verwende für dieses Beispiel eine neue statische Klasse:

      C#

      C#-Quellcode

      1. static class Commands
      2. {
      3. static ICommand customRoutedCommand;
      4. public static ICommand CustomRoutedCommand
      5. {
      6. get { return customRoutedCommand ?? (customRoutedCommand = new RoutedCommand()); }
      7. }
      8. }
      VB.Net

      VB.NET-Quellcode

      1. Module Commands
      2. Private _customRoutedCommand As ICommand
      3. Public ReadOnly Property CustomRoutedCommand As ICommand
      4. Get
      5. If _customRoutedCommand Is Nothing Then _customRoutedCommand = New RoutedCommand()
      6. Return _customRoutedCommand
      7. End Get
      8. End Property
      9. End Module

      Alles, was wir tun müssen, um unser eigenes RoutedCommand statt einem vorgefertigten zu nutzen, ist, es im XAML auszutauschen:

      XML-Quellcode

      1. <Window.CommandBindings>
      2. <CommandBinding Command="local:Commands.CustomRoutedCommand" CanExecute="CanExecuteCustomCommand" Executed="ExecuteCustomCommand"/>
      3. </Window.CommandBindings>
      4. <Button Command="local:Commands.CustomRoutedCommand"/>



      Command-Gruppen

      Es kommt vor, dass ein einzelnes Steuerelement mehrere Commands ausführen soll und mit der CommandGroup-Klasse wird dies möglich. Leider existiert auch diese Klasse ausschlißlich unter TeamFoundation, aber auch hier können wir uns mit einer eigenen Implementierung Abhilfe schaffen:

      C#

      C#-Quellcode

      1. [ContentProperty("Commands")]
      2. class CommandGroup : ICommand
      3. {
      4. public event EventHandler CanExecuteChanged;
      5. readonly ObservableCollection<ICommand> commands;
      6. public ObservableCollection<ICommand> Commands
      7. {
      8. get { return commands; }
      9. }
      10. public CommandGroup()
      11. {
      12. commands = new ObservableCollection<ICommand>();
      13. commands.CollectionChanged += OnCommandsChanged;
      14. }
      15. protected virtual void OnCanExecuteChanged(EventArgs e)
      16. {
      17. if (CanExecuteChanged != null)
      18. CanExecuteChanged(this, e);
      19. }
      20. private void OnChildCanExecuteChanged(object sender, EventArgs e)
      21. {
      22. OnCanExecuteChanged(e);
      23. }
      24. protected virtual void OnCommandsChanged(object sender, NotifyCollectionChangedEventArgs e)
      25. {
      26. if (e.NewItems != null && e.NewItems.Count > 0)
      27. {
      28. foreach (ICommand command in e.NewItems)
      29. command.CanExecuteChanged += OnChildCanExecuteChanged;
      30. }
      31. if (e.OldItems != null && e.OldItems.Count > 0)
      32. {
      33. foreach (ICommand command in e.OldItems)
      34. command.CanExecuteChanged -= OnChildCanExecuteChanged;
      35. }
      36. OnCanExecuteChanged(EventArgs.Empty);
      37. }
      38. public bool CanExecute(object parameter)
      39. {
      40. return commands.All(command => command.CanExecute(parameter));
      41. }
      42. public void Execute(object parameter)
      43. {
      44. foreach (ICommand command in commands)
      45. command.Execute(parameter);
      46. }
      47. }
      VB.Net

      VB.NET-Quellcode

      1. <ContentProperty("Commands")>
      2. Class CommandGroup : Implements ICommand
      3. Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
      4. ReadOnly _commands As ObservableCollection(Of ICommand)
      5. Public ReadOnly Property Commands As ObservableCollection(Of ICommand)
      6. Get
      7. Return _commands
      8. End Get
      9. End Property
      10. Public Sub New()
      11. _commands = New ObservableCollection(Of ICommand)()
      12. AddHandler _commands.CollectionChanged, AddressOf OnCommandsChanged
      13. End Sub
      14. Protected Overridable Sub OnCanExecuteChanged(e As EventArgs)
      15. RaiseEvent CanExecuteChanged(Me, e)
      16. End Sub
      17. Private Sub OnChildCanExecuteChanged(sender As Object, e As EventArgs)
      18. OnCanExecuteChanged(e)
      19. End Sub
      20. Protected Overridable Sub OnCommandsChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
      21. If e.NewItems IsNot Nothing AndAlso e.NewItems.Count > 0 Then
      22. For Each command As ICommand In e.NewItems
      23. AddHandler command.CanExecuteChanged, AddressOf OnChildCanExecuteChanged
      24. Next
      25. End If
      26. If e.OldItems IsNot Nothing AndAlso e.OldItems.Count > 0 Then
      27. For Each command As ICommand In e.NewItems
      28. RemoveHandler command.CanExecuteChanged, AddressOf OnChildCanExecuteChanged
      29. Next
      30. End If
      31. OnCanExecuteChanged(EventArgs.Empty)
      32. End Sub
      33. Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
      34. Return _commands.All(Function(command) command.CanExecute(parameter))
      35. End Function
      36. Public Sub Execute(parameter As Object) Implements ICommand.Execute
      37. For Each command As ICommand In _commands
      38. command.Execute(parameter)
      39. Next
      40. End Sub
      41. End Class

      Was hier geschieht mag auf den ersten Blick kompliziert wirken, aber eigentlich werden hier nur mehrere Commands in einer Liste gespeichert, das CanExecuteChanged-Event durchgeleitet und in Execute alle Commands ausgeführt.
      Ich habe die Klasse so designt, dass man bei Bedarf auch davon ableiten kann.

      Sollen nur RoutedCommands in eine CommandGroup gesteckt werden, können wir diese gleich im XAML definieren:

      XML-Quellcode

      1. <Button>
      2. <Button.Command>
      3. <local:CommandGroup>
      4. <x:StaticExtension Member="ApplicationCommands.Copy"/>
      5. <x:StaticExtension Member="local:Commands.CustomRoutedCommand"/>
      6. </local:CommandGroup>
      7. </Button.Command>
      8. </Button>

      Sollen auch RelayCommands mit enthalten sein oder soll die Liste im Laufe der Programmausführung geändert werden, kommen wir nicht darum herum, die CommandGroup im ViewModel zu erstellen:

      C#

      C#-Quellcode

      1. class MainViewModel : NotifyPropertyChangedBase
      2. {
      3. static MainViewModel instance;
      4. public static MainViewModel Instance
      5. {
      6. get { return instance ?? (instance = new MainViewModel()); }
      7. }
      8. public ICommand ButtonCommand { get; private set; }
      9. public ICommand CommandGroup { get; private set; }
      10. private MainViewModel()
      11. {
      12. ButtonCommand = new RelayCommand(ButtonClicked, CanClickButton);
      13. var commandGroup = new CommandGroup();
      14. commandGroup.Commands.Add(ButtonCommand);
      15. commandGroup.Commands.Add(ApplicationCommands.Copy);
      16. commandGroup.Commands.Add(Commands.CustomRoutedCommand);
      17. CommandGroup = commandGroup;
      18. }
      19. private bool CanClickButton()
      20. {
      21. return true;
      22. }
      23. private void ButtonClicked()
      24. {
      25. Debug.Print("Relay command executed!");
      26. }
      27. }
      VB.Net

      VB.NET-Quellcode

      1. Class MainViewModel : Inherits NotifyPropertyChangedBase
      2. Private Shared _instance As MainViewModel
      3. Public Shared ReadOnly Property Instance As MainViewModel
      4. Get
      5. If (_instance Is Nothing) Then _instance = New MainViewModel()
      6. Return _instance
      7. End Get
      8. End Property
      9. Private _buttonCommand As ICommand
      10. Private _commandGroup As ICommand
      11. Public Property ButtonCommand As ICommand
      12. Get
      13. Return _buttonCommand
      14. End Get
      15. Private Set(value As ICommand)
      16. _buttonCommand = value
      17. End Set
      18. End Property
      19. Public Property CommandGroup As ICommand
      20. Get
      21. Return _commandGroup
      22. End Get
      23. Private Set(value As ICommand)
      24. _commandGroup = value
      25. End Set
      26. End Property
      27. Private Sub New()
      28. ButtonCommand = New RelayCommand(AddressOf ButtonClicked, AddressOf CanClickButton)
      29. Dim commandGroup = New CommandGroup()
      30. commandGroup.Commands.Add(ButtonCommand)
      31. commandGroup.Commands.Add(ApplicationCommands.Copy)
      32. commandGroup.Commands.Add(Commands.CustomRoutedCommand)
      33. Me.CommandGroup = commandGroup
      34. End Sub
      35. Private Function CanClickButton() As Boolean
      36. Return True
      37. End Function
      38. Private Sub ButtonClicked()
      39. Debug.Print("Relay command executed!")
      40. End Sub
      41. End Class

      Hier können wir dann wiederum ganz normal binden:

      XML-Quellcode

      1. <Button Command="{Binding CommandGroup}"/>



      Typische Anwendungen für Commands

      Buttons
      Buttons werden eigentlich immer mit einem Command verknüpft, ansonsten machen sie ja nicht sonderlich viel Sinn (zumindest aus funktionaler Sicht, aber was weiß ich schon, wofür ihr eure Buttons benutzt. :P ).
      Da oben bei allen Beispielen Buttons verwendet wurden, muss ich hier, glaube ich, nicht mehr viel erklären, die Syntax lautet:

      XML-Quellcode

      1. <Button Command="CommandXYZ"/>



      Menüitems
      Auch Menüitems sind gute Kandidaten für Commands, gerade New, Open, Save, Copy, Paste, Cut, Undo oder Redo sind häufig in Menüs zu finden und lassen sich gut mit Commands umsetzen.
      Die Syntax ist der vom Button nahezu identisch und ebenso simpel:

      XML-Quellcode

      1. <MenuItem Command="CommandXYZ"/>



      InputBinings
      Wenn Menüitems vorhanden sind möchte man diese üblicherweise auch mit Tastenkombinationen verbinden. Das geht in WPF mit den sog. InputBindings, welche ebenfalls auf Basis von Commands funktionieren, womit es dann kein Problem ist, ihnen einfach den selben Command zuzuordnen wie ihrem entsprechenden Menüitem.
      Hier sind z.B. einige Inputbindings für die Menüeinträge Neu, Öffnen und Speichern, wie man sie in Editoren aller Art findet:

      XML-Quellcode

      1. <Window.InputBindings>
      2. <KeyBinding Modifiers="Control" Key="N" Command="{Binding NewCommand}"/>
      3. <KeyBinding Modifiers="Control" Key="O" Command="{Binding OpenCommand}"/>
      4. <KeyBinding Modifiers="Control" Key="S" Command="{Binding SaveCommand}"/>
      5. </Window.InputBindings>

      Neben Tastatureingaben können auch Mauseingaben mit Hilfe von MouseBindings an Commands gebunden werden.




      Ich hoffe ich konnte euch mit diesem Tutorial einige hilfreiche Informationen vermitteln. Anbei findet ihr noch ein Beispielprojekt für C# und VB.Net, in dem ihr euch alles hier besprochene noch einmal ausprogrammiert ansehen könnt.
      Dateien

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Artentus“ ()

      Ich hab mal ein bischen praktische Beispiele dazu entwickelt, ursprünglich aufbauend auf einer Vorlage, die du mir gemailt hattest:
      Angezeigt wird eine Liste von Personen, und wenn man auf eine doppelklickt, dann öffnet sich ein Person-Editier-Dialog.
      Der PersonEditor öffnet sich auch, wenn man eine Person anwählt und Enter drückt.
      Auch gibts Buttons dafür, und auch einen Button zum Hinzufügen einer neuen Person.
      Und mit Strg-N erzeugt man auch eine neue Person. Und mit Delete natürlich löschen.
      Besonderes Schmankerl: Mit Alt-Q schließt die Anwendung.
      Der Person-Editier-Dialog wird mit Button oder Enter bestätigt, und mit Escape gecancelt.

      RelayCommands
      Also im MainViewmodel sind die RelayCommands cmdNew, cmdDelete, cmdEdit angelegt wie folgt:

      VB.NET-Quellcode

      1. Private __Persons As New ObservableCollection(Of Person)()
      2. Public Property Persons() As ICollectionView = CollectionViewSource.GetDefaultView(__Persons)
      3. Public Property cmdNew() As New RelayCommand(AddressOf CreateNewPerson)
      4. Public Property cmdDelete() As New RelayCommand( _
      5. Sub() __Persons.RemoveAt(Persons.CurrentPosition), _
      6. Function() Persons.CurrentPosition >= 0)
      7. Public Property cmdEdit() As New RelayCommand( _
      8. Sub() Call New wndPerson(__Persons(Persons.CurrentPosition)).ShowDialog(), _
      9. Function() Persons.CurrentPosition >= 0)
      10. Public Sub CreateNewPerson()
      11. Dim person = New Person("a Name", "-", DateTime.Now)
      12. Dim editor = New wndPerson(person)
      13. If CBool(editor.ShowDialog()) Then
      14. __Persons.Add(person)
      15. Persons.MoveCurrentToPosition(__Persons.Count - 1)
      16. End If
      17. End Sub
      Während das Editieren der Current-Person ein anonymer Einzeiler ist (#11), ist das Anlegen einer neuen Person bisserl aufwändiger, und daher in eine richtige Methode gepackt (#14 - #21), die mit AddressOf addressiert wird (#4).
      Die Command-Ausführbarkeits-Bedingung ist beim Löschen und Editieren die gleiche: Die Person-CollectionView muss eine gültige CurrentPosition haben (#8, #12)
      Hingegen cmdNew (#4) braucht keine solche Bedingung, denn Zufügen kann man immer.
      c#

      C#-Quellcode

      1. public ICollectionView Persons { get; private set; }
      2. private ObservableCollection<Person> _Persons = new ObservableCollection<Person>();
      3. public RelayCommand cmdNew { get; private set; }
      4. public RelayCommand cmdEdit { get; private set; }
      5. public RelayCommand cmdDelete { get; private set; }
      6. private MainViewModel() {
      7. Persons = CollectionViewSource.GetDefaultView(_Persons);
      8. cmdNew = new RelayCommand(CreateNewPerson);
      9. cmdEdit = new RelayCommand(
      10. () => new wndPerson(_Persons[Persons.CurrentPosition]).ShowDialog(),
      11. () => Persons.CurrentPosition >= 0);
      12. cmdDelete = new RelayCommand(
      13. () => _Persons.RemoveAt(Persons.CurrentPosition),
      14. () => Persons.CurrentPosition >= 0);
      15. //...
      16. }
      17. public void CreateNewPerson() {
      18. var person = new Person("a Name", "-", DateTime.Now);
      19. var editor = new wndPerson(person);
      20. if ((bool)editor.ShowDialog()) {
      21. _Persons.Add(person);
      22. Persons.MoveCurrentToPosition(_Persons.Count - 1);
      23. }
      24. }
      In c# kann man Properties nicht mit der Deklaration initialisieren, sondern muss Konstruktor-Code schreiben. Ist dafür bischen sauberer, denn so sind die Properties public readonly


      Bindings an RelayCommands
      Geradezu langweilig einfach sind Button-Bindings an RelayCommands:

      XML-Quellcode

      1. <Button Content="New Person" Margin="3" Command="{Binding cmdNew}"/>
      2. <Button Content="Edit Person" Margin="3" Command="{Binding cmdEdit}"/>
      3. <Button Content="Delete Person" Margin="3" Command="{Binding cmdDelete}"/>
      (Ebenso wäre es auch mit MenuItem gegangen)

      InputBindings an RelayCommands
      Ich habe auch ein paar KeyBindings erstellt, damit Tastendrücke auch was tun: Auf UserControl-Ebene verarbeite ich Strg-N und Strg-Q

      XML-Quellcode

      1. <UserControl x:Class="uclDatagrid" DataContext="{x:Static my:MainViewModel.Instance}" [...] >
      2. <UIElement.InputBindings>
      3. <KeyBinding Command="{Binding cmdNew}" Gesture="Ctrl + N" />
      4. <KeyBinding Command="Close" Gesture="Alt + Q" />
      5. </UIElement.InputBindings>
      Das bewirkt - egal wo im Ucl der Focus ist - drückt einer Strg-N, kommt der "neue Person"-Dialog.
      Das andere KeyBinding löst das vorgefertigte Application.Close-Command aus. Selbiges leite ich via CommandBinding um in ein Event, um das Fenster zu schließen.
      Also das Application.Close kommt im Viewmodel überhaupt nicht an - doch dazu später.

      Den Scope von InputBindings wählen
      Erstmal im DataGrid hab ich auch InputBindings definiert, die sollen aber nicht im ganzen UserControl wirken, sondern speziell nur auf dem ItemsPanel des Datagrids:

      XML-Quellcode

      1. <ItemsControl.ItemsPanel>
      2. <ItemsPanelTemplate>
      3. <StackPanel>
      4. <UIElement.InputBindings>
      5. <MouseBinding Command="{Binding cmdEdit}" Gesture="LeftDoubleClick" />
      6. <KeyBinding Command="{Binding cmdEdit}" Gesture="Enter" />
      7. </UIElement.InputBindings>
      8. </StackPanel>
      9. </ItemsPanelTemplate>
      10. </ItemsControl.ItemsPanel>
      Nämlich Key.Enter oder Mouse.LeftDoubleClick auf eine Person sollen auch den Editier-Vorgang auslösen. :thumbsup:

      Wpf nervt

      Ich habe denselben View auch mit Listview statt Datagrid umgesetzt. Listview ist eigentlich angebrachter, denn die Editier-Funktionalität des Datagrids ist hier ja unerwünscht.
      Aber - nerv! - im Listview funktioniert das MouseBinding nicht, denn Listview empfängt die Mausklicks selber und leitet sie nicht weiter. Damit sind wir - extra für ListView-Doppelklickse - wieder bei Event-Verarbeitung und CodeBehind:

      XML-Quellcode

      1. <ListView MouseDoubleClick="ListView_MouseDoubleClick" [...] >
      CodeBehind:

      VB.NET-Quellcode

      1. Private Sub ListView_MouseDoubleClick(sender As Object, e As MouseButtonEventArgs)
      2. e.Handled = e.ChangedButton = MouseButton.Left AndAlso MainViewModel.Instance.cmdEdit.TryExecute
      3. End Sub
      Also wenns LeftDoubleClick ist wird versucht, das Edit-Command auszuführen, und dementsprechend wird e.Handled gesetzt.
      Wirklich nicht schön, dasses nicht mit Bindings geht, sondern dass man über die global zugreifbare MainViewmodel-Instanz eingrabschen muss. :thumbdown:

      CommandBindings und RoutedCommands
      CommandBindings sind nur an statische Commands möglich. Richtig (wie mit RelayCommands) ins Viewmodel hinein-commanden kann man daher gar nicht. Auch executen diese statischen RoutedCommands überhaupt nichts, sondern Wirkung entsteht erst durch die CommandBindings, welche Events feuern.
      Also statische RoutedCommands sind eigentlich nur Schlüssel-Werte, an die Buttons und InputBindings binden können, und via CommandBinding wird das in ein Event umgeleitet ins CodeBehind (und nicht ins Viewmodel :( ).
      Ziemlich um 3 Ecken gedacht, und hat mit Databinding und Commanding herzlich wenig zu tun.
      Hier das CommandBinding fürs "Close-Command":

      XML-Quellcode

      1. <Window x:Class="wndMain" [...] >
      2. <UIElement.CommandBindings>
      3. <CommandBinding Command="Close" Executed="Close_Executed"/>
      4. </UIElement.CommandBindings>
      Und hier der EventHandler dazu:

      VB.NET-Quellcode

      1. Private Sub Close_Executed(sender As Object, e As RoutedEventArgs)
      2. Me.Close()
      3. End Sub
      Also für ein RoutedCommand braucht man ein statisches RoutedCommand, Buttons oder KeyBindings daran, ein CommandBinding, was Events feuert, und CodeBehind, der die Events behandelt.
      In diesem Beispiel aber bemerkenswert, dass das Close wirklich gerouted wird: Nämlich es wird in UserControls ausgelöst - ein UserControl kann aber gar nicht geschlossen werden 8| ! (sondern nur das Window, wo es aufsitzt)
      Das CommandBinding zum Close-Command (und seine Event-Behandlung) befinden sich daher im MainWindow. Ist nur eine Spielerei - logischer und einfacher wäre gewesen, das Close-Command auch im MainWindow auszulösen, wo es verarbeitet werden kann.
      (also mit den RoutedCommands hat man eine Lösung mit dem Problem, ein Problem für diese Lösung zu finden, scheint mir :thumbdown: )
      Jdfs. in diesem Sample werden RoutedCommands insgesamt nur benötigt, um Fenster zu schließen (CommandBinding+RoutedCommand = View-Sache).

      Custom RoutedCommands
      Neben dem vorgefertigten Application.Close-RoutedCommand habe ich für den Person-Edit-Dialog zum Fenster-schließen noch 2 weitere Commands, selbstgemacht, sog. "Custom-Commands": Ok und Cancel
      VB

      VB.NET-Quellcode

      1. Imports System.Windows.Input
      2. Public NotInheritable Class Commands
      3. Shared _Cancel As New RoutedCommand
      4. Public Shared ReadOnly Property Cancel() As ICommand
      5. Get
      6. Return _Cancel
      7. End Get
      8. End Property
      9. Shared _OK As New RoutedCommand
      10. Public Shared ReadOnly Property OK() As ICommand
      11. Get
      12. Return _OK
      13. End Get
      14. End Property
      15. End Class
      c#

      C#-Quellcode

      1. static class Commands {
      2. private static RoutedCommand _Cancel = new RoutedCommand();
      3. public static ICommand Cancel { get { return _Cancel; } }
      4. private static RoutedCommand _OK = new RoutedCommand();
      5. public static ICommand OK { get { return _OK; } }
      6. }
      (Habe ich den "Helpers" zugeordnet, denn diese RoutedCommands braucht man für jeden Dialog, den man schreibt :) )

      CommandBindings daran
      Beim Ok-Command-CommandBinding im Person-Dialog muss - im Gegensatz zum Cancel - auch das CanExecute-Event behandelt werden, um ungültige Eingaben zu verhindern:

      XML-Quellcode

      1. <Window x:Class="wndPerson" xmlns:my="clr-namespace:CommandsSampleVb" [...] >
      2. <UIElement.CommandBindings>
      3. <CommandBinding Command="my:Commands.OK" Executed="OK_Executed" CanExecute="OK_CanExecute"/>
      4. <CommandBinding Command="my:Commands.Cancel" Executed="Cancel_Executed" />
      5. </UIElement.CommandBindings>
      CodeBehind:

      VB.NET-Quellcode

      1. Private Sub OK_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
      2. e.CanExecute = _DataContext.[Error] Is Nothing
      3. End Sub
      4. Private Sub OK_Executed(sender As Object, e As ExecutedRoutedEventArgs)
      5. _DataContext.CopyTo(_Person)
      6. DialogResult = True
      7. End Sub
      8. Private Sub Cancel_Executed(sender As Object, e As ExecutedRoutedEventArgs)
      9. DialogResult = False
      10. End Sub
      Ok kann nur executen, wenn _DataContext (eine temporäre Person) keinen Error hat. Und das Executen (Übernahme der Daten) besteht darin, dass _DataContext in die richtige _Person kopiert wird.
      Abschließend wirds DialogResult gesetzt, und das schließt den Dialog.


      Zur SampleSolution:
      Hab ich in Vb und in c# ausgeführt, und den View hab ich mal mit DataGrid und mal mit ListView umgesetzt (jeweils ein UserControl).
      Es ist wirklich gemein: Beim Listview ist die LeftDoubleClick-Gesture unmöglich. Datagrid reagiert komisch auf die Tab-Taste, Listview hingegen kann seine Spaltenbreite nicht automatisch anpassen.
      Also hat sich nichts geändert: der :evil: steckt im Detail.
      Dateien

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

      In diesem Zusammenhang auch ganz interessant: Patterns for Asynchronous MVVM Applications: Commands
      https://msdn.microsoft.com/en-us/magazine/dn630647.aspx

      Geht halt darum, wie man async-Methoden und Commands behandelt.
      Das Ergebnis, was in dem Artikel erarbeitet wird, ist sehr schön.

      Mit einigen wenigen XAML Zeilen und ein bisschen Code erstellt man eine kleine Anwendung, die in der Lage ist, je nach Status des Tasks bestimmte Aktionen auszuführen.

      Bilder





      (Bilder von MSDN)