Search code examples
c#wpfkey-bindings

KeyBinding works as UserControl but not when pusing property element syntax in XAML


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);
}

Solution

  • 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:

    1. The simplest is to just move the assignment of 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.
    2. Implement 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:

    1. Implement a proper view model object, with 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.