[WPF] Using InputBindings with the MVVM pattern

Very poorPoorAverageGoodExcellent (6 votes) 
Loading ... Loading ...

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 :

  1. The Command property is not a dependency property, so you cannot assign it through binding
  2. InputBindings are not part of the logical or visual tree of the control, so they don’t inherit the DataContext

A solution would be to create the InputBindings 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…

28 Comments

  1. Impeccable tout ça :)
    Faudra surveiller de prêt les classes liées aux InputBinding pour voir si y’a des changement avec .Net 4 !

  2. C’est clair… MVVM a l’air de se répandre comme une trainée de poudre, donc je pense que Microsoft aura a cœur de combler ce genre de lacunes ;)

  3. Thank you, this will be very helpful. Hopefully in the future Microsoft will make workarounds like this unnecessary.

    Merci beaucoup.

  4. Rick Strahl says:

    Thomas, thanks for this. The private reflection part to get to the root element is helpful.

    Actually you can also get the InheritanceParent on the _targetObject for a single Reflection call.

    Unfortunately this still fails in the designer – apparently the designer blows up when trying to do private Reflection.

    • Thanks Rick for pointing out the InheritanceParent property. However it can’t be used in the context of this post, because InputBindings are not DependencyObjects (but it might be a good idea for other scenarios)
      By the way, when working with DependencyObjects it should be quite easy to find the root element without using private reflection. Private reflection is not a very reliable method, since the private implementation can change between versions (and actually it did : the code above doesn’t work in WPF 4, I’ll have to update it)

  5. Vaishnavi says:

    Thomas I tried to use your code for a search box which acts on a treeview in wpf MVVM mode. Like visual studio/regedit’s functionality i tried to hook F3 key to next search(Enter button of type Simple command works). I hooked a ICommand like your EditCommand. but F3 does not fire on Button input bindings. Let me know where am I going wrong


    public ICommand ExitCommand
    {
    get
    {
    if (exitCommand == null)
    {
    exitCommand = new DelegateCommand(Exit);
    exitCommand.GestureKey = Key.F3;
    // exitCommand.GestureModifier = ModifierKeys.Control;
    // exitCommand.MouseGesture = MouseAction.LeftDoubleClick;
    }
    return exitCommand;
    }
    }

    private void Exit()
    {

    }

  6. Vaishnavi says:

    missed this

    private DelegateCommand exitCommand;

  7. Hi Vaishnavi,

    The mapping between the gesture (F3) and the command should be defined in XAML, not in the definition of the command, because the ExitCommand property won’t be called unless it is bound to something… Could you post your XAML ?

  8. Vaishnavi says:

    XAML:

    Code
    goCommand = new SimpleCommand(this.OnGoClick, this.CanGoClick, Go, Go);

    private void OnGoClick(object param)
    {
    //do soemthing
    }

    public bool CanGoClick(object param)
    {
    if (!string.IsNullOrEmpty(_searchCriteria) && _searchCriteria.Length > 2 )
    return true;
    else
    return false;
    }

    Let me know where am I going wrong

  9. Hi Vaishnavi,
    To post XAML you need to escape the < and > signs. You can use this site to format it automatically (choose the “html/xml/aspx” style). Post the resulting HTML between <code> tags.

  10. Vaishnavi says:


    </UserControl.CommandBindings>
    <UserControl.InputBindings>
    <KeyBinding Modifiers="Control" Key="F3" Command="{input:CommandBinding GoCommand}"/>
    </UserControl.InputBindings>

    <Button Name="BtnGo" HorizontalAlignment="Right" Height="20" Width="20" Content="{Binding GoCommand.Caption}" Command="{Binding Path=GoCommand}" IsDefault="True" Margin="7,2,2,2">
    </Button>

    Declaration of Go command
    goCommand = new SimpleCommand(this.OnGoClick, this.CanGoClick, "Go", "Go");

    • Hi Vaishnavi,

      I edited your message so that the code is properly displayed.

      It seems you’re doing it right, so the problem must be somewhere else… are you sure that your user control has the keyboard focus ? You can give the focus to your user control with the Keyboard.Focus method.

  11. Jack Ukleja says:

    I believe this is fixed in .NET 4.. you can use Binding for the Command…

  12. Vaishnavi says:

    It works now.
    But I get “No constructor for type ‘CommandBindingExtension’ has 1 parameters” error all the time but the application runs without any hurdle.

    Following are the changes I have made.


    private DelegateCommand nextSearchCommand;

    public ICommand NextSearchCommand
    {
    get
    {
    if (nextSearchCommand == null)
    {
    nextSearchCommand = new DelegateCommand(NextSearch);
    }
    return nextSearchCommand;
    }
    }

    private void NextSearch()
    {
    //Do something
    }

    Thanks a ton.

    • Yes, the error “No constructor for type …” is a known bug of the designer. Note that this error happens only when the markup extension is declared in the same assembly. If you put it in a different assembly, it works fine.

  13. Vaishnavi says:

    XAML code

    <TextBox TextWrapping="Wrap" x:Name="searchTextBox" HorizontalAlignment="Right" Width="200" MaxLength="15" Style="{DynamicResource Control_WaterMarkTextBox}" Focusable="True" Tag="{Binding SearchTag}" Text="{Binding Path=SearchCriteria, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center" IsReadOnly="{Binding CheckConfigValue}">
    <TextBox.InputBindings>
    <KeyBinding Modifiers="Control" Key="F3" Command="{input:CommandBinding NextSearchCommand}"/>

    </TextBox.InputBindings>
    </TextBox>

  14. Matt says:

    Works like a charm. Thanks for the help.

  15. nilesh says:

    Hi,

    Can I try this in code-behind..
    like following..
    the main moto is to overcome the limitation which you mentioned..

    //
    //

    KeyBinding k = new KeyBinding();
    // k.Command = (ICommand)new CommandBindingExtension(“SelectedWorkSpace.SaveUserCommand”);
    k.Command = (ICommand)new CommandBindingExtension(“ExitCommand”);

    k.Modifiers = ModifierKeys.Control;
    k.Key = Key.S;

    this.InputBindings.Add(k);

    • No, it won’t work. CommandBindingExtension doesn’t implement ICommand, so the cast will fail. Also, the XAML context is necessary for the extension to work.

      But anyway, why would you want to do that ? You can directly access the command on the ViewModel:

      MyViewModel vm = this.DataContext as MyViewModel;
      KeyBinding k = new KeyBinding();
      k.Command = vm.ExitCommand;
      k.Modifiers = ModifierKeys.Control;
      k.Key = Key.S;
      this.InputBindings.Add(k);
      
  16. C# says:

    Hello ,

    How to bind a custom short cut key to menu item for example if we have clear menu item , then the corresponding shortcut key has to be ctrl+E ?

    Thanking you.

  17. Buddhi says:

    hi friends,

    but not fire the escape.
    when i am click on a control in a window and click Escape its working properly have any help for my problem.

    • I don’t understand what you’re asking…

      • Buddhi says:

        sorry Thomas
        my post not posted properly its missing some part.

        When i am click escape after loading a view its not fire.
        after loading view i am changing some controller values and click “Escape” its fire properly,
        but i need to fire it with out control value changing.
        i’m using Usercontroll- inputbinding inside
        Key=”Escape”
        Command=”{local:CommandBinding BackCommand}”

        Thank you,

        • Hi Buddhi,
          I’ve seen this before, but it’s not related to the CommandBinding markup extension, it’s just a focus issue. The usercontrol doesn’t have the focus, so it can’t receive input.

2 Trackbacks

Leave a comment

css.php