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

时间:2009-03-04 23:25:59

标签: wpf mvvm

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

8 个答案:

答案 0 :(得分:204)

为了获得更新的答案,.net 4.0框架使您可以通过将KeyBinding命令绑定到视图模型中的命令来很好地完成此操作。

所以......如果你想听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>

答案 1 :(得分:28)

哇 - 有一千个答案,在这里我要再加一个......

“为什么 - 我没有意识到 - 这个 - 前额 - 一巴掌”的方式中真正明显的一点是代码隐藏和ViewModel坐在同一个房间里 - 说,所以他们没有理由不允许他们进行对话。

如果你考虑一下,XAML已经与ViewModel的API密切相关,所以你也可以从后面的代码中依赖它。

服从或忽略的其他明显规则仍然适用(接口,空检查&lt; - 特别是如果你使用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) { /* ... */ }
}

所有这些其他附加属性和伏都教非常酷,这些技术对其他一些东西非常有用,但是在这里你可能会有更简单的东西......

答案 2 :(得分: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(here)的文章中的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
    }

非常欢迎有关改进的意见和建议。

答案 3 :(得分:8)

有点晚了,但到了。

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

答案 4 :(得分: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/

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

答案 5 :(得分:2)

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

您能否提供有关您希望在处理程序中执行的操作的更多信息?

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

答案 6 :(得分: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。我知道这不是那么灵活,但使用时更清洁,99%的时间你需要什么。虽然改变不应该是一个问题。

答案 7 :(得分:0)

类似于karlipoppins的回答,但我发现如果没有以下添加/更改,它就无法运作:

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