Handling routed events
Events are essentially notifications from an object to the outside world – a variation on the "observer" design pattern. Most of the time an object is told what to do via properties and methods. Events are its way of talking back to whoever is interested. The concept of events existed in .NET since its inception, but WPF has something to say about the way events are implemented. WPF introduces routed events, an enhanced infrastructure for raising and handling events, which we'll look at in this recipe.
Getting ready
Make sure Visual Studio is up and running.
How to do it...
We'll create a simple drawing application that uses routed events to handle user interaction:
Create a new WPF Application named
CH01.SimpleDraw
. This will be a simple drawing program.Add some markup to
MainWindows.xaml
that includes aCanvas
and some rectangle objects to select drawing brushes:<Canvas Background="White" Name="_root"> </Canvas>
To do some drawing, we'll handle the
MouseLeftButtonDown
,MouseMove
, andMouseUp
events on the canvas object. Within theCanvas
tag, typeMouseLeftButtonDown=
. Intellisense will pop up, suggesting to add a default handler name. Resist the temptation, and typeOnMouseDown
:<Canvas Background="White" Name="_root" MouseLeftButtonDown="OnMouseDown">
Right-click on OnMouseDown and select Navigate to Event Handler. Visual Studio will add the required handler method in the code behind file (
MainWindow.xaml.cs
) and jump straight to it:private void OnMouseDown(object sender, MouseButtonEventArgs e) { }
Add similar handlers for the
MouseMove
andMouseUp
events, namedOnMouseMove
andOnMouseUp
, respectively.Let's add simple drawing logic. First, add the following fields to the
MainWindow
class:Point _pos; bool _isDrawing; Brush _stroke = Brushes.Black;
Now the
OnMouseDown
event handler:void OnMouseDown(object sender, MouseButtonEventArgs e) { _isDrawing = true; _pos = e.GetPosition(_root); _root.CaptureMouse(); }
Next, we'll handle mouse movement, like in the following code snippet:
void OnMouseMove(object sender, MouseEventArgs e) { if(_isDrawing) { Line line = new Line(); line.X1 = _pos.X; line.Y1 = _pos.Y; _pos = e.GetPosition(_root); line.X2 = _pos.X; line.Y2 = _pos.Y; line.Stroke = _stroke; line.StrokeThickness = 1; _root.Children.Add(line); } }
If we're in drawing mode, we create a
Line
object, set its two points locations and add it to theCanvas
.Finally, when the mouse button is released, just revert things to normal:
void OnMouseUp(object sender, MouseButtonEventArgs e) { _isDrawing = false; _root.ReleaseMouseCapture(); }
Run the application. We now have a functional little drawing program. Event handling seemed to be as simple as expected.
Let's make it a little more interesting, with the ability to change drawing color. We'll add some rectangle elements in the upper part of the canvas. Clicking any of them should change the drawing brushing from that point on. First, the rectangles:
<Rectangle Stroke="Black" Width="25" Height="25" Canvas.Left="5" Canvas.Top="5" Fill="Red" /> <Rectangle Stroke="Black" Width="25" Height="25" Canvas.Left="35" Canvas.Top="5" Fill="Blue" /> <Rectangle Stroke="Black" Width="25" Height="25" Canvas.Left="65" Canvas.Top="5" Fill="Yellow" /> <Rectangle Stroke="Black" Width="25" Height="25" Canvas.Left="95" Canvas.Top="5" Fill="Green" /> <Rectangle Stroke="Black" Width="25" Height="25" Canvas.Left="125" Canvas.Top="5" Fill="Black" />
How should we handle clicks on the rectangles? One obvious way is to attach an event handler to each and every rectangle. But that would we wasteful. Events such as
MouseLeftButtonDown
"bubble up" the visual tree and can be handled at any level. In this case, we'll just add code to theOnMouseDown
method:void OnMouseDown(object sender, MouseButtonEventArgs e) { var rect = e.Source as Rectangle; if(rect != null) { _stroke = rect.Fill; } else { _isDrawing = true; _pos = e.GetPosition(_root); _root.CaptureMouse(); } }
Run the application and click the rectangles to change colors. Draw something nice.
How it works...
WPF events are called routed events because most can be handled by elements that are not the source of the event. In the preceding example, the MouseLeftButtonDown
was handled on the Canvas
element, even though the actual event may have triggered on a particular Rectangle
element. This is referred to as a routing strategy of
bubbling.
When the left mouse button is pressed, we make a note that the drawing has started by setting _isDrawing
to true
(step 7). Then, we record the current mouse position relative to the canvas (_root
) by calling the MouseButtonEventArgs.GetPosition
method. And finally, although not strictly required, we "capture" the mouse, so that subsequent events will be sent to the Canvas
and not any other window, even if the mouse pointer technically is not over the Canvas
.
To properly ascertain which element was actually the source of the event, the RoutedEventArgs.Source
property should be used (and not the sender
, in our example the sender is always the Canvas
).
There's more...
Bubbling is not the only routing strategy WPF supports. The opposite of bubbling is called
tunneling; events with a tunneling strategy are raised first on the top level element (typically a Window
), and then on its child, and so on, towards the element that is the actual source of the event. After the tunneling event has finished (calling any handlers along the way), its bubbling counterpart is raised, from the source up the visual tree towards the top level element (window).
A tunneling event always has its name starting with Preview. Therefore, there is PreviewMouseLeftButtonDown
and its bubbling counterpart is simply MouseLeftButtonDown
.
A third routing strategy is supported, called Direct. This is the simplest strategy; the event is raised on the source element of the event and that's it. No bubbling or tunnelling occurs. By the way, only very few events use the Direct strategy (for example, MouseEnter
and MouseLeave
).
Stopping bubbling or tunneling
After a bubbling event is handled by some element – it continues to bubble. The bubbling can be stopped by setting the RoutedEventArgs.Handled
property to true.
If the event is a tunneling one – setting Handled
to true
stops the tunneling, but it also prevents the buddy-bubbling event from ever firing.
Attached events
Suppose we want to write a simple calculator application:
This is a Grid
that contains various Button
controls.
We would like to use as few handlers as we can. For the "=" button, we can attach a specific handler and prevent further bubbling:
void OnCalculate(object sender, RoutedEventArgs e) { // do operation e.Handled = true; }
What about the digit buttons? Again, we could add a click handler to each one, but that would be wasteful. A better approach would be to leverage the Click
event's bubbling strategy and set a single handler on the container Grid
.
Typing "Click=" on the Grid
tag seems to fail. Intellisense won't help and in fact this won't compile. It may be obvious – a Grid
has no Click
event. Click
is specific to buttons. Does this mean we can't set a Click
handler on the Grid
? Fortunately, we can.
WPF provides the notion of attached events. Such events can be handled by any element, even if that element's type does not define any such event. This is achieved through attached event syntax (similar to attached properties), such as the following code snippet:
<Grid ButtonBase.Click="OnKeyPressed">
The Click
event is defined on the ButtonBase
class, although Button.Click
works just as well, because Button
inherits from ButtonBase
. Now we can look at the actual source of the click with the same RoutedEventArgs.Source
described previously:
int digit;
string content = ((Button)e.Source).Content.ToString();
if(int.TryParse(content, out digit)) {
// a digit
}
You can find the complete calculator sample in the CH01.Calculator
project, available with the downloadable source for this chapter.