Routed commands en WPF en 2 min

Les commandes routées WPF (i.e. routed commands) correspondent à une fonctionnalité permettant de découpler les éléments qui déclenchent une commande des éléments cibles sur lesquels les commandes vont s’exécuter.

D’autres part les commandes routées permettent d’associer facilement des actions provenant de raccourcis clavier, d’actions réalisées avec la souris etc…
Enfin elles peuvent activer ou désactiver un control graphique en fonction de la disponibilité de la commande.

Caractéristiques des commandes routées

Interface ICommand

La fonctionnalité des commandes routées est accessible par l’intermédiare de l’interface System.Windows.Input.ICommand:

public interface ICommand 
{ 
    event EventHandler CanExecuteChanged; 
 
    bool CanExecute(object parameter); 
     
    void Execute(object parameter); 
}

Ainsi:

  • Execute: cette méthode contient le code qui sera exécuté au déclechement de la commande.
  • CanExecute: cette méthode sera exécutée avant Execute() pour déterminer si la commande peut être exécutée. Elle renvoie true si l’exécution est possible, false sinon.
  • CanExecuteChanged: cette évènement se déclenche quand la valeur de CanExecute change.

De nombreux éléments de base WPF fournissent cette interface de façon à y affecter une implémentation.

Par exemple:

  • ButtonBase.Command (dans System.Windows.Controls.Primitives)
  • MenuItem.Command (dans System.Windows.Controls)
  • CheckBox.Command (dans System.Windows.Controls)
  • etc…

De nombreux autres éléments de base proposent cette interface.

L’intérêt de cette propriété pour ces différents éléments est de pouvoir affecter une implémentation qui définit la commande qui sera déclenchée par l’élément.

Aspect routé

Les commandes routées ont la particularité d’être “routées” c’est-à-dire qu’elles sont associées à des évènements routés (cf. Routed events en WPF en 3 min).

Ainsi lorsque la commande est déclenchée, l’élément qui a déclenché cette commande ainsi que la commande elle même ignorent complêtement l’élément qui va exécuter réellement la commande. A vrai dire, il peut ne pas y avoir d’éléments qui va exécuter la commande.

Au déclenchement de la commande, des évènements routés seront déclenchés et se propageront le long de l’arbre visuel (cf. Arbre logique et arbre visuel WPF en 2 min) de façon à savoir si la commande peut être exécutée et ensuite pour l’exécuter réellement.

La propagation se fait de la même façon qu’un évènement routé normal. 2 séries de 2 évènements sont ainsi lancées. La première serie permet à un élément se trouvant dans l’arbre visuel s’il peut exécuté la commande:

  • PreviewCanExecute: évènement “tunnel” qui se propage de l’élément racine vers l’élément source (celui qui référence la commande).
  • CanExecute: évènement “bubble” qui se propage en sens inverse de la source vers l’élément racine de l’arbre.

Si un élément indique qu’il peut exécuter la commande, la 2e série d’évènements routés se déclenche:

  • PreviewExecuted: évènement “tunnel” se propageant de la racine de l’arbre vers l’élément source.
  • Executed: évènement “bubble” se propageant de l’élément source vers la racine de l’arbre.

Ainsi n’importe quel éléments capable d’intercepter ces évènements sera en mesure d’exécuter une action correspondant à la commande.

Routage des commandes

Lors du parcours des évènements le long de l’arbre visuel, les handlers (c’est-à-dire le code exécuté au déclenchement de la commande) sont exécutés successivement à la suite en fonction de leur position dans cet arbre.

Le parcours de fait jusqu’à l’élément qui a le focus. Dans le cas où cet élément se trouve dans un “container” avec FocusManager.IsFocusScope à true le parcours se fait de façon plus complexe que celui évoqué précédement. Pour plus de détails sur ce point, se reporter à The Truth about Routed Commands Routing.

Implémentations des commandes routées

Il existe des implémentations différentes permettant d’utiliser les commandes routées, toutefois dans tous les cas, les mêmes instantications sont nécessaires:

  • Définir la commande routée en elle-même: cette commande est une instance de System.Windows.Input.RoutedCommand. Cette définition permet d’indiquer la classe propriétaire de la commande et le nom de la commande.
    Il est aussi possible de définir des commandes étant des instances de System.Windows.Input.RoutedUICommand (RoutedUICommand dérive de RoutedCommand).
  • Définir un binding entre la commande et des handlers. Ces handlers correspondent à des implémentations pour CanExecute() et Execute(), ils seront exécutés lorsque les évènements correspondant seront déclenchés.
  • On indique l’élément qui va déclencher la commande c’est-à-dire l’invoker. Par exemple, ça peut être le bouton sur lequel on devra cliquer pour déclencher la commande.

Commandes prédéfinies

Un certain nombre de commandes sont déjà définies et peuvent être utilisées directement. On peut trouver une définition de ces commandes dans:

  • System.Windows.Input.ApplicationCommands,
  • System.Windows.Input.ComponentCommands,
  • System.Windows.Input.MediaCommands,
  • System.Windows.Input.NavigationCommands,
  • System.Windows.Documents.EditingCommands,

Par exemple, les commandes “Cut”, “Copy” et “Paste” se trouvent dans ApplicationCommands. On peut en voir la définition dans le code source de WPF: le fichier ApplicationCommands.cs

Pour utiliser ces commandes, il suffit de définir un binding et l’invoker.

Par exemple, si on veut exécuter une commande à l’exécution de la commande ApplicationCommands.Paste, il faut définir le binding avec un CommandBinding dans un user control:

<Window x:Class="RoutedCommandExample.Window1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:RoutedCommandExample" 
    Title="Window1" Height="300" Width="300"> 
  <Window.CommandBindings> 
    <CommandBinding 
      Command="ApplicationCommands.Paste" 
      CanExecute="PasteCommandHandler_CanExecute" 
      Executed="PasteCommandHandler_Executed" /> 
  </Window.CommandBindings> 
  <!-- ... --> 
</Window>

Le code behind contiendra l’implémentation correspondant aux handlers:

public partial class Window1 : Window 
{ 
  public Window1() 
  { 
      InitializeComponent(); 
  } 
 
  private void PasteCommandHandler_CanExecute(object sender, CanExecuteRoutedEventArgs e) 
  { 
      e.CanExecute = true; 
  } 
 
  private void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e) 
  { 
      // Code exécuté au déclenchement de la commande 
  } 
}

Les handlers se déclencheront à chaque fois que la commande ApplicationCommands.Paste est exécutée.

On peut complêter cet exemple en ajoutant un bouton pour déclencher l’évènement:

<Button  
      Command="ApplicationCommands.Paste"  
      Content="Raise Paste" />

Définir une commande

Si on souhaite définir sa propre commande sans passer par une commande prédéfinie, il suffit d’ajouter dans le code behind:

private static RoutedCommand customCommand =  
    new RoutedCommand("CustomCommand", typeof(Window1)); 
 
public static RoutedCommand CustomCommand 
{ 
    get { return customCommand; } 
}

Le binding et l’invoker sont similaires à l’exemple précédent et font référence à cette commande:

<Window.CommandBindings> 
    <CommandBinding  
        Command="{x:Static local:Window1.CustomCommand}" 
        CanExecute="PasteCommandHandler_CanExecute" 
        Executed="PasteCommandHandler_Executed" /> 
</Window.CommandBindings> 
<Grid> 
    <Button Command="{x:Static local:Window1.CustomCommand}" 
        Name="myButton" Content="Raise Custom command"/> 
</Grid>

RoutedUICommand

A la différence de RoutedCommand, une RoutedUICommand possède une propriété Text sur laquelle il est possible de binder des éléments. La définition d’une commande de ce type est semblable:

private static RoutedUICommand _pressMeCommand =  
    new RoutedUICommand("Custom Command", "CustomCommand", typeof(Window1));

On peut, par exemple, binder le texte du bouton sur le texte de la commande:

<Button Command="{x:Static local:Window1.CustomCommand}" 
        Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" />

Effectuer le binding dans le code behind

Au lieu de définir le binding dans le code XAML, on peut le faire dans le code behind:

public partial class Window1 : Window  
{ 
  public readonly RoutedCommand customCommand; 
  public readonly CommandBinding binding; 
 
  public Window1()  
  { 
      customCommand = new RoutedCommand("CustomCommand", typeof(Window1)); 
 
      InitializeComponent(); 
      this.DataContext = this; 
 
      this.binding = new CommandBinding(customCommand); 
      this.CommandBindings.Add(this.binding); 
 
      this.binding.Executed += PasteCommandHandler_Executed; 
      this.binding.CanExecute += PasteCommandHandler_CanExecute; 
  } 
 
  private void PasteCommandHandler_CanExecute(object sender, CanExecuteRoutedEventArgs e)  
  { 
      e.CanExecute = true; 
  } 
 
  private void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e)  
  { 
      // Code exécuté au déclenchement de la commande 
  } 
}

CommandTarget

Lors du déclenchement de la commande:

  • La source est l’élément qui a déclenché la commande,
  • Le “sender” est la fenêtre sur laquelle est définie le binding.

On peut changer la source de la commande en utilisant l’argument System.Windows.Input.ICommandSource.CommandTarget. La plupart des éléments implémentent cette propriété.

Dans l’exemple précédent, si on ajoute une TextBox:

<Window.CommandBindings> 
    <CommandBinding  
        Command="{x:Static local:Window1.CustomCommand}" 
        CanExecute="PasteCommandHandler_CanExecute" 
        Executed="PasteCommandHandler_Executed" /> 
</Window.CommandBindings> 
<Grid> 
    <TextBox Name="newTextBox" Width="200" Height="40" /> 
    <Button Command="{x:Static local:Window1.CustomCommand}" 
        Name="myButton" Content="Raise Custom command"/> 
</Grid>

On peut paramétrer l’argument CommandTarget du bouton pour que la source soit la TextBox:

<Button Command="{x:Static local:Window1.CustomCommand}" 
      CommandTarget="{Binding ElementName=newTextBox}" 
      Name="myButton" Content="Raise Custom command"/>

A l’exécution, dans le handler, la source de la commande sera la TextBox.

CommandParameter

On peut ajouter un paramètre qui sera renseigné au déclenchement de la commande en utilisant System.Windows.Input.ICommandSource.CommandParameter. La plupart des éléments de base implémentent cette propriété.

Si on l’utilise dans le cadre d’un bouton:

<Button Command="{x:Static local:Window1.CustomCommand}" 
      CommandParameter="Parameter1" 
      Name="myButton" Content="Raise Custom command"/>

Ce paramètre pourra être utilisé au déclenchement de la commande dans le handler:

public void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    MessageBox.Show(e.Parameter); 
}

KeyBinding

Les key bindings permettent d’indiquer des raccourcis clavier ou des actions à la souris qui déclencheront des commandes.

On peut définir ces key bindings dans le code XAML:

<Window x:Class="RoutedCommandExample.Window1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:RoutedCommandExample" 
    Title="Window1" Height="300" Width="300"> 
  <Window.CommandBindings> 
    <CommandBinding 
      Command="ApplicationCommands.Paste" 
      CanExecute="PasteCommandHandler_CanExecute" 
      Executed="PasteCommandHandler_Executed" 
      /> 
  </Window.CommandBindings> 
  <Window.InputBindings> 
    <KeyBinding Command="ApplicationCommands.Paste" 
      Gesture="Ctrl+M"/> 
  </Window.InputBindings> 
  <!-- ... --> 
</Window>

Il est possible de définir un key binding pour une commande qui n’est pas pré-définie:

<KeyBinding Command="{x:Static local:Window1.CustomCommand}" Gesture="Ctrl+M"/>

On peut aussi utiliser la syntaxe suivante pour définir le key binding:

<KeyBinding Command="{x:Static local:Window1.CustomCommand}" Modifiers="Control" Key="M"/>

On peut aussi définir ce key binding dans le code behind:

public Window1()  
{ 
    InitializeComponent(); 
 
    this.DataContext = this; 
  
    this.InputBindings.Add(new KeyBinding(this.customCommand, 
        new KeyGesture(Key.M, ModifierKeys.Control))); 
}

Leave a Reply