Understanding Windows Presentation Foundation Routed Events In .NET Core

September 22, 2020
Written by
Jeff Rosenthal
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

wpf-routed-events.png

Windows Presentation Foundation (WPF) is a very powerful framework for building desktop applications. Unlike Windows Forms, WPF provides the capabilities of nested UIElements, animation, layered presentation, and more. The focus of this tutorial is to demonstrate the aspects of Routed Events.

Routed Events are a construct specific to WPF that supports event routing. It allows for the processing of events in a very flexible fashion to meet a particular capability.

User interface elements (UIElements) have the ability to nest elements. A panel can contain a button which can contain a panel. Within that panel could be an image and some text. The user then clicks the mouse button. What element is expected to handle the event? Is it the text or the image? It could even be the panel that holds them or the button itself.

A common practice is to add a mouse click handler for a specific UIElement. The code may look something like this:

<Button Name="MyButton" Click="MyButton_Click"/>

In this case, the application will route the click event for the button directly to the MyButton_Click method. This type of event routing is called Direct. It is more exactly RoutingStrategy.Direct. In this implementation, the concept of Routed Events is invisible to the developer.

Continuing with this example, if you wanted to handle the click for an image and a text block and the button with the same action, you could add handlers on the image, textblock, stackpanel, and the button, or you could use routed events.

Looking further at this example, the elements are organized into a hierarchy:

Visual diagram of hierarchical representation of elements

The tutorial in this post will guide you through creating a project that will visually demonstrate the flow, or routing, of events. It is interesting to see the flow of events, both up and down the visual tree of the window.  

WPF provides several mechanisms for the handling of events:

  • Direct events
  • Tunneling events
  • Bubbling events

Direct events are most common and are used when you specify a handler on a specific UIElement. An example is when you handle the Click event on a button. The event will be handled by that specific handler.

Bubbling events are events that get routed up the element hierarchy from the source of the event to each successive parent element until the root element of the page, the base of the element hierarchy, is reached. If there is no handler event directly associated with an element the event “bubbles” upwards until it reaches a parent element with a handler method that can handle the event.

Tunneling events are those that start at the root of the element hierarchy and travel downwards through each successive child element until the element that invoked the event is reached. Tunneling events are also referred to as Preview events because of the naming convention used for handler methods, such as PreviewMouseDown and PreviewMouseUp. The Preview prefix helps differentiate in which direction the event will be handled.

Events are often created in pairs of bubbling and tunneling events, with the tunneling event having the Preview prefix on its method name. This is done to enable each input action to invoke both a bubbling and tunneling event. The tunneling event is invoked first, giving rise to its “preview” association, then the bubbling event is raised. For more information on how these pairs of events work together, see WPF Input Events in the WPF documentation on docs.microsoft.com.

Prerequisites

You’ll need the following tools and resources to build and run this project:

Windows 10 – It puts the Windows in Windows Presentation Foundation.

.NET Core SDK 3.1 – The SDK includes the APIs, runtime, and CLI.

Visual Studio 2019 with the following workloads and individual components:

  • .NET desktop development workload (Includes C#)
  • GitHub Extension for Visual Studio (If you want to clone the companion repository.)

You should have a general knowledge of Visual Studio and the C# language syntax. You will be adding and editing files, and debugging code.

There is a companion repository for this post available on GitHub. It contains the complete source code for the tutorial project.

Creating the project

Begin this tutorial by creating a WPF App (.NET Core) project for C# named RoutedEventsSpyGlass. You can put the solution and project folders wherever it’s most convenient for you.

In the MainWindow.xaml file, replace the content of the file with the following XAML markup:

<Window x:Class="RoutedEventsSpyGlass.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RoutedEventsSpyGlass"
        mc:Ignorable="d"
        Title="Routed Events Spy Glass" Height="450" Width="800">
    <Grid Name="grid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="100*"/>
        </Grid.RowDefinitions>
        <Button Name="btnClear" Click="btnClear_Click" >Clear</Button>
        <Button Name="btn" Grid.Row="1" Background="BlueViolet" HorizontalAlignment="Center" Margin="40,40" Padding="5,5">
            <TextBlock Name="text" Background="BlanchedAlmond" FontSize="24">Routed Events SpyGlass</TextBlock>
        </Button>
        <StackPanel Orientation="Horizontal" Grid.Row="2" Background="Bisque">
            <TextBlock Margin="40,0,0,0" FontWeight="Bold">Routed Event</TextBlock>
            <TextBlock Margin="125,0,0,0" FontWeight="Bold">Sender</TextBlock>
            <TextBlock Margin="100,0,0,0" FontWeight="Bold">Source</TextBlock>
            <TextBlock Margin="100,0,0,0" FontWeight="Bold">Original Source</TextBlock>
            <TextBlock Margin="54,0" FontWeight="Bold">Routing</TextBlock>
        </StackPanel>
        <ScrollViewer Name="ScrollViewer" Grid.Row="3">
            <StackPanel Name="myStackPanel" Orientation="Vertical"></StackPanel>
        </ScrollViewer>
    </Grid>
</Window>

The layout here is simple, as you can see from the rendered UI in the file’s preview pane: a grid with several rows, each containing a UIElement. At the bottom, a ScrollViewer is positioned that will be dynamically populated from code. Above that is the header for the data that will be in the scrollview. At the top are two buttons: one to clear the scrollview and one to trigger the demonstration.

In the MainWindow.xaml.cs file, which is nested under MainWindow.xaml in the Solution Explorer, add the following using statement to the existing list:

using System.Windows.Media;

Replace the existing contents of the MainWindow class with the following code:

        FontFamily fontfam = new FontFamily("arial");
        private DateTime before;
        public MainWindow()
        {
            InitializeComponent();

            //These are the elements that we will be focusing on
UIElement[] elements = {grid, btn, text};

                //Attach event handler to the events
            foreach (var element in elements)
            {
                element.PreviewKeyDown += DoEverythingEventHandler;
                element.PreviewKeyUp += DoEverythingEventHandler;
                element.PreviewTextInput += DoEverythingEventHandler;
                element.KeyDown += DoEverythingEventHandler;
                element.KeyUp += DoEverythingEventHandler;
                element.TextInput += DoEverythingEventHandler;

                element.MouseDown += DoEverythingEventHandler;
                element.MouseUp += DoEverythingEventHandler;
                element.PreviewMouseUp += DoEverythingEventHandler;
                element.PreviewMouseDown += DoEverythingEventHandler;

                element.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoEverythingEventHandler));
            }
        }

        void DoEverythingEventHandler(object sender, RoutedEventArgs args)
        {        
            System.Windows.Controls.StackPanel sp = new StackPanel();

                //Insert a separator if 100ms has lapsed. 
//This will increase readability
            DateTime now = DateTime.Now;
            if (now - before > TimeSpan.FromMilliseconds(100))
            {
                System.Windows.Controls.StackPanel sp_blank = new StackPanel
                {
                    Height = 20,
                    Background = Brushes.Gray
                };
                sp.Children.Add(sp_blank);
                }


            before = now;

            var width = 60;
                //Specify the orientation of the stackpanel.
            sp.Orientation = Orientation.Horizontal;


                //Add a new textblock, format and populate it and add it to the stackpanel
            TextBlock tb1 = new TextBlock();
            FormatTextBox(args, tb1, width * 2, args.RoutedEvent.Name);
            sp.Children.Add(tb1);
                
                //Add a new textblock, format and populate it and add it to the stackpanel
            TextBlock tb2 = new TextBlock();
            FormatTextBox(args, tb2, width, ShrinkTheName(sender));
            sp.Children.Add(tb2);

                //Add a new textblock, format and populate it and add it to the stackpanel
            TextBlock tb3 = new TextBlock();
            FormatTextBox(args, tb3, width, ShrinkTheName(args.Source));
            sp.Children.Add(tb3);

                //Add a new textblock, format and populate it and add it to the stackpanel
            TextBlock tb4 = new TextBlock();
            FormatTextBox(args, tb4, width, ShrinkTheName(args.OriginalSource));
            sp.Children.Add(tb4);

                //Add a new textblock, format and populate it and add it to the stackpanel
            TextBlock tb5 = new TextBlock();
            FormatTextBox(args, tb5, width, args.RoutedEvent.RoutingStrategy.ToString());
            sp.Children.Add(tb5);

                //Finally add the stackpanel that we just created to the scrollviewer stackpanel and scroll to the bottom
            myStackPanel.Children.Add(sp);
            ScrollViewer.ScrollToBottom();
        }

        private void FormatTextBox(RoutedEventArgs args, TextBlock tb, int width, string content)
        {
            tb.FontFamily = fontfam;
            tb.Width = width;
            tb.Foreground = args.RoutedEvent.RoutingStrategy == RoutingStrategy.Bubble ? Brushes.Green : Brushes.Red;
            tb.Margin = new Thickness(40, 5, 40, 5);
            tb.Text = $"{content}";
        }

        string ShrinkTheName(object obj)
        {
                //Remove the namespace and class information for readability
            var str = obj.GetType().ToString().Split('.');
            return str[str.Length - 1];
        }

        private void btnClear_Click(object sender, RoutedEventArgs e)
        {
                //Clear the view
            myStackPanel.Children.Clear();
        }
    }

The code first iterates over a few of the selected elements and assigns various events to the DoEverythingEventHandler where most of the work is done. The purpose of the DoEverythingEventHandler is to dynamically add rows of information to the ScrollView. The information will reveal what the event is (RoutedEvent), who is processing it (Sender), where it originated (Source) and what type of routing strategy it is implementing (Routing). Several helper functions reduce duplication and make the output more readable.

Testing the completed application

Before building the application, note the Background color attribute of the btn button. This is to assist you in differentiating the button from the elements that it contains: the image and the text.

Build and run the application. You will see the large button at the top with the text “Routed Events SpyGlass”. Right-click on the text of the button and a stream of data will be added to the scrollview portion of the window. An example of the output is provided below. If your output looks different, you may have pressed the left mouse button, which will produce a different result which will be discussed later.

Routed Events Spy Glass Application Screenshot - right click

The data presented is interesting in how it makes it clear to see the sequence of events generated by user actions such as mouse clicks and keyboard strokes. First, you will see the PreviewMouseDown event being processed by the Grid, which is at the top of the visual tree. It will “tunnel” down to the Button and then the TextBlock. These tunneling events are named with the prefix of “Preview”. The events then change direction and “Bubble” back up the visual tree through the button and then the Grid. The events also get routed to the Window, but for simplicity it has been ignored in the example.

Every element in the tree associated with the Source has the opportunity to intercept and take action on the event.

Try again. Clear the scrollview by clicking the Clear button at the top of the application window, and then left click on the button. The output will look similar to this:

Routed Events Spy Glass app screenshot - left click

The PreviewMouseDown event traverses the elements much as it did before when you right-clicked, but on the bubbling of MouseDown it stops. At the bottom you will notice the click events for the Button and the Grid but not for the TextBlock. The button processes the left mouse action differently and interprets it as a “Click”. It also marks the event as Handled, which stops the further routing of the event.

With the button in focus, try hitting keys on the keyboard and the corresponding data generated for those events will display.

The visual presentation of the sequence of events here is exclusively found within WPF. It is a very powerful mechanism that you can leverage, but it will seem invisible to the casual observer.

Routed events allow you to create an event handler anywhere with the visual tree of that element.

someElement.AddHandler(Button.Click, new RoutedEventHandler(MyEventHandler));

Installing and implementing a handler like this is convenient in situations where the event could originate from any of several child elements.

Potential enhancements

Mouse and keyboard events are not the only events possible here. On tablets you could experiment with stylus events. Focus and LostFocus are two additional events you could add and explore.

The application also demonstrates how to programmatically add and manipulate elements. XAML is a powerful tool, but anything that can be done in XAML can be done in code. In this case, the ability to use foreach loops made implementing the event handlers for the selected elements less repetitive.  

Summary

The data presented visually in the project lends to a deeper understanding of how events get routed through the visual tree. Tunneling and Bubbling of events come to life a bit showing you exactly what is happening and in what order.

Additional resources

Routed Events Overview – This docs.microsoft.com article describes how routed events work in Windows Presentation Foundation (WPF).

Events and routed events overview – For a view of how events work in Universal Windows Platform (UWP) see this article, which provides background on event-driven code as a concept.

Using Twilio Lookup in .NET Core WPF Applications – Check out this blog post for a practical example of using Twilio Lookup in a WPF application to obtain phone number information.

TwilioQuest – If you’d like to learn more about programming C# and other languages, try this action-adventure game inspired by the 16-bit golden era of computer gaming.

Jeffrey Rosenthal is a C/C++/C# developer and enjoys the architectural aspects of coding and software development. Jeff is a MCSD and has operated his own company, The Coding Pit since 2008. When not coding, Jeff enjoys his home projects, rescuing dogs, and flying his drone. Jeff is available for consulting on various technologies and can be reached via email, Twitter, or LinkedIn.