Saltar al contenido
Adición de compatibilidad con métodos abreviados de teclado a WPF XamOutlookBar

Adición de compatibilidad con métodos abreviados de teclado a WPF XamOutlookBar

Si eres usuario de Microsoft Outlook, es posible que hayas notado que puedes navegar a los diferentes grupos de Outlook usando simples atajos de teclado.  En esta entrada de blog, explicaremos cómo agregar fácilmente compatibilidad con métodos abreviados de teclado a WPF XamOutlookBar. Leer más.

11min read

Recientemente recibí una pregunta sobre la compatibilidad con los métodos abreviados de teclado para cambiar de grupo en el Infragistics WPF xamOutlookBar. Por ejemplo, puede presionar CRTL+2 para navegar al grupo Calendario o CRTL+3 para navegar al grupo Contactos.  La idea aquí es que un usuario pueda usar su teclado para navegar por los grupos contenidos en la barra de navegación de Outlook.  Desafortunadamente, descubrí que nuestro WPF xamOutlookBar no admitía este comportamiento.  Así comenzó mi búsqueda para encontrar una solución.  De hecho, se me ocurrieron dos soluciones.  Voy a describir ambos y tú puedes decidir cuál es el enfoque que más te guste.

Los requisitos

Antes de salir corriendo de la codificación de soluciones, probablemente deberíamos hablar sobre cómo debería funcionar esta característica.

  1. El primer requisito obvio es que deberíamos poder cambiar los grupos seleccionados dentro del control xamOutlookBar mediante métodos abreviados de teclado.
  2. Quiero poder asignar qué modificadores de teclas (CTRL, ALT, SHIFT) y la combinación de teclas del teclado (A, B, C) invocarán el cambio de grupos.
  3. Quiero poder asignar varias combinaciones de gestos de teclas a un solo grupo.  Por lo tanto, si quiero que CTRL+1 y SHIFT+1 naveguen al mismo grupo, debería ser compatible.
  4. Conozco los problemas que puede tener con los eventos de foco y teclado en WPF.  Por lo tanto, es importante que, independientemente de qué control tenga el foco o dónde esté en mi pantalla, debería poder cambiar los grupos de la barra de Outlook con mi combinación de teclas de método abreviado.

Eso es sobre las cubiertas.  ¡Vayamos a la resolución de problemas!

Acércate a uno a los atajos de teclado

El primer enfoque que tomé fue aprovechar los InputBindings de WPF integrados.  Aquí definiré una serie de combinaciones de teclas en el nivel de ventana, porque quiero que los accesos directos se ejecuten sin importar dónde esté en mi vista o qué tenga el foco.  Esto significa que voy a necesitar un ICommand para enlazar a mis KeyBindings e invocar el cambio de los grupos dependiendo de mi combinación de teclas.  El problema es que este comando necesitará conocer los modificadores de tecla, la tecla presionada y tener acceso a todos los grupos del control xamOutlookBar.  Por lo tanto, puedo dar a mi comando todos estos elementos si uso el propio objeto KeyBinding como CommandParameter.  Almacenaré el propio control xamOutlookBar a través de la propiedad CommandTarget.  Lo que me dará acceso a todo lo que necesito.  También usaré los InputBindings integrados para asignar mis combinaciones o gestos de métodos abreviados a un OutlookBarGroup.  Echemos un vistazo al código.

<Window.InputBindings>
    
    <KeyBinding Modifiers="Control" Key="D1"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>
    <KeyBinding Modifiers="Control" Key="NumPad1"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>

    <KeyBinding Modifiers="Control" Key="D2"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>
    <KeyBinding Modifiers="Control" Key="NumPad2"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>

    <KeyBinding Modifiers="Control" Key="D3"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>
    <KeyBinding Modifiers="Control" Key="NumPad3"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>

    <KeyBinding Modifiers="Control" Key="D4"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>
    <KeyBinding Modifiers="Control" Key="NumPad4"
                Command="{Binding ChangeGroupCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                CommandTarget="{Binding ElementName=xamOutlookBar1}"/>

</Window.InputBindings>

Como puede ver, definí una serie de KeyBindings en el elemento Window.InputBindings.  Debido a que quiero cambiar de grupo usando números, tengo que manejar tanto los números que se encuentran en la parte superior del teclado como los números en el teclado numérico.  Esto significa que para cada número tendré dos combinaciones de teclas.  Estoy enlazando datos mi KeyBinding a un Command que se define en un ViewModel llamado ChangeGroupCommand.  CommandParamter va a ser la instancia del objeto KeyBinding.  Por último, CommandTarget será el control xamOutlookBar.  Dado que CommandParameter es la instancia de KeyBinding, ahora tengo acceso al gesto de la tecla (modificadores y tecla presionada), así como a todos los grupos del control xamOutlookBar en mi ViewModel.  Echemos un vistazo rápido a ese VewModel.

public class MainViewModel
{
    public ICommand ChangeGroupCommand { get; set; }

    public MainViewModel()
    {
        ChangeGroupCommand = new RelayCommand<KeyBinding>(x => ChangeGroup(x));
    }

    private void ChangeGroup(KeyBinding keyBinding)
    {
        XamOutlookBar outlookBar = keyBinding.CommandTarget as XamOutlookBar;
        if (outlookBar != null)
        {
            foreach (var group in outlookBar.Groups)
            {
                foreach (KeyBinding binding in group.InputBindings)
                {
                    if (binding.Modifiers == keyBinding.Modifiers && binding.Key == keyBinding.Key)
                    {
                        group.IsSelected = true;
                        return;
                    }
                }
            }
        }
    }
}

Este es un ViewModel muy simple que tiene una sola propiedad ICommand definida.  ICommand es una implementación de RelayCommand.  Si no está familiarizado con un RelayCommand, Google/Bing es su amigo.  El método ChangeGroup toma el parámetro KeyBinding, toma el control xamOutlookBar de la propiedad CommandTarget y, a continuación, comienza a buscar en cada grupo de xamOutlookBar los grupos que tienen InputBindings que coincidan con el KeyBinding entrante.  ¿Cómo asignamos combinaciones de teclas a OutlookBarGroups?  Fácil, así:

<igWPF:XamOutlookBar x:Name="xamOutlookBar1" HorizontalAlignment="Left">
    <igWPF:OutlookBarGroup Header="Group 1">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D1" />
            <KeyBinding Modifiers="Control" Key="NumPad1" />
        </igWPF:OutlookBarGroup.InputBindings>

        <Label Content="Content for Group 1"/>
    </igWPF:OutlookBarGroup>
    <igWPF:OutlookBarGroup Header="Group 2">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D2" />
            <KeyBinding Modifiers="Control" Key="NumPad2" />
        </igWPF:OutlookBarGroup.InputBindings>
        
        <Label Content="Content for Group 2"/>
    </igWPF:OutlookBarGroup>
    <igWPF:OutlookBarGroup Header="Group 3">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D3" />
            <KeyBinding Modifiers="Control" Key="NumPad3" />
        </igWPF:OutlookBarGroup.InputBindings>
        
        <Label Content="Content for Group 3"/>
    </igWPF:OutlookBarGroup>
    <igWPF:OutlookBarGroup Header="Group 4">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D4" />
            <KeyBinding Modifiers="Control" Key="NumPad4" />
        </igWPF:OutlookBarGroup.InputBindings>
        
        <Label Content="Content for Group 4"/>
    </igWPF:OutlookBarGroup>
</igWPF:XamOutlookBar>

Simplemente estamos agregando una serie de KeyBindings a la colección OutlookBarGroup.InputBindings.  Recuerde, debido a las diferentes formas de ingresar números, tenemos que agregar un enlace de tecla para cada combinación de teclas.  Eso es todo lo que hay que hacer.  Ahora ejecute la aplicación y nuestros atajos de teclado funcionarán como se espera.  Al presionar CTRL+3, se seleccionará OutlookBarGroup en "Grupo 3".

Ahora ejecute la aplicación y nuestros atajos de teclado funcionarán como se espera.  Al presionar CTRL+3, se seleccionará OutlookBarGroup en "Grupo 3"

Enfoque Dos

Ese primer enfoque funcionó bien, pero tuvo cierta duplicación de esfuerzos.  No quería tener que mapear mis combinaciones de teclas más de una vez.  Preferiría una propiedad simple que pueda encender / apagar y hacer que todo funcione.  Así que decidí adoptar un enfoque diferente que implica un poco más de código, pero que es mucho más flexible y fácil de implementar.  Este segundo enfoque aprovecha una AttachedProperty, así como las combinaciones de teclas integradas.  Nuestras combinaciones de teclas en nuestros OutlookBarGroups no han cambiado ni un poco.  Esos se quedan donde están.  Todavía se usan para definir qué gestos de teclas de método abreviado invocarán el cambio de grupo.  A continuación, vamos a crear una AttachedProperty llamada EnableInputBindings.  Solo voy a proporcionar el código y luego hablaré sobre las distintas secciones.

public class InputBinding : DependencyObject
{
    public static readonly DependencyProperty InputBindingBehaviorProperty =
        DependencyProperty.RegisterAttached("InputBindingBehavior", typeof(XamOutlookBarKeyBindingBehavior), typeof(InputBinding), new PropertyMetadata(null));

    public static readonly DependencyProperty EnableKeyBindingsProperty =
        DependencyProperty.RegisterAttached("EnableKeyBindings", typeof(bool), typeof(InputBinding), new PropertyMetadata(false, new PropertyChangedCallback(EnableKeyBindingsChanged)));
    public static bool GetEnableKeyBindings(DependencyObject obj)
    {
        return (bool)obj.GetValue(EnableKeyBindingsProperty);
    }
    public static void SetEnableKeyBindings(DependencyObject obj, bool value)
    {
        obj.SetValue(EnableKeyBindingsProperty, value);
    }

    private static void EnableKeyBindingsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        XamOutlookBar outlookBar = d as XamOutlookBar;
        bool isEnabled = (bool)e.NewValue;

        if (outlookBar != null)
        {
            XamOutlookBarKeyBindingBehavior behavior = GetOrCreateBehavior(outlookBar);

            if (isEnabled)
                behavior.Attach();
            else
                behavior.Dettach();
        }
    }

    private static XamOutlookBarKeyBindingBehavior GetOrCreateBehavior(XamOutlookBar outlookBar)
    {
        XamOutlookBarKeyBindingBehavior behavior = outlookBar.GetValue(InputBindingBehaviorProperty) as XamOutlookBarKeyBindingBehavior;
        if (behavior == null)
        {
            behavior = new XamOutlookBarKeyBindingBehavior(outlookBar);
            outlookBar.SetValue(InputBindingBehaviorProperty, behavior);
        }

        return behavior;
    }
}

public class XamOutlookBarKeyBindingBehavior : InputBindingBehaviorBase<XamOutlookBar>
{
    Window _parentWindow;

    public XamOutlookBarKeyBindingBehavior(XamOutlookBar outlookBar)
        : base(outlookBar)
    {

    }

    public override void Attach()
    {
        //since we want to listen for all key events no matter which control has focus, we need to listen at the Window level
        //otherwise the KeyUp event will never execute
        if (_parentWindow == null)
            _parentWindow = Window.GetWindow(TargetObject);

        if (_parentWindow != null)
            _parentWindow.AddHandler(Keyboard.KeyUpEvent, (KeyEventHandler)HandleKeyUp, true);
    }

    public override void Dettach()
    {
        if (_parentWindow != null)
            _parentWindow.RemoveHandler(Keyboard.KeyUpEvent, (KeyEventHandler)HandleKeyUp);
    }

    void HandleKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
    {
        try
        {
            //We only want to check for shorcuts if we are dealing with modifier keys.
            if (Keyboard.Modifiers == ModifierKeys.None)
                return;

            foreach (OutlookBarGroup group in TargetObject.Groups)
            {
                foreach (KeyBinding binding in group.InputBindings)
                {
                    if (binding.Modifiers == Keyboard.Modifiers && binding.Key == e.Key)
                    {
                        group.IsSelected = true;
                        return;
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }
}

public abstract class InputBindingBehaviorBase<T> where T : UIElement
{
    private readonly WeakReference _targetObject;
    protected T TargetObject
    {
        get { return _targetObject.Target as T; }
    }

    public InputBindingBehaviorBase(T targetObject)
    {
        _targetObject = new WeakReference(targetObject);
    }

    public abstract void Attach();
    public abstract void Dettach();

}

Lo que tenemos aquí es la creación de un pequeño marco InputBinding.  Primero tenemos un AttachedProperty que me permite activar o desactivar los enlaces de entrada.  También hemos definido una propiedad adjunta que simplemente contendrá una instancia de una clase de comportamiento que proporcionará una funcionalidad denominada InputBindingBehavior.

Cuando se establece la propiedad adjunta EnableKeyBindings, se crea y almacena una nueva instancia de clase XamOutlookBarKeyBindingBehavior en la propiedad adjunta InputBindingBehavior.  Si EnableKeyBindings es true, el comportamiento se adjunta, si es false, se desasocia.  si observa la clase XamOutlookBarKeyBindingBehvaior, verá que en realidad es bastante simple.  Se deriva de una clase base abstracta denominada InputBindingBehaviorBase<T> que simplemente contiene una referencia débil al objeto de destino (en este caso, xamOutlookBar).  Cuando se adjunta el comportamiento, primero se obtiene una instancia en el control Window primario superior.  Recuerde, queremos ejecutar nuestros gestos de atajo sin importar lo que tenga foco en el evento.  Una vez que tenemos la instancia de Windows, ahora podemos agregar un controlador al evento KeyUp, asegurándonos de especificar que también queremos manejar los eventos "controlados".  El controlador KeyUp primero se asegura de que estamos tratando con modificadores.  Si no tenemos ninguno, entonces no queremos ejecutar la lógica.  Si lo hacemos, recorra todos los grupos y verifique si hay combinaciones de teclas que coincidan con el gesto de la tecla de acceso directo que se acaba de presionar.  Si hay una coincidencia, seleccione el grupo.  El último paso es simplemente establecer la propiedad adjunta en el control xamOutlookBar.

<igWPF:XamOutlookBar x:Name="xamOutlookBar1" HorizontalAlignment="Left"
                     local:InputBinding.EnableInputBindings="True">
    
    <igWPF:OutlookBarGroup Header="Group 1">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D1" />
            <KeyBinding Modifiers="Control" Key="NumPad1" />
        </igWPF:OutlookBarGroup.InputBindings>
        <Label Content="Content for Group 1"/>
    </igWPF:OutlookBarGroup>
    <igWPF:OutlookBarGroup Header="Group 2">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D2" />
            <KeyBinding Modifiers="Control" Key="NumPad2" />
                        </igWPF:OutlookBarGroup.InputBindings>
        <Label Content="Content for Group 2"/>
    </igWPF:OutlookBarGroup>
    <igWPF:OutlookBarGroup Header="Group 3">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D3" />
            <KeyBinding Modifiers="Control" Key="NumPad3" />
                        </igWPF:OutlookBarGroup.InputBindings>
        <Label Content="Content for Group 3"/>
    </igWPF:OutlookBarGroup>
    <igWPF:OutlookBarGroup Header="Group 4">
        <igWPF:OutlookBarGroup.InputBindings>
            <KeyBinding Modifiers="Control" Key="D4" />
            <KeyBinding Modifiers="Control" Key="NumPad4" />
                        </igWPF:OutlookBarGroup.InputBindings>
        <Label Content="Content for Group 4"/>
    </igWPF:OutlookBarGroup>
</igWPF:XamOutlookBar>

Como puede ver, este enfoque requiere un poco más de código para configurarlo, pero ahora es mucho más fácil habilitar el soporte de gestos de acceso directo.  Solo tiene que establecer una única propiedad adjunta en True y definir las combinaciones de teclas en los grupos y en funcionamiento.  Mismo resultado, solo que una forma diferente de hacerlo.

Mismo resultado, solo que una forma diferente de hacerlo

Descarga el código fuente y diviértete.

Por curiosidad, ¿qué enfoque prefieres?

Solicitar una demostración