[WPF] Using InputBindings with the MVVM pattern
If you develop WPF applications according to the Model-View-ViewModel pattern, you may have faced this issue : in XAML, how to bind a key or mouse gesture to a ViewModel command ? The obvious and intuitive approach would be this one :
<UserControl.InputBindings>
<KeyBinding Modifiers="Control" Key="E" Command="{Binding EditCommand}"/>
</UserControl.InputBindings>
Unfortunately, this code doesn’t work, for two reasons :
- The
Command
property is not a dependency property, so you cannot assign it through binding InputBinding
s are not part of the logical or visual tree of the control, so they don’t inherit theDataContext
A solution would be to create the InputBinding
s in the code-behind, but in the MVVM pattern we usually prefer to avoid this… I spent a long time looking for alternative solutions to do this in XAML, but most of them are quite complex and unintuitive. So I eventually came up with a markup extension that enables binding to ViewModel commands, anywhere in XAML, even for non-dependency properties or if the element doesn’t normally inherit the DataContext
This extension is used like a regular binding :
<UserControl.InputBindings>
<KeyBinding Modifiers="Control" Key="E" Command="{input:CommandBinding EditCommand}"/>
</UserControl.InputBindings>
(The input XML namespace is mapped to the CLR namespace where the markup extension is declared) In order to write this extension, I had to cheat a little… I used Reflector to find some private fields that would allow to retrieve the DataContext
of the root element. I then accessed those fields using reflection. Here is the code of the markup extension :
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;
namespace MVVMLib.Input
{
[MarkupExtensionReturnType(typeof(ICommand))]
public class CommandBindingExtension : MarkupExtension
{
public CommandBindingExtension()
{
}
public CommandBindingExtension(string commandName)
{
this.CommandName = commandName;
}
[ConstructorArgument("commandName")]
public string CommandName { get; set; }
private object targetObject;
private object targetProperty;
public override object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (provideValueTarget != null)
{
targetObject = provideValueTarget.TargetObject;
targetProperty = provideValueTarget.TargetProperty;
}
if (!string.IsNullOrEmpty(CommandName))
{
// The serviceProvider is actually a ProvideValueServiceProvider, which has a private field "_context" of type ParserContext
ParserContext parserContext = GetPrivateFieldValue<ParserContext>(serviceProvider, "_context");
if (parserContext != null)
{
// A ParserContext has a private field "_rootElement", which returns the root element of the XAML file
FrameworkElement rootElement = GetPrivateFieldValue<FrameworkElement>(parserContext, "_rootElement");
if (rootElement != null)
{
// Now we can retrieve the DataContext
object dataContext = rootElement.DataContext;
// The DataContext may not be set yet when the FrameworkElement is first created, and it may change afterwards,
// so we handle the DataContextChanged event to update the Command when needed
if (!dataContextChangeHandlerSet)
{
rootElement.DataContextChanged += new DependencyPropertyChangedEventHandler(rootElement_DataContextChanged);
dataContextChangeHandlerSet = true;
}
if (dataContext != null)
{
ICommand command = GetCommand(dataContext, CommandName);
if (command != null)
return command;
}
}
}
}
// The Command property of an InputBinding cannot be null, so we return a dummy extension instead
return DummyCommand.Instance;
}
private ICommand GetCommand(object dataContext, string commandName)
{
PropertyInfo prop = dataContext.GetType().GetProperty(commandName);
if (prop != null)
{
ICommand command = prop.GetValue(dataContext, null) as ICommand;
if (command != null)
return command;
}
return null;
}
private void AssignCommand(ICommand command)
{
if (targetObject != null && targetProperty != null)
{
if (targetProperty is DependencyProperty)
{
DependencyObject depObj = targetObject as DependencyObject;
DependencyProperty depProp = targetProperty as DependencyProperty;
depObj.SetValue(depProp, command);
}
else
{
PropertyInfo prop = targetProperty as PropertyInfo;
prop.SetValue(targetObject, command, null);
}
}
}
private bool dataContextChangeHandlerSet = false;
private void rootElement_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
FrameworkElement rootElement = sender as FrameworkElement;
if (rootElement != null)
{
object dataContext = rootElement.DataContext;
if (dataContext != null)
{
ICommand command = GetCommand(dataContext, CommandName);
if (command != null)
{
AssignCommand(command);
}
}
}
}
private T GetPrivateFieldValue<T>(object target, string fieldName)
{
FieldInfo field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field != null)
{
return (T)field.GetValue(target);
}
return default(T);
}
// A dummy command that does nothing...
private class DummyCommand : ICommand
{
#region Singleton pattern
private DummyCommand()
{
}
private static DummyCommand _instance = null;
public static DummyCommand Instance
{
get
{
if (_instance == null)
{
_instance = new DummyCommand();
}
return _instance;
}
}
#endregion
#region ICommand Members
public bool CanExecute(object parameter)
{
return false;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
}
#endregion
}
}
}
However this solution has a limitation : it works only for the DataContext
of the XAML root. So you can’t use it, for instance, to define an InputBinding on a control whose DataContext
is also redefined, because the markup extension will access the root DataContext
. It shouldn’t be a problem in most cases, but you need to be aware of that…