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
To handle global input events, you must use
RoutedCommand
definitions and then handle them at the visual root e.g., theMainWindow
.Using routed commands allows you to decouple the different scopes (data contexts) so that each data context don’t
ICommand
property and logicICommand
.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 defineCommandBinding
objects and therefore act as a routed command scope.MainWindow.cs
HomeToolbar.xaml
HomeToolbarViewModel.cs
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 andICommand
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 aButton.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
MainWindow.xaml
The rest, how the routed command is invoked, is exactly the same as in the previous example.
In your View, you have the following:
And in your ViewModel you have the following:
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
withpublic
.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.