caesay
caesay

Reputation: 17213

WPF KeyBinding swallowing keys, preventing TextBox use

Problem Overview:
Any KeyBinding's defined at a level higher than a TextBox (with no modifier keys assigned), prevents the user from typing those keys inside the TextBox.

Minimal XAML Hierarchy:

<Window>
  <UserControl>
    <Border>
      <UserControl>
        <TextBox>

Minimal Command/KeyBinding:

<UserControl.Resources>
    <RoutedUICommand x:Key="Commands.SomeCommand" />
</UserControl.Resources>
<UserControl.InputBindings>
    <KeyBinding Key="A" Command="{StaticResource Commands.SomeCommand}" />
</UserControl.InputBindings>
<UserControl.CommandBindings>
    <CommandBinding Command="{StaticResource Commands.SomeCommand}" Executed="..." />
</UserControl.CommandBindings>

The Command and KeyBinding, are defined at the first UserControl level. So in this example, in the textbox, the user can type freely until they press the A key, and then it just does not insert the letter into the textbox. I can clearly see that the TextBox.KeyDown and TextBox.PreviewKeyDown are firing when you press the A key (and Handled = false) , but the letter will not get added to the text of the textbox and TextBox.PreviewTextInput does not fire.

I'm looking for any suggestions that may indicate what is swallowing the keypress and stopping it from getting processed by the TextBox, or anything related to how I can debug this issue.

EDIT:
Thanks to Snoop, I was able to clearly see the problem.

  1. TextBox.PreviewKeyDown tunnels down and fires through the visual tree, starting at the Window, and ending at the TextBox
  2. TextBox.KeyDown bubbles back up starting at the TextBox and heading towards the window
  3. TextBox.KeyDown gets Handled set to true by the first UserControl that has the KeyBinding set.
  4. TextBox.PreviewTextInput never fires, nor does the textbox process the input, because the KeyDown event was set as handled.

This still leaves the problem, how do you prevent the UserControl from handling the input if a textbox has focus? Within the Command execution, I can check if a textbox has keyboard focus, but by this time it's too late.

Upvotes: 9

Views: 5789

Answers (4)

caesay
caesay

Reputation: 17213

I had used the TextComposition RaiseEvent approach for years, however this seems to break typing for non-latin keyboard layouts (eg. cyrillic).

The proper way to do this is to derive from InputBinding and return false in the Matches? check if the event originated from a text-box.

/// <summary>
/// This gesture doesn't handle keys originating in a text control. This allows key bindings without modifier keys
/// that don't break normal typing. A standard KeyGesture doesn't have such logic; this allows the parent of a
/// text box to handle such bare keypresses before the textbox gets to see it as normal text input, thus breaking
/// normal typing.
/// </summary>
public class BareKeyGesture : InputGesture
{
    public Key Key { get; set; }

    public override bool Matches(object targetElement, InputEventArgs inputEventArgs)
    {
        var keyEventArgs = inputEventArgs as KeyEventArgs;
        if (keyEventArgs == null)
            return false;

        if (inputEventArgs.OriginalSource is TextBoxBase)
            return false;

        return (int)Key == (int)keyEventArgs.Key && Keyboard.Modifiers == ModifierKeys.None;
    }
}

/// <summary>
/// This only exists because the InputBinding constructor is protected, but since we have to have it anyway
/// we also use this opportunity to simplify adding a BareKeyGesture to it.
/// </summary>
public class BareKeyBinding : InputBinding
{
    private BareKeyGesture _gesture = new();

    public BareKeyBinding()
    {
        Gesture = _gesture;
    }

    public Key Key
    {
        get => _gesture.Key;
        set { _gesture.Key = value; }
    }
}

And now that you have an InputGesture which will ignore events originating from textboxes, you can use it in XAML like normal:

<UserControl.InputBindings>
    <nsp:BareKeyBinding Key="D" Command="{...}" />
</UserControl.InputBindings>

Upvotes: 2

daniloquio
daniloquio

Reputation: 3902

As long as you use KeyBinding this not going to work without major hacks. A solution I implemented for this is:

  1. Use the KeyDown event to capture those keys being pressed (instead of KeyBindings). This will be on your code-behind and from there you'll need to switch on the pressed Key to call the required command (SomeCommand in your case).
  2. Now you have a different problem. The TextBox is getting the input but your key-bound commands are also firing. On the code behind, check the type of keyEventArgs.InputSource and ignore the key stroke if it's a TextBox.

It should look like this:

private void OnKeyDown(object sender, KeyEventArgs e)
{
    ICommand command = null;

    switch (e.Key)
    {
        case Key.A:
            command = Commands.SomeCommand;
            break;
        case Key.B:
            command = Commands.SomeOtherCommand;
            break;
    }

    bool isSourceATextBox = e.InputSource.GetType() == typeof(TextBox);
    if (command != null && !isSourceATextBox)
    {
        command.Execute(parameter:null);
    }
}

Upvotes: 1

Marek Mikulec
Marek Mikulec

Reputation: 64

I have the same problem. I took a look to documentation for key bindind, and there is described, that the key on which you bind shouldn't be just key, but key gesture, so it shall be

  • Modifier key + normal key
  • Numeric keypad key
  • Functional key.

Of course, it works with just A, but it's bad practice overall. You should consider to implement some of the posibilities mentioned behind. More at https://msdn.microsoft.com/cs-cz/library/system.windows.input.keybinding(v=vs.110).aspx

Upvotes: 1

Troels Larsen
Troels Larsen

Reputation: 4631

TextInput and PreviewTextInput only fires when the Text actually changes / might change.

As you updated your question to reflect, the Command intercepts the event and the (Preview)TextInput events are never raised.

The nicest solution would be to add a modifier key to your KeyBinding, but I suspect that is not your preferred way to go.

Another option would be to e.Handle the PreviewKeyDown event on the TextBox and raise the TextComposition events yourself, using something like:

target.RaiseEvent(new TextCompositionEventArgs(InputManager.Current.PrimaryKeyboardDevice, 
new TextComposition(InputManager.Current, target, "A")) 
{ 
    RoutedEvent = TextCompositionManager.TextInputEvent 
});

(Alternatively, insert into textBox.Text at the correct CaretIndex)

Truth be told, it would still be a hack.

Upvotes: 1

Related Questions