如何处理纯粹与视图相关的命令?

Posted

技术标签:

【中文标题】如何处理纯粹与视图相关的命令?【英文标题】:How to handle purely view-related commands? 【发布时间】:2013-10-30 09:39:31 【问题描述】:

在我的 WPF 窗口中,我放置了一个普通的文本框,我希望在按下 Ctrl+F 时将其聚焦。

因为我想尽可能地保持它类似于 MVVM,我在窗口上使用InputBindings 将该输入事件绑定到 ViewModel 中提供的命令(这已经破坏了 MVVM 模式,因为整个操作是只是想成为视图的一部分?我猜不是,因为 Command 是要绑定到的对象)。

ViewModel 如何与视图通信以聚焦文本框?我读到这已经打破了 MVVM 模式,但有时只是必要的,否则是不可能的。但是,将焦点设置在 ViewModel 本身将完全打破 MVVM 模式。

我原本打算将窗口中当前的焦点控件绑定到 ViewModel 的一个属性,但是甚至很难确定 WPF 中当前的焦点元素(这总是让我怀疑它是否真的是正确的做法所以)。

【问题讨论】:

【参考方案1】:

在这种情况下,没有办法不“破坏”纯 MVVM。再说一次,我几乎不会称之为破坏任何东西。我不认为 任何 大小合适的 MVVM 应用程序是“纯粹的”。所以,不要太在意打破你使用的任何模式,而是实施一个解决方案。

这里至少有两种方式:

只需在 View 后面的代码中执行所有操作:检查是否按下了键,如果是,则设置焦点。没有比这更简单的了,您可能会争辩说 VM 与真正与 View 相关的东西无关 否则,VM 和 View 之间显然必须进行一些通信。这让一切变得更加复杂:假设您使用 InputBinding,您的命令可以设置一个布尔属性,然后视图可以依次绑定到它以设置焦点。这种绑定可以像 Sheridan 的回答一样使用附加属性来完成。

【讨论】:

我看到和评估实施选项的次数越多,我就越有这样的想法。试图保持纯 MVVM 会导致实现比我预期的要复杂得多。好点。【参考方案2】:

一般来说,当我们想要在遵守 MVVM 方法的同时使用任何 UI 事件时,我们会创建一个附加属性。正如我昨天刚刚回答了同样的问题一样,我建议您查看 *** 上的 how to set focus to a wpf control using mvvm 帖子以获取完整的工作代码示例。

该问题与您的问题的唯一区别是您希望将元素集中在按键上...我将假设您知道如何做那部分,但如果您不能,就让我知道,我也会给你举个例子。

【讨论】:

对不起,我不明白这个场景中附加属性的用途。 简单地说,我们使用Attached Propertys 来处理事件,但是我们通常不使用后面的视图代码,而是公开一些Bindable 属性,我们可以使用这些属性来转发来自事件的信息,我们可以Bind 在视图模型中。【参考方案3】:

在使用 mvvm 时,以及在定义视图模型时使用:

视图模型不应该知道/引用视图

那么你不能通过视图模型设置焦点。

但我在 mvvm 中所做的是 viewmodel 中的以下内容:

将焦点设置到绑定到 viewmodel 属性的元素上

为此,我创建了一个简单地遍历可视化树中的所有控件并查找绑定表达式路径的行为。如果我找到一个路径表达式,那么只需关注 uielement。

编辑:

xaml 用法

<UserControl>
    <i:Interaction.Behaviors>
    <Behaviors:OnLoadedSetFocusToBindingBehavior BindingName="MyFirstPropertyIWantToFocus" SetFocusToBindingPath="Binding Path=FocusToBindingPath, Mode=TwoWay"/>
</i:Interaction.Behaviors>
</UserControl>

任何方法的视图模型

this.FocusToBindingPath = "MyPropertyIWantToFocus";

行为

public class SetFocusToBindingBehavior : Behavior<FrameworkElement>

    public static readonly DependencyProperty SetFocusToBindingPathProperty =
      DependencyProperty.Register("SetFocusToBindingPath", typeof(string), typeof(SetFocusToBindingBehavior ), new FrameworkPropertyMetadata(SetFocusToBindingPathPropertyChanged));

    public string SetFocusToBindingPath
    
        get  return (string)GetValue(SetFocusToBindingPathProperty); 
        set  SetValue(SetFocusToBindingPathProperty, value); 
    

    private static void SetFocusToBindingPathPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        var behavior = d as SetFocusToBindingBehavior;
        var bindingpath = (e.NewValue as string) ?? string.Empty;

        if (behavior == null || string.IsNullOrWhiteSpace(bindingpath))
            return;

        behavior.SetFocusTo(behavior.AssociatedObject, bindingpath);
        //wenn alles vorbei ist dann binding path zurücksetzen auf string.empty, 
        //ansonsten springt PropertyChangedCallback nicht mehr an wenn wieder zum gleichen Propertyname der Focus gesetzt werden soll
        behavior.SetFocusToBindingPath = string.Empty;
    

    private void SetFocusTo(DependencyObject obj, string bindingpath)
    
        if (string.IsNullOrWhiteSpace(bindingpath)) 
            return;

        var ctrl = CheckForBinding(obj, bindingpath);

        if (ctrl == null || !(ctrl is IInputElement))
            return;

        var iie = (IInputElement) ctrl;

        ctrl.Dispatcher.BeginInvoke((Action)(() =>
            
                if (!iie.Focus())
                
                    //zb. bei IsEditable=true Comboboxen funzt .Focus() nicht, daher Keyboard.Focus probieren
                    Keyboard.Focus(iie);

                    if (!iie.IsKeyboardFocusWithin)
                    
                        Debug.WriteLine("Focus konnte nicht auf Bindingpath: " + bindingpath + " gesetzt werden.");
                        var tNext = new TraversalRequest(FocusNavigationDirection.Next);
                        var uie = iie as UIElement;

                        if (uie != null)
                        
                            uie.MoveFocus(tNext);
                         
                    
                
            ), DispatcherPriority.Background);
    

    public string BindingName  get; set; 

    protected override void OnAttached()
    
        base.OnAttached();
        AssociatedObject.Loaded += AssociatedObjectLoaded;
    

    protected override void OnDetaching()
    
        base.OnDetaching();
        AssociatedObject.Loaded -= AssociatedObjectLoaded;
    

    private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
    
        SetFocusTo(AssociatedObject, this.BindingName);
    

    private DependencyObject CheckForBinding(DependencyObject obj, string bindingpath)
    
        var properties = TypeDescriptor.GetProperties(obj, new Attribute[]  new PropertyFilterAttribute(PropertyFilterOptions.All) );

        if (obj is IInputElement && ((IInputElement) obj).Focusable)
        
            foreach (PropertyDescriptor property in properties)
            
                var prop = DependencyPropertyDescriptor.FromProperty(property);

                if (prop == null) continue;

                var ex = BindingOperations.GetBindingExpression(obj, prop.DependencyProperty);
                if (ex == null) continue;

                if (ex.ParentBinding.Path.Path == bindingpath)
                    return obj;

            
        

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
        
            var result = CheckForBinding(VisualTreeHelper.GetChild(obj, i),bindingpath);
            if (result != null)
                return result;
        

        return null;
    

【讨论】:

【参考方案4】:

(这是否已经打破了 MVVM 模式,因为整个操作是 只意味着成为视图的一部分?我猜不是,因为命令是 要绑定的对象)

WPF 中的命令系统实际上并不是围绕数据绑定设计的,而是 UI —— 使用 RoutedCommands,单个命令将根据调用命令的元素在 UI 结构中的物理位置具有不同的实现。

Commanding Overview

您的流程是:

Ctrl+F 被按下 命令事件已引发并冒泡 事件到达窗口,该窗口具有与命令的 CommandBinding 窗口上的事件处理程序聚焦文本框

如果当前元素位于想要以不同方式处理命令的容器内,它将在到达窗口之前停止。

这可能更接近您想要的。如果在blindmeis's answer 中存在一些“活动属性”的概念,则涉及视图模型可能是有意义的,但否则我认为您最终会得到冗余/循环的信息流,例如按键 -> 视图通知视图模型按键 -> 视图模型通过通知按键的视图来响应。

【讨论】:

这是一个有趣的信息,正如我所写,我是 MVVM 的新手,并认为命令是其中的一部分。 @Narancs 命令是在 MVVM 中实现视图到视图模型通信的一种便捷方式,因此它通常是一个标准部分,但命令接口本身是开放式的。只是不想让你认为“这是使用命令,所以它必须是基于 MVVM/数据绑定的解决方案”。【参考方案5】:

经过几天更好地掌握所有这些,考虑和评估所有选项后,我终于找到了解决问题的方法。我在我的窗口标记中添加了一个命令绑定:

<Window.InputBindings>
    <KeyBinding Command="Binding Focus" CommandParameter="Binding ElementName=SearchBox" Gesture="CTRL+F" />
</Window.InputBindings>

我的 ViewModel 中的命令(我将课程缩减到在这种情况下最重要的部分):

class Overview : Base

    public Command.FocusUIElement Focus
    
        get;
        private set;
    

    public Overview( )
    
        this.Focus = new Command.FocusUIElement();
    

最后是命令本身:

class FocusUIElement : ICommand

    public event EventHandler CanExecuteChanged;

    public bool CanExecute ( object parameter )
    
        return true;
    

    public void Execute ( object parameter )
    
        System.Windows.UIElement UIElement = ( System.Windows.UIElement ) parameter;
        UIElement.Focus();
    

这可能不是直接的 MVVM - 但 stijn 的回答有一个好点:

所以,不要太在意打破你使用的任何模式 并实施解决方案。

通常我会按照惯例来整理东西,尤其是当我对某事还是陌生的时候,但我认为这没有任何问题。

【讨论】:

以上是关于如何处理纯粹与视图相关的命令?的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI之深入解析如何处理特定的数据和如何在视图中适配数据模型对象

如何处理局部视图中的模型

如何处理 iPad 模态视图旋转

UIPageViewController:如何处理视图控制器的不同方向?

如何处理自定义结果视图的 NSFetchedResultsControllerDelegate 更新

在 MVVM + Coordinator 中,如何处理子视图?