WPF: ICommands en RoutedCommands

 
07 april 2011

Voor wie de overstap van Windows Forms naar WPF net heeft gemaakt, is het opzetten van de interactie tussen je UI en de achterliggende business-logica even wennen. Ja, je kan bij buttons nog steeds gewoon het Button.Click event gebruiken, maar de koninklijke weg (lees je dan) is het gebruik van Commands, het liefst RoutedCommands. En de documentatie daarvan is nog wel eens verwarrend (hallo Microsoft training kit!), maar als je doorhebt hoe het zit is het eigenlijk best handig en eenvoudig. Hier is hoe ik het zie, in de hoop dat het niet net zo verwarrend is:

In XAML heb je twee mogelijkheden om op een button-klik te reageren:

<Button Click="btnLogin_Click" Content="Inloggen"/>

met in je code-behind-file een methode btnLogin_Click(...), of:

<Button Command="{Binding LoginCommand}" Content="Inloggen"/>

waarbij het commando (in dit geval) is gedefinieerd in je datacontext die voor die button geldt. Hoe werkt zo’n Command nou eigenlijk?

Het ‘instapmodel’ voor Commands is simpelweg het implementeren van de ICommand interface, en dan zo’n ICommand-object aan je Button hangen. De ICommand bevat drie dingen:

  • een methode bool CanExecute(object param)
  • een methode void Execute(object param), en
  • een event CanExecuteChangedEvent dat ik hier even grotendeels buiten beschouwing laat

Je raadt aan de namen waarschijnlijk al wat deze doen: CanExecute vertelt of het commando uitgevoerd kan worden op de gegeven parameter, en Execute voert het ook daadwerkelijk uit. (Het event moet je afvuren zodra er iets gebeurt waardoor de uitvoerbaarheid van het command verandert. In de code hieronder ben ik lui en laat ik WPF het werk doen: zodra die denkt dat er iets belangrijks is gebeurd krijgen mijn observers ook een seintje.)

Als CanExecute false teruggeeft, dan grijst WPF de Button automatisch uit – een van de handigste features van het gebruik van Commands, omdat je nu zelf dat soort synchronisatie niet meer hoeft te doen. En als de button niet is uitgegrijsd en je klikt erop, dan wordt de Execute-methode uitgevoerd. In de praktijk bouw ik vooral commando’s waar geen parameter bij hoeft (zoals in het voorbeeld hierboven); de methode gebruikt dan de param-parameter domweg niet in zijn body. Als je wel een parameter gebruikt dan zet je een CommandParameter-attribuut in je <Button>-tag

Om nu eenvoudig een ICommand te bouwen gebruiken we een hulpklasse waarbij we in de constructor twee delegates meegeven (de tweede optioneel) die Execute en CanExecute representeren:

public class DelegateCommand : ICommand
{
 readonly Action<object> execute;
 readonly Predicate<object> canExecute;
 
 public DelegateCommand(Action<object> execute,
                       Predicate<object> canExecute=null) {
  this.execute = execute;
  this.canExecute = canExecute;
 }
 
 public void Execute(object cmdParameter) {
  if (execute != null) {
    execute(cmdParameter);
  }
 }
 
 public bool CanExecute(object cmdParameter) {
  if (canExecute==null) {
    return true;
  }
  return canExecute(cmdParameter);
 }
 
 public event EventHandler CanExecuteChanged
 {
  add { CommandManager.RequerySuggested += value; }
  remove { CommandManager.RequerySuggested -= value; }
 }
}

Hiermee kunnen we de DataContext bijvoorbeeld de volgende property meegeven:

public ICommand LoginCommand {
 get {
  return new DelegateCommand(
     _ => loginService.Login(UserName, Password)
  ,
     _ => (!String.IsNullOrWhiteSpace(UserName) &&
           !String.IsNullOrWhiteSpace(Password))
  );
 }
}

(Ik gebruik vaak de variabele-naam _ (underscore) om aan te geven dat we niet geïnteresseerd zijn in de waarde ervan.)

Een in het oog springende eigenschap van de DelegateCommand-implementatie is dat de logica van het uitvoeren van het commando direct in het commando zelf is verwerkt. Als je dit commando gebruikt, dan ligt niet alleen vast wat er moet gebeuren, maar ook hoe dat dan gebeurt.

RoutedCommands (en RoutedUICommands) zorgen voor een loskoppeling van deze twee dingen: in feite bepaal je met het gebruik van een RoutedCommand alleen wat er moet gebeuren, maar er zit nog niet direct een implementatie aan vast, althans niet op de manier zoals bovenstaande DelegateCommands. Een RoutedUICommand is overigens gewoon een RoutedCommand met een Text-property die de Button gebruikt als er geen andere Content wordt aangegeven. .Net definieert zelf al een paar RoutedUICommands voor veelgebruikte acties (zoals Save en Copy) die je direct kan gebruiken.

Maar natuurlijk wil je normaliter wel dat er iets gebeurt als je het command ‘Execute’t. Dat wordt bij RoutedCommands geregeld via zg. CommandBindings. Die definieer je in je XAML in de bovenliggende elementen, oftewel in de parent nodes van je elementje in de visual tree. Een CommandBinding bestaat, naast een specificatie van om welk RoutedCommand het gaat, uit een of twee callbacks, nl. de handlers voor de Executed en de CanExecute routed events. Die verwijzen – helaas voor puristen zoals ik en fans van het Model-View-ViewModel-patroon – niet naar het ViewModel (je datacontext), maar naar de code-behind van het window of user control waar je op dat moment mee bezig bent. Dat maakt het gebruik van RoutedCommands wat mij betreft een stukje minder mooi, omdat je geen ‘schone’ ‘lege’ code-behind meer hebt.

Bij een RoutedCommand is de implementatie in eerste benadering in feite ongeveer deze (in pseudocode):

void CanExecute(object param) {
 var node = get XAML node referencing this command;
 var e = new CanExecuteRoutedEventArgs {
   CanExecute = false,
   Source = node,
   Parameter = param,
   Command = this,
   ...
 };
 
 while (node != null) {
   var binding = node.getCommandBindingFor(this);
   if (binding != null) {
       var method = binding.CanExecuteEventHandler;
       if (method == null) {
           return true;
       } else {
           method.Invoke(node, e); // kan e.CanExecute wijzigen
       }
   }
   if (e.CanExecute) { return true; }
   node = node.getParentXamlNode();
 }
 
 return false;
}
 
void Execute(object param) {
 var node = get XAML node referencing this command;
 var e = new ExecutedRoutedEventArgs {
   Source = node,
   Parameter = param,
   Command = this,
   ...
 };
 
 while (node != null) {
   var binding = node.getCommandBindingFor(this);
   if (binding != null) {
       var method = binding.ExecutedEventHandler;
       if (method != null) {
           method.Invoke(node, e);
           return;
       }
   }
   node = node.getParentXamlNode();
 }
}

Zoals je ziet worden de verschillende eventhandlers mogelijk door verschillende senders aangeroepen. De Button is dus niet de enige die de kans krijgt om op het commando te reageren: als hij er zelf niets mee wil dan krijgen andere elementen ook de gelegenheid om iets te doen. De Visual Tree wordt daarbij ‘van onder naar boven’ doorzocht totdat er een element is dat er iets mee doet.

Maar het is erg simpel om zelf nieuwe te maken: gewoon RoutedCommand subclassen. Je hoeft geen enkele implementatie te schrijven, het framework regelt alles al :-) Een aardig trucje is om (een instance van) je RoutedCommand vervolgens als resource in je XAML beschikbaar te maken:

<Window ...(namespaces declaraties)...>
    <Window.Resources>
        <myNamespace:MyRoutedCommand x:Key="myCmd"/>
        <RoutedCommand x:Key="ditWerktOok"/>
    </Window.Resources>
 
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource myCmd}"
                        Executed="TellUserToWait"/>
    </Window.CommandBindings>
 
    <StackPanel>
        <StackPanel.CommandBindings>
            <CommandBinding Command="{StaticResource myCmd}"
                            Executed="RecordUserChoice"
                            CanExecute="CanUserChoose"/>
        </StackPanel.CommandBindings>
 
        <Button Command="{StaticResource myCmd}"
                CommandParameter="A"
                Content="Keuze A"/>
        <Button Command="{StaticResource myCmd}"
                CommandParameter="B"
                Content="Keuze B"/>
    </StackPanel>
</Window>

Als de gebruiker nu op een van de twee buttons klikt dan wordt dat door geen van de buttons zelf afgehandeld, maar wel door de StackPanel of het Window. (Overigens niet allebei: in tegenstelling tot RoutedEvents krijgt maar een enkele CommandBinding de gelegenheid om zijn Executed-event daadwerkelijk af te vuren; daarna stopt de routing, zelfs als je expliciet e.Handled = false zegt – en je kan ook niet aangeven dat je al afgehandelde commands wil opvangen)

Dit was een eerste introductie over RoutedCommands; in werkelijkheid zijn ze nog wat flexibeler dan dit: zo kan je bijvoorbeeld ook bepalen dat de routering van het commando niet begint bij het element dat het commando afvuurt maar ergens anders (door in Button ook een CommandTarget mee te geven); bestaan er ook Preview-varianten van CanExecute en Executed zodat een element kan afvangen als er in een van zijn children een commando afgevuurd dreigt te gaan worden; en kan je, net als bij de voorgedefinieerde .Net-RoutedUICommands, ook toetsen- en muis-combinaties aan je commando hangen. Dat is allemaal ook niet al te moeilijk, maar daarvoor wil ik je dan toch naar de documentatie verwijzen.


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development