2

I'm using C#, WPF, ReactiveUI and Prism to create an application with many different views (user controls). On some views there are buttons/menu items that bind to a command in the view model. I would like these buttons to also activate using a key combination such as ctrl+s, etc....

What I've tried

  • InputBindings but that only works when the view that defines these input bindings has focus.
  • ApplicationCommands the predefined commands like ApplicationCommands.Close seem useful. I can reference them both in the view and the view model, but I don't know how subscribe to them in my view model. It also seems that I have to 'activate' the command first, or at least change CanExecute since any button bound to such command stays disabled.

What I wish for

Let's say I have a view that represents the top menu bar MenuView with a button myButton and a corresponding view model MenuViewModel with a command myCommand. I would like to bind myButton to myCommand and the keyboard shortcut ctrl+u to myCommand without MenuView knowing about the implementation of its view model. The keyboard shortcut should work as long as the window that contains MenuView has focus.

I don't really care if the keyboard short-cut is either in the view or view model.

Roy T.
  • 9,429
  • 2
  • 48
  • 70
  • 1
    I prefere InputBindings when I have to bind commands to keyboard shortcuts. Just to be sure: I guess your main window does not know any of your controls because they are loaded via PRISM? And that#s why you don't want to put your InputBindings in your window? – Mighty Badaboom Mar 23 '17 at 09:45
  • 2
    Have you tried to use e.g. an attached behavior (like [this one](http://stackoverflow.com/a/23432365/2846483)) for your `InputBinding`s in order to make them "focus-independent"? – dymanoid Mar 23 '17 at 10:38
  • @MightyBadaboom exactly! – Roy T. Mar 23 '17 at 11:03
  • @dymanoid that solution looks perfect actually! I came across quite a few stack overflow question, but I didn't see that one yet. I guess we should close this as a duplicate now? – Roy T. Mar 23 '17 at 11:04

3 Answers3

1

You could create an attached Blend behaviour that handles the PreviewKeyDown event of the parent window:

public class KeyboardShortcutBehavior : Behavior<FrameworkElement>
{
    private Window _parentWindow;

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register(nameof(Command), typeof(ICommand),
        typeof(KeyboardShortcutBehavior), new FrameworkPropertyMetadata(null));

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public static readonly DependencyProperty ModifierKeyProperty =
        DependencyProperty.Register(nameof(ModifierKey), typeof(ModifierKeys),
        typeof(KeyboardShortcutBehavior), new FrameworkPropertyMetadata(ModifierKeys.None));

    public ModifierKeys ModifierKey
    {
        get { return (ModifierKeys)GetValue(ModifierKeyProperty); }
        set { SetValue(ModifierKeyProperty, value); }
    }

    public static readonly DependencyProperty KeyProperty =
        DependencyProperty.Register(nameof(Key), typeof(Key),
            typeof(KeyboardShortcutBehavior), new FrameworkPropertyMetadata(Key.None));

    public Key Key
    {
        get { return (Key)GetValue(KeyProperty); }
        set { SetValue(KeyProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += AssociatedObject_Loaded;
        AssociatedObject.Unloaded += AssociatedObject_Unloaded;
    }


    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        _parentWindow = Window.GetWindow(AssociatedObject);
        if(_parentWindow != null)
        {
            _parentWindow.PreviewKeyDown += ParentWindow_PreviewKeyDown;
        }
    }

    private void ParentWindow_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if(Command != null && ModifierKey != ModifierKeys.None && Key != Key.None && Keyboard.Modifiers == ModifierKey && e.Key == Key)
            Command.Execute(null);
    }

    private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
    {
        if(_parentWindow != null)
        {
            _parentWindow.PreviewKeyDown -= ParentWindow_PreviewKeyDown;
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Loaded -= AssociatedObject_Loaded;
        AssociatedObject.Unloaded -= AssociatedObject_Loaded;
    }
}

Sample usage:

<TextBox xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
    <i:Interaction.Behaviors>
        <local:KeyboardShortcutBehavior ModifierKey="Ctrl" Key="U" Command="{Binding myCommand}" />
    </i:Interaction.Behaviors>
</TextBox>
mm8
  • 163,881
  • 10
  • 57
  • 88
0

In code behind easy. Create some utility function that eventually lead to an observable of the parent window key events. Note that you will need the ReactiveUI.Events library.

Some utils for handling load and unload of controls.

    public static void LoadUnloadHandler
       ( this FrameworkElement control
       , Func<IDisposable> action
       )
    {
        var state = false;
        var cleanup = new SerialDisposable();
        Observable.Merge
            (Observable.Return(control.IsLoaded)
                , control.Events().Loaded.Select(x => true)
                , control.Events().Unloaded.Select(x => false)
            )
            .Subscribe(isLoadEvent =>
            {
                if (!state)
                {
                    // unloaded state
                    if (isLoadEvent)
                    {
                        state = true;
                        cleanup.Disposable = new CompositeDisposable(action());
                    }
                }
                else
                {
                    // loaded state
                    if (!isLoadEvent)
                    {
                        state = false;
                        cleanup.Disposable = Disposable.Empty;
                    }
                }

            });
    }

    public static IObservable<T> LoadUnloadHandler<T>(this FrameworkElement control, Func<IObservable<T>> generator)
    {
        Subject<T> subject = new Subject<T>();
        control.LoadUnloadHandler(() => generator().Subscribe(v => subject.OnNext(v)));
        return subject;
    }

and one specifically for handling the window of a loaded control

    public static IObservable<T> LoadUnloadHandler<T>
        (this FrameworkElement control, Func<Window, IObservable<T>> generator)
    {
        Subject<T> subject = new Subject<T>();
        control.LoadUnloadHandler(() => generator(Window.GetWindow(control)).Subscribe(v => subject.OnNext(v)));
        return subject;
    }

and finally a key handler for the parent window of any control

    public static IObservable<KeyEventArgs> ParentWindowKeyEventObservable(this FrameworkElement control)
        => control.LoadUnloadHandler((Window window) => window.Events().PreviewKeyDown);

now you can do

  Button b;
  b.ParentWindowKeyEventObservable()
   .Subscribe( kEvent => {

        myCommand.Execute();
   }

It might seem a bit complex but I use the LoadUnloadHandler on most user controls to aquire and dispose resources as the UI lifecycle progresses.

bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217
  • This seems like it would also solve my problem. I like the solution by mm8 a tiny bit more. Do note that a control can have multiple load/unload events making disposing things a bit more tricky. https://social.msdn.microsoft.com/Forums/en-US/ee9672ed-a991-4dc1-9c92-a015ca2d99d3/when-exactly-are-the-loaded-unloaded-events-raised?forum=wpf – Roy T. Mar 24 '17 at 07:59
  • Yes. A control can have multiple load/unload. That is why the state variable is there to make sure that I don't get two load events in a row. I should rename the variable _state_ to _isLoaded_ then it would be clearer. – bradgonesurfing Mar 24 '17 at 09:15
  • It also depends on how you write. Sometimes I get really annoyed by the loops you have to jump through and the verbosity of dealing with events in XAML. A behavior is nice when you reuse it many times but for one offs that require a bit of logic it's easier to write RX combinators. – bradgonesurfing Mar 24 '17 at 09:17
0

You want to use KeyBindings for this. This allows you to bind keyboard key combos to a command. Read the docs here: https://msdn.microsoft.com/en-us/library/system.windows.input.keybinding(v=vs.110).aspx

  • 1
    Unfortunately those do not work when the user control on which the keybinding is defined is not in focus. – Roy T. Mar 23 '17 at 15:51