Bind Command to KeyBinding in Code Behind

2019-07-09 02:37发布

问题:

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?

回答1:

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:

  1. Remove the old InputCommand for the old ICommand.
  2. 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.



回答2:

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.