Command Binding für WPF vereinfachen

Das View-Models für WPF ein sehr mächtiges Architekturpattern sind, ist anerkannt.

Einfach nur nervig und redundant in der Notation sind dagegen Commands, wenn man sie nach dem Standardansatz mit RelayCommands implementiert.

Für die Umsetzung von umfangreichen Commands für neue Features für Quality Spy hatte ich genug davon.

Methoden als Command deklarieren

So einfach muss es sein: im Fokus steht die Domänenlogik (hier Selektions und UI-Logik) und nicht Infrastrukturcode für ICommands:

public abstract class TestPlanNode<T> : ViewModelBase<T>
{
  ...

  [ExposeAsCommand]
  public void CancelInPlaceEditing()
  { 
     this.InPlaceEditingActive = false;

     if (GetTestPlan().newNode == this)
     {
        var prevNode = this.GetPreviousNode() ?? this.GetParent();

        // cancel new node -> means remove it
        this.Remove();

        if (prevNode != null)
        {
           prevNode.IsFocused = true;
        }
     }
  } 
...
}

Wichtig ist das Attribut ExposeAsCommand. Mit Hilfe dieses Attributs kann man über die „magische“ Eigenschaft Commands auf die Methoden als Commands zugreifen:

 <TextBox.InputBindings> 
...
 <KeyBinding Key="Escape" Command="{Binding Commands.CancelInPlaceEditing}"/>
...
 </TextBox.InputBindings>

Commands ist vom Typ CommandSet und wird in einer View-Model-Basisklasse bereitgestellt. Die Implementierung von CommandSet stammt von Mebyon Kernow und nutzt ICustomTypeDescriptor von .NET, um der Reflection-API das Vorhandensein von Properties (den Commands) „vorzugaukeln“. Diesem CommandSet kann man einfach RelayCommands übergeben (bei Mebyon Kernow im Original DelegateCommand benannt). Aber er hat das nicht zuende gedacht, denn folgt man seinem Blog-Beispiel musste man die Delegaten schwach typisiert im CommandSet hinzufügen. Daher habe ich eine Helper-Klasse implementiert, welche die Methoden im View-Model nach dem Attribut ExposeAsCommand scannt und dafür Delegaten bereitstellt. Die Initialisierung sieht im View-Model wie folgt aus:

public abstract class ViewModelBase : INotifyPropertyChanged 
{
   private CommandMap commands;

   public CommandMap Commands
   {
      get
     {
        if (this.commands == null)
        {
            this.commands = CommandMapMethodBinder.CreateCommandMap(this);
         }

         return this.commands;
    }
 }

Kostet das Performance? Ja klar, es ist Reflection nötig, die Klasse nach Methoden mit den Attributen zu scannen und es muss mit Reflection eine Delegat auf die Instanz-Methode erstellt werden. Um die Performance zu verbessern, wird der Scan nur einmalig ausgeführt und die Erstellung des Delegaten für den Command wird bis zur ersten Verwendung verzögert:

internal static class CommandMapMethodBinder
{
 private static Dictionary<Type, MethodInfo[]> methodsExposedAsCommandsCache = new Dictionary<Type, MethodInfo[]>();

 internal static CommandMap CreateCommandMap(object viewModel)
 {
   // get the methods - use caching since this method is slow
   MethodInfo[] methods;

   if (methodsExposedAsCommandsCache.ContainsKey(viewModel.GetType()))
   {
     methods = methodsExposedAsCommandsCache[viewModel.GetType()];
   }
   else
   {
     methods = viewModel.GetType().GetMethods().Where(m => m.GetCustomAttributes(typeof(ExposeAsCommandAttribute), false).Length > 0).ToArray();

     methodsExposedAsCommandsCache.Add(viewModel.GetType(), methods);
   }

   CommandMap commands = new CommandMap();

   foreach (var methodInfo in methods)
   {
     // since the creation of the delegate is potentially heavy, we defer it until the first usage 
     DeferredActionCreator lazy = new DeferredActionCreator();
     lazy.targetMethod = methodInfo;
     lazy.targetObject = viewModel;

      commands.AddCommand(methodInfo.Name, (x) => lazy.Execute());
 }

    return commands;
 }

 class DeferredActionCreator
 {
    internal MethodInfo targetMethod;
    internal object targetObject;

    private Action action;

    public void Execute()
    {
      if (this.action == null)
      {
         this.action = (Action)Delegate.CreateDelegate(typeof(Action), targetObject, targetMethod);
      }

      this.action();
    }
  }
}

Und schon hat man WPF mächtig aufgebohrt.

Download vollständiger Quellcode