skip to Main Content

I’m quite new to WPF so it would be better that I will describe the intent:

I have multiple User Controls inside the main window. Each User Control has its own buttons, that I would like to bind to command using keyboard shortcuts.

The MainWindow.xaml is something like this:

<Window ...>
   <Window.DataContext>
      <viewModel:MainViewModel/>
   </Window.DataContext>    
   ...
   <ContentControl Grid.Column="1"
                   Margin="5"
                   Content="{Binding HomeToolbarView}"/>
    ...
</Window>

As you can See, I’m using some VM binding inside the Code-Behind. In app.xaml:

<Application ...>
   <Application.Resources>
       <ResourceDictionary>
           ...
           <DataTemplate DataType="{x:Type viewModel:HomeToolbarViewModel}">
               <view:HomeToolbar/>
           </DataTemplate>
           ...
       </ResourceDictionary>
   </Application.Resources>
</Application>

Where I bind the HomeToolbar to its HomeToolbarViewModel and in MainViewModel:

public class MainViewModel : ObservableObject {
    // This is bound to the XAML
    private object homeToolbarView;
    ...
    public HomeToolbarViewModel HomeToolbarVM { get; set; }
    ...
    public object HomeToolbarView
    {
        get { return homeToolbarView; }
        set
        {
            homeToolbarView = value;
            OnPropertyChanged();
        }
    }
    ...

    public MainViewModel() {
        ...
        HomeToolbarVM = new HomeToolbarViewModel();
        homeToolbarView = HomeToolbarVM;
        ...
    }
}

Finally the HomeToolbar.xaml:

<UserControl xmlns:self="clr-namespace:ChordTuner.MVVM.ViewModels"
             ...>
    <UserControl.CommandBindings>
        <!-- Error: Compiler binds this to the DataContext of the view, i.e. HomeToolbar.xaml.cs,  and not to the real DataContext, i.e. the HomeToolbarViewModel!
        <CommandBinding Command="self:HomeToolbarCommands.NewCommand" Executed="{Binding NewCommand_Executed}" .../> />
    </UserControl.CommandBindings>
    <!-- Only to simplify -->
    <StackPanel>
        <Button Command="self:HomeToolbarCommands.NewCommand" />
        <!-- No error, the command is correctly bound to the DataContext, i.e. the VM! -->
        <Button Command={Binding ARelayCommandProp}"/>
    </StackPanel>
</UserControl>

With HomeToolbarViewModel.cs:

namespace ChordTuner.MVVM.ViewModels {
    public class HomeToolbarViewModel : ObservableObject {
        ...
        private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show("New from XAML");
        }
        ...
    }

    public static class HomeToolbarCommands
    {
        public static readonly RoutedUICommand NewCommand = new RoutedUICommand
        (
            "New1",
            "New2",
            typeof(HomeToolbarCommands),
            new InputGestureCollection()
            {
                 new KeyGesture(Key.N, ModifierKeys.Control)
            }
        );}
}

Visual Studio outputs the following error:

CS1061 ‘HomeToolbar’ does not contain a definition for ‘NewCommand_Executed’ and no accessible extension method ‘NewCommand_Executed’ accepting a first argument of type ‘HomeToolbar’ could be found (are you missing a using directive or an assembly reference?) ChordTuner

It is strange to me that buttons using RelayCommand class are correctly bound to the the view model (as instructed in app.xaml), but the CommandBindings does not.

The goal of all of this is to assign global shortcuts to all User Control buttons. Using InputBindings imposes that User Control is focused. But I have multiple User Controls that need to respond to differente commands/shortcuts regardless focus.

UPDATE
This is the HomeToolbarView constructor

public HomeToolbar()
{
    InitializeComponent();

    for (int index = 0; index < MidiIn.NumberOfDevices; index++)
    {
        midiInComboBox.Items.Add(MidiIn.DeviceInfo(index).ProductName);
    }

    for (int index = 0; index < MidiOut.NumberOfDevices; index++)
    {
        midiOutComboBox.Items.Add(MidiOut.DeviceInfo(index).ProductName);
    }

    // This event is called when DataContext is changed. This happens because we binded the HomeToolbarViewModel inside the app.xaml.
    DataContextChanged += new DependencyPropertyChangedEventHandler(HomeToolbar_DataContextChanged);
}
...

void HomeToolbar_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    viewModel = (HomeToolbarViewModel?)DataContext;

    if (viewModel == null) return; 

    if (MidiOut.NumberOfDevices > 0)
    {
        viewModel.SelectedMidiOutIndex = 0;
    }

    if (MidiIn.NumberOfDevices > 0)
    {
        viewModel.SelectedMidiInIndex = 0;
    }
}

Do someone explain what I’m missing?

2

Answers


  1. To handle global input events, you must use RoutedCommand definitions and then handle them at the visual root e.g., the MainWindow.
    Using routed commands allows you to decouple the different scopes (data contexts) so that each data context don’t

    • has to define a duplicated ICommand property and logic
    • has to know a particular scope/data context (that is not their own) only to bind to the ICommand.

    Using a routed command in this scenario allows to execute the actual view model command from a single place amd within the correct data context.

    If commands should have a smaller scope, then define the command bindings on an appropriate common parent of the controls that must share a command action. Every UIElement can define CommandBinding objects and therefore act as a routed command scope.

    MainWindow.cs

    partial class MainWindow : Window
    {
      public static RoutedCommand SaveCommand { get; }
      private MainViewModel MainViewModel { get; }
    
      static MainWindow()
      {
        MainWindow.SaveCommand = new RoutedCommand(nameof(MainWindow.SaveCommand), typeof(MainWindow));
        var saveCommandGesture = new KeyGesture(Key.S, ModifierKeys.Control);
        _ = MainWindow.SaveCommand.InputGestures.Add(commandGestures);
      }
    
      public MainWindow()
      {
        InitializeComponent();
    
        this.MainViewModel = new MainViewModel();
        this.DataContext = this.MainViewModel;
    
        // Handle all executions of the routed SaveCommand, 
        // where the source is a child of MainWindow
        var saveCommandBinding = new CommandBinding(
          MainWindow.SaveCommand,
          ExecutedSaveCommand,
          CanExecuteSaveCommand);
        this.CommandBindings.Add(saveCommandBinding);
      }
    
      // Handle the routed command execution and delegate the action 
      // to the MainViewModel (or the DataContext of MainWindow in general)
      private void ExecutedSaveCommand(object sender, ExecutedRoutedEventArgs e)
        => this.MainViewModel.SaveDataCommand.Execute(e.Parameter);
    
    
      // Handle the routed command execution and delegate the action 
      // to the MainViewModel (or the DataContext of MainWindow in general)
      private void CanExecuteSaveCommand(object sender, CanExecuteRoutedEventArgs e)
        => e.CanExecute = this.MainViewModel.SaveDataCommand.CanExecute(e.Parameter);
    }
    

    HomeToolbar.xaml

    <UserControl>
      <Button Content="Save data"
              Command="{x:Static MainWindow.SaveCommand}" />
    </UserControl>
    

    HomeToolbarViewModel.cs

    class HomeToolbarViewModel : INotifyPropertyChanged
    {
      public ICommand SaveDataCommand { get; }
    }
    

    This is an alternative version where the MainWindow is allowed to handle the view model command anonymously, without an explicit reference to the view model i.e. without explicit knowledge of the actual view model type and ICommand identity (commands property name or member path).
    It’s an improved example, but requires to increase the complexity a tiny bit (nothing is for free).

    This decoupling is achieved by introducing a dependency property for each routed command. This property can then be bound to the view model’s ICommand implementation (the same way a Button.Command property is bound).
    This version successfully eliminates the requirement that the MainWindow has to explicitly reference its data context (although this ain’t something bad in general, we just strive to decouple as much as possible. Therefore, avoiding this explicit dependency is favorable).

    MainWindow.cs

    partial class MainWindow : Window
    {
      public ICommand SaveCommand
      {
        get => (ICommand)GetValue(SaveCommandProperty);
        set => SetValue(SaveCommandProperty, value);
      }
    
      public static readonly DependencyProperty SaveCommandProperty = DependencyProperty.Register(
        "SaveCommand", 
        typeof(ICommand), 
        typeof(MainWindow), 
        new PropertyMetadata(default));
    
      public static RoutedCommand SaveCommand { get; }
    
      static MainWindow()
      {
        MainWindow.SaveCommand = new RoutedCommand(nameof(MainWindow.SaveCommand), typeof(MainWindow));
        var saveCommandGesture = new KeyGesture(Key.S, ModifierKeys.Control);
        _ = MainWindow.SaveCommand.InputGestures.Add(commandGestures);
      }
    
      public MainWindow()
      {
        InitializeComponent();
    
        this.DataContext = new MainViewModel();
    
        // Handle all executions of the routed SaveCommand, 
        // where the source is a child of MainWindow
        var saveCommandBinding = new CommandBinding(
          MainWindow.SaveCommand,
          ExecutedSaveCommand,
          CanExecuteSaveCommand);
        this.CommandBindings.Add(saveCommandBinding);
      }
    
      // Handle the routed command execution and delegate the action 
      // to the MainViewModel (or the ICommand bound to 
      // the dedicated command properties in general)
      private void ExecutedSaveCommand(object sender, ExecutedRoutedEventArgs e)
        => this.SaveDataCommand?.Execute(e.Parameter);
    
    
      // Handle the routed command execution and delegate the action 
      // to the MainViewModel (or the ICommand bound to 
      // the dedicated command properties in general)
      private void CanExecuteSaveCommand(object sender, CanExecuteRoutedEventArgs e)
        => e.CanExecute = this.SaveCommand?.CanExecute(e.Parameter) ?? false;
    }
    

    MainWindow.xaml

    <Window>
      <Window.Resources>
        <Style TargetType="MainWindow">
    
          <!-- 
            Bind the command properties to the ICommand implementations 
            of the view model 
          -->
          <Setter Property="SaveCommand"
                  Value="{Binding SaveDataCommand}" />
        </Style>
      </Window.Resources>
    </Window>
    

    The rest, how the routed command is invoked, is exactly the same as in the previous example.

    Login or Signup to reply.
  2. In your View, you have the following:

    Executed="{Binding NewCommand_Executed}"
    

    And in your ViewModel you have the following:

    private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    

    This will not work, because a binding requires that the method to bind to must be public.

    You should be able to fix this simply by replacing private with public.

    Of course, there may be other problems. For example, I am not sure that you can simply bind to a method; it may be that you are required to bind to an object implementing ICommand. But fix this first, see what you get, and if it is unclear how to proceed, ask another question.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search