As title states, I can't get KeyBinding
to work when using property element syntax. By work I mean using the key combo of Ctrl+Del
to change the background color of the list box. The key combo can be used or the button can be clicked, both of which invoke the command, yet the command is never invoked. When a breakpoint is set while in debug mode it will never be encountered.
I've followed the InputBinding Class example from the documentation and can only get KeyBinding
to work when using a UserControl
and would like to understand why that is, and what I'm doing wrong.
Below is an MVCE of when the code, declared with property element syntax, that does not work. Commented out is a line for a UserControl
which encapsulates the StackPanel
and allows the KeyBinding
to work. Contingent on the commenting out each PropertyElementSyntax
region and uncommenting each UserControlSyntax
region in the code behind for MainWindow.xaml.cs
.
MainWindow.xaml:
<Window x:Class="LearningKeyBindingWPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:LearningKeyBindingWPFApp"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="300">
<!--<local:UserControl1 x:Name="CustomColorPicker" />-->
<StackPanel Margin="0,40,0,0">
<StackPanel.InputBindings>
<KeyBinding Command="{Binding ChangeColorCommand}"
CommandParameter="{Binding ElementName=ColorPicker, Path=SelectedItem}"
Key="{Binding ChangeColorCommand.Key}"
Modifiers="{Binding ChangeColorCommand.ModifierKeys}" />
<MouseBinding Command="{Binding ChangeColorCommand}"
CommandParameter="{Binding ElementName=ColorPicker, Path=SelectedItem}"
MouseAction="{Binding ChangeColorCommand.MouseAction}" />
</StackPanel.InputBindings>
<Button Content="Change Color"
Command="{Binding ChangeColorCommand}"
CommandParameter="{Binding ElementName=ColorPicker, Path=SelectedItem}" />
<ListBox Name="ColorPicker"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
SelectedIndex="0">
<sys:String>Red</sys:String>
<sys:String>Green</sys:String>
<sys:String>Blue</sys:String>
<sys:String>Yellow</sys:String>
<sys:String>Orange</sys:String>
<sys:String>Purple</sys:String>
</ListBox>
</StackPanel>
</Window>
Code-behind for MainWindow.xaml.cs:
public MainWindow()
{
DataContext = this;
InitializeComponent();
InitializeCommand();
#region UserControlSyntax
//CustomColorPicker.ColorPicker.Focus();
#endregion
#region PropertyElementSyntax
ColorPicker.Focus();
#endregion
}
public SimpleDelegateCommand ChangeColorCommand { get; private set; }
private SolidColorBrush _originalColor;
private void InitializeCommand()
{
#region UserControlSyntax
//_originalColor = (SolidColorBrush)CustomColorPicker.ColorPicker.Background;
#endregion
#region PropertyElementSyntax
_originalColor = (SolidColorBrush)ColorPicker.Background;
#endregion
ChangeColorCommand = new SimpleDelegateCommand(ChangeColor)
{
Key = Key.Delete,
ModifierKeys = ModifierKeys.Control
};
}
private void ChangeColor(object colorString)
{
if (colorString == null)
{
return;
}
var selectedColor = SelectedColor((string)colorString);
#region UserControlSyntax
//if (CustomColorPicker.ColorPicker.Background == null)
//{
// CustomColorPicker.ColorPicker.Background = selectedColor;
// return;
//}
//CustomColorPicker.ColorPicker.Background = ((SolidColorBrush)CustomColorPicker.ColorPicker.Background).Color == selectedColor.Color
// ? _originalColor
// : selectedColor;
#endregion
#region PropertyElementSyntax
if (ColorPicker.Background == null)
{
ColorPicker.Background = selectedColor;
return;
}
var isColorIdentical = ((SolidColorBrush)ColorPicker.Background).Color == selectedColor.Color;
ColorPicker.Background = isColorIdentical
? _originalColor
: selectedColor;
#endregion
}
private SolidColorBrush SelectedColor(string value)
{
#region UserControlSyntax
//var selectedColor = (Color)ColorConverter.ConvertFromString(value);
#endregion
#region PropertyElementSyntax
var selectedColor = (Color)ColorConverter.ConvertFromString((string)ColorPicker.SelectedItem);
#endregion
return new SolidColorBrush(selectedColor);
}
The problem is that in the no-UserControl
scenario, the DataContext
is set before the command object has been initialized.
WPF has a robust binding system, but it normally relies on property-change notifications, via INotifyPropertyChanged
. Some scenarios will work without that, as long as you get the order of operations correct. But, without property-change notifications, if you miss your window of opportunity to present some property value to WPF, it's not going to try again later.
When you use the UserControl
, the initialization of the bindings for the UserControl
occurs after you set up the ChangeColorCommand
property. This is just an artifact of how WPF initializes the various objects in the UI tree. But it means that by the time the UserControl
's bindings look at the ChangeColorCommand
property, it has the value you want.
On the other hand, when you put the StackPanel
explicitly into the window's XAML, it's too late by the time you set the property for WPF to see it. It already resolved those bindings during the InitializeComponent()
call. Setting the property later has no effect.
There are a couple of ways you could address that given the code you have now:
DataContext = this;
to after the call to InitializeCommand()
. Updating the DataContext
requires WPF to update all of the dependent bindings too, so doing that after the InitializeCommand()
call ensures the property has the value you want.INotifyPropertyChanged
in the MainWindow
class, and raise the PropertyChanged
event for the ChangeColorCommand
property when you set it. This will let WPF know that the value changed and that it should re-evaluate any bindings that depended on it.All that said, I'd go one further:
INotifyPropertyChanged
and a ChangeColorCommand
, and use that as the data context. Making your UI objects do double-duty as both UI and property binding source (i.e. the view model's job) doesn't fit with the normal WPF model, sacrifices the benefits that MVVM would normally provide, and of course introduces this kind of weird timing thing where it's not obvious why a property binding isn't working as expected.
Okay, technically there's a fourth approach you could take, which is to put the call to InitializeCommand()
before InitializeComponent()
. Main problem with that is, at the moment, it relies on retrieving directly the value of a UI object's property, and that UI object won't exist until after InitializeComponent()
is called.
Which brings me back to the #3 option above. Fact is, you shouldn't be accessing UI object properties directly. That should be another property in your view model, and you should make a more direct choice about what that initial color should be, than just grabbing it from the UI on startup.
I admit, there's some wiggle room for design here, but you should be trying to keep your view model and UI code as divorced from each other as possible.