WPF 4.5: Markup Extension for Events

April 7, 2012

tags: , , , , ,
no comments

One of the new features coming in WPF 4.5 (already available through the Visual Studio 11 Beta and .NET 4.5 Beta) is the ability to create markup extensions that work on events (as opposed to properties, which have always had this capability).

Markup extensions for properties are crucial, and there are quite a few useful ones, such as {Binding} and {StaticResource}. Would this ability be useful for events?

One such case is when a command is needed to be invoked (ICommand) because of some event, typically found in MVVM scenarios. The problem is that only some events can actually trigger a command (out of the box) such as Click for ButtonBase and Click for MenuItem. What about other events? What about other elements that don’t implement ICommandSource and therefore have no Command property to connect to?

This is where a markup extension for events can come in handy. The solutions so far to this issue usually involved using the Interactivity assembly from the Expression Blend SDK, that provides actions, triggers and behaviors, one of which provides a way to invoke a command (InvokeCommandAction). We can try to do something similar using the new feature, which will not require the Blend SDK at all.

How to use it

Let’s look at the usage first. Consider a simple paining program the draws lines on a Canvas. We want to handle a mouse down, move and up events in the ViewModel (as opposed to the view). This decouples things, and provides command objects that can later be used to manage (e.g.) undo/redo features. Here’s how the Canvas markup should look like:

  1. <Canvas ClipToBounds="True" Background="White"
  2.         MouseLeftButtonDown="{local:EventToCommand StartPaintCommand}"
  3.         MouseMove="{local:EventToCommand AddLineCommand}"
  4.                              MouseLeftButtonUp="{local:EventToCommand EndPaintCommand}">
  5. </Canvas>

The markup extension constructor is interpreted as a property path to bind to (relative to the current DataContext). At first, it seems easier to add a Command property (of type ICommand) to the markup extension (and I did) , but it’s really useful; that’s because it’s not possible to provide a {Binding} expression as the value of that property. To do that we’d have to turn the Command property to a dependency property – but we can’t because MarkupExtension does not derive from DependencyObject Sad smile

The solution I used was to declare a BindingCommandPath property (or via the constructor), that interprets the property path as a binding path (this is the most common scenario). A helper method digs into the property path and gets the actual value:

  1. static object ParsePropertyPath(object target, string path) {
  2.     var props = path.Split('.');
  3.     foreach(var prop in props) {
  4.         target = target.GetType().GetProperty(prop).GetValue(target);
  5.     }
  6.     return target;
  7. }

This little method does not support indexing, but that is not commonly used, and in any case wouldn’t be too hard to add.

Creating a markup extension

Writing a markup extension for events is fundamentally no different than writing it for properties. The first thing is to create a class deriving from MarkupExtension:

  1. public sealed class EventToCommandExtension : MarkupExtension {

The main thing to do is implement the abstract ProvideValue method. This should return something appropriate – in an event case, a delegate object appropriate for the event’s delegate type.

A command can receive a parameter – in this case it would probably want the sender and the RoutedEventArgs-derived type. For this a created a simple generic class:

  1. public sealed class EventCommandArgs<TEventArgs> where TEventArgs : RoutedEventArgs {
  2.     public TEventArgs EventArgs { get; private set; }
  3.     public object Sender { get; private set; }
  4.  
  5.     public EventCommandArgs(object sender, TEventArgs args) {
  6.         Sender = sender;
  7.         EventArgs = args;
  8.     }
  9. }

Now for the main event: overriding ProvideValue. Here we need to return an alternative (our own) handler, that will invoke the required command. First, let’s get the service that provides some context for the markup extension usage:

  1. public override object ProvideValue(IServiceProvider sp) {
  2.     var pvt = sp.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

Now we can examine the TargetProperty property to get the actual event object. Curiously enough, it’s sometimes an EventInfo (which is expected), but sometimes it’s a MethodInfo. So we need to deal with the difference and eventually connect to our own handler, called DoAction:

  1. if(pvt != null) {
  2.     var evt = pvt.TargetProperty as EventInfo;
  3.     var doAction = GetType().GetMethod("DoAction", BindingFlags.NonPublic | BindingFlags.Instance);
  4.     Type dlgType = null;
  5.     if(evt != null) {
  6.         dlgType = evt.EventHandlerType;
  7.     }
  8.     var mi = pvt.TargetProperty as MethodInfo;
  9.     if(mi != null) {
  10.         dlgType = mi.GetParameters()[1].ParameterType;
  11.     }
  12.     if(dlgType != null) {
  13.         _eventArgsType = dlgType.GetMethod("Invoke").GetParameters()[1].ParameterType;
  14.         return Delegate.CreateDelegate(dlgType, this, doAction);
  15.     }
  16. }

The trick is to create the exact delegate type (it’s RoutedEventHandler or something more specific – we need that specificity to make the command implementor itself simpler and type safe; more on that in a moment. That’s why we create the delegate dynamically with Delegate.CreateDelegate.

In our DoAction method, we need to invoke the connected command correctly:

  1. void DoAction(object sender, RoutedEventArgs e) {
  2.     var dc = (sender as FrameworkElement).DataContext;
  3.     if(BindingCommandPath != null) {
  4.         Command = (ICommand)ParsePropertyPath(dc, BindingCommandPath);
  5.     }
  6.     Type eventArgsType = typeof(EventCommandArgs<>).MakeGenericType(_eventArgsType);
  7.     var cmdParams = Activator.CreateInstance(eventArgsType, sender, e);
  8.     if(Command != null && Command.CanExecute(cmdParams))
  9.         Command.Execute(cmdParams);
  10. }

Again, because we don’t know in advance what kind of EventArgs are actually used by that particular event, we create the EventCommandArgs generic type dynamically, by first creaing the “open” type (with no specific generic parameter) and then calling MakeGenericType to make it a creatable “closed” type. Then we call ICommand.Execute if ICommand.CanExecute returns true.

Implementing the commands

The PaintingViewModel class implements the three commands shown above. Here’s the command to start painting:

  1. bool _isPainting;
  2. ICommand _startPaintCommand;
  3. public ICommand StartPaintCommand {
  4.     get {
  5.         return _startPaintCommand ?? (_startPaintCommand = new RelayCommand<EventCommandArgs<MouseButtonEventArgs>>(args => {
  6.             var element = args.Sender as IInputElement;
  7.             _lastPoint = args.EventArgs.GetPosition(element);
  8.             element.CaptureMouse();
  9.             _isPainting = true;
  10.         }));
  11.     }
  12. }

RelayCommand<> is the typical command used in MVVM scenarios (look at the source code). It’s sometimes called DelegateCommand (available in Prism, but they’re similar).

Here’s the command bound to a mouse move event:

  1. ICommand _addLineCommand;
  2. public ICommand AddLineCommand {
  3.     get {
  4.         return _addLineCommand ?? (_addLineCommand = new RelayCommand<EventCommandArgs<MouseEventArgs>>(args => {
  5.             var element = args.Sender as IInputElement;
  6.             var pt = args.EventArgs.GetPosition(element);
  7.             Model.AddLine(_lastPoint, pt);
  8.             _lastPoint = pt;
  9.         }, args => _isPainting
  10.         ));
  11.     }
  12. }

The Model property is of type Paining that holds a collection of LineInfo objects. Again, look at the source.

Here’s a view at runtime:

image

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>