使用 Visitor 模式在 WPF 中实现对话框的同时维护 MVVM 分层





5.00/5 (7投票s)
一种简化方法,用于在 ViewModel 需要显示窗体对话框时维护 WPF MVVM 层。
引言
在显示对话框窗体时,保持 MVVM (Model-View-ViewModel) 的分层结构会很困难,因为 ViewModel
层中的命令处理程序需要某种方式访问对话框窗口,而又不破坏分层结构。ViewModel
不应该与任何 View(Windows UI)对象存在编译时依赖,包括包含对话框窗口的 View,而对话框窗口需要一个引用,以操作其将要操作的数据,而这些数据是直到运行时才知道的。本文介绍了一种使用修改后的访问者模式 (Visitor Pattern)(Gamma 等人,《设计模式》)和依赖注入 (DI) 来保持分层结构分离的简化方法。
背景
访问者模式将数据与其要执行的操作分离开。在这种情况下,对话框窗体需要更新或填充对象的字段。Visitor
通过拥有一个具有重载方法的类来实现这一点,每个方法接受一个特定对象类型(类)的参数。因此,当调用带有特定参数类型的“Visit
”方法时,会自动选择正确的方法。这些 Visit
方法负责创建正确的对话框窗口,将参数对象指定为对话框的 DataContext
,并显示对话框(这里我们假设是模态对话框)。Visitor
通过属性注入(ViewModel
对象有 public Visitor
属性字段来持有对 Visitor
的引用)在 MainWindow
加载事件处理程序中被注入到 ViewModel
(VM) 对象中。我认为这比使用中介者 (mediator) 更简单,因为不需要事件在层之间传递。
此示例不需要依赖注入容器,尽管添加一个容器并不困难。它不引用 Prism Behaviors 或其他外部框架。可以将其添加到现有代码库中,而不会造成任何干扰。
Using the Code
ViewModel
使用 Relay Command (Hall,《Pro WPF and Silverlight MVVM》,Apress) 来提供命令行为。VM 对象在 XAML 中作为静态资源创建。MainWindow
的 Loaded
事件处理程序检索这些对象,实例化 View
的 Visitor
类,并通过设置其 Visitor
属性将其注入到 VM 对象中。主窗口的按钮在 XAML 中绑定到命令。当调用时,命令处理程序要么创建一个新的数据对象,要么检索当前选中的对象,然后调用 Visitor
的 DynamicVisitor
方法,该方法调用与参数匹配的重载 Visit
方法。
本文的重点是 DialogVisitor
。ViewModel
层定义了一个接口 IDialogVisitor
,其中有一个方法 DynamicVisit
。ViewModel
类包含对该接口类的公共引用。因此,ViewModel
类(ViewPersons
, ViewVehicles
)只对 ViewModel
和 Model
层有编译时依赖。需要注意的是,该接口没有定义任何显示对话框窗口的方法。
// The interface is defined in the ViewModel
public interface DialogVisitor
{
object DynamicVisit(Object data);
}
View
层定义了一个派生的 DialogVisitor
,它重写了 DynamicVisit
方法,并根据 Visit
方法的签名提供调用正确 Visit
方法的方法,并定义了 private Visit
方法。Visit
方法负责实例化并显示处理其参数中对象的对话框窗口。MainWindow
的 loaded
事件处理程序实例化 DynamicVisit
类,并将其(通过属性注入)注入到 ViewModel
类中。
/// <summary>
/// Modified Visitor. Using Dynamic to simplify the pattern.
/// See "Albahari, C# 7.0 in a Nutshell"
/// Daniel Ziegelmiller, author
/// </summary>
public class DialogVisitor : ViewModel.IDialogVisitor
{
/// <summary>
/// The method which is called by ViewModel classes to instantiate and show the dialog
/// windows. By dynamic member resolution, the correct private Visit method will
/// be invoked based on the method signature.
/// </summary>
/// <param name="data">The object which the dialog window
/// will manipulate.</param>
/// <returns>The object argument as modified.</returns>
public object DynamicVisit(Object data) => Visit((dynamic)data);
// create overloaded Visit methods. The correct one will
// be called based on the method signature, when the DynamicVisit delegate
// is invoked.
//
// This decouples the data (argument) from the action (dialog) performed
// on it.
private Person Visit(Person p)
{
var dlg = new PersonDialog();
dlg.DataContext = p;
dlg.ShowDialog();
return p;
}
private Vehicle Visit(Vehicle v)
{
var dlg = new VehicleDialog();
dlg.DataContext = v;
dlg.ShowDialog();
return v;
}
}
在 Visitor
被注入到 ViewModel
类之后,会调用 DynamicVisit
方法来显示对话框,例如:
public void NewPerson()
{
if (Visitor == null) return;
Person p = new Person();
Visitor.DynamicVisit(p);
PersonList.Add(p);
}
示例中的大部分代码都是支持和演示 Visitor
类的脚手架。数据参数可能包含控制对话框处理程序中更复杂行为的信息。DialogVisitor
可以轻松地扩展更多 Visit
方法,只要它们都基于参数类型具有不同的签名。
编译后,运行程序并单击“添加”按钮以打开对话框来创建一些数据行;选中一行,然后观察“更新”按钮会启用。单击“更新”按钮以打开包含行数据的对话框。修改它,当对话框关闭时,行数据将反映更新。
单元测试
此示例未演示单元测试。由于 ViewModel
层中没有对任何 View 或 UI 对象的依赖,可以通过实例化 DynamicVisitor
类的测试版本并将其注入到被测 ViewModel
类中来进行单元测试。
限制
一个 DynamicView
类每个对话框数据类型只能有一个方法。根据应用程序的复杂性,可能不止一个 DynamicView
类,并且可能具有包含多个参数的 Visit
方法。
此示例使用 MainWindow
的 loaded
事件处理程序来创建 DynamicView
并将其注入到 ViewModel
类中,以便 ViewModel
类可以拥有无参数构造函数。可以将 ViewModel
类重构为接受 DynamicView
类作为构造函数参数,并使用 XAML 中的 ObjectDataProvider
机制来创建 DynamicView
和 ViewModel
类,注入 DynamicView
。这取决于个人喜好。在我看来,示例机制使得工作内容更清晰,并且类似于单元测试的设置方式。
历史
- 2020年1月2日:初始版本