在MVVM架构中,视图模型(ViewModel)层需要能够访问对话框窗口,但又不能破坏层次结构。视图模型不应该对任何视图(Windows UI)对象有编译时依赖,包括包含对话框窗口的视图,而对话框窗口需要在运行时获得它将要操作的数据的引用。本文描述了一种简化的方法,使用修改后的访问者模式(Visitor Pattern)和依赖注入(DI)来维护层次的分离。
访问者模式将数据与要在其上执行的操作分离。在这种情况下,对话框表单需要更新或填充对象的字段。访问者通过具有重载方法的类实现这一点,每个方法接受一个特定的对象类型(类)参数。因此,当使用特定参数类型调用“访问”方法时,会自动选择正确的方法。这些访问方法负责创建正确的对话框窗口,将参数对象分配为对话框的DataContext,并显示对话框(假设这里是模态对话框)。访问者通过属性注入(ViewModel对象有一个公共访问者属性字段来持有访问者的引用)注入到ViewModel对象中。
这种方法比使用中介者更简单,因为不需要在层次之间传递事件。这个示例不需要依赖注入容器,尽管可以很容易地应用一个。它不引用Prism Behaviors或其他外部框架。它可以被添加到现有代码库中,而不会造成干扰。
ViewModel使用Relay Command来提供命令行为。ViewModel对象在XAML中作为静态资源创建。MainWindow的Loaded事件处理程序检索这些对象,实例化视图的Visitor类,并通过设置它们的Visitor属性将访问者注入到ViewModel对象中。主窗口按钮绑定到XAML中的命令。当被调用时,命令处理程序要么创建一个新的数据对象,要么检索当前选中的一个,并调用访问者的DynamicVisitor方法,该方法调用与参数匹配的重载访问方法。
本文的主要重点是DialogVisitor。ViewModel层定义了一个接口IDialogVisitor,其中有一个方法DynamicVisit。ViewModel类包含对接口类的公共引用。因此,ViewModel类(ViewPersons、ViewVehicles)仅在编译时依赖于ViewModel和Model层。重要的是要注意,接口没有定义显示对话框窗口的任何方法。
public interface IDialogVisitor
{
object DynamicVisit(Object data);
}
视图层定义了一个派生的DialogVisitor,它覆盖了DynamicVisit方法,提供了调用正确访问方法的方法,基于访问方法的签名,并定义了私有的访问方法。访问方法处理实例化和显示对话框窗口,该窗口处理它们参数中的对象。MainWindow的Loaded事件处理程序实例化DynamicVisit类,并通过属性注入将其注入到ViewModel类中。
public class DialogVisitor : ViewModel.IDialogVisitor
{
public object DynamicVisit(Object data) => Visit((dynamic)data);
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;
}
}
访问者注入到ViewModel类后,调用DynamicVisit方法来显示对话框,例如:
public void NewPerson()
{
if (Visitor == null)
return;
Person p = new Person();
Visitor.DynamicVisit(p);
PersonList.Add(p);
}
示例中的大部分代码都是为了支持和演示Visitor类。数据参数可能包含控制对话框处理程序中更复杂行为的信息。DialogVisitor可以很容易地用更多的访问方法进行扩展,只要每个方法都有一个基于参数类型的不同签名。
编译后运行程序,点击“添加”按钮以显示对话框来创建一些数据行;突出显示一行并观察“更新”按钮变为启用状态。点击一个“更新”按钮以显示对话框,其中包含对话框中的数据行。修改它,并在对话框关闭后,行数据将反映更新。
这个示例没有演示单元测试。因为在ViewModel层没有依赖于任何视图或UI对象,可以通过实例化测试版本的DynamicVisitor类并将它们注入到正在测试的ViewModel类中来进行单元测试。
DynamicView类只能有一个对话框数据类型的方法。根据应用程序的复杂性,可能需要多个DynamicView类,可能具有具有多个参数的访问方法。