Routed events en WPF en 3 min

Les évènements routés WPF (i.e. routed events) sont très similaires aux évènements classiques en .NET:

  • Ils sont définis dans un objet,
  • Peuvent être déclenchés par un objet différent du premier et
  • Conduisent à l’exécution d’une callback ou plusieurs callbacks définies dans une 3e série d’objets.

La grande différence entre les évènements routés WPF et les évènements .NET est l’aspect “routé” puisqu’ils suivent l’arbre des éléments WPF.

Caractéristiques des évènements routés

Comme indiqué plus haut, la caractéristique la plus importante des évènements routés est qu’ils suivent l’arbre des éléments. Au runtime, l’interface graphique d’une application WPF est composée de plusieurs éléments visuels qui sont organisés hiérarchiquement en une structure en arbre.

On distingue 2 types d’arbres:

  • L’arbre logique qui définit les relations entre les éléments implémentés par le développeur
  • L’arbre visuel définissant les relations entre les éléments au runtime de l’application.

Pour plus de détails sur ces 2 types d’arbres: arbre logique et arbre visual WPF en 2 min.

Stratégie de propagation

Lorsqu’un évènement routés est déclenché, il est propagé le long de l’arbre visuel. Cette propagation dépend de la stratégie utilisée au moment de la définition de l’évènement:

  • System.Windows.RoutingStrategy.Tunnel: suivant cette stratégie, un évènement routé sera propagé de la racine de l’arbre (l’élément le plus haut dans l’arbre ou celui englobant le plus les autres) vers l’élément source (control dans les feuilles de l’arbre). Par exemple pour un évènement de type click sur un bouton, l’élément racine pourrait être l’élément Window et l’élément source pourrait être un objet Button.
  • System.Windows.RoutingStrategy.Bubble: cette stratégie est l’opposé de la stratégie Tunnel, elle consiste à propager un évènement routé de l’élément source vers l’élément racine. Par exemple pour un évènement de click sur un bouton, la propagation se fait du bouton vers la racine de l’arbre, comme l’élément Window.
  • System.Windows.RoutingStrategy.Direct: cette stratégie correspond à celle d’un évènement .NET classique, il est directement exécuté par le handler auxquel il est rattaché.

En plus de la stratégie de propagation qui indique le sens de parcours de l’évènement, certaines caractériques sont très spécifiques:

  • Il n’est pas nécessaire que tous les éléments de l’arbre s’attachent à l’évènement pour qu’ils soient traversés par cet évènement au moment de sa propagation. Même sans implémentation particulière, un évènement routé parcourera l’arbre visuel de la racine vers l’élément source (ou l’inverse).
  • Il est possible d’arrêter cette propagation à un niveau de l’arbre en indiquant qu’il est handled.
  • N’importe quel objet de type UIElement est capable de s’abonner à un évènement routé même s’il ne le définit pas. Ainsi un élément de type StackPanel peut s’abonner à un évènement correspondant à un clique sur un bouton.

Convention de nommage des évènements routés

Par convention les évènements routés sont nommés de la façon suivante:

  • PreviewXXX: correspond à un évènement dont la stratégie de propagation est Tunnel. Par exemple l’évènement UIElement.PreviewKeyDownEvent.
  • XXX: sans préfixe “Preview”, un évènement possède une stratégie de propagation Bubble. Par exemple, l’évènement UIElement.KeyDownEvent.

Evènements routés définis par pair

La plupart du temps, les évènements routés sont définis par pair:

  • Un évènement Tunnel préfixé par “Preview”,
  • Un évènement Bubble sans prefixe.

Par exemple, les évènements suivants sont définis dans System.Windows.UIElement et dans System.Windows.ContentElement:

Evènement Tunnel Evènement Bubble Déclenché quand:
PreviewDragEnter DragEnter Un objet commence à être glissé avec la sourisà partir d’un élément
PreviewDragLeave DragLeave Un objet est déposé avec la souris dans un élément
PreviewDragOver DragOver Un objet est glissé avec la souris au dessus d’un élément
PreviewDrop Drop Un objet est déposé avec la souris au dessus d’un élément
PreviewGotKeyboardFocus GotKeyboardFocus Un élément obtient le focus de saisie
PreviewKeyDown KeyDown Une touche du clavier est enfoncée
PreviewKeyUp KeyUp Une touche du clavier est relachée
PreviewLostKeyboardFocus LostKeyboardFocus Un élément perd le focus de saisie
PreviewMouseDown MouseDown Un bouton de la souris est enfoncé
PreviewMouseLeftButtonDown MouseLeftButtonDown Le bouton gauche de la souris est enfoncé
PreviewMouseLeftButtonUp MouseLeftButtonUp Le bouton gauche de la souris est relâché
PreviewMouseMove MouseMove La souris est déplacée au dessus d’un élément
PreviewMouseRightButtonDown MouseRightButtonDown Le bouton droit de la souris est enfoncé
PreviewMouseRightButtonUp MouseRightButtonUp Le bouton droit de la souris est relâché
PreviewMouseUp MouseUp Un bouton de la souris est relâché
PreviewMouseWheel MouseWheel La roulette de la souris est actionnée
PreviewTextInput TextInput Une touche correspodant à du texte est frappés.

Toutefois certains évènements ne respectent pas cette convention. Par exemple, l’évènement Bubble ButtonBase.ClickEvent n’a pas d’équivalent Tunnel.

Séquencement des évènements définis par pair

Lorsque des évènements sont définis par pair, ils sont déclenchés dans un ordre précis:

  • L’évènement Tunnel (avec le préfixe “Preview”) est déclenché en premier: cet évènement se propage de la racine de l’arbre jusqu’à l’élément source.
  • L’évènement Bubble (sans le préfixe “Preview”) est déclenché ensuite: cet évènement se propage de l’élément source jusqu’à la racine de l’arbre.

L’intérêt de cet ordre est de pouvoir arrêter la propagation d’un évènement lors de la propagation de l’évènement Tunnel:

  • La propagation d’un évènement s’arrête lorsqu’il est marqué handled dans un handler.
  • Si on indique qu’un évènement Tunnel est handled, sa propagation s’arrête et son équivalent Bubble ne sera pas déclenché.

Par exemple, si on écrit:

<Window>  
    <StackPanel>  
        <Label Content="Label" />  
        <Button Content="Button" />  
    </StackPanel>  
</Window>

Si on clique sur le bouton, l’évènement UIElement.PreviewMouseRightDown va parcourir l’arbre de la racine vers l’élément ayant le focus:

  • Window,
  • StackPanel,
  • Button

Ensuite l’évènement UIElement.MouseRightDown parcourera l’arbre de l’élément ayant le focus vers la racine:

  • Button,
  • StackPanel,
  • Window.

Types d’évènements routés

Il existe 5 types d’évènements routés:

  • Evènements du cycle de vie (i.e. Lifetime events),
  • Evènements de la souris (i.e. Mouse events),
  • Evènements du clavier (i.e. Keyboard events),
  • Evènements du stylet (i.e. Stylus events) et
  • Evènements Multitouch.

Evènements du cycle de vie

La classe System.Windows.FrameworkElement définit des évènements liés au cycle de vie des éléments (i.e. lifetime event):

  • FrameworkElement.Initialized: déclenché lorsqu’un élément est initialisé.
  • FrameworkElement.Loaded: cet évènement est déclenché après initialisation, utilisation des styles et data binding des propriétés.
  • FrameworkElement.Unloaded: lorsqu’un élément est libéré.

D’autres évènements sont définis par la classe System.Window:

  • Window.SourceInitialized: se déclenche lorsque l’objet HwndSource permettant de s’interfacer avec l’API Win32 est instancié.
  • Window.ContentRendered: se déclenche après l’affichage de la fenêtre.
  • Window.Activated: se déclenche quand la fenêtre devient active.
  • Window.Deactivated: déclenché lorsque la fenêtre n’a plus le focus.
  • Window.Closing: se déclenche à la fermeture d’une fenêtre (ne se déclenche en cas de déconnexion ou d’arrêt du système).
  • Window.Closed: évènement déclenché lorsqu’une fenêtre est fermée.

Evènements du clavier

Les évènements du clavier (i.e. Keyboard events) sont:

  • UIElement.KeyDown et UIElement.PreviewKeyDown: évènements respectivement bubble et tunnel correspondant à un enfoncement de touche sur le clavier.
  • UIElement.TextInput et UIElement.PreviewTextInput: évènements respectivement bubble et tunnel déclenchés lorsque des touches correspondant à du texte sont frappées.
  • UIElement.KeyUp et UIElement.PreviewKeyUp: évènements respectivement bubble et tunnel déclenchés quand des touches du clavier sont relachées.

Des combinaisons des touches comme “Control + A”, “Majuscule + A”, “Alt + A”, “Windows + A” sont renseignées, aux déclenchements des évènements, dans la classe System.Windows.Input.KeyEventArgs. Les combinaisons correspondront à des évènements successifs (par exemple, un évènement pour “Control” et un autre pour “A”). Il faut s’aider de la classe statique System.Windows.Input.Keyboard et de la propriété Keyboard.Modifiers pour identifier une combinaison.

Evènements de la souris

Les évènements provoquées par la souris (i.e. Mouse events) sont:

  • UIElement.MouseEnter et UIElement.MouseLeave: ces évènements sont déclenchés respectivement, quand la souris commence à passer au dessus d’un élément et quand il le quitte.
  • UIElement.MouseMove et UIElement.PreviewMouseMove: évènements respectivement bubble et tunnel correspondant au déplacement de la souris au dessus d’un élément.
  • UIElement.MouseRightButtonDown et UIElement.PreviewMouseRightButtonDown: évènements respectivement bubble et tunnel correspondant à l’enfoncement du bouton droit de la souris.
  • UIElement.MouseLeftButtonDown et UIElement.PreviewMouseLeftButtonDown: évènements respectivement bubble et tunnel correspondant à l’enfoncement du bouton gauche de la souris.
  • UIElement.MouseRightButtonUp et UIElement.PreviewMouseRightButtonUp: évènements respectivement bubble et tunnel correspondant au relâchement du bouton droit de la souris.
  • UIElement.MouseLeftButtonUp et UIElement.PreviewMouseLeftButtonUp: évènements respectivement bubble et tunnel correspondant au relâchement du bouton gauche de la souris.

Implémentation des évènements routés

La syntaxe utilisée pour définir et utiliser les évènements routés est un peu particulière. Il existe plusieurs implémentations possibles si on ajoute une callback coté Xaml ou coté code behind ou pour la définition de l’évènement.

Le but de cette partie est de lister toutes les syntaxes possibles.

Ajouter une callback à un évènement

Pour s’abonner à un évènement routé et exécuter une callback à son déclenchement, il faut ajouter handler à cet évènement. La définition d’un handler peut se faire de plusieurs façons.

Handler défini dans UIElement

Dans la hiérarchie de classe des objets en WPF (cf. arbre logique et arbre visual WPF en 2 min), System.Windows.Controls.Control dérive de System.Windows.FrameworkElement qui dérive de System.Windows.UIElement. Donc si UIElement permet de rajouter un handler on peut s’y abonner facilement de n’importe quel control dans le code Xaml.

Par exemple, on peut s’abonner directement à UIElement.KeyDown car:

partial class UIElement 
{ 
    public event KeyEventHandler KeyDown 
    { 
        add { AddHandler(Keyboard.KeyDownEvent, value, false); } 
        remove { RemoveHandler(Keyboard.KeyDownEvent, value); } 
    } 
}

Ainsi on peut écrire:

<Window x:Class="WpfApplication.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="MainWindow" Height="400" Width="400" KeyDown="WindowKeyDown"> 
    <StackPanel KeyDown="StackPanelKeyDown"> 
        <Label Content="Text: "/> 
        <TextBox KeyDown="TextBoxKeyDown" /> 
    </StackPanel> 
</Window>

Les handlers doivent être implémentés dans le code behind:

namespace WpfApplication 
{ 
    public partial class MainWindow : Window 
    { 
        public MainWindow() 
        { 
            InitializeComponent(); 
        } 
 
        private void WindowKeyDown(object sender, KeyEventArgs e) 
        { 
            MessageBox.Show("WindowKeyDown"); 
        } 
 
        private void StackPanelKeyDown(object sender, KeyEventArgs e) 
        { 
            MessageBox.Show("StackPanelKeyDown"); 
        } 
 
        private void TextBoxKeyDown(object sender, KeyEventArgs e) 
        { 
            MessageBox.Show("TextBoxKeyDown"); 
        } 
    } 
}

Evènement défini dans une autre classe

On peut s’abonner à un évènement même s’il est définit dans une autre classe.

Par exemple, l’évènement Keyboard.KeyDown est, en réalité, défini dans la classe System.Windows.Input.Keyboard:

public static class Keyboard 
{ 
    public static readonly RoutedEvent KeyDownEvent = EventManager.RegisterRoutedEvent("KeyDown", RoutingStrategy.Bubble, typeof(KeyEventHandler), typeof(Keyboard)); 
  
    public static void AddKeyDownHandler(DependencyObject element, KeyEventHandler handler) 
    { 
        UIElement.AddHandler(element, KeyDownEvent, handler); 
    } 
  
    public static void RemoveKeyDownHandler(DependencyObject element, KeyEventHandler handler) 
    { 
        UIElement.RemoveHandler(element, KeyDownEvent, handler); 
    } 
}

On peut s’abonne à cet évènement en écrivant dans le code Xaml (on remplace KeyDown par Keyboard.KeyDown):

<Window x:Class="WpfApplication.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="MainWindow" Height="400" Width="400" Keyboard.KeyDown="WindowKeyDown"> 
    <StackPanel Keyboard.KeyDown="StackPanelKeyDown"> 
        <Label Content="Text: "/> 
        <TextBox Keyboard.KeyDown="TextBoxKeyDown" /> 
    </StackPanel> 
</Window>

Le code behind reste identique.

Ajouter un handler dans le code behind

Comme pour tous les évènements .NET, on peut s’abonner directement dans le code behind à condition qu’un handler y soit défini.

Ainsi l’évènement Keyboard.KeyDown possède un handler dans la classe UIElement:

partial class UIElement 
{ 
    public event KeyEventHandler KeyDown 
    { 
        add { AddHandler(Keyboard.KeyDownEvent, value, false); } 
        remove { RemoveHandler(Keyboard.KeyDownEvent, value); } 
    } 
}

Ainsi si le code Xaml est:

<Window x:Class="WpfApplication.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="MainWindow" Height="400" Width="400" Keyboard.KeyDown="WindowKeyDown"> 
    <StackPanel Name="stackPanel" Keyboard.KeyDown="StackPanelKeyDown"> 
        <Label Content="Text: "/> 
        <TextBox Name="textBox" Keyboard.KeyDown="TextBoxKeyDown" /> 
    </StackPanel> 
</Window>

On peut écrire:

namespace WpfApplication 
{ 
    public partial class MainWindow : Window 
    { 
        public MainWindow() 
        { 
            InitializeComponent(); 
 
            this.KeyDown += new RoutedEventHandler(WindowKeyDown); 
            this.stackPanel.KeyDown += new RoutedEventHandler(StackPanelKeyDown); 
            this.textBox.KeyDown += new RoutedEventHandler(TextBoxKeyDown); 
        } 
 
        private void WindowKeyDown(object sender, RoutedEventArgs e)  
        {  
            MessageBox.Show("WindowKeyDown");  
        }  
  
        private void StackPanelKeyDown(object sender, RoutedEventArgs e)  
        {  
            MessageBox.Show("StackPanelKeyDown");  
        }  
  
        private void TextBoxKeyDown(object sender, RoutedEventArgs e)  
        {  
            MessageBox.Show("TextBoxKeyDown");  
        } 
    } 
}

On peut aussi utiliser une notation plus concise:

this.KeyDown += WindowKeyDown; 
this.stackPanel.KeyDown += StackPanelKeyDown; 
this.textBox.KeyDown += TextBoxKeyDown;

Utiliser UIElement.AddHandler()

Dans le cas où il n’y a pas d’handlers, on peut utiliser UIElement.AddHandler() pour abonne n’importe quel élément à un évènement (de même on peut supprimer l’abonnement avec UIElement.RemoveHandler()).

Par exemple, l’évènement System.Windows.Controls.Primitives.ButtonBase.Click ne possède pas de handler dans la classe UIElement, on peut donc utiliser UIElement.AddHandler() dans le code behind.

Dans le cas de l’exemple précédent, on peut écrire:
this.textBox.AddHandler(ButtonBase.ClickEvent, (RoutedEventHandler)ButtonClick);

Avec:

private void ButtonClick(object sender, RoutedEventArgs e)  
{  
    MessageBox.Show("TextBoxKeyDown");  
}

Utiliser EventManager.RegisterClassHandler()

Pour abonner une callback à un évènement, on peut utiliser la classe statique System.Windows.EventManager avec la méthode:

EventManager.RegisterClassHandler(Type classType, RoutedEvent routedEvent, Delegate handler,  
    bool handledEventsToo)

Par exemple, à partir du constructeur statique d’un élément:

public class CustomControl : ContentControl  
{  
    static CustomControl()  
    {  
        EventManager.RegisterClassHandler(typeof(CustomControl), 
            ButtonBase.ClickEvent, new RoutedEventHandler(ButtonClick)); 
    }  
  
    private void ButtonClick(object sender, RoutedEventArgs e)  
    {  
        MessageBox.Show("ButtonClick");  
    } 
}

Interrompre la propagation d’un évènement

On peut interrompre la propagation d’un évènement routé quel que soit la stratégie de propagation en marquant l’évènement handled. Il faut modifier la valeur de la propriété RoutedEventArgs.Handled:

private void ButtonClick(object sender, RoutedEventArgs e)  
{  
    e.Handled = true; 
}
La propagation des évènements routés n’est pas réellement stoppée

En réalité, l’évènement routé n’est pas vraiment arrêté. Il continue sa propagation mais, par défaut, les éléments qui s’y sont abonnés ne seront pas déclenchés.

Pour être notifié même dans le cas où un évènement a déjà été marqué handled par un autre élément, il faut renseigner le paramètre handledEventsToo dans la méthode:
UIElement.AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo)

Par exemple:

this.textBox.AddHandler(ButtonBase.ClickEvent, (RoutedEventHandler)ButtonClick, true);

Définir un évènement routé

On peut définir un évènement routé en utilisant la classe statique System.Windows.EventManager avec la méthode:

EventManager.RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, 
    Type handlerType, Type ownerType)

Par exemple, si on souhaite définir l’évènement ClickEvent dans un control particulier:

public class CustomControl : ContentControl 
{ 
    public static readonly RoutedEvent ClickEvent; 
 
    static CustomControl() 
    { 
        CustomControl.ClickEvent = EventManager.RegisterRoutedEvent( 
          "ClickOnCustomControl", RoutingStrategy.Bubble, 
          typeof(RoutedEventHandler), typeof(CustomControl)); 
    } 
 
    public event RoutedEventHandler Click 
    { 
        add 
        { 
            base.AddHandler(CustomControl.ClickEvent, value); 
        } 
        remove 
        { 
            base.RemoveHandler(CustomControl.ClickEvent, value); 
        } 
    } 
}

On peut s’abonner à cet évènement en utilisant le handler:

this.customControl.Click += new RoutedEventHandler(ButtonClick);

Avec:

private void ButtonClick(object sender, RoutedEventArgs e)  
{  
    MessageBox.Show("ButtonClick");  
}

Partager un évènement routé entre plusieurs classes

Lorsqu’on évènement routé est défini avec System.Windows.EventManager.RegisterRoutedEvent(), il est défini pour un type particulier. Si on réexécute cette fonction pour un type différent avec le même nom d’évènement, une exception sera levée. Pour éviter cette exception, un évènement peut être partagé entre plusieurs classes avec:

System.Windows.RoutedEvent.AddOwner(Type ownerType)

Par exemple, dans l’exemple précédent, pour partager l’evènement avec une autre classe:

RoutedEvent newRoutedEvent = CustomControl.AddOwner(typeof(OtherCustomControl));
Evènements partagés entre UIElement et ContentElement

De nombreux évènements sont partagés entre System.Windows.UIElement et System.Windows.ContentElement comme par exemple Mouse.MouseDownEvent.

Ainsi cet évènement est partagé en utilisant:

public static readonly RoutedEvent MouseDownEvent = 
    Mouse.MouseDownEvent.AddOwner(UIElement);

Et:

public static readonly RoutedEvent MouseDownEvent = 
    Mouse.MouseDownEvent.AddOwner(ContentControl);

Déclencher un évènement routé

Déclencher un évènement se fait en utilisant la méthode:

UIElement.RaiseEvent(RoutedEventArgs e)

Ainsi pour déclencher l’évènement System.Windows.Controls.Primitives.ButtonBase.ClickEvent, on peut écrire:

RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent, this); 
this.RaiseEvent(e);

Leave a Reply