WPF MVVM 应用程序中的键盘事件?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/612966/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 20:05:44  来源:igfitidea点击:

Keyboard events in a WPF MVVM application?

wpfmvvm

提问by Carlos

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.

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

采纳答案by djcouchycouch

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.

微软的 WPF 团队最近发布了他们的WPF MVVM 工具包的早期版本 。在其中,您会找到一个名为 CommandReference 的类,它可以处理诸如键绑定之类的事情。查看他们的 WPF MVVM 模板以了解其工作原理。

回答by karlipoppins

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.

为了带来更新的答案,.net 4.0 框架使您能够通过将 KeyBinding 命令绑定到视图模型中的命令来很好地做到这一点。

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>

回答by Pieter Breed

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 ViewModelsit in the same room so-to-speak, so there is no reason why they're not allowed to have a conversation.

在“为什么我没有意识到这个额头一巴掌”这种方式中真正明显的事情是代码隐藏和ViewModel坐在同一个房间里可以这么说,所以没有理由为什么不允许他们交谈。

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.

如果您考虑一下,XAML 已经与 ViewModel 的 API 紧密耦合,因此您不妨从背后的代码中依赖它。

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

要遵守或忽略的其他明显规则仍然适用(接口、空检查 <-- 特别是如果您使用 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.

像您想要的那样在后面的代码中处理您的事件(UI 事件以 UI 为中心,所以没关系),然后在 ViewModelClass 上有一个可以响应该事件的方法。这些担忧仍然是分开的。

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...

所有这些其他附加属性和巫毒教都非常酷,而且这些技术对其他一些事情真的很有用,但在这里你可能会得到一些更简单的东西......

回答by Paul

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:

我通过使用具有 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;
    }
}

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

要从 xaml 使用它,您可以执行以下操作:

<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:

编辑: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
    }

Comments and suggestions for improvements would be very welcome.

欢迎提出改进意见和建议。

回答by Thomas Levesque

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/

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...

请注意,这种解决方法可能不是很“干净”,因为它通过反射使用了一些私有类和字段......

回答by Adrian

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

简短的回答是你不能在没有代码隐藏的情况下处理直接的键盘输入事件,但你可以使用 MVVM处理InputBindings(如果这是你需要的,我可以向你展示一个相关的例子)。

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.

使用 MVVM 不能完全避免代码隐藏。它仅用于与 UI 相关的任务。一个重要的例子是具有某种类型的“数据输入表单”,加载时需要将焦点设置为第一个输入元素(文本框、组合框等)。您通常会为该元素分配一个 x:Name 属性,然后连接 Window/Page/UserControl 的 'Loaded' 事件以将焦点设置到该元素。这完全符合模式,因为任务是以 UI 为中心的,与它所代表的数据无关。

回答by Jacob Poul Richardt

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.

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

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.

在找不到我要找的东西后,我写了这个简单的解决方案。原来这很简单。它应该适用于 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");
    }

Usage:

用法:

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

Notice that Commandis 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.

请注意,这Command是一个字符串而不是一个可绑定的ICommand。我知道这不是那么灵活,但它在使用时更干净,并且在 99% 的情况下都是您需要的。虽然改变应该不是问题。

回答by SurfingSanta

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

类似于 karlipoppins 的答案,但我发现如果没有以下添加/更改,它就不起作用:

<TextBox Text="{Binding UploadNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding FindUploadCommand}" />
    </TextBox.InputBindings>
</TextBox>