【问题标题】:Clean and elegant view models in WPFWPF 中干净优雅的视图模型
【发布时间】:2014-11-10 15:55:56
【问题描述】:

一旦我发现了 MVVM,我就非常喜欢它。绑定、将视图与逻辑分离、可测试性等的整个概念非常令人鼓舞。对于背后的混乱、永无止境的代码来说,这是一个不错的选择。比我了解到有可以绑定的命令,我一开始也很喜欢。

使用 MVVM 编写了几个控件后,我发现我的视图模型开始看起来或多或少像背后的代码。完整的命令几乎完全完成了事件处理程序之前在代码中所做的事情。

让我举几个例子。

有一个带有“详细信息”按钮的控件可以打开另一个窗口。

[方法 1] 你可以做的第一件事(也是最糟糕的)是在你的命令中调用这样的东西:

new DetailsWindow().ShowDialog();

这使得视图模型强烈引用表示层 - 丑陋。

[方法 2] 让我们使用弱引用来解决这个问题,并创建类似 IDialogService 的东西。我们可以注入一个简单的实现来创建和打开窗口。现在我们摆脱了对表示层的强引用,命令可以如下所示:

_dialogService.ShowDetailsWindow();

我仍然不喜欢这种方法。对我来说,感觉视图模型不应该决定是否显示窗口。它应该提供和处理数据。

[方法 3] 将视图模型与表示层完全分离的优雅方法是注入命令本身。比视图模型不知道表示层。它只会执行注入的动作——不管它是什么。

问题 1:

哪种方法最好?我猜数字 3 是赢家。

问题 2:

这甚至应该成为视图模型的一部分吗?我认为它不应该因为它似乎是表示层的关注点。也许最好将它放在代码后面的简单事件处理程序中?

第二个例子更复杂。在同一个控件中,我们有一个“删除”按钮。它应该打开一个对话框,要求用户确认,如果他说“是”,它应该删除一些东西。在这种情况下,将它放在视图模型中更有意义,因为它确实会影响数据。

问题 3

这个案子对我来说是最棘手的。我不能使用我最喜欢的方法编号 3,因为我必须显示一个作为表示层工作的对话框,但 另外我必须根据对话框的具体情况执行一些逻辑结果 - 另一方面是视图模型的工作。这里最好的方法是什么?

请记住,我真的不想使用方法 12。我希望视图模型是干净的,并且不知道与表示层相关的任何内容——即使是弱引用也不知道。

我想到的一件事是将视图模型层分成两层。比如:

视图 --> 表示视图模型 --> 逻辑视图模型

  • 演示视图模型
    • 用作控件的上下文
    • 包含逻辑视图模型作为直接绑定的公共属性
    • 使用方法编号 2 - 现在可以接受,因为整个班级都是为了执行与演示相关的操作
  • 逻辑视图模型
    • 它是“免费的”
    • 引用专门的逻辑服务
    • 某些命令可以直接绑定到视图
    • 某些命令可以由拥有它的表示视图模型执行

也许这是正确的方法?

[编辑]

针对 cme​​ts 中关于使用框架的建议:

使用框架肯定会更容易处理窗口,但这不是问题的解决方案。我根本不希望“逻辑视图模型”处理窗口,甚至在框架的帮助下也不行。参考我最后建议的方法,我仍然会将它放在“演示视图模型”中

【问题讨论】:

  • 这不是问题的直接答案,但您应该考虑使用 MVVM 框架(Caliburn.Micro、MVVM Light),因为他们已经为您完成了很多繁琐的工作,比如对话框或导航。例如,Caliburn.Micro 有IWindowManager 接口,可以将指定的视图模型显示为常规或模态窗口。这意味着您只能在视图模型上进行操作。
  • 如果不使用第 3 方框架,我会使用 _navigationService.Show(SomeDialogViewModel) 之类的东西,其中 SomeDialogViewModel 包含对话框需要显示的所有数据,可能还有一些标志来标识它应该是显示为对话框。然后是_navigationService 的工作来确定如何以及在何处显示视图模型
  • 为了呼应@Patryk 所说的,我发现Caliburn.Micro 非常适合这个。它按照约定解析适当的 Window 或 UserControl,然后允许您检查 ViewModel 实例以确定其在对话框关闭后的状态。您可以推出自己的实现;主要挑战是解决正确的视图。有多种方法可以做到这一点(基于约定、属性、注册映射的方法调用等)
  • IMO 最优雅的解决方案是,当执行命令时,VM 应该只引发一个事件并忘记它,并且任何订阅者都应该根据 VM 命令执行时应该执行的操作来处理该事件执行...

标签: c# .net wpf mvvm


【解决方案1】:

您的 ViewModel 应该通过触发一个简单事件来通知所有订阅者该命令已执行。视图应该订阅该事件并通过显示一个新窗口来处理该事件...

视图模型:

public event EventHandler<NotificationEventArgs<string>> DisplayDetailsNotice;

private DelegateCommand displayDetailsCommand;
public DelegateCommand DisplayDetailsCommand
{
    get { return displayDetailsCommand ?? (displayDetailsCommand = new DelegateCommand(DisplayDetails)); }
    }

public void DisplayDetailsWindow()
{
    //
    Notify(DisplayDetailsNotice, new NotificationEventArgs<string("DisplayDetails"));
}

View(注意VM已经是View的DataContext):

private readonly MyViewModel mvm;

//Get reference to VM and subscribe to VM event(s) in Constructor

mvm = DataContext as MyViewModel;
if (mvm == null) return;
mvm.DisplayDetailsNotice += DisplayDetails;

private void DisplayDetails(object sender, NotificationEventArgs<string> e)
{
    DetailsWindow = new DetailsWindow { Owner = this };
    DetailsWindow.ShowDialog();
}

这样,当命令执行时,VM 会引发事件,订阅 VM 事件的 View 通过使用 ShowDialog() 方法显示详细信息窗口来处理事件。由于 VM 所做的只是发布一个事件,因此它与 View 无关并与 View 分离,从而使 MVVM 模式保持不变。由于 VM 已经是 View 的 DataContext 并且 View 绑定到 VM 的属性,因为它正在获取对 VM 的引用也没有破坏 MVVM 模式...

请注意,此语法对 Simple MVVM Toolkit MVVM 框架有效。正如 cmets 中建议的那样,您应该开始使用 MVVM 框架(我的建议是 Simple MVVM Toolkit 或 MVVM Light,Caliburn 无缘无故地太复杂了,而且没有任何收益 IMO),以避免不得不自己处理管道......

【讨论】:

  • 我认为这不是一个好方法。视图不应依赖于视图模型(这是因为 DataContext 的强制转换)。这样你就不能用另一个视图模型来提供视图。
  • VM 已经是 View 的 DataContext 为什么会出现问题?
  • 问题在于,您可以将不同的视图模型类放在视图的 DataContext 中,这些类的行为不同 - 视情况而定。在您的情况下,这是不可能的。
  • 为什么这是不可能的。您如何在运行时更改视图的 DataContext?无论您在哪里更改 View 的 DataContext,您都可以从那里获取新 VM 的引用并订阅其事件...通过事件通知视图是 MVVM 中 VM 和 V 之间通信的标准方式,它可以保持 VM解耦...
  • 我现在想不出更好的例子,但假设我们有一个显示时间线的视图。它需要一个具有名称和日期集合(日期)的对象。您可以使用具有这两个属性的任何内容填充它。我知道这有点牵强,但我认为你应该明白这一点。
猜你喜欢
  • 2019-09-27
  • 1970-01-01
  • 1970-01-01
  • 2011-03-15
  • 2012-07-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多