web-dev-qa-db-fra.com

Événements de clavier dans une application WPF MVVM?

Comment gérer l'événement Keyboard.KeyDown sans utiliser code-behind? Nous essayons d'utiliser le modèle MVVM et d'éviter d'écrire un gestionnaire d'événements dans un fichier code-behind.

44
Carlos

Un peu tard, mais voilà.

L’équipe WPF de Microsoft a récemment publié une première version de leur WPF MVVM Toolkit .. Vous y trouverez une classe appelée CommandReference qui peut gérer des choses telles que les combinaisons de touches. Regardez leur modèle WPF MVVM pour voir comment cela fonctionne.

8
djcouchycouch

Pour apporter une réponse mise à jour, le framework .net 4.0 vous permet de le faire facilement en vous permettant de lier une commande KeyBinding à une commande d'un modèle de vue.

Alors ... Si vous vouliez écouter la touche Entrée, vous feriez quelque chose comme ceci:

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

WOW - il y a mille réponses et je vais en ajouter une autre ..

La chose vraiment évidente dans un genre de "pourquoi-je-ne-suis-pas-je-réalise-ce-front-slap" est que le code derrière et le ViewModel sont assis dans la même pièce pour ainsi dire, donc raison pour laquelle ils ne sont pas autorisés à avoir une conversation. 

Si vous y réfléchissez, le XAML est déjà intimement couplé à l'API de ViewModel, vous pouvez donc aussi bien en faire une dépendance à partir du code qui se trouve derrière. 

Les autres règles évidentes à obéir ou à ignorer s'appliquent toujours (interfaces, contrôles nuls <- surtout si vous utilisez Blend ...)

Je fais toujours une propriété dans le code-behind comme ceci:

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

C'est le code client. Le contrôle nul sert à aider à contrôler l’hébergement comme dans un mélange.

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

Traitez votre événement dans le code derrière comme vous le souhaitez (les événements de l'interface utilisateur sont centrés sur l'interface utilisateur, donc c'est OK), puis utilisez une méthode sur ViewModelClass qui peut répondre à cet événement. Les préoccupations sont encore séparées.

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

Toutes ces autres propriétés attachées et vaudou sont très cool et les techniques sont vraiment utiles pour d'autres choses, mais ici vous pourriez vous en tirer avec quelque chose de plus simple ...

28
Pieter Breed

Je le fais en utilisant un comportement attaché avec 3 propriétés de dépendance; l'un est la commande à exécuter, l'un est le paramètre à transmettre à la commande et l'autre est la clé qui fera exécuter la commande. Voici le code:

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

Pour utiliser ceci depuis xaml, vous pouvez faire quelque chose comme ceci:

<TextBox framework:CreateKeyDownCommandBinding.Command="{Binding MyCommand}">
    <framework:CreateKeyDownCommandBinding.Key>Enter</framework:CreateKeyDownCommandBinding.Key>
</TextBox>

Edit: CommandModelBase est une classe de base que j'utilise pour toutes les commandes. Il est basé sur la classe CommandModel de l'article de Dan Crevier sur MVVM ( here ). Voici le code source de la version légèrement modifiée que j'utilise avec 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
    }

Des commentaires et des suggestions d’améliorations seraient les bienvenus.

8
Paul

La réponse courte est que vous ne pouvez pas gérer les événements d'entrée au clavier sans code-derrière, mais vous pouvez gérer InputBindings avec MVVM (je peux vous montrer un exemple pertinent si vous en avez besoin).

Pouvez-vous fournir plus d'informations sur ce que vous voulez faire dans le gestionnaire?

Code-behind ne doit pas être entièrement évité avec MVVM. Il doit simplement être utilisé pour des tâches strictement liées à l'interface utilisateur. Un exemple cardinal serait d'avoir un type de «formulaire de saisie de données» qui, une fois chargé, doit définir le focus sur le premier élément d'entrée (zone de texte, liste déroulante, etc.). Vous attribuez généralement à cet élément un attribut x: Name, puis connectez l'événement 'Loaded' de Window/Page/UserControl pour définir le focus sur cet élément. Cela convient parfaitement au modèle, car la tâche est centrée sur l'interface utilisateur et n'a rien à voir avec les données qu'elle représente.

2
Adrian

J'ai examiné cette question il y a quelques mois et j'ai écrit une extension de balisage qui fait l'affaire. Il peut être utilisé comme une reliure normale:

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

Le code source complet de cette extension est disponible ici:

http://www.thomaslevesque.com/2009/03/17/wpf-using-inputbindings-with-the-mvvm-pattern/

Sachez que cette solution de contournement n’est probablement pas très "propre", car elle utilise des classes et des champs privés par réflexion ...

2
Thomas Levesque

Je sais que cette question est très ancienne, mais j’en suis venu à cela, car ce type de fonctionnalité a été rendu plus facile à implémenter dans Silverlight (5). Alors peut-être que d'autres vont venir ici aussi.

J'ai écrit cette solution simple après je ne pouvais pas trouver ce que je cherchais. Il s'est avéré que c'était plutôt simple. Cela devrait fonctionner à la fois dans Silverlight 5 et dans 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}"/>

Notez que Command est une chaîne et non une bindable ICommand. Je sais que ce n’est pas aussi flexible, mais c’est plus propre lorsqu’il est utilisé et ce dont vous avez besoin 99% du temps. Bien que cela ne devrait pas être un problème pour changer.

1

Semblable à karlipoppins répondre, mais j'ai trouvé que cela ne fonctionnait pas sans les ajouts/modifications suivants:

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