Das Command Pattern in WPF und Silverlight

12. Dezember 2011

Commanding ist ein anerkanntes Pattern zur Entkopplung von Anzeige und Anzeigelogik. Bei WPF und Silverlight ist es ganz tief im System verankert und erlaubt flexible, deklarative Lösungen für häufig wiederkehrende Probleme.

Wenn man ihn einmal hat, dann will man ihn nie wieder hergeben! Gemeint ist der Commanding-Mechanismus von WPF und Silverlight. Windows Forms haben ihn nicht und man kann ihn dort auch nur mit viel Aufwand nachbauen.

Man nehme beispielsweise eine TextBox, die als Suchfeld dient. Mit dem abschließenden Druck auf die Eingabetaste soll die Suche gestartet werden. Mit WPF kann das so einfach aussehen:

   1:  <TextBox x:Name="searchTextBox">
   2:    <TextBox.InputBindings>
   3:      <KeyBinding Command="{Binding Search}" Key="Enter"/>
   4:    </TextBox.InputBindings>
   5:  </TextBox>

Damit bindet man das in der Property Search des ViewModels hinterlegte ICommand an den Auslöser “Eingabetaste gedrückt” des Textfeldes.

Aber hier ist noch nicht Schluss. Man kann diesen Command-Aufrufen auch zusätzliche Parameter mitgeben.

Man stelle sich eine ListBox vor, die Suchergebnisse im Stile der Infocards in der Outlook-Inbox anzeigt.

Ausgewählte Elemente aus dieser Suchliste sollen nun in die Zwischenablage kopiert werden, um sie in anderen Anwendungen zu verwenden. Es soll aber auch die Möglichkeit geben alle Suchergebnisse auf einen Schlag zu kopieren.

Also definieren wir ein entsprechendes Command das wir ganz wagemutig CopySearchResultsToClipboardCommand nennen.

   1:  public class CopySearchResultsToClipboardCommand : ICommand 
   2:  { 
   3:    public void Execute(object parameter) 
   4:    { 
   5:      Guard.AssertNotNull(parameter, "parameter"); 
   6:      var list = parameter as IList; 
   7:      if (list != null) 
   8:      { 
   9:        var selectedSearchResults = list.OfType<SearchResult>(); 
  10:        string csv = CsvFormatter.ToCsv(selectedSearchResults); 
  11:        Clipboard.SetText(csv, TextDataFormat.UnicodeText); 
  12:      } 
  13:    } 
  14:  }

Dieses Command konvertiert die per Parameter übergebenen Datensätze in ein CSV-Format, das sowohl in einem Texteditor, als auch in Excel angezeigt werden kann.

Dieses Command wird in der Property Copy des ViewModels abgelegt.

   1:  public class ViewModel 
   2:  { 
   3:    public ICommand Copy { get; set; } 
   4:    public MainWindowViewModel() 
   5:    { 
   6:      this.Copy = new CopySearchResultsToClipboardCommand(); 
   7:    } 
   8:  }

Und jetzt kommt der interessante Teil. Wir binden das Command an die Tastenkombination CTRL+C

   1:  <Window.InputBindings> 
   2:    <KeyBinding 
   3:      Key="C" 
   4:      Modifiers="Ctrl" 
   5:      Command="{Binding Copy}" 
   6:      CommandParameter="{Binding ElementName=searchResults, Path=SelectedItems}"/> 
   7:  </Window.InputBindings> 

an einen Copy-Button

   1:  <Button Content="Copy" 
   2:    Command="{Binding Copy}" 
   3:    CommandParameter="{Binding ElementName=searchResults, Path=SelectedItems}"/> 

und an einen CopyAll-Button.

   1:  <Button 
   2:    Content="Copy all" 
   3:    Command="{Binding Copy}" 
   4:    CommandParameter="{Binding ElementName=searchResults, Path=Items}"/>

Wo ist der Unterschied zwischen den Buttons? Richtig! Der CommandParameter wird einmal mit den Werten aus der Property SelectedItems der ListBox und das andere Mal mit denen aus Items gefüttert.

Damit haben wir deklarativ ein Problem gelöst, das unter WinForms etliche Zeilen Code und eine Menge Aufwand für eine saubere Trennung zwischen View und ViewModel gekostet hätte.

Und falls sich jemand fragt, warum wir nicht ApplicationCommands.Copy verwendet haben: Commands kapseln Verhalten. Aber man kann kein eigenes Command an die Stelle eines ApplicationCommands setzen. Über ein CommandBinding lassen sich nur EventHandler an diesen ApplicationCommands einhängen. Das kann man sicher auch “schön” hinbekommen, aber es erscheint unnötig kompliziert, wenn man mit einem eigenen ICommand bereits eine saubere Kapselung zur Hand hat. Der häufig bevorzugte Weg ist daher der, per Binding auf Commands zuzugreifen, die über Properties am ViewModel angeboten werden. Dafür ist WPF ja so gut bei Data/Command/Input-Binding 🙂