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
Stratégie de propagation
Convention de nommage des évènements routés
Evènements routés définis par pair
Séquencement des évènements définis par pair
Types d’évènements routés
Implémentation des évènements routés
Ajouter une callback à un évènement
Handler défini dans UIElement
Evènement défini dans une autre classe
Ajouter un handler dans le code behind
Utiliser UIElement.AddHandler()
Utiliser EventManager.RegisterClassHandler()
Interrompre la propagation d’un évènement
Définir un évènement routé
Partager un évènement routé entre plusieurs classes
Déclencher un évènement routé
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émentWindow
et l’élément source pourrait être un objetButton
.System.Windows.RoutingStrategy.Bubble
: cette stratégie est l’opposé de la stratégieTunnel
, 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émentWindow
.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 typeStackPanel
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 estTunnel
. Par exemple l’évènementUIElement.PreviewKeyDownEvent
.XXX
: sans préfixe “Preview”, un évènement possède une stratégie de propagationBubble
. Par exemple, l’évènementUIElement.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’objetHwndSource
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
etUIElement.PreviewKeyDown
: évènements respectivement bubble et tunnel correspondant à un enfoncement de touche sur le clavier.UIElement.TextInput
etUIElement.PreviewTextInput
: évènements respectivement bubble et tunnel déclenchés lorsque des touches correspondant à du texte sont frappées.UIElement.KeyUp
etUIElement.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
etUIElement.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
etUIElement.PreviewMouseMove
: évènements respectivement bubble et tunnel correspondant au déplacement de la souris au dessus d’un élément.UIElement.MouseRightButtonDown
etUIElement.PreviewMouseRightButtonDown
: évènements respectivement bubble et tunnel correspondant à l’enfoncement du bouton droit de la souris.UIElement.MouseLeftButtonDown
etUIElement.PreviewMouseLeftButtonDown
: évènements respectivement bubble et tunnel correspondant à l’enfoncement du bouton gauche de la souris.UIElement.MouseRightButtonUp
etUIElement.PreviewMouseRightButtonUp
: évènements respectivement bubble et tunnel correspondant au relâchement du bouton droit de la souris.UIElement.MouseLeftButtonUp
etUIElement.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;
}
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));
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);
- Comprendre les Routed Events: http://wpffacile.fr/comprendre-les-routed-events-wpf/
- 5 WPF routing events: http://www.programering.com/a/MjNzIDMwATE.html
- AddHandler Method Can Add Handler for Any Event: https://wpf.2000things.com/2012/07/03/593-addhandler-method-can-add-handler-for-any-event/
- To bubble or tunnel basic WPF events: https://www.codeproject.com/articles/464926/to-bubble-or-tunnel-basic-wpf-events
- The Truth about Routed Commands Routing: http://briannoyes.net/2008/11/09/the-truth-about-routed-commands-routing/
- Class-level event handler in WPF: http://stackoverflow.com/questions/5364580/class-level-event-handler-in-wpf
- Routed Events: https://www.wpftutorial.net/RoutedEvents.html
- Events: http://www.diranieh.com/NET_WPF/Events.htm