InputBindings work only when focused

落爺英雄遲暮 提交于 2019-11-27 08:27:24

InputBindings won't be executed for a control that isn't focused because of the way they work - a handler for the input binding is searched in the visual tree from the focused element to the visual tree's root (the window). When a control is not focused, he won't be a part of that search path.

As @Wayne has mentioned, the best way to go would be simply move the input bindings to the parent window. Sometimes however this isn't possible (for example when the UserControl isn't defined in the window's xaml file).

My suggestion would be to use an attached behavior to move these input bindings from the UserControl to the window. Doing so with an attached behavior also has the benefit of being able to work on any FrameworkElement and not just your UserControl. So basically you'll have something like this:

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);
        }
    }
}

Usage:

<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>

Yes, UserControl KeyBindings will only work when the control has focus.

If you want the KeyBinding to work on the window, then you have to define it on the window itself. You do that on the Windows XAML using :

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

However you have said you want the UserControl to define the KeyBinding. I don't know of any way to do this in XAML, so you would have to set up this in the code-behind of the UserControl. That means finding the parent Window of the UserControl and creating the 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;
}

The ViewModel.FunctionKey would need to be of type Key in this case, or else you'll need to convert from a string to type Key.

Having to do this in code-behind rather than XAML does not break the MVVM pattern. All that is being done is moving the binding logic from XAML to C#. The ViewModel is still independent of the View, and as such can be Unit Tested without instantiating the View. It is absolutely fine to put such UI specific logic in the code-behind of a view.

<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 is the name of container of your target uielement

remember to set the Focusable="True" in usercontrol

Yet a bit late and possibly not 100% MVVM conform, one can use the following onloaded-event to propagate all Inputbindings to the window.

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

Since this only affects the View-Layer I would be fine with this solution in terms of MVVM. found this bit here

We extended Adi Lesters attached behavior code with an unsubscribing mechanism on UnLoaded to clean up the transferred bindings. If the control exits the Visual Tree, the InputBindings are removed from the Window to avoid them being active. (We did not explore using WPF-Triggers on the attached property.)

As controls get reused by WPF in our solution, the behavior does not detach: Loaded/UnLoaded get called more than once. This does not lead to leaking, as the behavior doesn't hold a reference to the 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);
            }
        }
    }

Somehow in our solution some controls are not throwing the UnLoaded event, although they never get used again and even get garbage collected after a while. We are taking care of this with tracking with HashCode/WeakReferences and taking a copy of the InputBindings.

Full class is:

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);
            }
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!