Blog Archives

Routed Events (part 2)

BubblingEvents

Referring back to Routed Events (to part 1), let’s have a closer look at this part of the example source code:

// bubbling
private void MyMouseUp(object sender, MouseButtonEventArgs e) {
  FrameworkElement lElement = sender as FrameworkElement;
  string lAppend = Environment.NewLine;
  if (sender is Window) lAppend += Environment.NewLine;
  Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend;
  e.Handled = false;
  Results.ScrollToEnd();
} //

 

Suppressing Events

e.Handled allows you to halt the event routing process. Set this boolean to true and the event stops traveling any further. A small change demonstrates the altered behavior:

// bubbling
private void MyMouseUp(object sender, MouseButtonEventArgs e) {
  FrameworkElement lElement = sender as FrameworkElement;
  string lAppend = Environment.NewLine;
  if (sender is Window) lAppend += Environment.NewLine;
  Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend;
  e.Handled = (e.ChangedButton == MouseButton.Right);
  Results.ScrollToEnd();
} //

If you use the right MouseButton now, the bubbling routing event process stops. The same applies to the tunneling process when you change the MyPreviewMouseUp() method accordingly.

 


Raising Suppressed Events

You can avoid the suppression of Routed Events. This cannot be done through XAML. Use the AddHandler() method instead. An overload accepts a boolean for its third parameter. Set this one to true and you will receive events even if the e.Handled flag was set to true.

Let’s slightly change our example source code to:

<Window x:Class="DemoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:DemoApp"
        Title="MainWindow" Height="500" Width="630"
        Name="MyWindow" PreviewMouseUp="MyPreviewMouseUp">
    ...
...
public MainWindow() {
 InitializeComponent();
 
 List<Data> lItems = new List<Data>() {
    new Data() {Name = "Otto", LastName = "Waalkes"},
    new Data() {Name = "Heinz", LastName = "Rühmann"},
    new Data() {Name = "Michael", LastName = "Herbig"},
    new Data() {Name = "Sky", LastName = "du Mont"},
    new Data() {Name = "Dieter", LastName = "Hallervorden"},
    new Data() {Name = "Diether", LastName = "Krebs"},
    new Data() {Name = "Helga", LastName = "Feddersen"},
    new Data() {Name = "Herbert", LastName = "Grönemeyer"},
  };
    
  MyListView.ItemsSource = lItems;
  MyWindow.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(MyMouseUp), true);      
} //
...

Et voilà! The Routed Event gets executed despite the set e.Handled flag.

 
Attached Events

The Click event is defined in the ButtonBase class. It is a kind of combination of a Button press and release. But how can you use the bubbling behavior on a higher level like eg. a Grid that does not derive from the ButtonBase class? Attached events enable you to add event handlers to arbitrary elements, which do not define or inherit these.
Let’s add a Click event to the window level by adding Button.Click=”MyClick”:

<Window x:Class="DemoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:DemoApp"
        Title="MainWindow" Height="500" Width="630"
        Name="MyWindow" PreviewMouseUp="MyPreviewMouseUp"
        Button.Click="MyClick" >
    ...
private void MyClick(object sender, RoutedEventArgs e) {
  MessageBox.Show("Click received!");
} //

The program does not raise any Click events. We did even override the Click event of our TripleClickButton class. You won’t see a lot. But have a look at the two scroll bars. The scroll bar background (not the scroll bar itself) raises click events. As we are on the window level, we now receive these unexpected events. Indeed, this is a good example. A click of the scroll bar background bubbles through the hierarchy and finally raises the attached Click event on the window level.
Don’t forget to analyse the e.Source of your event parameter. You need to filter out the right Click event.

 

Style EventSetter

While Property setters are most common in Styles, EventSetters are rarely seen. They can be used for more complex problems. The simple ones should be solved by using Style.Triggers. Let’s say you want to change the color of a TextBlock when entering or leaving the area with the mouse cursor.

...
<Window.Resources>
    <Style x:Key="ChangeBackgroundColor" TargetType="TextBlock">
        <EventSetter Event="TextBlock.MouseEnter" Handler="ChangeBackgroundColorOnMouseEnter" /> // direct event
        <EventSetter Event="TextBlock.MouseLeave" Handler="ChangeBackgroundColorOnMouseLeave" /> // direct event
    </Style>
</Window.Resources>
...
<TextBlock Text="LastName:  " Grid.Column="2"  VerticalAlignment="Center" FontSize="16" Style="{StaticResource ChangeBackgroundColor}"/>
...
    private void ChangeBackgroundColorOnMouseEnter(object sender, MouseEventArgs e) { ((TextBlock)sender).Background = Brushes.Red; }
    private void ChangeBackgroundColorOnMouseLeave(object sender, MouseEventArgs e) { ((TextBlock)sender).Background = null; }

 

This example could be simplified. No C# code required:

...
<Window.Resources>
    <Style x:Key="ChangeBackgroundColor" TargetType="TextBlock">
        <Style.Triggers>
            <Trigger Property="TextBlock.IsMouseOver" Value="True">
                <Setter Property="TextBlock.Background" Value="Red" />
            </Trigger>
        </Style.Triggers>                       
    </Style>
</Window.Resources>
...
<TextBlock Text="LastName:  " Grid.Column="2"  VerticalAlignment="Center" FontSize="16" Style="{StaticResource ChangeBackgroundColor}"/>
...

 

I see the need for further explanations on Trigger types. I have just added a reminder on my To-Do-List.
But for now a simple list must suffice:

  • Trigger: Simplest trigger form. Reacts on DependencyProperty changes and then uses setters to change styles.
  • MultiTrigger: Combines multiple Triggers. All conditions must be met.
  • DataTrigger: Reacts on changes in bound data.
  • MultiDataTrigger: Combines multiple DataTriggers. All conditions must be met.
  • EventTrigger: Reacts on events. Used for animations.

In a nutshell: There are three trigger types. They use dependency properties, routed events or data binding.

Routed Events (part 1)

BubblingEvents

Events notify your code that something has happened. You subscribe to events like you subscribe eg. to a monthly magazine. Let’s say a new magazine comes out. The postal worker delivers it to your mail box. You don’t need to ask for the new magazine to be sent to you each time.
Something similar takes place in WPF. A button is pressed and the program delivers that information to your mail box, which is a callback method.

Links:
Events Part1
Events Part2
Events Part3
 

The classical event has a standard pattern:

public delegate void dMyDelegate();
public event dMyDelegate MyEvent;

public void addEvent() { MyEvent += Callback; }
public void removeEvent() { MyEvent -= Callback; }

public void CallEvent() {
  dMyDelegate d = MyEvent;
  if (d == null) return;
  d();
} //

void Callback() { Console.WriteLine("Event raised"); }

Or this:

private EventHandler _Handler;
public event EventHandler MyEvent {
  add { _Handler += value; }
  remove { _Handler -= value; }
} //

...

 
 

But what exactly is a “Routed Event”?

Routed Events are used in connection with WPF. They are required for a certain routing concept. In WinForms the callback is sent by the corresponding component (eg. the button). But in WPF event routing allows an event to take place in one element, but be raised by another one. That way you can handle events in the most convenient place.
A Button could be part of a template, which in turn is a part of a ListView. The Button could have an image, an ornated border, a grid and many extras. It does make sense to receive events on the group level sometimes.

We know direct events from WinForms. A mouse click raises an event. But in WPF there are two more types:

  1. Bubbling Events: They travel up the hierarchy of the visual tree. The click raises an event on the button, then on the grid, then on the window. It also raises events on all objects on the way.
  2. Tunneling Events: These events travel to the opposite direction. A keyboard entry would start at the window level, then travel through the grid towards the button.

 

Let’s just delve into a practical example. Run the code and observe what is happening.

RoutedEvents
 

<Window x:Class="DemoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="500" Width="630"
        MouseUp="MyMouseUp" PreviewMouseUp="MyPreviewMouseUp">
    <Grid MouseUp="MyMouseUp" PreviewMouseUp="MyPreviewMouseUp">
        <Grid.RowDefinitions>
            <RowDefinition Height="300" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <ListView Name="MyListView" Margin="0,0,0,0" Grid.IsSharedSizeScope="True" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="True" MouseUp="MyMouseUp"  PreviewMouseUp="MyPreviewMouseUp" Grid.Row="0">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <GroupBox Header="Item" FontSize="10">
                        <Grid PreviewMouseUp="MyPreviewMouseUp" MouseUp="MyMouseUp">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition />
                                <ColumnDefinition SharedSizeGroup="A" />
                                <ColumnDefinition />
                                <ColumnDefinition SharedSizeGroup="A" />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>
                            <TextBlock Text="Name:  " Grid.Column="0" VerticalAlignment="Center"  FontSize="16" PreviewMouseUp="MyPreviewMouseUp"/>
                            <TextBlock Text="{Binding Name}" FontWeight="Bold" Grid.Column="1"  VerticalAlignment="Center" FontSize="16"/>
                            <TextBlock Text="LastName:  " Grid.Column="2"  VerticalAlignment="Center" FontSize="16"/>
                            <TextBlock Text="{Binding LastName}" FontWeight="Bold" Grid.Column="3" VerticalAlignment="Center" FontSize="16"/>
                            <TextBlock Text="Click me" Grid.Column="4" FontSize="16" MouseUp="MyMouseUp" Background="AliceBlue"/>
                        </Grid>
                    </GroupBox>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <TextBox Name="Results" IsReadOnly="True" FontSize="14" ScrollViewer.VerticalScrollBarVisibility="Auto" Height="Auto" Grid.Row="1" />
    </Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;

namespace DemoApp {
  public partial class MainWindow : Window {

    public class Data {
      public string Name { get; set; }
      public string LastName { get; set; }
    } //

    public MainWindow() {
      InitializeComponent();

      List<Data> lItems = new List<Data>() {
        new Data() {Name = "Otto", LastName = "Waalkes"},
        new Data() {Name = "Heinz", LastName = "Rühmann"},
        new Data() {Name = "Michael", LastName = "Herbig"},
        new Data() {Name = "Sky", LastName = "du Mont"},
        new Data() {Name = "Dieter", LastName = "Hallervorden"},
        new Data() {Name = "Diether", LastName = "Krebs"},
        new Data() {Name = "Helga", LastName = "Feddersen"},
        new Data() {Name = "Herbert", LastName = "Grönemeyer"},
      };
      MyListView.ItemsSource = lItems;
    } //

    // bubbling
    private void MyMouseUp(object sender, MouseButtonEventArgs e) {
      FrameworkElement lElement = sender as FrameworkElement;
      string lAppend = Environment.NewLine;
      if (sender is Window) lAppend += Environment.NewLine;
      Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend;
      e.Handled = false;
      Results.ScrollToEnd();
    } //

    // tunneling
    private void MyPreviewMouseUp(object sender, MouseButtonEventArgs e) {
      FrameworkElement lElement = sender as FrameworkElement;
      string lAppend = Environment.NewLine;
      if (sender.Equals(e.OriginalSource)) lAppend += Environment.NewLine;
      Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend;
      e.Handled = false;
      Results.ScrollToEnd();
    } //

  } // class
} // namespace

 
Tunneling Events are raised before Bubbling Events. The convention for Tunneling Events in .Net is that names have to start with the prefix “Preview”. Therefore The “MouseUp” event is a Bubbling Event and “PreviewMouseUp” is a Tunneling Event.
Today’s XAML source code subscribes to bubbling/tunneling events at several places. Compare the output with the code. In theory you could deal with all events on the window level. Use the e.OriginalSource field of the event argument MouseButtonEventArgs e to distinguish between the many possible events. You will also receive events that you did not explicitly subscribe to. Click into the lower textbox or on a scroll bar; this will raise events on the Grid, (ListView) and Window.

Now, let’s add a tunneling event to the “Click me” TextBlock.

<TextBlock Text="Click me" Grid.Column="4" FontSize="16" MouseUp="MyMouseUp" PreviewMouseUp="MyPreviewMouseUp" Background="AliceBlue"/>

 
This activates the code that was redundant so far:

if (sender.Equals(e.OriginalSource)) lAppend += Environment.NewLine;

 
 
Custom Routed Events

How can we add a custom routed event? Just follow this pattern:

  • Inherit an element from another class.
  • Define a public static readonly RoutedEvent.
  • Register that Event by using the EventManager.
  • Add an “old school” C# event that forwards “old school” subscriptions to your WPF routed event.

 
And here we go. Let’s add a button with a triple click event.

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace DemoApp {
  public class TripleClickButton : Button {

    /*
     * The methods AddHandler, RemoveHandler and RaiseEvent are inherited from UIElement
     */

    // public static readonly !
    public static readonly RoutedEvent TripleClickEvent = EventManager.RegisterRoutedEvent("TripleClick", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TripleClickButton));

    // Link classical approach to the above WPF RoutedEvent.
    // Do not add any complex code here!
    public event RoutedEventHandler TripleClick {
      add { AddHandler(TripleClickEvent, value); }
      remove { RemoveHandler(TripleClickEvent, value); }
    } //

    // How to trigger our Routed Event. 
    void RaiseTripleClickEvent() {
      RoutedEventArgs lRoutedEventArgs = new RoutedEventArgs(TripleClickButton.TripleClickEvent);
      RaiseEvent(lRoutedEventArgs);
    } //

    private List<DateTime> _Clicks = new List<DateTime>();
    protected override void OnClick() {
      lock (_Clicks) { // In theory we do not need a lock, because the event is always raised on the Dispatcher thread.
        DateTime lNow = DateTime.Now;
        _Clicks.Add(lNow);
        if (_Clicks.Count < 3) return;
        if (lNow.Subtract(_Clicks[0]).TotalMilliseconds < 1000) {
          _Clicks.Clear();
          RaiseTripleClickEvent();
          return;
        }
        _Clicks.RemoveAt(0);
      }      
    } //

  } // class
} // namespace

 
Declare the new class in the XAML window namespace and replace the rightmost TextBlock by our new custom button.

<Window x:Class="DemoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:DemoApp"
        ...
       <TextBlock Text="{Binding LastName}" FontWeight="Bold" Grid.Column="3" VerticalAlignment="Center" FontSize="16"/>
       <app:TripleClickButton Content="TripleClick me" Grid.Column="4" FontSize="16" Background="Salmon" TripleClick="TripleClickButton_TripleClick" Tag="{Binding Name}" />
     </Grid>
     ...

 

Define the event itself in the main window. Here is the full code:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;

namespace DemoApp {
  public partial class MainWindow : Window {

    public class Data {
      public string Name { get; set; }
      public string LastName { get; set; }
    } //

    public MainWindow() {
      InitializeComponent();

      List<Data> lItems = new List<Data>() {
        new Data() {Name = "Otto", LastName = "Waalkes"},
        new Data() {Name = "Heinz", LastName = "Rühmann"},
        new Data() {Name = "Michael", LastName = "Herbig"},
        new Data() {Name = "Sky", LastName = "du Mont"},
        new Data() {Name = "Dieter", LastName = "Hallervorden"},
        new Data() {Name = "Diether", LastName = "Krebs"},
        new Data() {Name = "Helga", LastName = "Feddersen"},
        new Data() {Name = "Herbert", LastName = "Grönemeyer"},
      };
      MyListView.ItemsSource = lItems;

      
    } //

    // bubbling
    private void MyMouseUp(object sender, MouseButtonEventArgs e) {
      FrameworkElement lElement = sender as FrameworkElement;
      string lAppend = Environment.NewLine;
      if (sender is Window) lAppend += Environment.NewLine;
      Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend;
      e.Handled = false;
      Results.ScrollToEnd();
    } //

    // tunneling
    private void MyPreviewMouseUp(object sender, MouseButtonEventArgs e) {
      FrameworkElement lElement = sender as FrameworkElement;
      string lAppend = Environment.NewLine;
      if (sender.Equals(e.OriginalSource)) lAppend += Environment.NewLine;
      Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend;
      e.Handled = false;
      Results.ScrollToEnd();
    }

    // custom event
    private void TripleClickButton_TripleClick(object sender, RoutedEventArgs e) {
      Button lButton = e.OriginalSource as Button;
      if (lButton == null) return;
      
      Results.Text = "TripleClick received from " + lButton.Tag;
    } //

  } // class
} // namespace

 

And the full XAML:

<Window x:Class="DemoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:DemoApp"
        Title="MainWindow" Height="500" Width="630"
        MouseUp="MyMouseUp" PreviewMouseUp="MyPreviewMouseUp">
    <Grid MouseUp="MyMouseUp" PreviewMouseUp="MyPreviewMouseUp">
        <Grid.RowDefinitions>
            <RowDefinition Height="300" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <ListView Name="MyListView" Margin="0,0,0,0" Grid.IsSharedSizeScope="True" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="True" MouseUp="MyMouseUp"  PreviewMouseUp="MyPreviewMouseUp" Grid.Row="0">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <GroupBox Header="Item" FontSize="10">
                        <Grid PreviewMouseUp="MyPreviewMouseUp" MouseUp="MyMouseUp">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition />
                                <ColumnDefinition SharedSizeGroup="A" />
                                <ColumnDefinition />
                                <ColumnDefinition SharedSizeGroup="A" />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>
                            <TextBlock Text="Name:  " Grid.Column="0" VerticalAlignment="Center"  FontSize="16" PreviewMouseUp="MyPreviewMouseUp"/>
                            <TextBlock Text="{Binding Name}" FontWeight="Bold" Grid.Column="1"  VerticalAlignment="Center" FontSize="16"/>
                            <TextBlock Text="LastName:  " Grid.Column="2"  VerticalAlignment="Center" FontSize="16"/>
                            <TextBlock Text="{Binding LastName}" FontWeight="Bold" Grid.Column="3" VerticalAlignment="Center" FontSize="16"/>
                            <app:TripleClickButton Content="TripleClick me" Grid.Column="4" FontSize="16" Background="Salmon" TripleClick="TripleClickButton_TripleClick" Tag="{Binding Name}" />
                        </Grid>
                    </GroupBox>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <TextBox Name="Results" IsReadOnly="True" FontSize="14" ScrollViewer.VerticalScrollBarVisibility="Auto" Height="Auto" Grid.Row="1" />
    </Grid>
</Window>

 

TripleClick

 

The next post will be about:

  • Marking Routed Events as handled to avoid further processing.
  • Suppressing the suppression of the Handled flag.
  • Attached Events
  • Style EventSetters

Stay tuned 😉