Keyboard shortcuts in code using WPF MVVM and Caliburn Micro

Recently I had to replace a keyboard-shortcut solution in a WPF application where the existing solution used a global hook using unmanaged code. My first tryout was using the ComponentDispatcher. At first, it seemed ok, but sometimes commands wasn’t intercepted and sometimes modifier-keys wasn’t cleared from the buffer. Lets just say “there were problems”. I then turned to simple InputBindings. And since the application used Caliburn Micro, it was quite easy:

Step 1 – Hook up

Extend Screen in a custom ViewModelBase and override OnViewLoaded, extract a Window from the view being loaded and hook up KeyBindings to its InputBindings-collection.

protected override void OnViewLoaded(object view)
{
	base.OnViewLoaded(view);

	var window = (view as FrameworkElement).GetWindow();
	_inputBindings = new InputBindings(window);
	_inputBindings.RegisterCommands(GetInputBindingCommands());
}

In the same ViewModelBase, add a method to return InputBindingCommands that each view model should implement.

protected virtual IEnumerable<InputBindingCommand> GetInputBindingCommands()
{
	yield break;
}

To get hold of the Window (which is where we wan’t to add our input bindings) we just use some traditional recursion to traverse the tree:

{
	public static Window GetWindow(this FrameworkElement element)
	{
		if (element == null)
			return null;

		if (element is Window)
			return (Window)element;

		if (element.Parent == null)
			return null;

		return GetWindow(element.Parent as FrameworkElement);
	}
}

To support overriding previously registered bindings we need to keep track of the input bindings associated with the current view, and to reverse the registration process we just have to stash them away in a stack.

public class InputBindings
{
	private readonly InputBindingCollection _inputBindings;
	private readonly Stack<KeyBinding> _stash;

	public InputBindings(Window bindingsOwner)
	{
		_inputBindings = bindingsOwner.InputBindings;
		_stash = new Stack<KeyBinding>();
	}

	public void RegisterCommands(IEnumerable<InputBindingCommand> inputBindingCommands)
	{
		foreach (var inputBindingCommand in inputBindingCommands)
		{
			var binding = new KeyBinding(inputBindingCommand, inputBindingCommand.GestureKey, inputBindingCommand.GestureModifier);

			_stash.Push(binding);
			_inputBindings.Add(binding);
		}
	}

	public void DeregisterCommands()
	{
		if (_inputBindings == null)
			return;

		foreach (var keyBinding in _stash)
			_inputBindings.Remove(keyBinding);
	}
}

Step 2 – Specify commands

Easy, just override the GetInputBindingCommands method in each ViewModel you want to provide key-commands:

protected override IEnumerable<InputBindingCommand> GetInputBindingCommands()
{
	yield return new InputBindingCommand(NewWindow)
	{
		GestureModifier = ModifierKeys.Control,
		GestureKey = Key.N
	};
}

Step 3 – Clean up

The last step is to cleanup when a view is being deactivated. In the ViewModelBase, just add:

protected override void OnDeactivate(bool close)
{
	base.OnDeactivate(close);

	_inputBindings.DeregisterCommands();
}

The DeregisterCommands method will remove the inputbindings that are associated with the view and added to the window.

Wrap up

Well, I will not show more code. There’s a simple sample app available here: https://github.com/danielwertheim/InputBindingCommand-Lab

//Daniel

9 thoughts on “Keyboard shortcuts in code using WPF MVVM and Caliburn Micro

    • You said it. XAML. We have a lot of runtime adjustable key-commands and are dynamically keeping track of them to output help-documentation.

      //Daniel

  1. Code works fine for me except for the following scenario
    ShellView/Model (Window) is a Conductor
    – MainView/Model (UserControl) is a ViewModelBase (Keybindings here still work)
    MainView/Model has a TabControl of SubView/Models, if I extend ViewModelBase in these it will throw an exception here:

    public InputBindings(Window bindingsOwner)
    {
    this.inputBindings = bindingsOwner.InputBindings; // <– NullReference
    this.stash = new Stack();
    }

    I’ve tried to fix this but I havent figured out how yet. Any thoughts?
    Thanks

  2. Thank you sharing your code! It very useful.
    However there is a problem if you want execute your commands with CM Action Context. so I added here some solution using command parameter

    public class InputBindings
    {
        private readonly InputBindingCollection _inputBindings;
        private readonly Stack _stash;
        private readonly FrameworkElement _owner;
    
        public InputBindings(Window bindingsOwner)
        {
            _owner = bindingsOwner;
            _inputBindings = bindingsOwner.InputBindings;
            _stash = new Stack();
        }
    
        public void RegisterCommands(IEnumerable inputBindingCommands)
        {
            foreach (var inputBindingCommand in inputBindingCommands)
            {
                var binding = new KeyBinding(inputBindingCommand, inputBindingCommand.GestureKey, inputBindingCommand.GestureModifier);
                binding.CommandParameter = _owner;
    
                _stash.Push(binding);
                _inputBindings.Add(binding);
            }
        }
    
        public void DeregisterCommands()
        {
            if (_inputBindings == null)
                return;
    
            foreach (var keyBinding in _stash)
                _inputBindings.Remove(keyBinding);
        }
    }
    
    *****************************************************************************
    
    public void Execute(object parameter)
    {
        var context = new ActionExecutionContext() {View = parameter as FrameworkElement};
        Caliburn.Micro.Coroutine.BeginExecute(Execute().GetEnumerator(), context);
    }
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s