【问题标题】:Bind command in view model to keyboard shortcut将视图模型中的命令绑定到键盘快捷键
【发布时间】:2017-03-23 09:37:52
【问题描述】:

我正在使用 C#、WPF、ReactiveUI 和 Prism 创建具有许多不同视图(用户控件)的应用程序。在某些视图上,有一些按钮/菜单项绑定到视图模型中的命令。我希望这些按钮也可以使用 ctrl+s 等组合键来激活......

我的尝试

  • InputBindings 但这仅在定义这些输入绑定的视图具有焦点时才有效。
  • ApplicationCommandsApplicationCommands.Close 这样的预定义命令似乎很有用。我可以在视图和视图模型中引用它们,但我不知道如何在我的视图模型中订阅它们。看来我必须先“激活”命令,或者至少更改 CanExecute,因为绑定到此类命令的任何按钮都保持禁用状态。

我的愿望

假设我有一个视图,它表示顶部菜单栏MenuView,带有一个按钮myButton,对应的视图模型MenuViewModel 带有一个命令myCommand。我想将myButton 绑定到myCommand 并将键盘快捷键ctrl+u 绑定到myCommandMenuView 不知道其视图模型的实现。只要包含MenuView 的窗口有焦点,键盘快捷键就应该起作用。

我并不关心键盘快捷键是在视图中还是在视图模型中。

【问题讨论】:

  • 当我必须将命令绑定到键盘快捷键时,我更喜欢 InputBindings。只是为了确定:我猜您的主窗口不知道您的任何控件,因为它们是通过 PRISM 加载的?这就是为什么您不想将 InputBindings 放在窗口中?
  • 您是否尝试过使用例如为您的InputBindings 附加行为(如this one),以使它们“独立于焦点”?
  • @MightyBadaboom 完全正确!
  • @dymanoid 该解决方案实际上看起来很完美!我遇到了很多堆栈溢出问题,但我还没有看到那个问题。我想我们现在应该将其作为副本关闭?

标签: c# wpf prism reactiveui


【解决方案1】:

您可以创建一个附加的 Blend 行为来处理父窗口的 PreviewKeyDown 事件:

public class KeyboardShortcutBehavior : Behavior<FrameworkElement>
{
    private Window _parentWindow;

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register(nameof(Command), typeof(ICommand),
        typeof(KeyboardShortcutBehavior), new FrameworkPropertyMetadata(null));

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public static readonly DependencyProperty ModifierKeyProperty =
        DependencyProperty.Register(nameof(ModifierKey), typeof(ModifierKeys),
        typeof(KeyboardShortcutBehavior), new FrameworkPropertyMetadata(ModifierKeys.None));

    public ModifierKeys ModifierKey
    {
        get { return (ModifierKeys)GetValue(ModifierKeyProperty); }
        set { SetValue(ModifierKeyProperty, value); }
    }

    public static readonly DependencyProperty KeyProperty =
        DependencyProperty.Register(nameof(Key), typeof(Key),
            typeof(KeyboardShortcutBehavior), new FrameworkPropertyMetadata(Key.None));

    public Key Key
    {
        get { return (Key)GetValue(KeyProperty); }
        set { SetValue(KeyProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += AssociatedObject_Loaded;
        AssociatedObject.Unloaded += AssociatedObject_Unloaded;
    }


    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        _parentWindow = Window.GetWindow(AssociatedObject);
        if(_parentWindow != null)
        {
            _parentWindow.PreviewKeyDown += ParentWindow_PreviewKeyDown;
        }
    }

    private void ParentWindow_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if(Command != null && ModifierKey != ModifierKeys.None && Key != Key.None && Keyboard.Modifiers == ModifierKey && e.Key == Key)
            Command.Execute(null);
    }

    private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
    {
        if(_parentWindow != null)
        {
            _parentWindow.PreviewKeyDown -= ParentWindow_PreviewKeyDown;
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Loaded -= AssociatedObject_Loaded;
        AssociatedObject.Unloaded -= AssociatedObject_Loaded;
    }
}

示例用法:

<TextBox xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
    <i:Interaction.Behaviors>
        <local:KeyboardShortcutBehavior ModifierKey="Ctrl" Key="U" Command="{Binding myCommand}" />
    </i:Interaction.Behaviors>
</TextBox>

【讨论】:

    【解决方案2】:

    在代码后面很容易。创建一些最终导致可观察到的父窗口键事件的实用程序函数。请注意,您将需要 ReactiveUI.Events 库。

    一些用于处理控件加载和卸载的工具。

        public static void LoadUnloadHandler
           ( this FrameworkElement control
           , Func<IDisposable> action
           )
        {
            var state = false;
            var cleanup = new SerialDisposable();
            Observable.Merge
                (Observable.Return(control.IsLoaded)
                    , control.Events().Loaded.Select(x => true)
                    , control.Events().Unloaded.Select(x => false)
                )
                .Subscribe(isLoadEvent =>
                {
                    if (!state)
                    {
                        // unloaded state
                        if (isLoadEvent)
                        {
                            state = true;
                            cleanup.Disposable = new CompositeDisposable(action());
                        }
                    }
                    else
                    {
                        // loaded state
                        if (!isLoadEvent)
                        {
                            state = false;
                            cleanup.Disposable = Disposable.Empty;
                        }
                    }
    
                });
        }
    
        public static IObservable<T> LoadUnloadHandler<T>(this FrameworkElement control, Func<IObservable<T>> generator)
        {
            Subject<T> subject = new Subject<T>();
            control.LoadUnloadHandler(() => generator().Subscribe(v => subject.OnNext(v)));
            return subject;
        }
    

    一个专门用于处理加载控件的窗口

        public static IObservable<T> LoadUnloadHandler<T>
            (this FrameworkElement control, Func<Window, IObservable<T>> generator)
        {
            Subject<T> subject = new Subject<T>();
            control.LoadUnloadHandler(() => generator(Window.GetWindow(control)).Subscribe(v => subject.OnNext(v)));
            return subject;
        }
    

    最后是任何控件的父窗口的关键处理程序

        public static IObservable<KeyEventArgs> ParentWindowKeyEventObservable(this FrameworkElement control)
            => control.LoadUnloadHandler((Window window) => window.Events().PreviewKeyDown);
    

    现在可以了

      Button b;
      b.ParentWindowKeyEventObservable()
       .Subscribe( kEvent => {
    
            myCommand.Execute();
       }
    

    这可能看起来有点复杂,但我在大多数用户控件上使用 LoadUnloadHandler 来随着 UI 生命周期的进行获取和处置资源。

    【讨论】:

    • 这似乎也可以解决我的问题。我更喜欢 mm8 的解决方案。请注意,一个控件可以有多个加载/卸载事件,这使得处理事情变得更加棘手。 social.msdn.microsoft.com/Forums/en-US/…
    • 是的。一个控件可以有多个加载/卸载。这就是为什么要使用状态变量来确保我不会连续收到两个加载事件。我应该将变量 state 重命名为 isLoaded 那样会更清晰。
    • 这也取决于你的写作方式。有时,我对必须跳过的循环以及在 XAML 中处理事件的冗长感到非常恼火。当你多次重用一个行为时它很好,但对于需要一些逻辑的一次性行为,编写 RX 组合器更容易。
    【解决方案3】:

    您想为此使用 KeyBindings。这允许您将键盘组合键绑定到命令。在此处阅读文档:https://msdn.microsoft.com/en-us/library/system.windows.input.keybinding(v=vs.110).aspx

    【讨论】:

    • 不幸的是,当定义键绑定的用户控件不在焦点上时,这些方法不起作用。
    猜你喜欢
    • 2012-04-29
    • 2016-06-18
    • 2011-01-23
    • 1970-01-01
    • 2011-06-06
    • 1970-01-01
    • 2022-01-09
    • 1970-01-01
    • 2013-09-25
    相关资源
    最近更新 更多