当TextBox获得焦点时,UserControl中的KeyBinding无法工作

25

以下是情况简述。我有一个带有五个按键绑定的用户控件(UserControl)。当文本框(TextBox)获得焦点时,用户控件的按键绑定功能就停止了。

有没有办法解决这个“问题”?

<UserControl.InputBindings>
    <KeyBinding Key="PageDown" Modifiers="Control" Command="{Binding NextCommand}"></KeyBinding>
    <KeyBinding Key="PageUp" Modifiers="Control" Command="{Binding PreviousCommand}"></KeyBinding>
    <KeyBinding Key="End" Modifiers="Control"  Command="{Binding LastCommand}"></KeyBinding>
    <KeyBinding Key="Home" Modifiers="Control" Command="{Binding FirstCommand}"></KeyBinding>
    <KeyBinding Key="F" Modifiers="Control" Command="{Binding SetFocusCommand}"></KeyBinding>
</UserControl.InputBindings>
<TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Gesture="Enter" Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl }}, Path=DataContext.FilterCommand}"></KeyBinding>
    </TextBox.InputBindings>
</TextBox>

看起来功能键(F1等)和ALT+[key]可以正常工作。我猜测CTRLSHIFT修饰符有些方式阻止了事件冒泡到用户控件。


文本框是否在用户控件内? - D J
@DJ 没错。UserControl 是容器,(在这个例子中)里面有一个 TextBox。它也适用于 ComboBox、DataGrid 等。 - Ralf de Kleine
6个回答

54
一些输入绑定有效而另一些无效的原因是TextBox控件会捕捉并处理一些按键绑定。例如,它会处理粘贴的CTRL+V,将光标移到文本开头的CTRL+Home等。然而,其他一些按键组合如CTRL+F3则未被TextBox处理,因此这些按键将冒泡传递到上层。
如果你只是想禁用TextBox的输入绑定,那很简单-你可以使用命令来禁用默认行为。例如,在下面的例子中,使用CTRL+V进行粘贴将被禁用:
<TextBox>
    <TextBox.InputBindings>
        <KeyBinding Key="V" Modifiers="Control" Command="ApplicationCommands.NotACommand" />
    </TextBox.InputBindings>
</TextBox>

不过,让它冒泡到用户控件上有点棘手。我的建议是创建一个附加的行为,应用于 UserControl,并在其 PreviewKeyDown 事件中注册,然后在 TextBox 接收输入绑定之前根据需要执行其输入绑定。这将使 UserControl 在执行输入绑定时具有优先权。

我编写了一个基本的行为,以实现此功能,帮助您入门:

public class InputBindingsBehavior
{
    public static readonly DependencyProperty TakesInputBindingPrecedenceProperty =
        DependencyProperty.RegisterAttached("TakesInputBindingPrecedence", typeof(bool), typeof(InputBindingsBehavior), new UIPropertyMetadata(false, OnTakesInputBindingPrecedenceChanged));

    public static bool GetTakesInputBindingPrecedence(UIElement obj)
    {
        return (bool)obj.GetValue(TakesInputBindingPrecedenceProperty);
    }

    public static void SetTakesInputBindingPrecedence(UIElement obj, bool value)
    {
        obj.SetValue(TakesInputBindingPrecedenceProperty, value);
    }

    private static void OnTakesInputBindingPrecedenceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((UIElement)d).PreviewKeyDown += new KeyEventHandler(InputBindingsBehavior_PreviewKeyDown);
    }

    private static void InputBindingsBehavior_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        var uielement = (UIElement)sender;

        var foundBinding = uielement.InputBindings
            .OfType<KeyBinding>()
            .FirstOrDefault(kb => kb.Key == e.Key && kb.Modifiers == e.KeyboardDevice.Modifiers);

        if (foundBinding != null)
        {
            e.Handled = true;
            if (foundBinding.Command.CanExecute(foundBinding.CommandParameter))
            {
                foundBinding.Command.Execute(foundBinding.CommandParameter);
            }
        }
    }
}

用法:

<UserControl local:InputBindingsBehavior.TakesInputBindingPrecedence="True">
    <UserControl.InputBindings>
        <KeyBinding Key="Home" Modifiers="Control" Command="{Binding MyCommand}" />
    </UserControl.InputBindings>
    <TextBox ... />
</UserControl>

希望这能有所帮助。


@AdiLester,我已经尝试了您的解决方案。我只是用kb => kb.Key == Key.Enter替换了您的lambda表达式。我还将KeyBinding中的KeyHome更改为Enter。并删除了Modifiers。我的TextBox位于DataGridColumnHeader内部。因此,我将附加属性应用于DataGrid。现在,当我按下除Enter键以外的任何键时,例如向下箭头键,命令都会触发。我只想在按下Enter键时触发我的命令。您能否对上述代码进行一些更改? - Vishal
@Adi Lester PreviewKeyDown += new KeyEventHandler,你要从事件中取消注册吗? - Gilad
1
@Gilad 就内存泄漏而言,没有必要取消注册事件,因为没有任何对象被持有。您“持有”对静态方法的引用,该方法属于没有实际对象实例的类。即使假设这不是一个静态方法,您仍然不必取消注册事件,因为在UIElement 不再从其他地方引用时,InputBindingsBehavior 实例将会被释放。 - Adi Lester
@AdiLester,你的例子帮了我很多。我尝试了很多想法几天才达到这个结果。谢谢你。 - marcelo
@AdiLester 这正是我在寻找的!非常感谢!! - Andrea Cattaneo

4
Adi Lester的解决方案很好。这里是使用Behavior的类似解决方案。 以下是C#代码:
public class AcceptKeyBinding : Behavior<UIElement>
{


    private TextBox _textBox;



    /// <summary>
    ///  Subscribes to the PreviewKeyDown event of the <see cref="TextBox"/>.
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();

        _textBox = AssociatedObject as TextBox;

        if (_textBox == null)
        {
            return;
        }

        _textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;
    }

    private void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs keyEventArgs)
    {
        var uielement = (UIElement)sender;

        var foundBinding = uielement.InputBindings
            .OfType<KeyBinding>()
            .FirstOrDefault(kb => kb.Key == keyEventArgs.Key && kb.Modifiers ==           keyEventArgs.KeyboardDevice.Modifiers);

        if (foundBinding != null)
        {
            keyEventArgs.Handled = true;
            if (foundBinding.Command.CanExecute(foundBinding.CommandParameter))
            {
                foundBinding.Command.Execute(foundBinding.CommandParameter);
            }
        }
    }

    /// <summary>
    ///     Unsubscribes to the PreviewKeyDown event of the <see cref="TextBox"/>.
    /// </summary>
    protected override void OnDetaching()
    {
        if (_textBox == null)
        {
            return;
        }

        _textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;

        base.OnDetaching();
    }

}

以下是 XAML 代码:

<TextBox>
  <TextBox.InputBindings>
      <KeyBinding Key="Enter" Modifiers="Shift" Command="{Binding CommandManager[ExecuteCommand]}"
          CommandParameter="{Binding ExecuteText}" />
  </TextBox.InputBindings>
      <i:Interaction.Behaviors>
         <behaviours:AcceptKeyBinding />
      </i:Interaction.Behaviors>
</TextBox>

4
除了Adi Lester的(非常有帮助的)回答之外,我还想建议一些改进和扩展,这些对我的实现有所帮助。
手势匹配
可以通过调用Gesture.Matches来进行foundBinding的匹配。将foundBinding Linq查询更改为以下内容:
KeyBinding foundBinding = ((UIElement)this).InputBindings
            .OfType<KeyBinding>()
            .FirstOrDefault(inputBinding => inputBinding.Gesture.Matches(sender, eventArgs));

鼠标绑定

此外,您还可以定义鼠标绑定。

<MouseBinding Command="{Binding DataContext.AddInputValueCommand, ElementName=root}" CommandParameter="{Binding}" Gesture="Shift+MiddleClick" />

您还需要订阅PreviewMouseEvents,例如PreviewMouseUp和PreviewMouseDoubleClick。实现方式与KeyBindings几乎相同。
private void OnTextBoxPreviewMouseUp(object sender, MouseButtonEventArgs eventArgs)
{
    MouseBinding foundBinding = ((UIElement)this).InputBindings
        .OfType<MouseBinding>()
        .FirstOrDefault(inputBinding => inputBinding.Gesture.Matches(sender, eventArgs));

    if (foundBinding != null)
    {
        eventArgs.Handled = true;
        if (foundBinding.Command.CanExecute(foundBinding.CommandParameter))
        {
            foundBinding.Command.Execute(foundBinding.CommandParameter);
        }
    }
}

2

这个帖子虽然有点老,但是很多人都遇到了这个问题。我的研究表明,Adi Lester的解决方案是唯一一个不是“肮脏”变通的方案。 对于任何需要的人,VisualBasic.NET实现如下:

Public Class InputBindingsBehavior
    Public Shared ReadOnly TakesInputBindingPrecedenceProperty As DependencyProperty = DependencyProperty.RegisterAttached("TakesInputBindingPrecedence", GetType(Boolean), GetType(InputBindingsBehavior), New UIPropertyMetadata(False, AddressOf OnTakesInputBindingPrecedenceChanged))

    Public Shared Function GetTakesInputBindingPrecedence(obj As UIElement) As Boolean
        Return obj.GetValue(TakesInputBindingPrecedenceProperty)
    End Function

    Public Shared Sub SetTakesInputBindingPrecedence(obj As UIElement, value As Boolean)
        obj.SetValue(TakesInputBindingPrecedenceProperty, value)
    End Sub

    Public Shared Sub OnTakesInputBindingPrecedenceChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        AddHandler DirectCast(d, UIElement).PreviewKeyDown, AddressOf InputBindingsBehavior_PreviewKeyDown
    End Sub

    Public Shared Sub InputBindingsBehavior_PreviewKeyDown(sender As Object, e As KeyEventArgs)
        Dim uielement = DirectCast(sender, UIElement)

        Dim foundBinding = uielement.InputBindings.OfType(Of KeyBinding).FirstOrDefault(Function(kb As KeyBinding) kb.Key = e.Key And kb.Modifiers = e.KeyboardDevice.Modifiers)

        If foundBinding IsNot Nothing Then
            e.Handled = True
            If foundBinding.Command.CanExecute(foundBinding.CommandParameter) Then
                foundBinding.Command.Execute(foundBinding.CommandParameter)
            End If
        End If
    End Sub

End Class


提到的其余内容如下。

0
<UserControl.Style>
    <Style TargetType="UserControl">
        <Style.Triggers>
            <Trigger Property="IsKeyboardFocusWithin" Value="True">
                <Setter Property="FocusManager.FocusedElement" Value="   {Binding ElementName=keyPressPlaceHoler}" />
                </Trigger>
        </Style.Triggers>
    </Style>
</UserControl.Style>

keyPressPlaceHoler 是您目标 uielement 的容器名称。

请记得在用户控件中设置 Focusable="True"


0

当涉及到文本框时,代码后台是唯一的方法,我同意@adi-lester的答案。

 private void Control_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.SystemKey == Key.PageDown && (e.KeyboardDevice.Modifiers == ModifierKeys.Alt || .KeyboardDevice.Modifiers == ModifierKeys.Alt) )
        {
            //do something
        }
    }

.

anyTextBox.PreviewKeyUp += TabControl_PreviewKeyDown;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接