Home > Software engineering >  WPF Button shortcut key using Dependency Property
WPF Button shortcut key using Dependency Property

Time:05-17

I am using a set of standard buttons, reused across a WPF app and want to add a shortcut key to each of the buttons.

So I have a ControlTemplate containing the buttons with Command bound to standard commands

I am trying to add a KeyBinding to the UserControl that contains the Button by adding Dependency Property to the Button and navigating up the Parent tree to the nearest UserControl.

I can get the DP values to set Key and Modifiers from the template Button, but cannot get hold of the Command (maybe because it is not bound until later??)

Any ideas how I can:

  1. Either get hold of or create the Command from the Template per this approach
  2. Or get the Command when it is resolved and then set the KeyBinding

PS: I have set the Key and Modifiers in separate DPs but would prefer to have a single DP of KeyBinding, then set ShortcutBinding.Key and ShortcutBinding.Modifers in XAML. Is there a way to set the properties of a DP class in XAML like that?

Extract of the XAML of from the button group template:

                <ctrl:ButtonShortcut 
                    x:Name="btnUpdate"
                    Style="{StaticResource EditButtonStyle}"
                    Command="{Binding DataContext.UpdateCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
                    Content="Update"
                    ShortcutKey="U"
                    ShortcutModifiers="Ctrl"/>

The DP class, inherited from Button, implementing the Key and Modifiers to link with the same Command that is bound to the Button:

    public partial class ButtonShortcut : Button
{
    public KeyBinding ShortcutBinding { get; set; }

    public Key ShortcutKey
    {
        get { return (Key)GetValue(ShortcutKeyProperty); }
        set { SetValue(ShortcutKeyProperty, value); }
    }
    public static readonly DependencyProperty ShortcutKeyProperty =
        DependencyProperty.Register("ShortcutKey", typeof(Key), typeof(ButtonShortcut), new PropertyMetadata(Key.None, ShortcutKeyChanged));

    public ModifierKeys ShortcutModifiers
    {
        get { return (ModifierKeys)GetValue(ShortcutModifiersProperty); }
        set { SetValue(ShortcutModifiersProperty, value); }
    }
    public static readonly DependencyProperty ShortcutModifiersProperty =
        DependencyProperty.Register("ShortcutModifiers", typeof(ModifierKeys), typeof(ButtonShortcut), new PropertyMetadata(ModifierKeys.None, ShortcutKeyChanged));

    private static void ShortcutKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var btn = d as ButtonShortcut;
        if (btn != null)
        {
            FrameworkElement uc = btn.Parent as FrameworkElement;
            while (uc?.Parent != null && uc is not UserControl)
                uc = uc.Parent as FrameworkElement;

            if (btn.ShortcutBinding == null)
            {
                btn.ShortcutBinding = new KeyBinding();
            }
            var bindings = btn.CommandBindings;
            if (e.NewValue is Key)
                btn.ShortcutBinding.Key = (Key)e.NewValue;
            if (e.NewValue is ModifierKeys)
                btn.ShortcutBinding.Modifiers = (ModifierKeys)e.NewValue;

            //So far, so good, but I cannot find the Command to apply to the KeyBinding
            btn.ShortcutBinding.Command = btn.Command;
            btn.ShortcutBinding.CommandParameter = btn.CommandParameter;
            if (btn.Command == null)
                System.Diagnostics.Debug.Print("not in Commmand");
            if (btn.CommandBindings.Count == 0)
                System.Diagnostics.Debug.Print("not in CommandBindings");
            if (btn.ReadLocalValue(CommandProperty) == DependencyProperty.UnsetValue)
                System.Diagnostics.Debug.Print("not in DP CommandProperty");


            if (btn.ShortcutBinding.Key != Key.None && uc != null)
            {
                if (!uc.InputBindings.Contains(btn.ShortcutBinding))
                    uc.InputBindings.Add(btn.ShortcutBinding);

            }
        }
    }

    public ButtonShortcut()
    {
        InitializeComponent();
    }

}

CodePudding user response:

This looks like a very bad smelling design. The child control should not configure the parent control. Especially not to add "features" that the child pretends to offer: in your case the button is not able to execute key gestures - it only defines them.
Since the parent control is the target of the gestures, it must define the required handlers to execute the gesture. This means, all the action and responsibilities are in the parent UserControl and not in the Button (or child element). Classes should never know details of other classes. A button should never know which parent will execute its command and how to register the command in the first place.

The button is just a passive source. It does not handle commands or gestures itself. Therefore, it would never register a CommandBinding or InputBinding.
Usually, you bind the Button.Command to a command defined on the command target. The button invokes this command on the target (or raises a RoutedCommand) and the target executes a corresponding operation.

It doesn't make sense to define commands or key gestures on a button. The button won't execute them. Commands, key and mouse gestures must be defined on the target, where the related responsibilities are.

You can let the parent UserControl (which I assume in this example is the command target) define a RoutedCommand. Register the corresponding key gesture(s) with this command:

Solution 1

MyUserControl.xaml.cs

partial class MyUserControl : UserControl
{
  public static RoutedCommand DoActionCommand { get; }

  static MyUserControl()
  {
    var gestures = new InputGestureCollection
    {
      new KeyGesture(Key.U, ModifierKeys.Control),
    };

    DoActionCommand = new RoutedUICommand(
      "Do something", 
      nameof(DoActionCommand), 
      typeof(CommandTargetUserControl),
      gestures);
  }

  public MyUserControl()
  {
    InitializeComponent();

    this.CommandBindings.Add(new CommandBinding(DoActionCommand, ExecuteDoActionCommand));
  }
}

CommandTargetUserControl.xaml

<MyUserControl>
  <Button Command="{x:Static local:CommandTargetUserControl.DoActionCommand}" />
</MyUserControl>

Solution 2

To allow configuring default key gestures without modifying the source and the target, you can implement an attached behavior. This behavior basically delegates a hooked command, which is invoked by a key gesture, to the actual command sources that are registered with the particular key gesture. Command source and command target are totally decoupled. Input bindings are configured explicitly to respect encapsulation (no silent and unexpected modidifications).

Usage example

<Window local:KeyGestureDelegate.IsKeyGestureDelegationEnabled="True">
  <local:KeyGestureDelegate.TargetKeyGestures>
    <InputGestureCollection>
      <KeyGesture>Ctrl U</KeyGesture>
      <KeyGesture>Ctrl Shift M</KeyGesture>
    </InputGestureCollection>
  </local:KeyGestureDelegate.TargetKeyGestures>

  <StackPanel>
    <Button Command="{Binding SomeCommand}" 
            local:KeyGestureDelegate.SourceKeyGesture="Ctrl U"
            local:KeyGestureDelegate.IsKeyGestureCommandExecutionEnabled="True" />
    <Button Command="{Binding SomeOtherCommand}"
            local:KeyGestureDelegate.SourceKeyGesture="Shift Ctrl M"
            local:KeyGestureDelegate.IsKeyGestureCommandExecutionEnabled="True" />
  </StackPanel>
</Window>

Implementation example

KeyGestureDelegate.cs
An implementation of the RelayCommand used in this example can be found at Microsoft Docs: Relaying Command Logic.

public class KeyGestureDelegate : DependencyObject
{
  // Custom KeyGesture comparer for the Dictionary
  private class KeyGestureComparer : EqualityComparer<KeyGesture>
  {
    public override bool Equals(KeyGesture? x, KeyGesture? y)
      => (x?.Key, x?.Modifiers).Equals((y?.Key, y?.Modifiers));

    public override int GetHashCode([DisallowNull] KeyGesture obj)
      => HashCode.Combine(obj.Key, obj.Modifiers);
  }

  private static ICommand KeyGestureDelegateCommand { get; } = new RelayCommand(ExecuteKeyGestureDelegateCommand);

  public static bool GetIsKeyGestureDelegationEnabled(DependencyObject attachedElement) => (bool)attachedElement.GetValue(IsKeyGestureDelegationEnabledProperty);
  public static void SetIsKeyGestureDelegationEnabled(DependencyObject attachedElement, bool value) => attachedElement.SetValue(IsKeyGestureDelegationEnabledProperty, value);
  public static readonly DependencyProperty IsKeyGestureDelegationEnabledProperty = DependencyProperty.RegisterAttached(
    "IsKeyGestureDelegationEnabled",
    typeof(bool),
    typeof(KeyGestureDelegate),
    new PropertyMetadata(default(bool), OnIsKeyGestureDelegationEnabled));

  public static bool GetIsKeyGestureCommandExecutionEnabled(DependencyObject attachedElement) => (bool)attachedElement.GetValue(IsKeyGestureCommandExecutionEnabledProperty);
  public static void SetIsKeyGestureCommandExecutionEnabled(DependencyObject attachedElement, bool value) => attachedElement.SetValue(IsKeyGestureCommandExecutionEnabledProperty, value);
  public static readonly DependencyProperty IsKeyGestureCommandExecutionEnabledProperty = DependencyProperty.RegisterAttached(
    "IsKeyGestureCommandExecutionEnabled",
    typeof(bool),
    typeof(KeyGestureDelegate),
    new PropertyMetadata(default(bool), OnIsKeyGestureCommandExecutionEnabled));

  public static InputGestureCollection GetTargetKeyGestures(DependencyObject obj) => (InputGestureCollection)obj.GetValue(TargetKeyGesturesProperty);
  public static void SetTargetKeyGestures(DependencyObject obj, InputGestureCollection value) => obj.SetValue(TargetKeyGesturesProperty, value);

  public static readonly DependencyProperty TargetKeyGesturesProperty = DependencyProperty.RegisterAttached(
    "TargetKeyGestures",
    typeof(InputGestureCollection),
    typeof(KeyGestureDelegate),
    new PropertyMetadata(default(InputGestureCollection), OnTargetKeyGesturesChanged));

  public static KeyGesture GetSourceKeyGesture(DependencyObject attachedElement) => (KeyGesture)attachedElement.GetValue(SourceKeyGestureProperty);
  public static void SetSourceKeyGesture(DependencyObject attachedElement, KeyGesture value) => attachedElement.SetValue(SourceKeyGestureProperty, value);
  public static readonly DependencyProperty SourceKeyGestureProperty = DependencyProperty.RegisterAttached(
    "SourceKeyGesture",
    typeof(KeyGesture),
    typeof(KeyGestureDelegate),
    new PropertyMetadata(default(KeyGesture), OnSourceKeyGestureChanged));

  // Remember added InputBindings to enable later removal
  private static Dictionary<UIElement, IList<InputBinding>> InputBindingTargetMap { get; } = new Dictionary<UIElement, IList<InputBinding>>();

  // Lookup command sources that map to a particular gesture
  private static Dictionary<KeyGesture, IList<ICommandSource>> InputBindingSourceMap { get; } = new Dictionary<KeyGesture, IList<ICommandSource>>(new KeyGestureComparer());

  private static void OnIsKeyGestureDelegationEnabled(DependencyObject attachedElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachedElement is not UIElement keyGestureHandler)
    {
      throw new ArgumentException($"Attached element must be of type {typeof(UIElement)}.");
    }

    InputGestureCollection gestures = GetTargetKeyGestures(keyGestureHandler);
    if ((bool)e.NewValue)
    {
      RegisterKeyBinding(keyGestureHandler, gestures);
    }
    else
    {
      UnregisterKeyBinding(keyGestureHandler);
    }
  }

  private static void OnIsKeyGestureCommandExecutionEnabled(DependencyObject attachedElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachedElement is not ICommandSource commandSource)
    {
      throw new ArgumentException($"Attached element must be of type {typeof(ICommandSource)}.");
    }

    KeyGesture keyGesture = GetSourceKeyGesture(attachedElement);
    if ((bool)e.NewValue)
    {
      RegisterCommandBinding(commandSource, keyGesture);
    }
    else
    {
      UnregisterCommandBinding(commandSource, keyGesture);
    }
  }

  private static void OnTargetKeyGesturesChanged(DependencyObject attachedElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachedElement is not UIElement keyGestureHandler)
    {
      throw new ArgumentException($"Attached element must be of type {typeof(UIElement)}.");
    }

    if (e.OldValue is InputBindingCollection)
    {
      UnregisterKeyBinding(keyGestureHandler);
    }

    if (!GetIsKeyGestureDelegationEnabled(keyGestureHandler))
    {
      return;
    }
    RegisterKeyBinding(keyGestureHandler, e.NewValue as InputGestureCollection);
  }

  private static void OnSourceKeyGestureChanged(DependencyObject attachedElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachedElement is not ICommandSource commandSource)
    {
      throw new ArgumentException($"Attached element must be of type {typeof(ICommandSource)}.");
    }

    UnregisterCommandBinding(commandSource, e.OldValue as KeyGesture);

    if (!GetIsKeyGestureCommandExecutionEnabled(attachedElement))
    {
        return;
    }
    RegisterCommandBinding(commandSource, e.NewValue as KeyGesture);
  }

  private static void ExecuteKeyGestureDelegateCommand(object commandParameter)
  {
    if (InputBindingSourceMap.TryGetValue(commandParameter as KeyGesture, out IList<ICommandSource> commandSources))
    {
      foreach (ICommandSource commandSource in commandSources)
      {
        ExecuteCommandSource(commandSource);
      }
    }
  }

  private static void ExecuteCommandSource(ICommandSource commandSource)
  {
    if (commandSource.Command is RoutedCommand routedCommand)
    {
      IInputElement commandTarget = commandSource.CommandTarget ?? commandSource as IInputElement;
      if (routedCommand.CanExecute(commandSource.CommandParameter, commandTarget))
      {
        routedCommand.Execute(commandSource.CommandParameter, commandTarget);
      }
    }
    else if (commandSource.Command?.CanExecute(parameter: commandSource.CommandParameter) ?? false)
    {
      commandSource.Command.Execute(commandSource.CommandParameter);
    }
  }

  private static void RegisterKeyBinding(UIElement keyGestureHandler, InputGestureCollection inputGestureCollection)
  {
    if (inputGestureCollection == null)
    {
      return;
    }

    IList<InputBinding>? inputBindings = new List<InputBinding>();
    InputBindingTargetMap.Add(keyGestureHandler, inputBindings);
    foreach (KeyGesture gesture in inputGestureCollection.OfType<KeyGesture>())
    {
      var inputBinding = new KeyBinding(KeyGestureDelegateCommand, gesture) { CommandParameter = gesture };
      keyGestureHandler.InputBindings.Add(inputBinding);
      inputBindings.Add(inputBinding);
    }
  }

  private static void UnregisterKeyBinding(UIElement keyGestureHandler)
  {
    if (InputBindingTargetMap.TryGetValue(keyGestureHandler, out IList<InputBinding>? inputBindings))
    {
      foreach (InputBinding inputBinding in inputBindings)
      {
        keyGestureHandler.InputBindings.Remove(inputBinding);
      }
      InputBindingTargetMap.Remove(keyGestureHandler);
    }
  }

  private static void RegisterCommandBinding(ICommandSource commandSource, KeyGesture keyGesture)
  {
    if (keyGesture == null)
    {
      return;
    }

    if (!InputBindingSourceMap.TryGetValue(keyGesture, out IList<ICommandSource>? commandSources))
    {
      commandSources = new List<ICommandSource>();
      InputBindingSourceMap.Add(keyGesture, commandSources);
    }
    commandSources.Add(commandSource);
  }

  private static void UnregisterCommandBinding(ICommandSource commandSource, KeyGesture keyGesture)
  {
    if (keyGesture == null)
    {
      return;
    }

    if (InputBindingSourceMap.TryGetValue(keyGesture, out IList<ICommandSource>? commandSources))
    {
      commandSources.Remove(commandSource);
      if (!commandSources.Any())
      {
        InputBindingSourceMap.Remove(keyGesture);
      }
    }
  }
}
  • Related