Beware of routed events
WPF introduces the concept of routed events. There are different routing strategies, but the most commonly used strategy is the bubbling strategy. With the bubbling strategy, the event handlers on the event source are invoked. The routed event then routes to successive parent elements until reaching the element tree root or when a handler sets the Handled
property.
Always make sure that you set the Handled
property, when you handle a routed event. It’s easy to forget, but the framework walks up the tree to find another handler. This has a performance penalty, but it might have unwanted side-effects that are even worse.
When you use controls that can be nested, then you should take extra care when handling routed events. Suppose we have the following code:
<Expander x:Name="Expander1" Header="Expander 1" Collapsed="OnExpander1Collapsed">
<Expander x:Name="Expander2" Header="Expander 2">
<TextBlock Text="Routed event test application."/>
</Expander>
</Expander>
And we implement the Collapsed
handler like this:
private void OnExpander1Collapsed(object sender, RoutedEventArgs e)
{
MessageBox.Show("Expander 1 is collapsed.");
e.Handled = true;
}
When you collapse Expander 1, then the message is shown. But guess what happens if you collapse the inner expander? The inner Expander raises the Collapsed
event and the framework will search for an appropriate handler. The Collapsed
event is defined as a bubbling event, so it will start at the source (the inner expander) and will go up the visual tree. When it reaches the outer expander, then it will call the event handler.
You can solve the problem by adding an event handler for the inner expander and set its Handled
property. It works in this example, but it won’t if a third-party control raised the event. The only way to solve this problem is to check the source of the event, so the event handler should read:
private void OnExpander1Collapsed(object sender, RoutedEventArgs e)
{
if (e.OriginalSource == this.Expander1)
{
MessageBox.Show("Expander 1 is collapsed.");
e.Handled = true;
}
}
In this example the problem is clearly visible. But things can get really nasty in more complex scenarios. Suppose you use an expander control in your code and some changes the template of another control so it uses an expander too. If this control is used within your expander, then things might get pretty nasty if you handle the Expanded
and/or Collapsed
events.
So make sure you check the source of the event for routed events. The disadvantage is that you have to name your control, which adds additional fields to your class. If the expander is inside a data template, then checking the name is a little bit more difficult, but not impossible:
private void OnExpander1Collapsed(object sender, RoutedEventArgs e)
{
var originalSource = e.OriginalSource as FrameworkElement;
if ((originalSource != null) && (originalSource.Name == "Expander1"))
{
MessageBox.Show("Expander 1 is collapsed.");
e.Handled = true;
}
}