WPF event handling: anatomy of a button click

We’ve seen a simple example of how to add an event handler in WPF. However, a glance at the list of events available for any object shows that WPF provides a lot of flexibility in how you handle a user action such as pressing a Button with a mouse click. We’ll have a look at some of those features here.

First, we need to mention that it is possible, and in fact easy, to add pretty well any content to a Button. By default, a Button contains some text as its Content, but that can be replaced by as complex a layout as you wish. As an example, we’ll create a Button whose content is a Grid, and the Grid in turn contains two cells, one of which contains a coloured square, and the other of which contains a TextBlock.

In Expression Blend (EB) we create a project and insert a single Button in the top-level Grid (the one named LayoutRoot by default). Centre the Button in the window. Turn off the Focusable property if you don’t want the Button to flash after it’s clicked. Now, using the Assets button (with a double chevron symbol) in the EB toolbar on the left, find a Grid and add it to the Button as its Content. In the Layout panel (under Properties, on the right) add one row and two columns to the Grid.

In column 0, add a Rectangle, and give it a size of 50 by 50, with a background colour of pure green (RGB = 0, 255, 0). In column 1, add a TextBlock, centre it horizontally and vertically, give it a margin of 5 all round, and set its text to “Press the square”. The result should look like this:

You can run the program at this point, and you should find that the button is pressable, but of course nothing will happen since we haven’t added any event handlers.

We’ve seen that you can add the standard handler for the Click event, which is triggered when the mouse’s left button is pressed and then released over the button. However, WPF actually gives you much finer control over events. Any component on a display, not just controls such as Buttons, can give rise to events. Thus in our example, the Button itself, the Grid it contains, and the Rectangle and TextBlock within the Grid can all give rise to events.

To understand how these events arise and what you can do with them, we need to remember that compound objects like the Button containing graphics are structured as a tree. In the example, the Button is the root of the tree. The Grid is the single child of the Button, and the Rectangle and TextBlock are children of the Grid. If the mouse’s left button is clicked over the Rectangle, say, then events are generated for the Rectangle, the Grid that contains it, and the Button that contains the Grid.

For some events, such as a mouse click, there are two types of events: tunneling and bubbling. Tunneling events are generated by starting at the root of the tree that contains the object over which the mouse was clicked, and then travelling down the tree until the object itself is found. Thus clicking the mouse over the Rectangle generates an event first in the Button, then in the Grid and finally in the Rectangle itself. All tunneling events have names beginning with ‘Preview’. Thus pressing the left mouse button generates tunneling events called PreviewMouseLeftButtonDown.

Once the specific object over which the mouse was clicked is located in the tree, a series of bubbling events is generated. Bubbling events are generated in the opposite order to tunneling events, so they start at the node in the tree corresponding to the object over which the mouse was clicked, and then travel up the tree, ending at the root. Typically, each tunneling event has a matching bubbling event, whose name is the same as that of the tunneling event but without the ‘Preview’ at the start. Thus the bubbling event for pressing the left mouse button is MouseLeftButtonDown.

The easiest way to see these events is to add handlers to all of them, and get each handler to print out a message when it is called. In EB, select each of the Button, Grid, square and TextBlock in turn, and add handlers for PreviewMouseLeftButtonDown and MouseLeftButtonDown. 

One way to see textual output from a program is to make the application a Console application, as we mentioned earlier. A somewhat more professional way is to run the project in debug mode in Visual Studio (VS), and use the System.Diagnostics.Debug class to generate some text. To do this, add the line

using System.Diagnostics;

at the top of the C# code file. Then a typical event handler would look like this (for the tunneling event generated by pressing the left mouse button over the square:

    private void Square_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)

You can write similar handlers for all the other events. When the program is run in VS under Debug mode, look in the Output window at the bottom to see the output from these Debug statements.

Try clicking on various parts of the Button and look at the output. If you click over the green square, you’ll see that 3 tunneling events are generated, for the Button, then the Grid and finally the Rectangle. Following this, you will see 2 bubbling events, first for the Rectangle and then the Grid. The Button itself doesn’t get a bubbling event for mouse down, since WPF combines the mouse down with the mouse up events to generate a Click event, which is the one most commonly handled for a Button.

In practice, the tunneling events aren’t used that much, but they are useful to have in some situations. For example, you may want to change something at the root level in the tree before an event is handled by a lower-down component.

Sometimes, you may want to catch an event at a higher level and prevent any further processing of that event by lower level components. You can do this by setting the event’s Handled flag to ‘true’. In the sample of an event handler above, you’ll notice that the second argument to the handler is an object of class MouseButtonEventArgs. You can use this object to set the Handled flag by inserting this line in the handler:

  e.Handled = true;

Try inserting that line in the Preview handler for the Grid and then run the program again. If you click on the square you’ll see that the Preview handlers for the Button and Grid are generated, but no further event handlers are called.

You will also notice that the first argument to the event handler method is called sender. This is the object that contains the actual component that generated the event. In our example, sender will correspond to the level in the tree at which the event is generated. For example, if we are in the handler for the Button’sPreviewMouseLeftButtonDown event and we click on the square, then sender will be the Button, not the square.

If you want to know the actual component that generated the event no matter where in the tree we are, this is contained in the MouseButtonEventArgs object, in its Source field. We can see both of these objects, sender and source, by adding a bit to the Debug statements in each handler. For example, we can write the handler for the Button’s PreviewMouseLeftButtonDown as follows.

    private void Button_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
      Debug.WriteLine("Button_PreviewMouseLeftButtonDown sender: " + sender.ToString()
        + "; Source: " + e.Source.ToString());

Calling the ToString() method of the objects here will return the class name of the object, so you can see which type of object it is. In the code shown, this handler will print out:

Button_PreviewMouseLeftButtonDown sender: System.Windows.Controls.Button; Source: System.Windows.Shapes.Rectangle

As an example of how these features might be used, suppose we wanted to change the colour of the square, but only if the user clicks directly on the square and not any other part of the button. We could put the code for doing this in the bubbling event handler for the square:

    SolidColorBrush RosyBrownBrush = new SolidColorBrush(Colors.RosyBrown);
    SolidColorBrush LimeBrush = new SolidColorBrush(Colors.Lime);
    private void Square_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
      Debug.WriteLine("Square_MouseLeftButtonDown sender: " + sender.ToString()
        + "; Source: " + e.Source.ToString());
      if (!Square.Fill.Equals(RosyBrownBrush))
        Square.Fill = RosyBrownBrush;
        Square.Fill = LimeBrush;
      ButtonText.Text = "Press the square";
      e.Handled = true;

We define a couple of colours from the Colors class (which contains a large number of pre-defined colours with given names). The if statement on line 7 will swap the square’s colour between RosyBrown and Lime. On line 15, we reset the text of the TextBlock (we change it in another handler to be seen shortly). Finally, we set the event to Handled to prevent any further processing.

We want to catch any other mouse click that is inside the Button but not over the square, and print a helpful message for the user reminding him to click on the square. To put this message in the right place, we need to remember the order in which events are generated. Tunneling events are generated from the top down, starting with the Button. Bubbling events are generated from the bottom up, starting with either the Rectangle or the TextBlock.

We might think, therefore, that we can place the message code in the bubbling handler for the Grid, since the Grid lies behind both the Rectangle and TextBlock. So our first attempt might be something like this:

    private void Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
      Debug.WriteLine("Grid_MouseLeftButtonDown sender: " + sender.ToString()
        + "; Source: " + e.Source.ToString());
      Square.Fill = new SolidColorBrush(Colors.Red);
      ButtonText.Text = "The SQUARE, idiot!";

This sets the square to red as a warning, and changes the TextBlock’s text. We find that if we click directly over the current TextBlock text, this works, but if we click over the empty space within the Button above or below the text, nothing happens.

This is because the Grid cells size themselves to fit round the elements they contain, so the cell containing the TextBlock exists only behind the TextBlock itself and doesn’t extend to the top and bottom of the Button. We could try fiddling with the Grid’s settings to fix this, but in this case there’s an easier way.

Our next thought might be to put the code in the handler for the button’s MouseLeftButtonDown event, but remember that this event doesn’t get triggered for a Button, so again nothing will happen. We can solve the problem by putting the code in the Click event handler for the Button, since this doesn’t get called until the mouse button is released. Thus we get:

    private void Button_Click(object sender, RoutedEventArgs e)
      Debug.WriteLine("Button_Click sender: " + sender.ToString()
        + "; Source: " + e.Source.ToString());
      Square.Fill = new SolidColorBrush(Colors.Red);
      ButtonText.Text = "The SQUARE, idiot!";

This works no matter where outside the square the mouse is clicked. Remember that we stopped the event from further processing in the square’s event handler, so if we click over the square, the Button’s Click handler is never called.

Complete project files for this example are available here.

Post a comment or leave a trackback: Trackback URL.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: