Tuesday, January 10, 2012

Wire any WPF Event to Command on ViewModel in MVVM

In the last post I explained how to write a behaviour to hook a commmand to an event. That works well but a new behaviour is needed to written for each event. I tried a generic approach to have a single behaviour that can be used to wire any event to a command. It comes witha little performance penalty as reflection is used to get the event and attached a delegate to that event. Below is the code:

using System.Windows.Input;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
using System.Reflection;
using System.Diagnostics;

public class CommandExecuter
{
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(CommandExecuter), new PropertyMetadata(CommandPropertyChangedCallback));

public static readonly DependencyProperty OnEventProperty = DependencyProperty.RegisterAttached("OnEvent", typeof(string), typeof(CommandExecuter));

public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached("CommandParameter", typeof(object), typeof(CommandExecuter));

public static void CommandPropertyChangedCallback(DependencyObject depObj, DependencyPropertyChangedEventArgs args)
{
  string onEvent = (string)depObj.GetValue(OnEventProperty);
  Debug.Assert(onEvent != null, "OnEvent must be set.");
  var eventInfo = depObj.GetType().GetEvent(onEvent);
  if (eventInfo != null)
  {
    var mInfo = typeof(CommandExecuter).GetMethod("OnRoutedEvent", BindingFlags.NonPublic | BindingFlags.Static);
    eventInfo.GetAddMethod().Invoke(depObj, new object[] { Delegate.CreateDelegate(eventInfo.EventHandlerType, mInfo) });
  }
  else
  {
    Debug.Fail(string.Format("{0} is not found on object {1}", onEvent, depObj.GetType()));

}

}
public static ICommand GetCommand(UIElement element)
{
   return (ICommand)element.GetValue(CommandProperty);
}
public static void SetCommand(UIElement element, ICommand command)
{
   element.SetValue(CommandProperty, command);
}
public static string GetOnEvent(UIElement element)
{
return (string)element.GetValue(OnEventProperty);
}
public static void SetOnEvent(UIElement element, string evnt)
{
   element.SetValue(OnEventProperty, evnt);
}
public static object GetCommandParameter(UIElement element)
{
  return (object)element.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(UIElement element, object commandParam)
{
   element.SetValue(CommandParameterProperty, commandParam);
}
private static void OnRoutedEvent(object sender, RoutedEventArgs e)
{
  UIElement element = (UIElement)sender;
  if (element != null)
  {    ICommand command = element.GetValue(CommandProperty) as ICommand;
    if (command != null && command.CanExecute(element.GetValue(CommandParameterProperty)))
    {
      command.Execute(element.GetValue(CommandParameterProperty));
    }
  }
}
}


Below code shows how to set the command on the controls for any event:   (local: is namespace prefix)
<ComboBox local:CommandExecuter.Command="{Binding CommandImpl}" local:CommandExecuter.OnEvent="SelectionChanged" ></ComboBox>

<Button local:CommandExecuter.Command="{Binding AnotherCommandImpl}" local:CommandExecuter.OnEvent="MouseEnter" local:CommandExecuter.CommandParameter="{x:Static null}"></Button>


11 comments:

  1. Great Stuff - Thx for publishing

    ReplyDelete
  2. After I've read it : perhaps it is a good idea the check ICommand.CanExecute before calling the Command - just to make it perfect ;-)

    private static void OnRoutedEvent(object sender, RoutedEventArgs e)
    {
    UIElement element = (UIElement)sender;
    if (element != null)
    {
    ICommand command = element.GetValue(CommandProperty) as ICommand;
    if (command != null)
    {
    if(command.CanExecute(CommandParameterProperty))
    command.Execute(element.GetValue(CommandParameterProperty));
    }
    }
    }

    ReplyDelete
  3. Glad that you liked it. Thanks for the suggestion, I have modified that code.

    ReplyDelete
    Replies
    1. Hi Naveen,
      Its a nice Stuff.Thanks for this.
      I just want to know how can I get the sender, e values as we normally get in eventhandlers ?

      Delete
  4. You are already getting those in below method. Do you want to pass them to commands?
    private static void OnRoutedEvent(object sender, RoutedEventArgs e)

    ReplyDelete
  5. So it won't work if the property "Command" is set before "OnEvent"?

    ReplyDelete
  6. what if you needed to bind several events to several commands on the same UI element?

    ReplyDelete
    Replies
    1. then you use this : https://github.com/ReedCopsey/FsXaml/blob/master/demos/WpfSimpleDrawingApplication/MainWindow.xaml

      Delete
  7. I tried this on a Window's Closing without success.
    It works for MouseEnter, but with Closing as the event it fails on the CreateDelegate line in the CommandChangedPropertyCallback function.

    The error: Unable to bind to target event.

    ReplyDelete