web-dev-qa-db-de.com

Tastaturereignisse in einer WPF-MVVM-Anwendung?

Wie kann ich mit dem Keyboard.KeyDown-Ereignis umgehen, ohne Code-Behind zu verwenden? Wir versuchen, das MVVM-Muster zu verwenden und zu vermeiden, einen Ereignishandler in eine Code-Behind-Datei zu schreiben.

44
Carlos

Ein bisschen spät, aber hier geht es.

Das WPF-Team von Microsoft hat kürzlich eine frühe Version des WPF MVVM Toolkit . veröffentlicht. Darin finden Sie eine Klasse namens CommandReference, die beispielsweise Schlüsselbindungen verarbeiten kann. Sehen Sie sich die WPF-MVVM-Vorlage an, um zu sehen, wie es funktioniert.

8
djcouchycouch

Um eine aktualisierte Antwort zu erhalten, können Sie dies mit dem .NET 4.0-Framework gut tun, indem Sie einen KeyBinding-Befehl an einen Befehl in einem Viewmodel binden können.

Also ... Wenn Sie auf die Eingabetaste warten wollten, würden Sie Folgendes tun:

<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 - es gibt wie tausend Antworten und hier werde ich noch eine hinzufügen ...

Die wirklich offensichtliche Sache in einer "warum-nicht-mir-realisierbaren-diese-Stirn-Ohrfeige" Art und Weise ist, dass der Code hinter und die ViewModel im selben Raum sitzen, sozusagen, Es gibt also keinen Grund, warum sie kein Gespräch führen dürfen. 

Wenn Sie darüber nachdenken, ist die XAML bereits eng mit der ViewModel-API gekoppelt, sodass Sie genauso gut von dem dahinterliegenden Code abhängig sind. 

Die anderen offensichtlichen Regeln, die eingehalten oder ignoriert werden sollen, gelten weiterhin (Schnittstellen, Nullprüfungen <- insbesondere wenn Sie Blend verwenden ...).

Ich mache immer eine Eigenschaft im Code-Behind wie folgt:

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

Dies ist der Client-Code. Die Nullprüfung dient dazu, das Hosting wie in einer Mischung zu kontrollieren.

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

Behandeln Sie Ihr Ereignis im Code dahinter wie gewünscht (UI-Ereignisse sind auf die Benutzeroberfläche zentriert, daher ist es in Ordnung), und verfügen Sie dann über eine Methode in ViewModelClass, die auf dieses Ereignis reagieren kann. Die Bedenken sind immer noch getrennt.

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

All diese anderen Eigenschaften und das Voodoo sind sehr cool und die Techniken sind wirklich nützlich für einige andere Dinge, aber hier kann es sein, dass Sie mit etwas Einfacherem davonkommen ...

28
Pieter Breed

Ich mache dies, indem ich ein angefügtes Verhalten mit 3 Abhängigkeitseigenschaften verwende. Einer ist der Befehl, der ausgeführt werden soll, einer ist der Parameter, der an den Befehl übergeben wird, und der andere ist der Schlüssel, der die Ausführung des Befehls bewirkt. Hier ist der 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;
    }
}

Um dies von xaml aus zu verwenden, können Sie so etwas tun:

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

Edit: CommandModelBase ist eine Basisklasse, die ich für alle Befehle verwende. Es basiert auf der CommandModel-Klasse aus Dan Creviers Artikel über MVVM ( hier ). Hier ist die Quelle für die leicht modifizierte Version, die ich mit CreateKeyDownCommandBinding verwende:

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
    }

Kommentare und Verbesserungsvorschläge wären sehr willkommen.

8
Paul

Eine kurze Antwort ist, dass Sie keine direkten Tastatureingabeereignisse ohne Code-Behind behandeln können. Sie können jedoch InputBindings mit MVVM behandeln (ich kann Ihnen ein relevantes Beispiel zeigen, wenn Sie dies benötigen).

Können Sie weitere Informationen dazu bereitstellen, was Sie im Handler tun möchten?

Code-Behind ist mit MVVM nicht gänzlich zu vermeiden. Es ist einfach für streng UI-bezogene Aufgaben zu verwenden. Ein Kardinalbeispiel wäre beispielsweise eine Art "Dateneingabeformular", das beim Laden den Fokus auf das erste Eingabeelement (Textfeld, Kombinationsfeld oder was auch immer) legen muss. Normalerweise weisen Sie diesem Element ein x: Name-Attribut zu und verknüpfen dann das 'Loaded' -Ereignis von Window/Page/UserControl, um den Fokus auf dieses Element zu setzen. Dies ist aufgrund des Musters vollkommen in Ordnung, da die Aufgabe UI-zentriert ist und nichts mit den Daten zu tun hat, die sie repräsentiert.

2
Adrian

Ich habe mir vor ein paar Monaten mit diesem Thema beschäftigt und eine Markup-Erweiterung geschrieben, die den Trick beherrscht. Es kann wie eine reguläre Bindung verwendet werden:

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

Den vollständigen Quellcode für diese Erweiterung finden Sie hier:

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

Bitte beachten Sie, dass diese Problemumgehung wahrscheinlich nicht sehr "sauber" ist, da einige private Klassen und Felder durch Reflektion verwendet werden.

2
Thomas Levesque

Ich weiß, dass diese Frage sehr alt ist, aber ich bin dazu gekommen, weil diese Art der Funktionalität in Silverlight (5) einfach zu implementieren war. Vielleicht kommen auch andere hier vorbei.

Ich habe diese einfache Lösung geschrieben, nachdem ich nicht gefunden habe, wonach ich gesucht habe. Es stellte sich heraus, dass es ziemlich einfach war. Es sollte sowohl in Silverlight 5 als auch in WPF funktionieren.

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

Verwendungszweck:

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

Beachten Sie, dass Command eine Zeichenfolge und keine bindbare ICommand ist. Ich weiß, dass dies nicht so flexibel ist, aber es ist sauberer, wenn Sie es verwenden, und Sie benötigen 99% der Zeit. Das Ändern sollte jedoch kein Problem sein.

1

Ähnlich wie bei karlipoppins Antwort, aber ich fand es nicht ohne die folgenden Ergänzungen/Änderungen:

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