How to adjust repetition delay for WPF Command-KeyBindings - Part 2


In the blog post A delayed KeyBinding for WPF I have shown how a repetition delay for KeyBindings can be realized in WPF. However, the proposed solution still has a drawback. The delay is enforced, even when the user actively presses the button repeatedly (as in pushing it down, releasing it and then pushing it down again and so on). Depending on the delay this might lead to user irritation or frustration because it is not what usually is expected. Unfortunately, there is no easy way to extend the wrapped command method used in the previous post, so we are going to engineer a new solution.

Previously, we derived our DelayKeyBinding from WPF’s KeyBinding. If we take a closer look at this class, we see that its base class is InputBinding which is basically creating a relationship between an ICommand and an InputGesture. In the case of KeyBinding the InputGesture is always a KeyGesture. If we inspect the (abstract) class InputGesture we see that it basically only consists of one important method with the following signature:

public abstract bool Matches (object targetElement, System.Windows.Input.InputEventArgs inputEventArgs);

Now it becomes clear how the InputBinding system works: once you add an instance of an InputBinding class to a control’s InputBindings collection all RoutedCommand related to input (i.e. mouse, keyboard, stylus and touch events) are fed to the Matches function of InputBinding’s InputGesture. These are the same arguments which are handed to the input events of WPF controls (KeyDown, KeyUp, MouseDown, MouseUp, etc.). If this function returns true the framework executes the ICommand associated with the InputBinding. Consequently, for keyboard events the corresponding KeyEventArgs are fed to the InputGesture. Fortunately, the KeyEventArgs contain the IsRepeat property which indicates that the key press has been generated due to the user holding down the button. Now, we can use this in a custom InputGesture to limit our delay to repeated key presses. The following DelayKeyGesture wraps an InputGesture and delays the command execution (i.e. Matches returns false) when the RepeatDelay has not elapsed while the KeyEvent’s IsRepeat property is true:

class DelayKeyGesture : KeyGesture
{
 long lastExecutionTick;

 InputGesture WrappedGesture { get; }
 
 /// <summary>
 /// The repeat delay in milliseconds
 /// </summary>
 public int RepeatDelay { get; set; }
 
 public DelayKeyGesture(KeyGesture wrappedGesture, int repeatDelay)
     : base (wrappedGesture.Key, wrappedGesture.Modifiers, wrappedGesture.DisplayString)
 {
     WrappedGesture = wrappedGesture;
     RepeatDelay = repeatDelay;
 }
 
 public override bool Matches(object targetElement, InputEventArgs inputEventArgs)
 {
     bool baseResult = WrappedGesture.Matches(targetElement, inputEventArgs);
 
     if (baseResult)
     {
         if (inputEventArgs is KeyEventArgs kargs)
         {
             // only delay repetitions (i.e. when the user keeps the key pressed)
             if (kargs.IsRepeat)
             {
                 // check if the time elapsed already
                 if (Environment.TickCount - lastExecutionTick >= RepeatDelay)
                 {
                     lastExecutionTick = Environment.TickCount;
                     return true;
                 }
                 else
                 {
                     return false;
                 }
             }
             else
             {
                 lastExecutionTick = Environment.TickCount;
             }
         }
     }
 
     return baseResult;
}

Now we can adapt our DelayKeyBinding class from the last time as follows:

class DelayKeyBindingEx : KeyBinding
{
    // Using a DependencyProperty as the backing store for RepeatDelay.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RepeatDelayProperty =
        DependencyProperty.Register(nameof(RepeatDelay), typeof(int), typeof(DelayKeyBindingEx), new PropertyMetadata(0, RepeatDelayPropertyChangedCallback));

    /// <summary>
    /// The repeat delay in milliseconds
    /// </summary>
    public int RepeatDelay
    {
        get { return (int)GetValue(RepeatDelayProperty); }
        set { SetValue(RepeatDelayProperty, value); }
    }

    [System.ComponentModel.TypeConverter(typeof(System.Windows.Input.KeyGestureConverter))]
    public override InputGesture Gesture {
        get => base.Gesture;
        set => base.Gesture = value == null ? null : new DelayKeyGesture(value as KeyGesture, RepeatDelay); }

    static void RepeatDelayPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {            
        // Ensure that a changed RepeatDelay is passed to the underlying InputGesture
        if ((d as DelayKeyBindingEx).Gesture is DelayKeyGesture gesture)
        {
            gesture.RepeatDelay = (int)e.NewValue;
        }            
    }        
}

The reason why we derive DelayKeyGesture from KeyGesture and not from InputGesture is that the KeyBinding class itself, internally checks if the assigned InputGesture is of type KeyGesture. If it is not it throws an exception. Since we still want our InputBinding to identify as KeyBinding to maximize compatibility we have to comply with this. This is also the reason why we still use the default System.Windows.Input.KeyGestureConverter for the override of the Gesture property’s setter and wrap the assigned InputGesture in there instead of rolling our own converter which creates a DelayKeyGesture. Also this allows to assign any KeyGesture programmatically which is then still enabled to support delay. That’s it, now we have a DelayKeyBinding which only delays command execution when the user holds down the key but not when they hammer it repeatedly.