WPF Simplified Part 7: Routed Events
December 31, 2009 2 Comments
In plain old .NET, an event is based on the idea of pub-sub model, the publisher publishes an “event” and the subscriber is notified of the “event”. In WPF the idea is still the same, but updated to take into account the rather complex visual trees that often occur in WPF.
In WPF, when an event is raised on an element in the visual tree, the event will either travel up the visual tree (from the element till it reaches the root) or down the tree (from the root to the element). Which direction it will take depends on the event’s predefined “routing strategy”. What is meant by travel is that the same event will fire on the element’s parent and the parent’s parent and so on, if the event is moving up. That is, if you have a Button inside a Grid, and if a MouseRightButonDown event is raised on the Button, a MouseRightButtonDown event is then raised by the WPF framework on the Grid, because the MouseRightButonDown event moves up (“bubbles up”).
The advantage here is that a high-level visual element need not explicitly hook the same event on all of its descendants, it can hook into the event on itself and wait for the framework to raise the event on itself (just like the Grid hooks into it’s own MouseRightButonDown event and waits for the framework to raise a MouseRightButonDown event on the Grid). Descendants don’t need to explicitly notify parents when an event occurs.
Take this simple XAML,
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Grid> <Button Height="100" Width="100"> <StackPanel Height="80" Width="80"> <RadioButton Content="Click Me"/> </StackPanel> </Button> </Grid> </Window>
Adding routed event handlers to this code,
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" MouseRightButtonDown="WindowMouseLeftButtonDownHandler" PreviewMouseRightButtonDown="WindowPreviewMouseLeftButtonDownHandler" Title="Window1" Height="300" Width="300"> <Grid MouseRightButtonDown="GridMouseLeftButtonDownHandler" PreviewMouseRightButtonDown="GridPreviewMouseLeftButtonDownHandler">
<Button Height="100" Width="100" MouseRightButtonDown="ButtonMouseLeftButtonDownHandler" PreviewMouseRightButtonDown="ButtonPreviewMouseLeftButtonDownHandler">
<StackPanel Height="80" Width="80" MouseRightButtonDown="StackPanelMouseLeftButtonDownHandler" PreviewMouseRightButtonDown="StackPanelPreviewMouseLeftButtonDownHandler">
<RadioButton Content="Click Me" MouseRightButtonDown="RadioButtonMouseLeftButtonDownHandler" PreviewMouseRightButtonDown="RadioButtonPreviewMouseLeftButtonDownHandler"/> </StackPanel> </Button> </Grid> </Window>
And in the code behind class add handlers that look like this,
private void WindowMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e){ Debug.WriteLine("Window caught event from " + e.OriginalSource.ToString()); } private void GridMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e){ Debug.WriteLine("Grid caught event from " + e.OriginalSource.ToString()); }
// Add similar handlers for Button, StackPanel, and RadioButton
private void WindowPreviewMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e){ Debug.WriteLine("Window caught preview event from " + e.OriginalSource.ToString()); } private void GridPreviewMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e){ Debug.WriteLine("Grid caught preview event from " + e.OriginalSource.ToString()); }
// Add similar handlers for Button, StackPanel, and RadioButton
Now if you run this example, and click on the radio button’s circle, you’ll see the output look like this,
Window caught preview event from Microsoft.Windows.Themes.BulletChrome Grid caught preview event from Microsoft.Windows.Themes.BulletChrome Button caught preview event from Microsoft.Windows.Themes.BulletChrome StackPanel caught preview event from Microsoft.Windows.Themes.BulletChrome RadioButton caught preview event from Microsoft.Windows.Themes.BulletChrome RadioButton caught event from Microsoft.Windows.Themes.BulletChrome StackPanel caught event from Microsoft.Windows.Themes.BulletChrome Button caught event from Microsoft.Windows.Themes.BulletChrome Grid caught event from Microsoft.Windows.Themes.BulletChrome Window caught event from Microsoft.Windows.Themes.BulletChrome
As you can see, the PreviewMouseRightButtonDown routed event is a tunneling event, which means it starts from the root (Window) and goes to the source (RadioButon), whereas the MouseRightButtonDown routed event is a bubbling event, which means that it starts at the source and goes to the root of the tree. By convention tunneling events have a prefix of Preview. There is also a third “routing strategy”, called “direct”, which means that the event is only raised on the element and does not travel either up or down the visual tree.
Input events are usually in pairs, like the MouseRightButtonDown and the PreviewMouseRightButtonDown both occur on a single user input. First the tunneling event is raised and travels its route (to the element from the root) and then the bubbling event is raised and it travels its route.
If you want to stop the tunneling or bubbling at any point (say at the Button level), simply set the Handled to true.
private void ButtonMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e){ Debug.WriteLine("Button caught " + e.OriginalSource.ToString()); e.Handled = true; }
Notice in the output that Grid and Window don’t receive the bubbling MouseLeftButtonDown routed event.
To get all the routed events raised by a class, use the EventManager class,
var events = EventManager.GetRoutedEvents(); foreach (var routedEvent in events) { EventManager.RegisterClassHandler(typeof(Window1), routedEvent, new RoutedEventHandler(handler)); } internal static void handler(object sender, RoutedEventArgs e) { Debug.WriteLine(e.OriginalSource + "=>" + e.RoutedEvent); }
Writing a custom routed event is similar to writing a dependency property,
public static readonly RoutedEvent MyRoutedEvent = EventManager.RegisterRoutedEvent( "MyRoutedEvent", // Name of the custom routed event RoutingStrategy.Bubble, // The routing strategy typeof(RoutedEventHandler), // Type of the event handler typeof(MyControl)); // The type of the owner of this routed event // Provide CLR property wrapper for the routed event public event RoutedEventHandler MyEvent { add { AddHandler(MyRoutedEvent, value); } remove { RemoveHandler(MyRoutedEvent, value); } }
The tunneling and bubbling of a routed event occurs when every element in the route exposes that event. But WPF supports tunneling and bubbling of routed events through elements that don’t even define that event – this is possible via attached properties. Attached events operate much like attached properties (and their use with tunneling or bubbling is very similar to using attached properties with property value inheritance), elements can handle events that are declared in a different element.
<Grid Button.Click="ButtonClickHandler"> <Button Height="100" Width="100" Content="Some Text"/> </Grid>
Grid doesn’t have a Click event, but WPF allows the Button.Click event to be raised on the Grid. The Grid can then handle this event in it’s handler “ButtonClickHandler”. Every routed event can be used as an attached event. We can also hook up the attached event in code,
<Grid x:Name="MyGrid"> <Button Height="100" Width="100" Content="Some Text"/> </Grid>
// In code... this.MyGrid.AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonClickHandler));