InputBindings работают только при фокусировке

Я разработал повторно используемый пользовательский контроль. Он содержит UserControl.InputBindings. Это довольно просто, поскольку в нем есть только ярлык и кнопка (и новые свойства и т.д.).

Когда я использую элемент управления в своем окне, он работает хорошо. Но привязка ключа работает только при фокусировке. Когда один элемент управления имеет привязку к alt + f8, этот ярлык работает только тогда, когда он сфокусирован. Когда фокусируется другой, связанный с собственным привязкой, он работает, но не более + f8. Когда ни один из элементов управления не имеет фокуса, ничего не работает.

Как я могу добиться того, что мой usercontrol определяет привязки оконных окон?

Особенно после шаблона проектирования 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>

Как сказано, каждая кнопка работает (Execute увольняется) при щелчке мыши, и когда она сфокусирована, я могу использовать пробел для активации кнопки, а привязка ввода сфокусированной кнопки работает, но никогда не сфокусированная.

Ответ 1

InputBindings не будут выполняться для элемента управления, который не сфокусирован из-за того, как они работают - обработчик для привязки ввода ищет в визуальном дереве от сфокусированного элемента к корню визуального дерева (окно). Когда элемент управления не сфокусирован, он не будет частью этого пути поиска.

Как уже упоминал @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>

Ответ 2

Да, 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 на С#. ViewModel по-прежнему не зависит от представления, и как таковой может быть Unit Test, без создания экземпляра View. Совершенно верно поставить такую ​​специфическую логику пользовательского интерфейса в кодовое представление представления.

Ответ 3

<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" в usercontrol

Ответ 4

Тем не менее, немного поздно и, возможно, не 100% MVVM, можно использовать следующее onloaded-событие для распространения всех входных данных в окне.

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

Поскольку это влияет только на View-Layer, мне было бы хорошо с этим решением с точки зрения MVVM. нашел этот бит здесь

Ответ 5

Мы расширили присоединенный код поведения Ади Лестера с помощью механизма отмены подписки в UnLoaded, чтобы очистить переданные привязки. Если элемент управления выходит из дерева визуалов, привязки ввода удаляются из окна, чтобы они не были активными. (Мы не исследовали использование триггеров 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);
            }
        }
    }
}