Keyboard events in a WPF MVVM application?
Asked Answered
B

8

59

How can I handle the Keyboard.KeyDown event without using code-behind? We are trying to use the MVVM pattern and avoid writing an event handler in code-behind file.

Boanerges answered 4/3, 2009 at 23:25 Comment(2)
To save searchers time reading the responses, the quick answer as of 4/2011 is that this is not possible without code behind. The reason is - KeyDown is an EventTrigger but Conditions are only supported on DataTriggers. This is easily solved with a simple converter, but this is not currently possible with just XAML unless you create an EventTrigger that sets a DataTrigger - but if you do that, then say "goodbye" to readability of your code.Aconite
The leading answer here applies to single key capture only (e.g., the "Enter" key). A more complete and uptodate answer is here: https://mcmap.net/q/330734/-bind-any-key-pressed-to-command-in-vm-wpf. (Uses EventTriggers from the Windows Interactivity library to respond to any KeyDown event.)Mihrab
M
8

A little late, but here goes.

Microsoft's WPF Team recently released an early version of their WPF MVVM Toolkit . In it, you'll find a class called CommandReference that can handle things like keybindings. Look at their WPF MVVM template to see how it works.

Machination answered 27/5, 2009 at 21:20 Comment(3)
I was just trying that yesterday, and I think it's the cleanest way to bind a key. Unfortunately, there is no way to bind all the keys at once (or I couldn't figure out how), and I need the whole keyboard, so you would have to create a binding for each key and then again all the keys when shift is pressed, and then again when Ctrl is pressed... It gets long. We opted for just having a KeyUp hander in the code behind that calls a method in the VM. Like 5 lines of code instead of all those bindings. And the VM is still unaware of the View. Thanks for the responseBoanerges
2020 Update. The WPF is dead and the posts from the link above are archived and placed here archive.codeplex.com/?p=wpfLimousine
Use better KeyBinding CommandKimball
W
254

To bring an updated answer, the .net 4.0 framework enables you to do this nicely by letting you bind a KeyBinding Command to a command in a viewmodel.

So... If you wanted to listen for the Enter key, you'd do something like this:

<TextBox AcceptsReturn="False">
    <TextBox.InputBindings>
        <KeyBinding 
            Key="Enter" 
            Command="{Binding SearchCommand}" 
            CommandParameter="{Binding Path=Text, RelativeSource={RelativeSource AncestorType={x:Type TextBox}}}" />
    </TextBox.InputBindings>
</TextBox>
Windburn answered 17/8, 2011 at 0:54 Comment(7)
This is definitely the proper method as of .net 4. Needs more upvotesInjection
This works with mouse clicks, too, BTW. Example: <MouseBinding MouseAction="LeftDoubleClick" Command="{Binding MyCommand}" />Bcd
Another note: This does not work with TextBlock, because TextBlock is not focusable. The InputBinding needs to be set on an object higher up in the hierarchy.Bcd
So as the OP was originally asking, how would you link to this to any text entered? Ie. OnKeyDown event?Supplejack
This consumes the input, is there anyway to make a command that just responds to the previewing of a key press so that the normal functionality that would occur with that key will still happen anyway? For example if I want to run a command when the Tab key is pressed on a certain control but I still want the default behavior of tab to function properly (moving focus to the next control in tab order). Using this key binding for the Tab key no longer moves focus when pressing Tab.Newsman
Needed to add UpdateSourceTrigger=PropertyChanged to my binding to get this to work. Thanks to this answer.Splenitis
For a working version, Key value should be Return, not EnterAmputee
N
33

WOW - there's like a thousand answers and here I'm going to add another one..

The really obvious thing in a 'why-didn't-I-realise-this-forehead-slap' kind of way is that the code-behind and the ViewModel sit in the same room so-to-speak, so there is no reason why they're not allowed to have a conversation.

If you think about it, the XAML is already intimately coupled to the ViewModel's API, so you might just as well go and make a dependency on it from the code behind.

The other obvious rules to obey or ignore still applies (interfaces, null checks <-- especially if you use Blend...)

I always make a property in the code-behind like this:

private ViewModelClass ViewModel { get { return DataContext as ViewModelClass; } }

This is the client-code. The null check is for helping control hosting as like in blend.

void someEventHandler(object sender, KeyDownEventArgs e)
{
    if (ViewModel == null) return;
    /* ... */
    ViewModel.HandleKeyDown(e);
}

Handle your event in the code behind like you want to (UI events are UI-centric so it's OK) and then have a method on the ViewModelClass that can respond to that event. The concerns are still seperated.

ViewModelClass
{
    public void HandleKeyDown(KeyEventArgs e) { /* ... */ }
}

All these other attached properties and voodoo is very cool and the techniques are really useful for some other things, but here you might get away with something simpler...

Novocaine answered 18/9, 2009 at 11:36 Comment(1)
Always grab a local copy of ViewModel instead of calling to the property each time... var vm = ViewModel;, from there use vm instead of ViewModel.Bender
M
8

I do this by using an attached behaviour with 3 dependency properties; one is the command to execute, one is the parameter to pass to the command and the other is the key which will cause the command to execute. Here's the code:

public static class CreateKeyDownCommandBinding
{
    /// <summary>
    /// Command to execute.
    /// </summary>
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.RegisterAttached("Command",
        typeof(CommandModelBase),
        typeof(CreateKeyDownCommandBinding),
        new PropertyMetadata(new PropertyChangedCallback(OnCommandInvalidated)));

    /// <summary>
    /// Parameter to be passed to the command.
    /// </summary>
    public static readonly DependencyProperty ParameterProperty =
        DependencyProperty.RegisterAttached("Parameter",
        typeof(object),
        typeof(CreateKeyDownCommandBinding),
        new PropertyMetadata(new PropertyChangedCallback(OnParameterInvalidated)));

    /// <summary>
    /// The key to be used as a trigger to execute the command.
    /// </summary>
    public static readonly DependencyProperty KeyProperty =
        DependencyProperty.RegisterAttached("Key",
        typeof(Key),
        typeof(CreateKeyDownCommandBinding));

    /// <summary>
    /// Get the command to execute.
    /// </summary>
    /// <param name="sender"></param>
    /// <returns></returns>
    public static CommandModelBase GetCommand(DependencyObject sender)
    {
        return (CommandModelBase)sender.GetValue(CommandProperty);
    }

    /// <summary>
    /// Set the command to execute.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="command"></param>
    public static void SetCommand(DependencyObject sender, CommandModelBase command)
    {
        sender.SetValue(CommandProperty, command);
    }

    /// <summary>
    /// Get the parameter to pass to the command.
    /// </summary>
    /// <param name="sender"></param>
    /// <returns></returns>
    public static object GetParameter(DependencyObject sender)
    {
        return sender.GetValue(ParameterProperty);
    }

    /// <summary>
    /// Set the parameter to pass to the command.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="parameter"></param>
    public static void SetParameter(DependencyObject sender, object parameter)
    {
        sender.SetValue(ParameterProperty, parameter);
    }

    /// <summary>
    /// Get the key to trigger the command.
    /// </summary>
    /// <param name="sender"></param>
    /// <returns></returns>
    public static Key GetKey(DependencyObject sender)
    {
        return (Key)sender.GetValue(KeyProperty);
    }

    /// <summary>
    /// Set the key which triggers the command.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="key"></param>
    public static void SetKey(DependencyObject sender, Key key)
    {
        sender.SetValue(KeyProperty, key);
    }

    /// <summary>
    /// When the command property is being set attach a listener for the
    /// key down event.  When the command is being unset (when the
    /// UIElement is unloaded for instance) remove the listener.
    /// </summary>
    /// <param name="dependencyObject"></param>
    /// <param name="e"></param>
    static void OnCommandInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        UIElement element = (UIElement)dependencyObject;
        if (e.OldValue == null && e.NewValue != null)
        {
            element.AddHandler(UIElement.KeyDownEvent,
                new KeyEventHandler(OnKeyDown), true);
        }

        if (e.OldValue != null && e.NewValue == null)
        {
            element.RemoveHandler(UIElement.KeyDownEvent,
                new KeyEventHandler(OnKeyDown));
        }
    }

    /// <summary>
    /// When the parameter property is set update the command binding to
    /// include it.
    /// </summary>
    /// <param name="dependencyObject"></param>
    /// <param name="e"></param>
    static void OnParameterInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        UIElement element = (UIElement)dependencyObject;
        element.CommandBindings.Clear();

        // Setup the binding
        CommandModelBase commandModel = e.NewValue as CommandModelBase;
        if (commandModel != null)
        {
            element.CommandBindings.Add(new CommandBinding(commandModel.Command,
            commandModel.OnExecute, commandModel.OnQueryEnabled));
        }
    }

    /// <summary>
    /// When the trigger key is pressed on the element, check whether
    /// the command should execute and then execute it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    static void OnKeyDown(object sender, KeyEventArgs e)
    {
        UIElement element = sender as UIElement;
        Key triggerKey = (Key)element.GetValue(KeyProperty);

        if (e.Key != triggerKey)
        {
            return;
        }

        CommandModelBase cmdModel = (CommandModelBase)element.GetValue(CommandProperty);
        object parameter = element.GetValue(ParameterProperty);
        if (cmdModel.CanExecute(parameter))
        {
            cmdModel.Execute(parameter);
        }
        e.Handled = true;
    }
}

To use this from xaml you can do something like this:

<TextBox framework:CreateKeyDownCommandBinding.Command="{Binding MyCommand}">
    <framework:CreateKeyDownCommandBinding.Key>Enter</framework:CreateKeyDownCommandBinding.Key>
</TextBox>

Edit: CommandModelBase is a base class I use for all commands. It's based on the CommandModel class from Dan Crevier's article on MVVM (here). Here's the source for the slightly modified version I use with CreateKeyDownCommandBinding:

public abstract class CommandModelBase : ICommand
    {
        RoutedCommand routedCommand_;

        /// <summary>
        /// Expose a command that can be bound to from XAML.
        /// </summary>
        public RoutedCommand Command
        {
            get { return routedCommand_; }
        }

        /// <summary>
        /// Initialise the command.
        /// </summary>
        public CommandModelBase()
        {
            routedCommand_ = new RoutedCommand();
        }

        /// <summary>
        /// Default implementation always allows the command to execute.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = CanExecute(e.Parameter);
            e.Handled = true;
        }

        /// <summary>
        /// Subclasses must provide the execution logic.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void OnExecute(object sender, ExecutedRoutedEventArgs e)
        {
            Execute(e.Parameter);
        }

        #region ICommand Members

        public virtual bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged;

        public abstract void Execute(object parameter);

        #endregion
    }

Comments and suggestions for improvements would be very welcome.

Mathematician answered 10/3, 2009 at 9:36 Comment(2)
This has been asked before, but where does CommandModelBase come from? I think I'm missing something...Herzl
I've updated the answer to include a description of CommandModelBase. I should have included it in the first place. Hope it helps!Mathematician
M
8

A little late, but here goes.

Microsoft's WPF Team recently released an early version of their WPF MVVM Toolkit . In it, you'll find a class called CommandReference that can handle things like keybindings. Look at their WPF MVVM template to see how it works.

Machination answered 27/5, 2009 at 21:20 Comment(3)
I was just trying that yesterday, and I think it's the cleanest way to bind a key. Unfortunately, there is no way to bind all the keys at once (or I couldn't figure out how), and I need the whole keyboard, so you would have to create a binding for each key and then again all the keys when shift is pressed, and then again when Ctrl is pressed... It gets long. We opted for just having a KeyUp hander in the code behind that calls a method in the VM. Like 5 lines of code instead of all those bindings. And the VM is still unaware of the View. Thanks for the responseBoanerges
2020 Update. The WPF is dead and the posts from the link above are archived and placed here archive.codeplex.com/?p=wpfLimousine
Use better KeyBinding CommandKimball
A
4

Similar to karlipoppins answer, but I found it didn't work without the following additions/changes:

<TextBox Text="{Binding UploadNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding FindUploadCommand}" />
    </TextBox.InputBindings>
</TextBox>
Airtoair answered 9/6, 2016 at 7:0 Comment(0)
M
3

I looked into that issue a few months ago, and I wrote a markup extension that does the trick. It can be used like a regular binding :

<Window.InputBindings>
    <KeyBinding Key="E" Modifiers="Control" Command="{input:CommandBinding EditCommand}"/>
</Window.InputBindings>

The full source code for this extension can be found here :

http://www.thomaslevesque.com/2009/03/17/wpf-using-inputbindings-with-the-mvvm-pattern/

Please be aware that this workaround is probably not very "clean", because it uses some private classes and fields through reflection...

Monoceros answered 6/5, 2009 at 23:11 Comment(0)
P
2

Short answer is you can't handle straight keyboard input events without code-behind, but you can handle InputBindings with MVVM (I can show you a relevant example if this is what you need).

Can you provide more information on what you want to do in the handler?

Code-behind isn't to be avoided entirely with MVVM. It's simply to be used for strictly UI-related tasks. A cardinal example would be having some type of 'data entry form' that, when loaded, needs to set focus to the first input element (text box, combobox, whatever). You would commonly assign that element an x:Name attribute, then hook up the Window/Page/UserControl's 'Loaded' event to set focus to that element. This is perfectly ok by the pattern because the task is UI-centric and has nothing to do with the data it represents.

Pentastyle answered 5/3, 2009 at 0:13 Comment(0)
Z
1

I know this question is very old, but I came by this because this type of functionality was just made easier to implement in Silverlight (5). So maybe others will come by here too.

I wrote this simple solution after I could not find what I was looking for. Turned out it was rather simple. It should work in both Silverlight 5 and WPF.

public class KeyToCommandExtension : IMarkupExtension<Delegate>
{
    public string Command { get; set; }
    public Key Key { get; set; }

    private void KeyEvent(object sender, KeyEventArgs e)
    {
        if (Key != Key.None && e.Key != Key) return;

        var target = (FrameworkElement)sender;

        if (target.DataContext == null) return;

        var property = target.DataContext.GetType().GetProperty(Command, BindingFlags.Public | BindingFlags.Instance, null, typeof(ICommand), new Type[0], null);

        if (property == null) return;

        var command = (ICommand)property.GetValue(target.DataContext, null);

        if (command != null && command.CanExecute(Key))
            command.Execute(Key);
    }

    public Delegate ProvideValue(IServiceProvider serviceProvider)
    {
        if (string.IsNullOrEmpty(Command))
            throw new InvalidOperationException("Command not set");

        var targetProvider = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));

        if (!(targetProvider.TargetObject is FrameworkElement))
            throw new InvalidOperationException("Target object must be FrameworkElement");

        if (!(targetProvider.TargetProperty is EventInfo))
            throw new InvalidOperationException("Target property must be event");

        return Delegate.CreateDelegate(typeof(KeyEventHandler), this, "KeyEvent");
    }

Usage:

<TextBox KeyUp="{MarkupExtensions:KeyToCommand Command=LoginCommand, Key=Enter}"/>

Notice that Command is a string and not an bindable ICommand. I know this is not as flexible, but it is cleaner when used, and what you need 99% of the time. Though it should not be a problem to change.

Zeppelin answered 1/9, 2011 at 13:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.