Adding Keyboard Shortcut Support to the WPF XamOutlookBar

Brian Lagunas / Friday, April 26, 2013

I recently received a question regarding support for keyboard shortcuts for changing groups in the Infragistics WPF xamOutlookBar.  If you are a user of Microsoft Outlook, you may have noticed that you can navigate to the different outlook groups using simple keyboard shortcuts.  For example, you can press CRTL+2 to navigate to the Calendar group, or CRTL+3 to navigate to the Contacts group.  The idea here is that a user can use their keyboard to navigate the groups contained within the Outlook bar navigation.  Unfortunately, I discovered that our WPF xamOutlookBar did not support this behavior.  So began my quest to find a solution.  I actually came up with two solutions.  I will describe them both and you can decide which approach you like best.

The Requirements

Before we run off coding up solutions, we should probably talk about how this feature should work.

  1. The first obvious requirement is that we should be able to change selected groups within the xamOutlookBar control using keyboard shortcuts.
  2. I want to be able to assign which key modifiers (CTRL, ALT, SHIFT) and keyboard key (A, B, C) combination will invoke the changing of groups.
  3. I want to be able to assign multiple key gesture combinations to a single group.  So if I want CTRL+1 and SHIFT+1 to navigate to the same group, that should be supported.
  4. I know the problems you can have with focus and keyboard events in WPF.  So, it is important that no matter which control has focus or where I am at on my screen, I should be able to change outlook bar groups with my shortcut key combination.

That’s about covers it.  Let’s get to the problem solving!

Approach One

The first approach I took was to take advantage of the built-in WPF InputBindings.  Here I will define a number of KeyBindings at the Window level, because I want the shortcuts to execute no matter where I am in my view or what has focus.  This means I am going to need an ICommand to bind to my KeyBindings and invoke the changing of the groups depending on my key combination.  The problem is, this command will need to know the key modifiers, the key pressed, and have access to all the groups in the xamOutlookBar control.  So I can give my command all these items if I use the KeyBinding object itself as the CommandParameter.  I will store the xamOutlookBar control itself through the CommandTarget property.  Which will give me access to everything I need.  I will also use the built-in InputBindings to assign my shortcut combinations/gestures to an OutlookBarGroup.  Let’s take a look at the code.

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

As you can see, I defined a number of KeyBindings in the Window.InputBindings element.  Because I want to change groups using numbers, I have to handle both the numbers the run along top of the keyboard as well as the numbers on the number pad of the keyboard.  This means that for each number I will have two KeyBindings.  I am data binding my KeyBinding to a Command that is defined on a ViewModel called ChangeGroupCommand.  The CommandParamter is going to be the KeyBinding object instance.  Lastly, the CommandTarget will be the xamOutlookBar control.  Since the CommandParameter is the KeyBinding instance, I now have access to the key gesture (modifiers and key pressed), as well as all the groups in the xamOutlookBar control in my ViewModel.  Let’s take a quick look at that 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;
                    }
                }
            }
        }
    }
}

This is a very simple ViewModel that has a single ICommand property defined.  The ICommand is a RelayCommand implementation.  If you are not familiar with a RelayCommand, Google/Bing is your friend.  The ChangeGroup method takes in the KeyBinding parameter, grabs the xamOutlookBar control from the CommandTarget property, and then begins searching each group in the xamOutlookBar for any groups that have InputBindings that match the incoming KeyBinding.  How do we assign KeyBindings to the OutlookBarGroups?  Easy, like this:

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

We are simply adding a number of KeyBindings to the OutlookBarGroup.InputBindings collection.  Remember, because of the different ways to input numbers, we have to add a KeyBinding for each key combination.  That’s all there is to it.  Now run the app, and our keyboard shortcuts will work as expected.  Pressing CTRL+3 will select the OutlookBarGroup to “Group 3”.

image

Approach Two

That first approach worked just fine, but it had some duplication of efforts.  I didn’t want to have to map my KeyBindings more than once.  I would prefer a simple property that I can turn ON/OFF and just have it all work.  So I decided to take a different approach that involves just a little more code, but it much more flexible and easy to implement.  This second approach takes advantage of an AttachedProperty, as well as the built-in KeyBindings.  Our KeyBindings on our OutlookBarGroups haven’t changed one bit.  Those stay where they are.  They are still used to define which shortcut key gestures will invoke the group change.  Next we are going to create an AttachedProperty called EnableInputBindings.  I am just going to provide the code and then talk about the various sections.

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();

}

What we have here is the making of a little InputBinding framework.  First we have an AttachedProperty that allows me to turn ON/OFF the input bindings.  We have also defined an attached property that will simply hold an instance of a behavior class that will be providing out functionality called InputBindingBehavior.

When the EnableKeyBindings attached property is set, a new XamOutlookBarKeyBindingBehavior class instance in created and stored in the InputBindingBehavior attached property.  If the EnableKeyBindings is true, the behavior is attached, if it is false it is detached.  if you look at the XamOutlookBarKeyBindingBehvaior class, you will see it is actually qute simple.  It derives from an abstract base class called InputBindingBehaviorBase<T> which simply holds a weak reference to the target object (in this case the xamOutlookBar).  When the behavior is attached, we first get an instance to the topmost parent Window control.  Remember, we want to execute our shortcut gestures no matter what has focus in the event.  Once we have the Window instance, we can now add a handler to the KeyUp event, making sure to specify we want to handle “handled” events too.  The KeyUp handler first makes sure we are dealing with modifiers.  If we don’t have any, then we don’t want to run the logic.  If we do, then loop through all the groups and check for any KeyBindings that match shortcut key gesture that was just pressed.  If there is a match, select the group.  The last step is to simply set the attached property on the xamOutlookBar control.

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

As you can see, this approach requires a little more code to get setup, but now it is much easier to enable the shortcut gesture support.  Just set a single attached property to True and define your KeyBindings on your groups, and your up and running.  Same result, just a different way of doing it.

image

Download the source code and have fun.

Out of curiosity, which approach do you prefer?