In this third part of a four-part series on the concepts you need to understand to work with Windows Presentation Foundation, you'll learn all about routing events. This article is excerpted from chapter three of the book Windows Presentation Foundation Unleashed, written by Adam Nathan (Sam's Publishing; ISBN: 0672328917).
In most cases, routed events don’t look very different from normal .NET events. As with dependency properties, no .NET languages (other than XAML) have an intrinsic understanding of the routed designation. The extra support is based on a handful of WPF APIs.
Listing 3.6 demonstrates how Button effectively implements its Click routed event. (Click is actually implemented by Button’s base class, but that’s not important for this discussion.)
Just as dependency properties are represented as public static DependencyProperty fields with a conventional Property suffix, routed events are represented as public static RoutedEvent fields with a conventional Event suffix. The routed event is registered much like a dependency property in the static constructor, and a normal .NET event—or event wrapper—is defined to enable more familiar use from procedural code and adding a handler in XAML with event attribute syntax. As with a property wrapper, an event wrapper must not do anything in its accessors other than call AddHandler and RemoveHandler.
LISTING 3.6 A Standard Routed Event Implementation
public class Button : ButtonBase { // The routed event public static readonly RoutedEvent ClickEvent;
These AddHandler and RemoveHandler methods are not inherited from DependencyObject, but rather System.Windows.UIElement, a higher-level base class of elements such as Button. (This class hierarchy is examined in more depth at the end of this chapter.) These methods attach and remove a delegate to the appropriate routed event. Inside OnMouseLeftButtonDown, RaiseEvent (also defined on the base UIElement class) is called with the appropriate RoutedEvent field to raise the Click event. The current Button instance (this) is passed as the source element of the event. It’s not shown in this listing, but Button’s Click event is also raised in response to a KeyDown event to support clicking with the spacebar or sometimes the Enter key.
When registered, every routed event chooses one of three routing strategies—the way in which the event raising travels through the element tree. These strategies are exposed as values of a RoutingStrategy enumeration:
Tunneling—The event is first raised on the root, then on each element down the tree until the source element is reached (or until a handler halts the tunneling by marking the event as handled).
Bubbling—The event is first raised on the source element, then on each element up the tree until the root is reached (or until a handler halts the bubbling by marking the event as handled).
Direct—The event is only raised on the source element. This is the same behavior as a plain .NET event, except that such events can still participate in mechanisms specific to routed events such as event triggers.
Handlers for routed events have a signature matching the pattern for general .NET event handlers: The first parameter is a System.Object typically named sender, and the second parameter (typically named e) is a class that derives from System.EventArgs. The sender parameter passed to a handler is always the element to which the handler was attached. The e parameter is (or derives from) an instance of RoutedEventArgs, a subclass of EventArgs that exposes four useful properties:
Source—The element in the logical tree that originally raised the event.
OriginalSource—The element in the visual tree that originally raised the event (for example, the TextBlock or ButtonChrome child of a standard Button).
Handled—A Boolean that can be set to true to mark the event as handled. This is precisely what halts any tunneling or bubbling.
RoutedEvent—The actual routed event object (such as Button.ClickEvent), which can be helpful for identifying the raised event when the same handler is used for multiple routed events.
The presence of both Source and OriginalSource enable you to work with the higher-level logical tree or the lower-level visual tree. This distinction only applies to physical events like mouse events, however. For more abstract events that don’t necessarily have a direct relationship with an element in the visual tree (like Click due to its keyboard support), the same object is passed for both Source and OriginalSource.
The UIElement class defines many routed events for keyboard, mouse, and stylus input. Most of these are bubbling events, but many of them are paired with a tunneling event. Tunneling events can be easily identified because, by convention, they are named with a Preview prefix. These events, also by convention, are raised immediately before their bubbling counterpart. For example, PreviewMouseMove is a tunneling event raised before the MouseMove bubbling event.
DIGGING DEEPER
Using Stylus Events
A stylus--the pen-like device used by Tablet PCs--acts like a mouse by default. In other words, its use raises events such as MouseMove, MouseDown, and MouseUp. This behavior is essential for a stylus to be usable with programs that aren’t designed specifically for a Tablet PC. However, if you want to provide an experience that is optimized for a stylus, you can handle stylus-specific events such as StylusMove, StylusDown, and StylusUp. A stylus can do more "tricks" than a mouse, as evidenced by some of its events that have no mouse counterpart, such as StylusInAirMove, StylusSystemGesture, StylusInRange, and StylusOutOfRange. There are other ways to exploit a stylus without handling these events directly, however. The next chapter, "Introducing WPF’s Controls," shows how this can be done with a powerful InkCanvas element.
The idea behind having a pair of events for various activities is to give elements a chance to effectively cancel or otherwise modify an event that’s about to occur. By convention, WPF’s built-in elements only take action in response to a bubbling event (when a bubbling and tunneling pair is defined), ensuring that the tunneling event lives up to its “preview” name. For example, imagine you want to implement a TextBox that restricts its input to a certain pattern or regular expression (such as a phone number or zip code). If you handle TextBox’s KeyDown event, the best you can do is remove text that has already been displayed inside the TextBox. But if you handle TextBox’s PreviewKeyDown event instead, you can mark it as “handled” to not only stop the tunneling but also stop the bubbling KeyDown event from being raised. In this case, the TextBox will never receive the KeyDown notification and the current character will not get displayed.
To demonstrate the use of a simple bubbling event, Listing 3.7 updates the original About dialog from Listing 3.1 by attaching an event handler to Window’s MouseRightButtonDown event. Listing 3.8 contains the C# code-behind file with the event handler implementation.
LISTING 3.7 The About Dialog with an Event Handler on the Root Window
// In this example, all possible sources derive from Control Control source = e.Source as Control;
// Toggle the border on the source control if (source.BorderThickness != new Thickness(5)) { source.BorderThickness = new Thickness(5); source.BorderBrush = Brushes.Black; } else source.BorderThickness = new Thickness(0); } }
The AboutDialog_MouseRightButtonDown handler performs two actions whenever a right-click bubbles up to the Window: It prints information about the event to the Window’s title bar, and it adds (then subsequently removes) a thick black border around the specific element in the logical tree that was right-clicked. Figure 3.7 shows the result. Notice that
Figure 3.7. The modified About dialog, after the first Label is right-clicked.
right-clicking on the Label reveals Source set to the Label but OriginalSource set to its TextBlock visual child.
If you run this example and right-click on everything, you’ll notice two interesting behaviors:
Window never receives the MouseRightButtonDown event when you right-click on either ListBoxItem. That’s because ListBoxItem internally handles this event as well as the MouseLeftButtonDown event (halting the bubbling) to implement item selection.
Window receives the MouseRightButtonDown event when you right-click on a Button, but setting Button’s Border property has no visual effect. This is due to Button’s default visual tree, which was shown back in Figure 3.3. Unlike Window, Label, ListBox, ListBoxItem, and StatusBar, the visual tree for Button has no Border element.
FAQ
? Where is the event for handling the pressing of a mouse's middle button?
If you browse through the various mouse events exposed by UIElement or ContentElement, you’ll find events for MouseLeftButtonDown, MouseLeftButtonUp, MouseRightButtonDown, and MouseRightButtonUp (as well as the tunneling Preview version of each event). But what about the additional buttons present on some mice?
This information can be retrieved via the more generic MouseDown and MouseUp events (which also have Preview counterparts). The arguments passed to such event handlers include a MouseButton enumeration that indicates which button’s state just changed: Left, Right, Middle, XButton1, or XButton2. A corresponding MouseButtonState enumeration indicates whether that button is Pressed or Released.
DIGGING DEEPER
Halting a Routed Event Is an Illusion
Although setting the RoutedEventArgs parameter’s Handled property to true in a routed event handler appears to stop the tunneling or bubbling, individual handlers further up or down the tree can opt to receive the events anyway! This can only be done from procedural code, using an overload of AddHandler that adds a Boolean handledEventsToo parameter.
For example, the event attribute could be removed from Listing 3.7 and replaced with the following AddHandler call in AboutDialog’s constructor:
public AboutDialog() { InitializeComponent(); this.AddHandler(Window.MouseRightButtonDownEvent, new MouseButtonEventHandler(AboutDialog_MouseRightButtonDown), true); }
With true passed as a third parameter, AboutDialog_MouseRightButtonDown now receives events when you right-click on a ListBoxItem, and can add the black border!
You should avoid processing handled events whenever possible, because there is likely a reason the event is handled in the first place. Attaching a handler to the Preview version of an event is the preferred alternative.
The bottom line, however, is that the halting of tunneling or bubbling is really just an illusion. It’s more correct to say that tunneling and bubbling still continue when a routed event is marked as handled, but that event handlers only see unhandled events by default.