Search code examples
c#wpfmvvmkeyboardicommand

How to bind keyboard input command to main window?


I am relatively new to WPF Binding, and having a difficulty in binding a ICommand implemented Class which is to detect user keyboard input in the window? At first I think I need to bind it to the Window on XAML, but, it appears that Window doesn't have Command.

This is my ViewModel Class

public class NoteBoxViewModel
{
    public MusicalNotation MusicalNotation { get; set; }
    public ICommand KeyboardHotkeyCommand { get; private set; }
    public bool IsSelected { get; set; }

    public NoteBoxViewModel()
    {
        MusicalNotation = new MusicalNotation();
        KeyboardHotkeyCommand = new KeyboardHotkeyCommand(this);
        IsSelected = true;
    }
}

This is my KeyboardHotkeyCommand Class

public class KeyboardHotkeyCommand : ICommand
{
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    private NoteBoxViewModel _vm;

    public KeyboardHotkeyCommand(NoteBoxViewModel vm)
    {
        _vm = vm;
    }

    public bool CanExecute(object parameter)
    {
        return _vm.IsSelected;
    }

    public void Execute(object parameter)
    {
        if (Keyboard.IsKeyDown(Key.OemPeriod))
        {
            _vm.MusicalNotation.Note = Notes.Blank;
        }
        else if (Keyboard.IsKeyDown(Key.D0))
        {
            _vm.MusicalNotation.Note = Notes.Rest;
        }
        else if (Keyboard.IsKeyDown(Key.D1))
        {
            _vm.MusicalNotation.Note = Notes.N1;
        }
        else if (Keyboard.IsKeyDown(Key.D2))
        {
            _vm.MusicalNotation.Note = Notes.N2;
        }
        else if (Keyboard.IsKeyDown(Key.D3))
        {
            _vm.MusicalNotation.Note = Notes.N3;
        }
        else if (Keyboard.IsKeyDown(Key.D4))
        {
            _vm.MusicalNotation.Note = Notes.N4;
        }
        else if (Keyboard.IsKeyDown(Key.D5))
        {
            _vm.MusicalNotation.Note = Notes.N5;
        }
        else if (Keyboard.IsKeyDown(Key.D6))
        {
            _vm.MusicalNotation.Note = Notes.N6;
        }
        else if (Keyboard.IsKeyDown(Key.D7))
        {
            _vm.MusicalNotation.Note = Notes.N7;
        }
        else if (Keyboard.Modifiers == ModifierKeys.Control && Keyboard.IsKeyDown(Key.OemPlus))
        {
            _vm.MusicalNotation.Octave++;
        }
        else if (Keyboard.Modifiers == ModifierKeys.Control && Keyboard.IsKeyDown(Key.OemMinus))
        {
            _vm.MusicalNotation.Octave--;
        }
    }
}

This is my Notes Enumeration

public enum Notes
{
    N1,
    N2,
    N3,
    N4,
    N5,
    N6,
    N7,
    Rest,
    Blank
}

Questions :

  1. How to bind KeyboardHotkeyCommand to my XAML Class so I can detect user input (input does not going to Textbox or any sort of text editor first)? I also tried to bind it to Window_IsKeyDown (it's a bad practice, I know) but it failed afterall. Is it possible to achieve it without event?
  2. In my KeyboardHotkeyCommand, there's an event named CanExecuteChanged. I filled it with exact direction with what is told here : http://blog.lab49.com/archives/2650. But I have no idea why it's filled that way. Can anyone explain to me?
  3. I also studied the tutorial here : http://reedcopsey.com/series/windows-forms-to-mvvm/ and it seems that he can directly instantiate the ICommand with ActionCommand, but I can't find any in my Visual Studio. Anyone knows why?

Note :

  1. Even though I'm new to MVVM (just knew it yesterday), but I want to use as much as MVVM pattern as possible, so, I would like to avoid to use events (if that's possible, of course), since I read that it's not a good MVVM practice.
  2. I think, whatever my MusicalNotation class look like, it'll not affect the solution of this problem at all, since it's just a 'Model'. But if it's needed, I am 100% willing to show you as well.

Solution

  • Using the MVVM pattern, you can bind keystrokes to your VM commands by using the InputBindings on the Main Window. A sample Xaml snippet looks like this...

    <Window.InputBindings>
        <KeyBinding Key="Right" Command="{Binding NavigateCommand}" CommandParameter="f"/>
        <KeyBinding Key="Left" Command="{Binding NavigateCommand}" CommandParameter="b"/>
        <KeyBinding Key="Delete"  Command="{Binding NavigateCommand}"  CommandParameter="delete"/>
        <KeyBinding Key="F5" Command="{Binding GetAllCommand}"/>
    </Window.InputBindings>
    

    The KeyBinding class takes several parameters: the key itself, which is an enumeration, along with any modifiers like CTRL, ALT, and SHIFT.

    The two parameters of immediate interest are the Command parameter, which gets binded to the VM, and the CommandParameter, which appears as the argument in your command delegates.

    In this example, the Xaml is binding three different keystrokes to the NavigateCommand and using the CommandParameter to let the VM know which key was pressed.

    The docs for InputBindings are here http://msdn.microsoft.com/en-us/library/system.windows.input.inputbinding(v=vs.110).aspx with more samples of usage.

    Note: the usage assumes that the DataContext of the Window has been set to the VM instance that implements these commands, otherwise there will be a data binding error.

    Your implementation would look something like this...

    <Window.InputBindings>
        <KeyBinding Key="A"  Command="{Binding KeyBoardHotKeyCommand}" CommandParameter="N1"/>
    </Window.InputBindings>
    

    With the ICommand delegates like...

        private void ExecuteKeyboardHotKeyCommand(object obj)
        {
            Notes note;
            if (obj!=null && Enum.TryParse(obj.ToString(), out note))
            {
                Console.WriteLine(@"User pressed {0}", note);
            }
        }
        private bool CanExecuteKeyboardHotKeyCommand(object obj)
        {
            return true;
        }
    

    Your 'Notes' enumeration is fine. Your ICommand delegates are along the right track for MVVM, but need to be abstracted away from Keyboard.IsKeyDown because these references cannot be easily tested. Well written View Models are hardware agnostic, and do not really care if the event occurred on the keyboard or some other device (like a touch screen).

    For your last question, I use the Josh Smith's RelayCommand. It looks like this...

    public class RelayCommand : ICommand
    {   //http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            _execute = execute;
            _canExecute = canExecute;
        }
        public bool CanExecute(object parameter)
        {
            return _canExecute(parameter);
        }
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public void Execute(object parameter)
        {
            _execute(parameter);
        }
        private readonly Action<object> _execute;
        private readonly Predicate<object> _canExecute;
    }
    

    CanExecuteChanged is an event that the WPF binding engine (or some other exogenous class) can subscribe to. An implementation looks like this...

        public ICommand KeyBoardHotKeyCommand { get; set; }
        public ViewModel()
        {
            KeyBoardHotKeyCommand = new RelayCommand(ExecuteKeyboardHotKeyCommand, CanExecuteKeyboardHotKeyCommand);
        }
        private void ExecuteKeyboardHotKeyCommand(object obj)
        {
            Notes note;
            if (obj!=null && Enum.TryParse(obj.ToString(), out note))
            {
                Console.WriteLine(@"User pressed {0}", note);
            }
        }
        private bool CanExecuteKeyboardHotKeyCommand(object obj)
        {
            return true;
        }
    

    Lastly, for your question about Reed's implementation, Reed will probably be along when members in the US wake up. But for the moment, his ActionCommand (now part of Expression Studio http://msdn.microsoft.com/en-us/library/microsoft.expression.interactivity.core.actioncommand(v=expression.40).aspx ) is fundamentally the same as Josh Smith's RelayCommand. They are all using the same principles to implement the ICommand interface.