События клавиатуры в приложении WPF MVVM?

StackOverflow https://stackoverflow.com/questions/612966

  •  03-07-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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

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

Я всегда делаю свойство в выделенном коде следующим образом:

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) { /* ... */ }
}

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

Я делаю это, используя прикрепленное поведение с 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>

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

Комментарии и предложения по улучшению будут приветствоваться.

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

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

Имейте в виду, что этот обходной путь, вероятно, не очень " clean " потому что он использует некоторые частные классы и поля через отражение ...

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

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

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

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

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

<TextBox Text="{Binding UploadNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding FindUploadCommand}" />
    </TextBox.InputBindings>
</TextBox>
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top