输入绑定只有在焦点状态下才能起作用。

11

我设计了一个可重用的用户控件。它包含 UserControl.InputBindings。它非常简单,只包含一个标签和一个按钮(以及新的属性等)。

当我将控件用于我的窗口时,它运行良好。但是按键绑定仅在焦点下工作。当一个控件具有 alt+f8 的绑定时,仅当它获得焦点时,该快捷键才有效。当另一个控件获得焦点并具有自己的绑定时,该绑定将起作用,但 alt+f8 不再起作用。当没有任何控件获得焦点时,什么也不会发生。

如何实现我的用户控件定义全局的键盘绑定?

特别是采用 MVVM 设计模式(使用 Caliburn.Micro),但是任何帮助都将不胜感激。


用户控件的 XAML:

<UserControl x:Class="MyApp.UI.Controls.FunctionButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MyApp.UI.Controls"
             xmlns:cm="http://www.caliburnproject.org"
             x:Name="Root"
             Focusable="True"
             mc:Ignorable="d" 
             d:DesignHeight="60" d:DesignWidth="120">
    <UserControl.Resources>
        ...
    </UserControl.Resources>
    <UserControl.InputBindings>
        <KeyBinding Key="{Binding ElementName=Root, Path=FunctionKey}" Modifiers="{Binding ElementName=Root, Path=KeyModifiers}" Command="{Binding ElementName=Root, Path=ExecuteCommand}" />
    </UserControl.InputBindings>
    <DockPanel LastChildFill="True">
        <TextBlock DockPanel.Dock="Top" Text="{Binding ElementName=Root, Path=HotkeyText}" />
        <Button DockPanel.Dock="Bottom" Content="{Binding ElementName=Root, Path=Caption}" cm:Message.Attach="[Event Click] = [Action ExecuteButtonCommand($executionContext)]" cm:Action.TargetWithoutContext="{Binding ElementName=Root}" />
    </DockPanel>
</UserControl>

示例用法:

    <Grid>
    <c:FunctionButton Width="75" Height="75" Margin="10,10,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" FunctionKey="F1" ShiftModifier="True" cm:Message.Attach="[Event Execute] = [Action Button1Execute]" />
    <c:FunctionButton Width="75" Height="75" Margin="10,90,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" FunctionKey="F2" ShiftModifier="True" cm:Message.Attach="[Event Execute] = [Action Button2Execute]" />
</Grid>

如所述,每个按钮都会在鼠标单击时工作(触发执行),当聚焦时,我可以使用空格键激活按钮,聚焦的按钮的输入绑定有效,但未聚焦的按钮则无效。

5个回答

37

由于输入绑定的工作方式,对于未获得焦点的控件,输入绑定不会被执行 - 在可视树中从焦点元素到可视树根(窗口)搜索输入绑定的处理程序。当一个控件没有焦点时,它不会成为该搜索路径的一部分。

正如@Wayne所提到的,最好的方法是将输入绑定简单地移动到父窗口。然而,有时这是不可能的(例如,当UserControl没有在窗口的xaml文件中定义时)。

我的建议是使用附加行为将这些输入绑定从UserControl移动到窗口。通过使用附加行为来实现这一点,还可以使其能够适用于任何FrameworkElement,而不仅仅是您的UserControl。因此,基本上你会有类似下面的东西:

public class InputBindingBehavior
{
    public static bool GetPropagateInputBindingsToWindow(FrameworkElement obj)
    {
        return (bool)obj.GetValue(PropagateInputBindingsToWindowProperty);
    }

    public static void SetPropagateInputBindingsToWindow(FrameworkElement obj, bool value)
    {
        obj.SetValue(PropagateInputBindingsToWindowProperty, value);
    }

    public static readonly DependencyProperty PropagateInputBindingsToWindowProperty =
        DependencyProperty.RegisterAttached("PropagateInputBindingsToWindow", typeof(bool), typeof(InputBindingBehavior),
        new PropertyMetadata(false, OnPropagateInputBindingsToWindowChanged));

    private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((FrameworkElement)d).Loaded += frameworkElement_Loaded;
    }

    private static void frameworkElement_Loaded(object sender, RoutedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)sender;
        frameworkElement.Loaded -= frameworkElement_Loaded;

        var window = Window.GetWindow(frameworkElement);
        if (window == null)
        {
            return;
        }

        // Move input bindings from the FrameworkElement to the window.
        for (int i = frameworkElement.InputBindings.Count - 1; i >= 0; i--)
        {
            var inputBinding = (InputBinding)frameworkElement.InputBindings[i];
            window.InputBindings.Add(inputBinding);
            frameworkElement.InputBindings.Remove(inputBinding);
        }
    }
}

使用方法:

<c:FunctionButton Content="Click Me" local:InputBindingBehavior.PropagateInputBindingsToWindow="True">
    <c:FunctionButton.InputBindings>
        <KeyBinding Key="F1" Modifiers="Shift" Command="{Binding FirstCommand}" />
        <KeyBinding Key="F2" Modifiers="Shift" Command="{Binding SecondCommand}" />
    </c:FunctionButton.InputBindings>
</c:FunctionButton>

附加的行为可以正常工作,并且也可以被重复使用于其他组件或用途。同时,绑定仍然有效。 - ZoolWay
尝试了另一种方式,但它也向MainWindow添加了InputBindings元素。但是它不会传播commandParameter :(有什么解决办法吗?<KeyBinding Gesture="CTRL+S" Command="{Binding SaveCommand, Source={StaticResource Locator}}" CommandParameter="{Binding}" /> - John
如果在组件加载后设置DataContext,则此解决方案将无效。相反,处理DataContextChanged事件而不是Loaded应该可以解决问题,但我尚未测试过(但是,如果DataContext更改超过一次,则可能会引起问题)。 - larsmoa
1
它能够工作,但是会抛出以下错误: System.Windows.Data Error: 2 : 找不到目标元素的主控 FrameworkElement 或 FrameworkContentElement。BindingExpression:Path=SeekBackCommand; DataItem='MediaPlayerUI' (Name='UI'); 目标元素为 'KeyBinding' (HashCode=60332585); 目标属性为 'Command' (类型为 'ICommand')。 - Etienne Charland
我使用了这段代码,但进行了一些编辑:在加载事件处理程序中,我还将“CommandTarget”设置为frameworkElement,以便将绑定的命令重定向到它。我删除了frameworkElement.InputBindings.Remove()调用,并添加了一个卸载事件处理程序,该处理程序执行与加载处理程序相反的操作,即从父窗口中删除输入绑定。 - Sasino

4

是的,UserControl KeyBindings仅在控件具有焦点时才起作用。

如果你希望KeyBinding在窗口上工作,则必须在窗口本身上定义它。你可以在Windows XAML中使用以下代码来实现:

<Window.InputBindings>
  <KeyBinding Command="{Binding Path=ExecuteCommand}" Key="F1" />
</Window.InputBindings>

然而,您说您希望UserControl定义KeyBinding。 我不知道在XAML中如何做到这一点,因此您需要在UserControl的代码后台中设置它。这意味着找到UserControl的父窗口,并创建KeyBinding。

{
    var window = FindVisualAncestorOfType<Window>(this);
    window.InputBindings.Add(new KeyBinding(ViewModel.ExecuteCommand, ViewModel.FunctionKey, ModifierKeys.None));
}

private T FindVisualAncestorOfType<T>(DependencyObject d) where T : DependencyObject
{
    for (var parent = VisualTreeHelper.GetParent(d); parent != null; parent = VisualTreeHelper.GetParent(parent)) {
        var result = parent as T;
        if (result != null)
            return result;
    }
    return null;
}

ViewModel.FunctionKey在这种情况下需要是Key类型,否则您需要将其从字符串转换为Key类型。
在代码后台而不是XAML中执行此操作不会破坏MVVM模式。所有正在进行的操作都是将绑定逻辑从XAML移动到C#中。ViewModel仍然独立于View,因此可以在不实例化View的情况下进行单元测试。在视图的代码后台中放置此类UI特定的逻辑是完全可以的。

2
我们在Adi Lesters的附加行为代码中添加了一个取消订阅机制,以清理传输的绑定,当控件退出可视树时,输入绑定将从窗口中移除,以避免它们处于活动状态。(我们没有探索在附加属性上使用WPF触发器。)
由于在我们的解决方案中,WPF会重复使用控件,因此该行为不会分离:Loaded/Unloaded会被多次调用。这不会导致泄漏,因为该行为不持有对FrameworkElement的引用。
    private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((FrameworkElement)d).Loaded += OnFrameworkElementLoaded;
        ((FrameworkElement)d).Unloaded += OnFrameworkElementUnLoaded;
    }

    private static void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)sender;

        var window = Window.GetWindow(frameworkElement);
        if (window != null)
        {
            // transfer InputBindings into our control
            if (!trackedFrameWorkElementsToBindings.TryGetValue(frameworkElement, out var bindingList))
            {
                bindingList = frameworkElement.InputBindings.Cast<InputBinding>().ToList();
                trackedFrameWorkElementsToBindings.Add(
                    frameworkElement, bindingList);
            }

            // apply Bindings to Window
            foreach (var inputBinding in bindingList)
            {
                window.InputBindings.Add(inputBinding);
            }
            frameworkElement.InputBindings.Clear();
        }
    }

    private static void OnFrameworkElementUnLoaded(object sender, RoutedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)sender;
        var window = Window.GetWindow(frameworkElement);

        // remove Bindings from Window
        if (window != null)
        {
            if (trackedFrameWorkElementsToBindings.TryGetValue(frameworkElement, out var bindingList))
            {
                foreach (var binding in bindingList)
                {
                    window.InputBindings.Remove(binding);
                    frameworkElement.InputBindings.Add(binding);
                }

                trackedFrameWorkElementsToBindings.Remove(frameworkElement);
            }
        }
    }

在我们的解决方案中,一些控件不会触发“UnLoaded”事件,尽管它们再也没有被使用,并且在一段时间后被垃圾回收。我们通过使用HashCode/WeakReferences跟踪和复制InputBindings来处理这个问题。
完整的类如下:
public class InputBindingBehavior
{
    public static readonly DependencyProperty PropagateInputBindingsToWindowProperty =
        DependencyProperty.RegisterAttached("PropagateInputBindingsToWindow", typeof(bool), typeof(InputBindingBehavior),
            new PropertyMetadata(false, OnPropagateInputBindingsToWindowChanged));

    private static readonly Dictionary<int, Tuple<WeakReference<FrameworkElement>, List<InputBinding>>> trackedFrameWorkElementsToBindings =
        new Dictionary<int, Tuple<WeakReference<FrameworkElement>, List<InputBinding>>>();

    public static bool GetPropagateInputBindingsToWindow(FrameworkElement obj)
    {
        return (bool)obj.GetValue(PropagateInputBindingsToWindowProperty);
    }

    public static void SetPropagateInputBindingsToWindow(FrameworkElement obj, bool value)
    {
        obj.SetValue(PropagateInputBindingsToWindowProperty, value);
    }

    private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((FrameworkElement)d).Loaded += OnFrameworkElementLoaded;
        ((FrameworkElement)d).Unloaded += OnFrameworkElementUnLoaded;
    }

    private static void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)sender;

        var window = Window.GetWindow(frameworkElement);
        if (window != null)
        {
            // transfer InputBindings into our control
            if (!trackedFrameWorkElementsToBindings.TryGetValue(frameworkElement.GetHashCode(), out var trackingData))
            {
                trackingData = Tuple.Create(
                    new WeakReference<FrameworkElement>(frameworkElement),
                    frameworkElement.InputBindings.Cast<InputBinding>().ToList());

                trackedFrameWorkElementsToBindings.Add(
                    frameworkElement.GetHashCode(), trackingData);
            }

            // apply Bindings to Window
            foreach (var inputBinding in trackingData.Item2)
            {
                window.InputBindings.Add(inputBinding);
            }

            frameworkElement.InputBindings.Clear();
        }
    }

    private static void OnFrameworkElementUnLoaded(object sender, RoutedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)sender;
        var window = Window.GetWindow(frameworkElement);
        var hashCode = frameworkElement.GetHashCode();

        // remove Bindings from Window
        if (window != null)
        {
            if (trackedFrameWorkElementsToBindings.TryGetValue(hashCode, out var trackedData))
            {
                foreach (var binding in trackedData.Item2)
                {
                    frameworkElement.InputBindings.Add(binding);
                    window.InputBindings.Remove(binding);
                }
                trackedData.Item2.Clear();
                trackedFrameWorkElementsToBindings.Remove(hashCode);

                // catch removed and orphaned entries
                CleanupBindingsDictionary(window, trackedFrameWorkElementsToBindings);
            }
        }
    }

    private static void CleanupBindingsDictionary(Window window, Dictionary<int, Tuple<WeakReference<FrameworkElement>, List<InputBinding>>> bindingsDictionary)
    {
        foreach (var hashCode in bindingsDictionary.Keys.ToList())
        {
            if (bindingsDictionary.TryGetValue(hashCode, out var trackedData) &&
                !trackedData.Item1.TryGetTarget(out _))
            {
                Debug.WriteLine($"InputBindingBehavior: FrameWorkElement {hashCode} did never unload but was GCed, cleaning up leftover KeyBindings");

                foreach (var binding in trackedData.Item2)
                {
                    window.InputBindings.Remove(binding);
                }

                trackedData.Item2.Clear();
                bindingsDictionary.Remove(hashCode);
            }
        }
    }
}

1

有些晚了,可能不是100%符合MVVM,但可以使用以下的onloaded事件将所有InputBindings传播到窗口。

void UserControl1_Loaded(object sender, RoutedEventArgs e)
    {
        Window window = Window.GetWindow(this);
        foreach (InputBinding ib in this.InputBindings)
        {
            window.InputBindings.Add(ib);
        }
    }

由于这只影响视图层,就MVVM而言,我认为这个解决方案是可行的。在这里找到了这一段链接


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 是您目标 UI 元素的容器名称

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


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