События клавиатуры в приложении WPF MVVM? - PullRequest
44 голосов
/ 05 марта 2009

Как я могу обработать событие Keyboard.KeyDown без использования выделенного кода? Мы пытаемся использовать шаблон MVVM и избегать записи обработчика событий в файл с выделенным кодом.

Ответы [ 8 ]

204 голосов
/ 17 августа 2011

Чтобы получить обновленный ответ, платформа .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>
28 голосов
/ 18 сентября 2009

ВАУ - там как тысяча ответов, и здесь я собираюсь добавить еще один ..

Действительно очевидной вещью в духе «почему-я-не-осознал-это-пощечина» является то, что кодовый сзади и ViewModel сидят в одной комнате, так сказать Таким образом, нет никаких причин, почему им не разрешают разговаривать.

Если вы подумаете об этом, XAML уже тесно связан с API ViewModel, так что вы могли бы также пойти и сделать зависимость от него из кода.

Другие очевидные правила, которым нужно подчиняться или игнорировать, по-прежнему применяются (интерфейсы, нулевые проверки <- особенно если вы используете Blend ...) </p>

Я всегда делаю свойство в коде позади так:

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

Это код клиента. Нулевая проверка предназначена для управления хостингом, как в blend.

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

Обработайте ваше событие в коде так, как вы хотите (события пользовательского интерфейса ориентированы на пользовательский интерфейс, поэтому все в порядке), а затем создайте метод в ViewModelClass, который может ответить на это событие. Проблемы по-прежнему разделены.

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

Все эти другие прикрепленные свойства и вуду очень крутые, и техники действительно полезны для некоторых других вещей, но здесь вам может сойтись с чем-то более простым ...

8 голосов
/ 28 мая 2009

Немного поздно, но здесь идет.

Команда Microsoft WPF недавно выпустила раннюю версию WPF MVVM Toolkit , В нем вы найдете класс CommandReference, который может обрабатывать такие вещи, как привязки клавиш. Посмотрите на их шаблон WPF MVVM, чтобы увидеть, как он работает.

8 голосов
/ 10 марта 2009

Я делаю это, используя прикрепленное поведение с 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 - это базовый класс, который я использую для всех команд. Он основан на классе CommandModel из статьи Дэна Кревье о MVVM ( здесь ). Вот источник слегка измененной версии, которую я использую с 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 голосов
/ 07 мая 2009

Я изучил эту проблему несколько месяцев назад и написал расширение для разметки, которое помогает. Может использоваться как обычная привязка:

<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 голосов
/ 05 марта 2009

Краткий ответ: вы не можете обрабатывать события прямого ввода с клавиатуры без кода, но вы можете обрабатывать InputBindings с MVVM (я могу показать вам соответствующий пример, если это то, что вам нужно).

Можете ли вы предоставить больше информации о том, что вы хотите сделать в обработчике?

С MVVM не следует полностью избегать использования кода. Это просто для использования в строго связанных с пользовательским интерфейсом задачах. Кардинальный пример будет иметь некоторый тип «формы ввода данных», который при загрузке должен установить фокус на первый элемент ввода (текстовое поле, поле со списком, что угодно). Вы обычно назначаете этому элементу атрибут x: Name, а затем подключаете событие Window / Page / UserControl 'Loaded', чтобы установить фокус на этот элемент. Это совершенно нормально для шаблона, поскольку задача ориентирована на пользовательский интерфейс и не имеет ничего общего с данными, которые она представляет.

1 голос
/ 01 сентября 2011

Я знаю, что этот вопрос очень старый, но я пришел по нему, потому что этот тип функциональности был просто реализован в 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% случаев. Хотя это не должно быть проблемой, чтобы измениться.

0 голосов
/ 09 июня 2016

Аналогично ответу karlipoppins, но я обнаружил, что он не работает без следующих дополнений / изменений:

<TextBox Text="{Binding UploadNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding FindUploadCommand}" />
    </TextBox.InputBindings>
</TextBox>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...