MVVM 模式中代码隐藏的实用使用

Posted

技术标签:

【中文标题】MVVM 模式中代码隐藏的实用使用【英文标题】:Pragmatic use of code-behind in MVVM pattern 【发布时间】:2011-07-28 07:42:04 【问题描述】:

我正在尝试尽可能好地在 WPF 应用程序中遵循 MVVM 模式,主要是为了能够为我的 ViewModel 逻辑创建单元测试。

在大多数情况下,ViewModel 属性和可视元素属性之间的数据绑定工作正常且简单。但有时我遇到的情况是,我看不到明显和直接的方法,而从代码隐藏访问和操作控件的解决方案非常简单。

这是我的意思的一个例子:在当前插入符号位置将文本片段插入TextBox

由于CaretIndex 不是依赖属性,它不能直接绑定到 ViewModel 的属性。 Here 是一种通过创建依赖属性来解决此限制的解决方案。 here 是在代码隐藏中执行此操作的解决方案。在这种情况下,我更喜欢代码隐藏方式。我最近遇到的另一个问题是将动态列集合绑定到 WPF 数据网格。在代码隐藏中编程既清晰又简单。但是对于 MVVM 友好的数据绑定方法,我只能在几个博客中找到解决方法,这些博客对我来说都非常复杂,并且在一个或另一个方面都有各种限制。

我不想不惜一切代价保持 MVVM 架构的代码隐藏逻辑的清洁。如果工作量太大,对 MVVM 友好的解决方案需要大量我不完全理解的代码(我仍然是 WPF 初学者)并且太耗时我更喜欢代码隐藏解决方案并牺牲我的应用程序的几个部分的自动可测试性。

出于上述务实的原因,我现在正在寻找“模式”,以便在不破坏 MVVM 架构或不破坏太多架构的情况下在应用程序中控制使用代码隐藏。

到目前为止,我已经找到并测试了两种解决方案。我将使用插入符号位置示例绘制粗略的草图:

解决方案1)通过抽象接口给ViewModel一个View的引用

我会有一个接口,其中包含将由视图实现的方法:

public interface IView

    void InsertTextAtCaretPosition(string text);


public partial class View : UserControl, IView

    public View()
    
        InitializeComponent();
    

    // Interface implementation
    public void InsertTextAtCaretPosition(string text)
    
        MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, text);
    

将此接口注入到 ViewModel 中

public class ViewModel : ViewModelBase

    private readonly IView _view;

    public ViewModel(IView view)
    
        _view = view;
    

通过接口方法从 ViewModel 的命令处理程序执行代码隐藏

public ICommand InsertCommand  get; private set; 
// Bound for instance to a button command

// Command handler
private void InsertText(string text)

    _view.InsertTextAtCaretPosition(text);

要创建一个 View-ViewModel 对,我将使用依赖注入来实例化具体 View 并将其注入 ViewModel。

解决方案2)通过事件执行代码隐藏方法

ViewModel 是特殊事件的发布者,命令处理程序引发这些事件

public class ViewModel : ViewModelBase

    public ViewModel()
    
    

    public event InsertTextEventHandler InsertTextEvent;

    // Command handler
    private void InsertText(string text)
    
        InsertTextEventHandler handler = InsertTextEvent;
        if (handler != null)
            handler(this, new InsertTextEventArgs(text));
    

视图订阅这些事件

public partial class View : UserControl

    public View()
    
        InitializeComponent();
    

    private void UserControl_Loaded(object sender, RoutedEventArgs e)
    
        ViewModel viewModel = DataContext as ViewModel;
        if (viewModel != null)
            viewModel.InsertTextEvent += OnInsertTextEvent;
    

    private void UserControl_Unloaded(object sender, RoutedEventArgs e)
    
        ViewModel viewModel = DataContext as ViewModel;
        if (viewModel != null)
            viewModel.InsertTextEvent -= OnInsertTextEvent;
    

    private void OnInsertTextEvent(object sender, InsertTextEventArgs e)
    
        MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, e.Text);
    

我不确定UserControlLoadedUnloaded 事件是否是订阅和取消订阅事件的好地方,但我在测试期间找不到问题。

我在两个简单的示例中测试了这两种方法,它们似乎都有效。现在我的问题是:

    您认为哪种方法更可取?其中一种解决方案是否有我可能看不到的优点或缺点?

    您是否看到(并且可能正在实践)其他解决方案?

提前感谢您的反馈!

【问题讨论】:

一种行为可能对此很有效,并且没有代码背后。导致插入的要求是什么? @Derek Beattie:左侧有一个树形视图。树视图中的元素表示文本片段。中间是一个按钮,右边是提到的文本框。用户在树视图中选择一个节点并单击该按钮。然后必须将与所选节点关联的文本片段插入到文本框中的插入符号当前位置已经存在的文本中。 (用户也可以手动将文本片段写入文本框,所描述的操作仅有助于避免拼写错误,因为只有树视图中的文本是“有效的”。) 【参考方案1】:

在我看来,第一个选项更可取。它仍然保持 View 和 ViewModel 之间的分离(通过视图接口),并将事物保持在它们的逻辑位置。事件的使用不太直观。

我赞成在无法通过绑定实现或需要您添加数百行 XAML 来实现我们可以通过 3 行代码实现的情况下务实地使用代码后面。

我的直觉是,如果您可以或多或少地通过对背后代码的代码审查来确定正确性(这与我们对 XAML 所做的相同),并将主要复杂性保持在我们可以对其进行单元测试的地方 -即 ViewModel,那么我们就有了一个快乐的媒介。创建技术上纯 MVVM 太容易了,这简直就是可维护性的噩梦。

恕我直言:D

【讨论】:

【参考方案2】:

开发 WPF 应用程序我发现这两种方法都很有用。如果您只需要从 ViewModel 到 View 的一次调用,那么带有事件处理程序的第二个选项看起来更简单且足够好。但是如果你需要这些层之间更复杂的接口,那么引入接口是有意义的。

我个人的偏好是恢复您的选项一,并让我的 ViewModel 实现一个 IViewAware 接口,并将这个 ViewModel 注入到 View 中。看起来像是选项三。

public interface IViewAware

    void ViewActivated();
    void ViewDeactivated();

    event Action CloseView;


public class TaskViewModel : ViewModelBase, IViewAware


    private void FireCloseRequest()
    
        var handler = CloseView;
        if (handler != null)
            handler();
    

    #region Implementation of IViewAware        
    public void ViewActivated()
    
        // Do something 
    

    public void ViewDeactivated()
    
        // Do something 
    

    public event Action CloseView;    
    #endregion

这是您视图的简化代码:

    public View(IViewAware viewModel) : this()
    
        _viewModel = viewModel;

        DataContext = viewModel;
        Loaded += ViewLoaded;

    

    void ViewLoaded(object sender, RoutedEventArgs e)
    
        Activated += (o, v) => _viewModel.ViewActivated();
        Deactivated += (o, v) => _viewModel.ViewDeactivated();

        _viewModel.CloseView += Close;
    

在实际应用中,我通常使用外部逻辑来连接 V 和 VM,例如 Attached Behaviors。

【讨论】:

嗯,我只能在我不使用的 Caliburn 或 Prism 的上下文中找到有关 IViewAware 的信息。如果没有这些框架,我不清楚这个界面是否对我有用。基本想法是什么?它将如何改进我的问题中的 2 个解决方案? 当然,这个想法可以在这两个框架之外(或没有)使用。我用一个简化的例子更新了我的答案,请检查。 嗯,这看起来有点像 View 和 ViewModel 之间的“双向”事件机制。您示例中的 CloseView 事件类似于我在问题中的解决方案 2(View 订阅了 ViewModel 中的事件)。 ViewActivatedViewDeactivated 是另一个方向的示例(ViewModel 订阅 View 中的事件)。很有意思!感谢您提供的示例! IView 与 MVVM 完全相反。 MVVM 的大部分优势来自于 ViewModel 对 View 的完全无知。 我不同意您使用 IView 会丢失任何东西。最后,您将 UI 与您的代码分开,并且 ViewModel 是完全可测试的。另一方面,通过引入附加行为,您将部分代码移动到 UI 端 - 您拆分了逻辑并降低了可测试性。【参考方案3】:

我会尽量避免让 ViewModel 引用 View。

在这种情况下这样做的一种方法:

从 TextBox 派生并添加一个依赖属性,该属性通过订阅 OnSelectionChanged 事件来包装 CaretIndex,让您知道插入符号已移动。

通过这种方式,ViewModel 能够通过绑定到插入符号来知道插入符号的位置。

【讨论】:

【参考方案4】:

专门针对这个问题

这个特定案例的最简单的解决方案是添加一个附加的属性来完成它或一个行为。对于 mvvm 中大多数这些不支持丰富 gui 的情况,行为可能是灵丹妙药。

至于一般情况

ViewModel 在任何情况下都不应该知道视图,甚至不知道 IView。在 MVVM 中,它的“总是向上查找”,这意味着 View 可以查看@VM,VM 可以查看 Model。永远不要反过来。 这创造了更好的可维护性,因为这样 ViewModel 不做两件事(负责逻辑和 gui),而只做一件事。这就是 MVVM 优于任何先前的 MV* 模式的地方。

我也会尽量避免让 View 以耦合的方式依赖 ViewModel。这会产生丑陋的代码,以及两个类之间的易碎依赖关系,但有时正如您所说,这更务实。 更漂亮的方法是从 ViewModel 向 View 发送 Loose Message(例如 MVVMLight 中的 Messenger,或 Prism 中的 EventAggregator),因此两者之间没有强依赖关系。有些人认为这更好,尽管 IMO 这仍然是一个依赖项。

在某些情况下,在视图中编写代码是可以的,这可能是其中一种情况。您可以使用附加行为来实现完美的解决方案,但原则很重要,就像您问的那样。

当您需要非常丰富的 GUI 或 UI 没有要绑定的正确属性时,MVVM 会出现问题。 在这些情况下,您会采取以下三种方式之一:

    附加行为。 从现有控件派生并添加您想要的属性。 实际上是在视图中编写代码。

所有这些方式都是合法的,但我已经根据你应该首先采取的方式对它们进行了排序。

总结

您必须在 MVVM 中保留的最重要的事情不是保持代码隐藏免费,而是将所有逻辑和数据保留到 ViewModel,因为 View 必须只包含与 View 相关的代码。架构师告诉你根本不要在后面写代码的原因仅仅是因为它是一个滑坡。你开始写一些小的东西,你最终会在视图中做一些合乎逻辑的事情或维护应用程序状态,这是最大的禁忌。

MVVMing 快乐 :)

【讨论】:

感谢您让我了解附加行为。我刚刚开始在这里阅读它:codeproject.com/KB/WPF/AttachedBehaviors.aspx 很高兴知道这个选项!【参考方案5】:

我会尝试将其实现为文本框的混合行为,类似于在不使用代码的情况下选择和扩展树视图的示例。我将尝试一起举一个例子。 http://www.codeproject.com/KB/silverlight/ViewModelTree.aspx

编辑:Elad 已经提到过使用附加行为,在做了几次之后,确实让做这样的事情变得简单。

另一个 mvvm 方式的弹出窗口行为示例:http://www.codeproject.com/KB/silverlight/HisowaSimplePopUpBehavior.aspx

【讨论】:

【参考方案6】:

当控件几乎无法与 MVVM 兼容时,您通常需要使用来自代码的控件。在这种情况下,您可以使用 blend SDK 中的 AttachedProperties、EventTriggers、Behaviors 来扩展控件的功能。但我经常使用inheritance 来扩展控制功能并使其更兼容 MVVM。您可以创建自己的一组从基础继承的控件,并实现视图功能。这种方法的一大优势是您可以访问 ControlTemplate 控件,这通常是实现特定视图功能所必需的。

【讨论】:

以上是关于MVVM 模式中代码隐藏的实用使用的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 MVVM 模式在 TreeView 中获取选定节点而不使用代码隐藏? [复制]

为啥要避免 WPF MVVM 模式中的代码隐藏?

WPF MVVM模式如何控制DataGrid的列隐藏和显示

Qt C++ 项目中 Xamarin 项目中代码的可重用性

如何使用 MVVM 自动隐藏 WPF 中的 DataGrid 列? [复制]

使用 BooleanToVisibilityConverter 的 WPF MVVM 隐藏按钮