WPF中简洁优雅的视图模型

6
一旦我了解了MVVM,我非常喜欢它。绑定、将视图与逻辑分离、可测试性等整个概念非常令人鼓舞。这是一个很好的选择,可以替代混乱、永无止境的代码后台。然后我了解到可以绑定命令,一开始我也很喜欢。
但是,我写了几个使用MVVM的控件之后,发现我的视图模型开始看起来有点像代码后台。充满了几乎与事件处理程序中以前完成的相同的命令。
让我给你举几个例子。
有一个带有“详细信息”按钮的控件,可以打开另一个窗口。 [方法1] 您可以在命令中调用以下内容(第一种(也是最糟糕的)方法):
new DetailsWindow().ShowDialog();

这造成了视图模型对表示层的强引用,很丑陋。

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

_dialogService.ShowDetailsWindow();

我仍然不喜欢这种方法。对我来说,视图模型不应该决定是否显示窗口,而应该服务于数据并处理数据。

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

问题1:

哪种方法最好?我想数字3获胜了。

问题2:

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

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

问题3:

这种情况对我来说最棘手。我不能使用我最喜欢的第3种方法,因为我必须显示一个对话框,这是表示层的工作,但同时我还必须执行一些逻辑,这取决于对话框的结果 - 另一方面,这是视图模型的工作。这里最好的方法是什么?

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

我想到的一件事是将视图模型层分成两个层。像这样:

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

  • 表示视图模型
    • 用作控件的上下文
    • 包含逻辑视图模型作为公共属性以进行直接绑定
    • 使用第2种方法 - 现在它可以接受,因为整个类都旨在执行与表示相关的操作
  • 逻辑视图模型
    • 它是“无表示”的
    • 引用专业逻辑服务
    • 一些命令可以直接绑定到视图
    • 一些命令可以由拥有它的表示视图模型执行

也许这是正确的方法?

[编辑]

针对评论中关于使用框架的建议做出回应:

使用框架可以更轻松地处理窗口,但这并不是解决问题的办法。我不希望“逻辑视图模型”处理任何窗口,即使借助框架的帮助也不行。参考我在最后提出的方法,我仍会将其放在“展示视图模型”中。

1
这不是对问题的直接回答,但你应该考虑使用MVVM框架(Caliburn.Micro、MVVM Light),因为它们已经为你完成了很多繁琐的工作,比如对话框或导航。例如,Caliburn.Micro有IWindowManager接口,可以将指定的视图模型显示为常规或模态窗口。这意味着你只需要操作视图模型。 - Patryk Ćwiek
不使用第三方框架,我会使用类似 _navigationService.Show(SomeDialogViewModel) 的方法,其中 SomeDialogViewModel 包含对话框显示所需的所有数据,以及可能用于标识其应该作为对话框显示的标志。然后,由 _navigationService 负责确定如何以及在哪里显示视图模型。 - Rachel
回应@Patryk所说的话,我发现Caliburn.Micro非常适合这个问题。它通过约定解析适当的窗口或用户控件,然后允许您检查ViewModel实例以确定对话框关闭后其状态。您可以自己编写实现;主要挑战是解析正确的视图。有各种方法可以做到这一点(基于约定、属性、方法调用来注册映射等)。 - Dan Bryant
我认为最优雅的解决方案是,当执行命令时,虚拟机应该只触发一个事件并忘记它,任何订阅者都应该根据其执行VM命令时应该执行的操作来处理该事件... - Dean Kuga
1个回答

0

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

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"));
}

视图(注意VM已经是视图的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的不可知性和解耦,从而保持MVVM模式的完整性。由于VM已经是View的DataContext,并且View绑定到VM的属性,因此获取对VM的引用也不会破坏MVVM模式...

请注意,这种语法在Simple MVVM Toolkit MVVM框架中是有效的。如评论所建议,您应该开始使用MVVM框架(我的建议是Simple MVVM Toolkit或MVVM Light,Caliburn过于复杂,没有必要也没有收益)以避免自己处理管道问题...


VM已经是View的DataContext了,为什么会有问题? - Dean Kuga
问题在于,您可以将不同的视图模型类放置在视图的DataContext中,并根据情况进行不同的行为。但在您的情况下,这是不可能的。 - Andrzej Gis
为什么会不可能呢?你如何在运行时更改视图的DataContext?无论您在哪里更改View的DataContext,都可以从那里获取新VM的引用并订阅其事件...通过事件通知视图是MVVM中VM和V之间通信的标准方式,这使得VM解耦... - Dean Kuga
你将DataContext属性(一个对象)转换为特定的视图模型类(MyViewModel)。这个DataContext属性可以在运行时被拥有视图的任何对象多次设置,例如:{{myView.DataContext = new AnotherViewModelClass();}}。虽然可以在代码后台处理所有可能的视图模型类,但这绝对不是优雅的做法。 :) - Andrzej Gis
1
我现在想不出更好的例子,但我们假设有一个显示时间轴的视图。它需要一个带有名称和日期集合(Dates)的对象。您可以使用任何具有这2个属性的数据填充它。我知道这有点牵强,但我认为您应该看到重点所在。 - Andrzej Gis
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接