Attached Events By Example - Adding an Activate event to any Selector element (ListView, ListBox, TreeView, etc...)
I couldn't resist using a long title. I've had this article on the back of my mind for a while but only managed to get in the mood for a big writing session tonight, after a glass of Stella and a soapy bath. Go figure.
A common struggle with ListViews, TreeViews and other controls is the lack of an ItemActivate event like the one found in WinForms. Several solutions have been offered, most of them involving inheriting from these controls to add the missing functionality. In the spirit of Windows Presentation Foundation's (oh, Avalon, where art thou...) emphasis on composition, I thought I'd offer a generalized solution for any Selector control, with no need for inheritance, using a nearly unknown little gem called attached events.
Attached events are not much talked about (Nick mentions them in passing), documented (msdn mentions them in one paragraph), or printed about (as far as I can read, neither the Petzold nor Chris & Ian's book talk about them, although Ian mentioned to me it will be in the next version). So what is an attached event, and what is not? Let's start with what it is not.
[Disclaimer: At core, attached events are just routed events used in a different way, and as such are more a pattern than an actual specific piece of technology. And as any pattern, they end up with a fancy name. Naming conventions are based on current msdn documentation, which may (and should) change in the future.]
Qualified Event Names
Often, when people think they use attached events, they in fact use Qualified Event Names. This special syntax lets you attach an event handler for a RoutedEvent anywhere in the tree above the element triggering that event. For example, the following code attaches the Click event triggered by the Button element type, but on its direct parent.
1 <Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1">
2 <StackPanel Background="LightGray" Orientation="Horizontal" Button.Click="CommonClickHandler">
3 <Button Name="YesButton" Width="Auto" >Yes</Button>
4 <Button Name="NoButton" Width="Auto" >No</Button>
5 <Button Name="CancelButton" Width="Auto" >Cancel</Button>
6 </StackPanel>
7 </Border>
This syntax is the same you'll use for attached events, so it could be said that to add a listener to an attached event, you use the Qualified Event Name notation.
Attached Events
So what are attached events? Let's see what our friends at msdn have to say.
An attached event allows you to attach a handler for a particular event to some child element rather than to the parent that actually defines the event, even though neither the object potentially raising the event nor the destination handling instance define or otherwise "own" that event in their namespace.
If you're like me, it takes a few readings to understand what it means. Let's take it one bit at a time. The first thing to realize is that we're talking about a normal routed event, declared the same way as usual.
public static readonly RoutedEvent ItemActivateEvent =
EventManager.RegisterRoutedEvent("ItemActivate",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ItemActivation));
Then, it is said attached events are defined on an element. Actually they don't need to be defined on an element that is in your element tree at all, and not even on an element. For example, the Mouse class is a sealed class, and yet it defines all mouse-related attached events. What would be more accurate would be to say that the class defining the event is often neither the user of the event (whoever consumes it by adding a handler on it) nor the source of the event (whatever code raises it). For example, the previous attached event has been defined on my ItemActivation class.
namespace SerialSeb.Windows.Controls
{
public static class ItemActivation
{
...
What is common to all attached events however, is the absence of an event declaration using the add{} and remove{} accessors to call AddHandler and RemoveHandler on the instance of the object. Instead, and I suppose it is by convention, two static methods are defined to achieve the adding and removing of an handler.
public static void AddItemActivateHandler(DependencyObject o, RoutedEventHandler handler)
{
((UIElement)o).AddHandler(ItemActivation.ItemActivateEvent, handler);
}
public static void RemoveItemActivateHandler(DependencyObject o, RoutedEventHandler handler)
{
((UIElement)o).RemoveHandler(ItemActivation.ItemActivateEvent, handler);
}
You'll find that all attached events follow the same AddEventNameHandler and RemoveEventNameHandler convention.
The pieces of the puzzle start falling into place slowly. Of course, now that an event has been defined, you want to consume it. To do so, you want to add a handler for that event, but it's not defined on an element present in your tree. The following XAML code shows how it can be done. Note that you can of course add and remove handlers programmatically through the two methods you've defined.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:my="clr-namespace:SerialSeb.Windows.Controls">
<Grid my:ItemActivation.ItemActivate="HandleItemActivate">
...
</Grid>
</Window>
I believe it strikes a phenomenal resemblance to the Qualified Event Name notation I mentioned earlier. Now, whenever the routed event ItemActivate we've defined earlier bubbles up from somewhere within the grid, it will be caught just like any normal routed event. After all, as I said before, this is just a normal routed event.
We've seen how you declare an attached event, and how to add a handler for it anywhere on our tree. The big question is now to know who is going to raise it? Like any routed event, any code can raise this event, as long as it knows on which element it wants to start bubbling it.
Raising an Attached Event
Raising our ItemActivate event needs to be done whenever, within a Selector, a user double-click on an item, or presses the Enter key after selecting one. To do so, we need to attach some behavior to that element, and we're going to use Dan Crevier's excellent attached property trick.
Whenever you declare an attached property, you can define a PropertyChangedCallback that will get called whenever the value of the property changes, and that includes the first time it's applied. And you get a reference to the element it's applied to, absolutely perfect to hook our behavior code to the Selector element!
Let's start by defining the attached property in our ItemActivation class.
public static ActivationMode GetActivationMode(DependencyObject obj)
{
return (ActivationMode)obj.GetValue(ActivationModeProperty);
}
public static void SetActivationMode(DependencyObject obj, ActivationMode value)
{
obj.SetValue(ActivationModeProperty, value);
}
public static readonly DependencyProperty ActivationModeProperty =
DependencyProperty.RegisterAttached("ActivationMode",
typeof(ActivationMode),
typeof(ItemActivation),
new FrameworkPropertyMetadata(ActivationMode.None,
ItemActivation.HandleActivationModeChanged));
Overall a very simple attached property called ActivationMode. The type is a simple enumeration defining what can trigger the raising of our event, Mouse, Keyboard or both. And finally, a call to our HandleActivationModeChanged static method that will provide for the subscription to the events we're interested in.
private static MouseButtonEventHandler SelectorMouseDoubleClickHandler = new MouseButtonEventHandler(ItemActivation.HandleSelectorMouseDoubleClick);
private static KeyEventHandler SelectorKeyDownHandler = new KeyEventHandler(ItemActivation.HandleSelectorKeyDown);
private static void HandleActivationModeChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
Selector selector = target as Selector;
if (target == null) // if trying to attach to something else than a Selector, just ignore
return;
ActivationMode newActivation = (ActivationMode)e.NewValue;
if ((newActivation & ActivationMode.Mouse) == ActivationMode.Mouse)
{
selector.MouseDoubleClick += SelectorMouseDoubleClickHandler;
}
if ((newActivation & ActivationMode.Keyboard) == ActivationMode.Keyboard)
{
selector.KeyDown += SelectorKeyDownHandler;
}
else
{
selector.KeyDown -= SelectorKeyDownHandler;
selector.MouseDoubleClick -= SelectorMouseDoubleClickHandler;
}
}
I defined two handlers for the KeyDown and the MouseDoubleClick events of our Selector. Only thing left to do is to raise our event when the user double-clicked on something.
static void HandleSelectorMouseDoubleClick(object o, MouseButtonEventArgs e)
{
ItemsControl sender = o as ItemsControl;
DependencyObject originalSender = e.OriginalSource as DependencyObject;
if (sender == null || originalSender == null) return;
DependencyObject container = ItemsControl.ContainerFromElement(sender as ItemsControl, e.OriginalSource as DependencyObject);
// just in case, check if the double click doesn't come from somewhere else than something in a container
if (container == null || container == DependencyProperty.UnsetValue) return;
// found a container, now find the item.
object activatedItem = sender.ItemContainerGenerator.ItemFromContainer(container);
if (activatedItem != null && activatedItem != DependencyProperty.UnsetValue)
sender.RaiseEvent(new ItemActivateEventArgs(ItemActivation.ItemActivateEvent, sender, activatedItem, ActivationMode.Mouse));
}
We get the container (the one returned by the ItemsControl.ItemTemplate property), from which we can get the Item that is being represented by this fragment of the tree.
And there you have it, we raise our attached event by calling the RaiseEvent method of our target element! Our attached event is now going to bubble from the Selector and you'll be able to catch it wherever you want in your tree.
Conclusion
We now have an ItemActivate routed event that can be used on any selector, by using attached properties and attached events. Through composition we've added behavior and functionality to a type without having to inherit from it.
Next time, we'll learn how to bind a RoutedEvent to a Command anywhere in your XAML. Stay tuned...
P.S. I'll post the complete code, with keyboard support, and tunneling PreviewItemActivate event when I find the time to set-up my other web sites. In the meantime, don't hesitate to copy and paste!