【问题标题】:InputBindings work only when focusedInputBindings 仅在获得焦点时起作用
【发布时间】:2014-04-26 20:39:57
【问题描述】:

我设计了一个可重复使用的用户控件。它包含 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>

正如所说的,每个按钮在鼠标单击时都有效(执行被触发),当聚焦时,我可以使用空间来激活按钮,并且聚焦按钮的输入绑定有效,但从不聚焦。

【问题讨论】:

    标签: wpf mvvm user-controls .net-4.5 inputbinding


    【解决方案1】:

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

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

    我的建议是使用附加行为将这些输入绑定从 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>
    

    【讨论】:

    • 附加行为有效,也可以重用于其他组件/用途。绑定仍然有效。
    • 以另一种方式尝试了相同的方法。但这也将 InputBindings 元素添加到 MainWindow。但它不会传播 commandParameter :( 任何解决方案?&lt;KeyBinding Gesture="CTRL+S" Command="{Binding SaveCommand, Source={StaticResource Locator}}" CommandParameter="{Binding}" /&gt;
    • 如果在组件加载后设置了DataContext,这个解决方案将不起作用。处理 DataContextChanged 事件而不是 Loaded 应该可以,但我没有对此进行测试(但是,如果 DataContext 被多次更改,这可能会导致问题)。
    • 它正在工作,但会引发这些错误 System.Windows.Data 错误:2:找不到目标元素的管理 FrameworkElement 或 FrameworkContentElement。绑定表达式:路径=SeekBackCommand; DataItem='MediaPlayerUI'(名称='UI');目标元素是“KeyBinding”(HashCode=60332585);目标属性是“Command”(输入“ICommand”)
    • 我使用了这段代码,但做了一些修改:在加载的事件处理程序中,我还将 CommandTarget 设置为 frameworkElement,以便将绑定的命令重定向到它。我删除了 frameworkElement.InputBindings.Remove() 调用,并添加了一个未加载的事件处理程序,它与加载的处理程序相反,即从父窗口中删除输入绑定。
    【解决方案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 转移到 C#。 ViewModel 仍然独立于 View,因此可以在不实例化 View 的情况下进行单元测试。将这种 UI 特定 逻辑放在视图的代码隐藏中绝对没问题。

    【讨论】:

      【解决方案3】:

      我们扩展了 Adi Lesters 附加的行为代码,在 UnLoaded 上使用取消订阅机制来清理传输的绑定。如果控件退出可视树,则 InputBindings 将从窗口中删除以避免它们处于活动状态。 (我们没有探索在附加属性上使用 WPF-Triggers。)

      当控件在我们的解决方案中被 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);
                  }
              }
          }
      }
      

      【讨论】:

        【解决方案4】:

        有点晚了,可能不是 100% 符合 MVVM,可以使用以下 onloaded-event 将所有 Inputbindings 传播到窗口。

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

        由于这只影响视图层,就 MVVM 而言,我可以使用此解决方案。找到这个位here

        【讨论】:

          【解决方案5】:
          <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"

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2014-02-16
            • 2013-10-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多