WPF MVVM应用中的键盘事件?

57

如何在不使用代码后台的情况下处理键盘.KeyDown事件?我们正在尝试使用MVVM模式,避免在代码后台文件中编写事件处理程序。


1
为了节省搜索者阅读响应的时间,截至2011年4月的快速答案是,没有代码支持是不可能实现的。原因是-KeyDown是一个EventTrigger,但Conditions仅支持DataTriggers。这可以通过简单的转换器轻松解决,但除非创建设置DataTrigger的EventTrigger,否则无法仅使用XAML实现-但如果这样做,那么就要对您的代码可读性说“再见”了。 - Jerry Nixon
这里的主要答案仅适用于单个按键捕获(例如,“Enter”键)。更完整和最新的答案在此处:https://dev59.com/jZnga4cB1Zd3GeqPSgZo#38433681。 (使用Windows Interactivity库中的EventTriggers来响应任何 KeyDown事件。) - kmote
8个回答

252

为了提供一个更新的答案,.net 4.0框架可以让你很好地将KeyBinding命令绑定到ViewModel中的一个命令上。

所以...如果你想监听Enter键,你需要做这样的事情:

<TextBox AcceptsReturn="False">
    <TextBox.InputBindings>
        <KeyBinding 
            Key="Enter" 
            Command="{Binding SearchCommand}" 
            CommandParameter="{Binding Path=Text, RelativeSource={RelativeSource AncestorType={x:Type TextBox}}}" />
    </TextBox.InputBindings>
</TextBox>

15
这绝对是.NET 4的正确方法。需要更多的赞同。 - Kyeotic
4
顺便说一下,这也适用于鼠标点击。例如:<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding MyCommand}" /> - Helge Klein
1
另外需要注意的是:这个方法不能用在TextBlock上,因为TextBlock无法获取焦点。InputBinding需要设置在层级更高的对象上。 - Helge Klein
4
如同原始提问者所询问的,你如何将此链接与任何输入的文本进行关联?例如,在OnKeyDown事件中? - GONeale
1
这会消耗输入,有没有办法创建一个命令,只响应按键的预览,以便该键的正常功能仍然会发生?例如,如果我想在某个控件上按下Tab键时运行一个命令,但我仍然希望Tab键的默认行为正常工作(将焦点移动到tab顺序中的下一个控件)。使用Tab键的此键绑定不再在按下Tab键时移动焦点。 - Nick
显示剩余2条评论

32

哇,这里竟有上千条回答,而我要再加一条……

一个非常显而易见的事情是,代码后台和ViewModel在同一个房间内,所以它们可以自由交流。

如果你思考一下,XAML已经与ViewModel的API密切耦合了,因此你可以在代码后台中对其进行依赖。

还有其他明显的规则需要遵守或忽视(如接口,空检查,尤其是使用Blend时...)

我总是在代码后台中创建一个像这样的属性:

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

这是客户端代码。空值检查是为了帮助像混合界面设计一样控制宿主。

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

在代码后端像你想要的那样处理事件(因为UI事件是以UI为中心的,所以这没问题),然后在ViewModelClass上有一个方法可以响应该事件。这样关注点仍然是分离的。

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

所有这些其他附加属性和巫术都非常酷,这些技术对于一些其他事情确实非常有用,但在这里你可能可以使用更简单的方法...


4
始终抓取ViewModel的本地副本,而不是每次调用属性... var vm = ViewModel;,从那里使用vm代替ViewModel - myermian

8

我使用了一个带有3个依赖属性的附加行为来实现这一点;其中一个是要执行的命令,另一个是要传递给命令的参数,还有一个是触发命令执行的关键字。以下是代码:

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;
    }
}

要在xaml中使用它,您可以这样做:
<TextBox framework:CreateKeyDownCommandBinding.Command="{Binding MyCommand}">
    <framework:CreateKeyDownCommandBinding.Key>Enter</framework:CreateKeyDownCommandBinding.Key>
</TextBox>

编辑:CommandModelBase是我用于所有命令的基类。它基于Dan Crevier在MVVM上的CommandModel类(这里)。这是我稍微修改后与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
    }

非常欢迎您提出评论和改进建议。


这个问题以前已经被问过了,但是CommandModelBase从哪里来呢?我觉得我漏掉了什么... - Darren Oster
我已经更新了答案,包括CommandModelBase的描述。我本应该在一开始就加上它。希望能有所帮助! - Paul

8
有点晚了,但还是来介绍一下。
微软的WPF团队最近发布了一个早期版本的WPF MVVM Toolkit。其中,你会发现一个叫做CommandReference的类,它可以处理诸如按键绑定之类的事情。看看他们的WPF MVVM模板,就能知道它是如何工作的。

2
昨天我刚试过,我认为这是绑定按键的最干净的方法。不幸的是,没有办法一次性绑定所有按键(或者我没找到怎么做),而我需要整个键盘,所以你必须为每个按键创建一个绑定,然后再为Shift按下时的所有按键创建一个绑定,再为Ctrl按下时的所有按键创建一个绑定... 这会变得很长。我们选择在代码后台中只有一个KeyUp处理程序,它调用VM中的一个方法。只需5行代码,而不是所有这些绑定。而且VM仍然不知道View的存在。谢谢回复。 - Carlos
2020年更新。WPF已经过时,上面链接的帖子已被归档并放置在此处https://archive.codeplex.com/?p=wpf - Dr. Freddy Dimethyltryptamine
使用更好的KeyBinding命令。 - Anton Nikolayevich

4
类似karlipoppins的回答,但我发现需要以下修改才能使其正常工作:
<TextBox Text="{Binding UploadNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding FindUploadCommand}" />
    </TextBox.InputBindings>
</TextBox>

3

我几个月前研究了这个问题,并编写了一个标记扩展来解决它。它可以像常规绑定一样使用:

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

此扩展的完整源代码可以在此处找到: http://www.thomaslevesque.com/2009/03/17/wpf-using-inputbindings-with-the-mvvm-pattern/ 请注意,这个解决方法可能不是很“干净”,因为它通过反射使用了一些私有的类和字段...

2
简短的回答是,你无法处理直接的键盘输入事件而不使用代码后台,但是你可以使用MVVM处理InputBindings(如果需要,我可以为你展示相关示例)。
你能否提供更多有关处理程序中想要做什么的信息?
在MVVM中并不完全避免使用代码后台。它只用于严格的UI相关任务。一个基本的例子是具有某种“数据输入表单”的情况,当加载时需要将焦点设置到第一个输入元素(文本框、组合框等)。你通常会给该元素分配一个x:Name属性,然后将窗口/页面/用户控件的“Loaded”事件与该元素设置焦点连接起来。这在模式中是完全可以的,因为该任务是UI中心的,与其所代表的数据无关。

1

我知道这个问题很老了,但是我来到这里是因为在Silverlight(5)中实现这种功能变得更加容易。所以也许其他人也会来到这里。

在找不到我想要的东西之后,我写了这个简单的解决方案。结果证明它相当简单。它应该适用于Silverlight 5和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");
    }

使用方法:

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

请注意,Command 是一个字符串而不是可绑定的 ICommand。我知道这样做可能不够灵活,但在使用时更加简洁,而且大多数情况下都是你所需要的。虽然更改也不应该成为问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接