UPDATE: WPF 4.5’s MarkupExtension : Invoke a method on the ViewModel / DataContext when an event is raised.

, , 8 Comments

We have seen in a previous post that WPF 4.5 enable the use of custom markup extensions to provide event handlers.

In this post we’ll see that we can execute a method on the DataContext of the control when an event is raised using this new ability of the MarkupExtension.

With some tiny modifications you can run it on Silverlight too !

How does it work ?

For a start, I suggest you to read my first post describing “markup extensions for events in WPF 4.5”.
The code used is based on the one presented in this previous post.

As already said, the DataContext may not be available when the MarkupExtension is used so it is not possible to register directly a Delegate on the ViewModel because we do not have it.

So the tip I use is to create a proxy handler on the markup extension itself. When this handler is called, I will retrieve the viewModel from the DataContext because it will then be set. From it I am able to get the targeted method trough reflection. Then I just have to execute the method if it truly exists.

The name of the method to call is passed in the constructor of the extension and is exposed as a property named ActionName.

The code !

It also can be found on my dropbox folder after registration.
[csharp]
public class Call : MarkupExtension
{
public string ActionName { get; set; }
public Call(string actionName){ ActionName = actionName; }

public override object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget targetProvider = serviceProvider
.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (targetProvider == null)
throw new InvalidOperationException(@"The CallAction extension
can’t retrieved the IProvideValueTarget service.");

targetProvider.TargetObject as FrameworkElement;
if (target == null)
throw new InvalidOperationException(@"The CallAction extension
can only be used on a FrameworkElement.");

var targetEventAddMethod = targetProvider.TargetProperty as MethodInfo;
if (targetEventAddMethod == null)
throw new InvalidOperationException(@"The CallAction extension
can only be used on a event.");

//Retrieve the handler of the event
ParameterInfo[] pars = targetEventAddMethod.GetParameters();
Type delegateType = pars[1].ParameterType;

//Retrieves the method info of the proxy handler
MethodInfo methodInfo = this.GetType().GetMethod("MyProxyHandler",
BindingFlags.NonPublic | BindingFlags.Instance);

//Create a delegate to the proxy handler on the markupExtension
Delegate returnedDelegate =
Delegate.CreateDelegate(delegateType, this, methodInfo);

return returnedDelegate;

}

void MyProxyHandler(object sender, EventArgs e)
{
FrameworkElement target = sender as FrameworkElement;
if (target == null) return;
var dataContext = target.DataContext;
if (dataContext == null) return;

//get the method on the datacontext from its name
MethodInfo methodInfo = dataContext.GetType()
.GetMethod(ActionName, BindingFlags.Public | BindingFlags.Instance);
methodInfo.Invoke(dataContext, null);
}
}
[/csharp]

How to use it ?

Use it is simplier thant to create it ! You simply have to use the markupExtension as you would declare a Binding in the XAML:
[xml]<Grid PreviewMouseDown="{custMarkup:Call MyMethodToCallOnTheViewModel}"
Background="Transparent"/>[/xml]

For the record, here is the method I created on my ViewModel :
[csharp]public void MyMethodToCallOnTheViewModel()
{
MessageBox.Show("oh no, you’ve got me …");
}
[/csharp]

This example leads to really funny things 🙂 :

Update !

As pointed out by ThomasX in a comment below, the returned target property is not always a MethodInfo but can be an EventInfo.
It seems to be a bug in this release (a connection is open for it) and I am pretty sure it will be corrected in the final release. By the time, I updated the code to work in all case. Here are only the revelant two parts.

First we check the type of the target property:
[csharp]Delegate returnedDelegate =null;

var targetEventAddMethod = targetProvider.TargetProperty as MethodInfo;
if (targetEventAddMethod != null)
returnedDelegate= CreateDelegateForMethodInfo(targetEventAddMethod);

var targetEventInfo = targetProvider.TargetProperty as EventInfo;
if (targetEventInfo != null)
returnedDelegate=CreateDelegateForEventInfo(targetEventInfo);[/csharp]

Then, here is the method to create a delegate in case the target property is of EventInfo type:
[csharp] private Delegate CreateDelegateForEventInfo(EventInfo targetEventInfo)
{

if (targetEventInfo == null)
throw new InvalidOperationException(
@"The CallAction extension can only be used on a event.");

Type delegateType = targetEventInfo.EventHandlerType;

//Retrieves the method info of the proxy handler
MethodInfo methodInfo = this.GetType().GetMethod("MyProxyHandler",
BindingFlags.NonPublic | BindingFlags.Instance);

//Create a delegate to the proxy handler on the markupExtension
Delegate returnedDelegate =
Delegate.CreateDelegate(delegateType, this, methodInfo);
return returnedDelegate;
}[/csharp]

How can it be improved ?

Here is the improvement I lacked the time to implement:

  • Check if the method on the ViewModel has the correct signature,
  • Pass the event’s arguments to the ViewModel method if it accepts it,
  • Cache the reflected method to save execution time,

As said in my previous post, I think that because of the Blend integration, the preferred way to do this kind of feature has to be the behavior/triggers.

 

8 Responses

  1. ThomasX

    23/10/2011 22 h 05 min

    Note the line var targetEventAddMethod = targetProvider.TargetProperty as MethodInfo;

    Are you sure it will always return a MethodInfo? I tried it myself. In case of the Click event targetProvider.TargetProperty is of type EventInfo. In the case of GotFocus it is *NULL*.

Comments are closed.