I want to add some generic keyboard shortcuts to my application. Currently, in every View XAML I add this code:
<Window.InputBindings>
<KeyBinding Command="{Binding ZoomInCommand}" Key="Add" Modifiers="Control" />
<KeyBinding Command="{Binding ZoomOutCommand}" Key="Subtract" Modifiers="Control" />
</Window.InputBindings>
I order to generalize this, I want to subclass the WPF Window Class and use the newly created subclass instead. Now I'm wondering how I could bind these Keyboard commands in the corresponding code. Currently it looks like this:
public class MyWindow : Window
{
public MyWindow()
{
DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
InputBindings.Clear();
var dataContext = DataContext as IZoomableViewModel;
if (dataContext != null)
{
InputBindings.Add(new KeyBinding(dataContext.ZoomInCommand, Key.Add, ModifierKeys.Control));
InputBindings.Add(new KeyBinding(dataContext.ZoomOutCommand, Key.Subtract, ModifierKeys.Control));
}
}
}
But this doesn't look right to me, as I need to have access to the DataContext directly and cast it instead of using a Binding() Object. How can I change the code to make it look more MVVM-like?
What you need are Dependency Properties.
In MyWindow
, create an ICommand
dependency property for both of your commands, you will also need to implement a callback method for when the dependency property value changes, here's one for the ZoomInCommand
:
public ICommand ZoomInCommand
{
get { return (ICommand)GetValue(ZoomInCommandProperty); }
set { SetValue(ZoomInCommandProperty, value); }
}
// Using a DependencyProperty as the backing store for ZoomInCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ZoomInCommandProperty =
DependencyProperty.Register("ZoomInCommand", typeof(ICommand), typeof(MyWindow), new PropertyMetadata(null, new PropertyChangedCallback(OnZoomInCommandChanged)));
...
private static void OnZoomInCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyWindow wnd = (MyWindow)d;
//Remove the old key binding if there is one.
wnd.RemoveInputBinding(e.OldValue as ICommand);
//Add the new input binding.
if (e.NewValue != null)
wnd.InputBindings.Add(new KeyBinding((ICommand)e.NewValue, Key.Add, ModifierKeys.Control));
}
private void RemoveInputBinding(ICommand command)
{
if (command == null)
return;
//Find the old binding if there is one.
InputBinding oldBinding = null;
foreach (InputBinding binding in InputBindings)
{
if (binding.Command == command)
{
oldBinding = binding;
break;
}
}
//Remove the old input binding.
if (oldBinding != null)
InputBindings.Remove(oldBinding);
}
So what exactly does the code above do?
On the dependency property, it is optional to have a PropertyChangedCallback
method which will fire whenever the property value changes, this is great because we can use this to remove the old InputBinding
and create a new InputBinding
if the value were to change. In your case, the value will change whenever the DataContext
changes.
So the steps are quite simple, whenever the property value changes:
- Remove the old
InputCommand
for the old ICommand
.
- Add a new
InputCommand
for the new ICommand
.
I've create a handy RemoveInputBinding
method which should make it easier to reuse the code for your other dependency property, which is up to you to implement.
To fit this all together, in your DataContextChanged
event handler you simply need to write a manual binding:
private void MyWindow_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
//Bind this.ZoomInCommand to DataContext.ZoomInCommand
Binding zoomInCommandBinding = new Binding("ZoomInCommand");
zoomInCommandBinding.Source = DataContext;
this.SetBinding(MyWindow.ZoomInCommandProperty, zoomInCommandBinding);
...
}
This will ensure that you no longer need to worry about casting the DataContext
to an IZoomableViewModel
, you simply try to bind to the ZoomInCommand
. If there is no such command in the DataContext
, then it will just silently fail. If it does succeed however, the PropertyChangedCallback
will be fired and an InputBinding
will be created for the command.
I found a simple solution which works nicely and seems to imitate the XAML parser behavior. Basically, for the zoom in functionality I put the following code in the MyWindow constructor:
var zoomInKeyBinding = new KeyBinding { Key = Key.Add, Modifiers = ModifierKeys.Control };
BindingOperations.SetBinding(
zoomInKeyBinding,
InputBinding.CommandProperty,
new Binding { Path = new PropertyPath("ZoomInCommand") }
);
InputBindings.Add(zoomInKeyBinding);
Of course the bound ViewModel needs to implement the ZoomInCommand appropriately.