Pregunta

¿Cómo puedo manejar el evento Keyboard.KeyDown sin usar código subyacente? Estamos tratando de usar el patrón MVVM y evitar escribir un controlador de eventos en un archivo de código subyacente.

¿Fue útil?

Solución

Un poco tarde, pero aquí va.

El equipo WPF de Microsoft lanzó recientemente una versión inicial de su WPF MVVM Kit de herramientas . En él, encontrará una clase llamada CommandReference que puede manejar cosas como las combinaciones de teclas. Mire su plantilla MVPM de WPF para ver cómo funciona.

Otros consejos

Para brindar una respuesta actualizada, el marco .net 4.0 le permite hacer esto de manera agradable al permitirle vincular un comando KeyBinding a un comando en un modelo de vista.

Entonces ... Si quisieras escuchar la tecla Enter, harías algo como esto:

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

WOW - hay como mil respuestas y aquí voy a agregar otra ...

Lo realmente obvio en una forma de 'por qué no me di cuenta de esta bofetada' es que el código subyacente y el ViewModel se sientan en la misma habitación, por así decirlo , por lo que no hay ninguna razón por la que no se les permita tener una conversación.

Si lo piensas bien, el XAML ya está íntimamente acoplado a la API de ViewModel, por lo que deberías ir y depender del código que se encuentra detrás.

Las otras reglas obvias para obedecer o ignorar aún se aplican (interfaces, comprobaciones nulas < - especialmente si usa Blend ...)

Siempre hago una propiedad en el código subyacente como esta:

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

Este es el código del cliente. La comprobación nula es para ayudar a controlar el alojamiento como en una mezcla.

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

Maneje su evento en el código detrás como lo desee (los eventos de la interfaz de usuario están centrados en la interfaz de usuario, por lo que está bien) y luego tenga un método en ViewModelClass que pueda responder a ese evento. Las preocupaciones aún están separadas.

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

Todas estas otras propiedades adjuntas y el vudú son muy interesantes y las técnicas son realmente útiles para algunas otras cosas, pero aquí podría salirse con la suya con algo más simple ...

Hago esto usando un comportamiento adjunto con 3 propiedades de dependencia; uno es el comando a ejecutar, uno es el parámetro para pasar al comando y el otro es la clave que hará que el comando se ejecute. Aquí está el código:

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

Para usar esto desde xaml puedes hacer algo como esto:

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

Editar: CommandModelBase es una clase base que uso para todos los comandos. Se basa en la clase CommandModel del artículo de Dan Crevier sobre MVVM ( aquí ). Aquí está la fuente de la versión ligeramente modificada que uso con 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
    }

Comentarios y sugerencias para mejoras serían bienvenidos.

Investigué ese problema hace unos meses y escribí una extensión de marcado que hace el truco. Se puede usar como un enlace regular:

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

El código fuente completo de esta extensión se puede encontrar aquí:

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

Tenga en cuenta que esta solución probablemente no sea muy " clean " ;, porque utiliza algunas clases y campos privados a través de la reflexión ...

La respuesta corta es que no puede manejar eventos de entrada de teclado sin código subyacente, pero puede manejar InputBindings con MVVM (puedo mostrarle un ejemplo relevante si esto es lo que necesita).

¿Puede proporcionar más información sobre lo que desea hacer en el controlador?

El código subyacente no se debe evitar por completo con MVVM. Simplemente se usa para tareas estrictamente relacionadas con la interfaz de usuario. Un ejemplo cardinal sería tener algún tipo de 'formulario de entrada de datos' que, cuando se carga, necesita enfocar el primer elemento de entrada (cuadro de texto, cuadro combinado, lo que sea). Normalmente asignaría a ese elemento un atributo x: Name, luego conectaría el evento 'Cargado' de Window / Page / UserControl para establecer el foco en ese elemento. Esto está perfectamente bien por el patrón porque la tarea está centrada en la interfaz de usuario y no tiene nada que ver con los datos que representa.

Sé que esta pregunta es muy antigua, pero encontré esto porque este tipo de funcionalidad se hizo más fácil de implementar en Silverlight (5). Entonces tal vez otros vendrán por aquí también.

Escribí esta solución simple después de que no pude encontrar lo que estaba buscando. Resultó que era bastante simple. Debería funcionar tanto en Silverlight 5 como en 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");
    }

Uso:

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

Observe que Command es una cadena y no un ICommand enlazable. Sé que esto no es tan flexible, pero es más limpio cuando se usa, y lo que necesita el 99% del tiempo. Aunque no debería ser un problema cambiar.

Similar a la respuesta de karlipoppins, pero descubrí que no funcionaba sin las siguientes adiciones / cambios:

<TextBox Text="{Binding UploadNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding FindUploadCommand}" />
    </TextBox.InputBindings>
</TextBox>
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top