1. 背景

MVVM是一种常用的设计模式,它的最主要功能是将数据与代码隔离,实现viewmodel的可测试。架构图如下:

2. 命令-Command

2.1 WPF 路由命令

WPF提供一种内置的命令实现称为路由命令。这与MVVM设计模式中的命令不同。路由命令通过UI Tree进行路由。路由命令可沿着UI Tree向上或者向下路由,但是不会路由到UI Tree以外部分,如与view关联的View Model。

2.2 CompositeCommand

有时我们希望点击Shell中的一个按钮,Shell包含的多个view对应的view model都执行相应命令,也就是一个命令包含多个命令。Prism提供类CompositeCommand,它由多个子命令组成。当组合命令被激活,它所有子命令按顺序执行。 CompositeCommand包含成员:

  • 属性,子命令集合
  • 方法,Execute,执行命令
  • 方法,CanExecute,如果任意子命令不能被执行,那么组合命令也无法执行。
2.2.1 注册和注销子命令

可以通过方法RegisterCommand和UnRegisterCommand实现命令的注册和注销。

2.2.2 在活跃的子View上执行命令

使用组合命令我们可以在多个view model上执行命令,但是有时我们只需要在激活的子View上执行即可。为了实现该种特性,Prism提供接口IActiveAware,该接口包含属性IsActive和事件IsActiveChanged,属性IsActive表明当前是否处于激活状态,事件用于处理状态转变情况。子view,view model均可实现该接口,Prism提供的DelegateCommand也继承至该接口。基于提供的属性,我们可以配置组合命令是否检测子命令状态,方法是在构造函数中为monitorCommandActivity赋值TRUE。

2.3 在集合中使用命令

有时我们需要在集合中使用命令,但这些集合的项目需要使用父容器的命令,这就有点棘手,项目中的控件只能绑定到项目DataContext,解决的方法有两种,一是使用ElementName强制指定到父容器上,如下:

<Grid x:Name="root">
<ListBox ItemsSource="{Binding Path=Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Path=Name}"
Command="{Binding ElementName=root, Path=DataContext.DeleteCommand}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>

另一种是使用Blend提供的interaction triggers,见 5-学习MVVM。

2.3.1 传递参数

传统上来说使用CommandParameter向命令传入参数,但是如果你需要的参数来自父类事件的参数,这就麻烦了。Prism提供InvokeCommandAction,这个有别于Blend的同名类,前者能实时更新绑定该命令控件的状态,同时能传入父触发器的事件参数,如下:

<ListBox Grid.Row="1" Margin="5" ItemsSource="{Binding Items}"
SelectionMode="Single">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<!-- This action will invoke the selected command in the view model and
pass the parameters of the event to it. -->
<prism:InvokeCommandAction Command="{Binding SelectedCommand}"
TriggerParameterPath="AddedItems" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>

2.4. 处理异步交互

当你需要与远程Web 服务或者远程服务器交互时,你将需要经常面对IAsyncResult模式。在这个模式中,相比于直接调用方法,你使用方法对BeginGet*和EndGet*来获取结果。使用BeginGet*来初始化异步请求,然后使用EndGet*来获取请求结果或者发生的异常。为了决定什么时候调用EndGet*,你可以直接使用轮询或者在BeginGet*中指定回调。通过指定回调方法,当目标方法完成或者异常中断会自动调用回调。

IAsyncResult asyncResult =
this.service.BeginGetQuestionnaire(GetQuestionnaireCompleted, null // object state,
not used in this example);
private void GetQuestionnaireCompleted(IAsyncResult result)
{
try
{
questionnaire = this.service.EndGetQuestionnaire(ar);
}
catch (Exception ex)
{
// Do something to report the error.
}
}

获取完数据以后,如果需要更新UI,你需要调用Dispatcher或者SynchronizationContext。如下:

var dispatcher = System.Windows.Deployment.Current.Dispatcher;
if (dispatcher.CheckAccess())
{
QuestionnaireView.DataContext = questionnaire;
}
else
{
dispatcher.BeginInvoke(
() => { Questionnaire.DataContext = questionnaire; });
}

3. 用户交互

设计出的程序是为了供用户使用,这就需要与用户交互,比如弹出一个对话框或者一个消息框,在非MVVM型程序,这个很容易实现,直接在后台代码使用MessageBox.Show等即可。但是在MVVM型架构中,这个是比较困难的,view model不能直接调用MessageBox,逻辑与界面UI必须保证解耦。view model 负责初始化交互请求,获取或者处理响应。View实际管理与用户交互逻辑。为了保证解耦,解决方法有两种:

  • 实现一种交互服务,view model能使用该服务初始化交互,然后在view的实现中保持独立
  • 使用交互请求对象,view model引发事件表达希望交互的意愿,view中与这些事件绑定的控件管理交互的可视化部分

3.1 交互服务

这种方法中view model依赖一个交互服务组件来初始化交互。该服务组件封装了交互中调用可视化逻辑的代码,可以使用DI容器获取该服务。由于服务已经封装了相应功能,我们可以用模态和非模态方式进行交互,也可以以同步或者异步方式交互,如下:

//同步方式
var result =
interactionService.ShowMessageBox(
"Are you sure you want to cancel this operation?",
"Confirm",
MessageBoxButton.OK );
if (result == MessageBoxResult.Yes)
{
CancelRequest();
}

异步方式:

interactionService.ShowMessageBox(
"Are you sure you want to cancel this operation?",
"Confirm",
MessageBoxButton.OK,
result =>
{
if (result == MessageBoxResult.Yes)
{
CancelRequest();
}
});

3.2 交互请求对象

这种方法允许view model使用封装了行为的交互请求对象直接与view交互。交互请求对象封装了交互请求和相应,使用事件与view进行交互。view订阅这些事件初始化交互。典型的view将交互封装在行为中,这些行为绑定到交互请求对象上。

这种方法提供一个简单,灵活机制保持解耦。它允许view model封装应用呈现逻辑,view封装交互的可视化逻辑。这种实现可以使交互逻辑能够被轻松测试,UI Designer也可以更灵活选择需要的交互。这种方法与MVVM模式是一致的,允许view反应观测到的状态变化,使用双向数据绑定与view model交互。

这种方式也是Prism使用的交互方法,包含接口IInteractionRequest以及InteractionRequest。接口IInteractionRequest定义了初始化交互的事件。view绑定该接口,并订阅事件。类InteractionRequest实现前面接口,定义两个Raise方法,允许view model初始化交互,并为请求指定内容。

3.2.1 原理

Prism提供类InteractionRequest将view model的交互请求送达view。该类的方法Raise允许view model初始化交互,并指定一个T类型的context对象。context对象允许 view model向view传入数据和状态。如果view需要回传数据给view model,方法Raise有一个重载,允许传入需要的回调函数。当交互完成时,自动调用回调函数。该类在命名空间Prism.Interactivity.InteractionRequest,类原型如下:

public interface IInteractionRequest
{
event EventHandler<InteractionRequestedEventArgs> Raised;
} public class InteractionRequest<T> : IInteractionRequest
where T : INotification
{
public event EventHandler<InteractionRequestedEventArgs> Raised;
public void Raise(T context)
{
this.Raise(context, c => { });
}
public void Raise(T context, Action<T> callback)
{
var handler = this.Raised;
if (handler != null)
{
handler(
this,
new InteractionRequestedEventArgs(
context,
() => { if (callback != null) callback(context); } ));
}
}
}

Prism提供接口INotification,所有Context对象均需实现该接口。该接口包含两个属性Tile和Content。典型的,通知是单向的,所以该交互过程中Context只读。类Notification是该接口的默认实现。

接口IConfirmation扩展接口INotification,并添加属性Confirmed,表明用户是否确认或者取消该操作。类Confirmation提供IConfirmation实现,实现了消息框类型交互逻辑。

3.2.2 实战-MVVM模式实现

3.2.2.1 ViewModel

在MVVM模式中,view model负责创建InteractionRequest 对象,定义一个只读属性,用于数据绑定,这里泛型T可以是通知类型接口INotification,消息框型接口IConfirmation,也可以是自定义类型接口。当view model需要初始化请求时,调用类InteractionRequest的Raise方法,并传入需要的Context,以及可选的回调委托。以弹出对话框型窗口为例:

public class InteractionRequestViewModel
{
public InteractionRequest<IConfirmation> ConfirmationRequest { get; private set; }
public ICommand RaiseConfirmationCommand; public InteractionRequestViewModel()
{
this.ConfirmationRequest = new InteractionRequest<IConfirmation>();

// Commands for each of the buttons. Each of these raise a differen t interaction request.
this.RaiseConfirmationCommand = new DelegateCommand(this.RaiseConfirmation);

} private void RaiseConfirmation()
{
this.ConfirmationRequest.Raise(
new Confirmation { Content = "Confirmation Message", Title = "Confirmation"
},
c => { InteractionResultMessage = c.Confirmed ? "The user accepted." : "The
user cancelled."; });
}
}
}
3.2.2.2 View

任何一次交互我们都可以把其分解为逻辑交互以及界面交互。所谓逻辑交互指的是状态和数据交互,这个已经封装在交互请求对象中。所谓界面交互是用户实际看到的内容。行为经常用于封装界面交互。

view必须能探测交互请求事件,然后呈现合适的显示。触发器用于实现该逻辑,一旦有事件产生,立即做出相应行动。由Blend提供的标准EventTrigger能够监听由view model暴露的事件。更进一步,Prism框架提供一个扩展的EventTrigger,名为InteractionRequestTrigger,开发者只需为该触发器绑定数据源,就能自动连接交互请求对象的Raised事件,避免输入事件名称。

当一个事件触发,InteractionRequestTrigger激活指定的动作。对于WPF,Prism框架提供类PopupWindowAction,向用户呈现对话窗口。窗口的Data Context为交互请求对象的context。通过使用PopupWindowAction的WindowContent属性,你可以指定需要在窗口中显示的view。窗口的标题则是context的Title。不同类型的context具有不同的类型窗口,对于Notification类型Context,弹出窗口类型为DefaultNotificationWindow,这种类型窗口仅包含通知消息;对于Confirmation类型context,弹出窗口类型为DefaultConfirmationWindow,包含取消和确认按钮,捕获用户反馈。可以在默认窗口类型上实现自定义类型。如下:

<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding ConfirmationRequest,
Mode=OneWay}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/>
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

PopupWindowAction有3个重要属性,IsModal表明窗口是否为模态,CenterOverAssociatedObject,为TRUE时在父窗口中央显示弹出窗口。WindowContent,指定在窗口显示的view,为空显示DefaultConfirmationWindow。PopupWindowAction设定Notification对象为DefaultNotificationWindow的datacontext,并在窗口显示Notification的Content属性内容。当交互完成,使用回调将结果返回view model。

4. 高级构造,组合

为了实现MVVM设计模式,你需要知道每个部分view,view model,model的具体 职责,同时也需要很好将各个部分组装起来。DI容器的使用是非常有必要的。一般使用Unity。我们可以使用构造注入和属性注入,在WPF中使用属性注入是非常有必要的,一方面保留默认构造函数,方便设计时调用。另一方面建立view与view model的依赖关系。如下:

//Unity示例
public Shell()
{
InitializeComponent();
} [Dependency]
public ShellViewModel ViewModel
{
set { this.DataContext = value; }
}

5. 测试MVVM

测试MVVM的Model,view model与普通类没有区别,可以使用一些Mock类帮助测试。相比于普通类,MVVM使用一些特殊通信模式,有一些功能或者机制需要单独测试。

5.1. 测试INotifyPropertyChanged实现

由于需要使用数据绑定机制,所以需要测试某个属性值是否正确发生改变。

5.1.1. 单个属性

我们可以使用类PropertyChangeTracker来跟踪某个类的属性是否正确发生改变,如下:

var changeTracker = new PropertyChangeTracker(viewModel);
viewModel.CurrentState = "newState";
CollectionAssert.Contains(changeTracker.ChangedProperties, "CurrentState");

如果ViewModel正确实现接口INotifyPropertyChanged,上述测试通过。

5.1.2 完整对象

当你实现接口INotifyPropertyChanged,如果需要表明当前对象所有属性均发生过改变,只需要向Contains方法传入null或者空字符,如下。

var changeTracker = new PropertyChangeTracker(viewModel);
//some change
CollectionAssert.Contains(changeTracker.ChangedProperties, "");

5.2. 测试INotifyDataErrorInfo实现

测试该接口包含两部分:一是测试验证规则是否正确实现,二是测试接口需要的内容是否正常工作。

5.2.1 测试验证规则

验证规则是保证Model数据处于一个正常范围。一条验证规则是否正常工作我们可以调用接口INotifyDataErrorInfo的方法GetErrors进行测试,前提是测试类需要实现接口。对于一些使用标记声明的共享验证规则,只需要测试一次即可,对于自定义验证规则则需要单独测试。

// Invalid case
var notifyErrorInfo = (INotifyDataErrorInfo)question;
question.Response = -15;
Assert.IsTrue(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any());
// Valid case
var notifyErrorInfo = (INotifyDataErrorInfo)question;
question.Response = 15;
Assert.IsFalse(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any());
5.2.2. 测试接口的触发条件

除了GetErrors方法需要被测试,让接口INotifyDataErrorInfo正常工作还需要保证ErrorChanged事件正确触发。除此之外属性HasErrors也需要反应对象的全局状态。测试类NotifyDataErrorInfoTestHelper可以帮助接口的触发条件,如下:

//question是待测试的Model
var helper =
new NotifyDataErrorInfoTestHelper<NumericQuestion, int?>(
question,
q => q.Response);
//测试任何条件
helper.ValidatePropertyChange(
6,
NotifyDataErrorInfoBehavior.Nothing);
//测试ErrorChanged事件是否触发以及HasErrors是否有误
helper.ValidatePropertyChange(
20,
NotifyDataErrorInfoBehavior.FiresErrorsChanged
| NotifyDataErrorInfoBehavior.HasErrors
| NotifyDataErrorInfoBehavior.HasErrorsForProperty);//?

5.3. 测试异步服务调用

在MVVM模式中,view model经常需要异步调用服务。一般的测试方式是用模拟替换真实服务。

6.感想

使用MVVM模式最重要的作用是实现解耦和封装,Winform设计出来软件基本是一个整体,你中有我,我中有你。这就会带来很多问题,特别是多人协作的情况下。确实,把好好的一个软件整体解耦出来,分成一个一个独立模块,每个模块只执行相应任务,并保持对其他模块的最小引用,解耦完成以后又引入大量通信模式,如数据绑定,命令,通知等,表面上是增加了软件的复杂度,但是随着软件功能增多,复杂度越来越高,解耦的牺牲就非常必要了。舍小逐大。

6 MVVM进阶的更多相关文章

  1. C#使用Xamarin开发可移植移动应用(3.进阶篇MVVM双向绑定和命令绑定)附源码

    前言 系列目录 C#使用Xamarin开发可移植移动应用目录 源码地址:https://github.com/l2999019/DemoApp 可以Star一下,随意 - - 说点什么.. 嗯..前面 ...

  2. C#使用Xamarin开发可移植移动应用(4.进阶篇MVVM双向绑定和命令绑定)附源码

    前言 系列目录 C#使用Xamarin开发可移植移动应用目录 源码地址:https://github.com/l2999019/DemoApp 可以Star一下,随意 - - 说点什么.. 嗯..前面 ...

  3. Android进阶笔记13:RoboBinding(实现了数据绑定 Presentation Model(MVVM) 模式的Android开源框架)

    1.RoboBinding RoboBinding是一个实现了数据绑定 Presentation Model(MVVM) 模式的Android开源框架.从简单的角度看,他移除了如addXXListen ...

  4. Silverlight中使用MVVM(3)—进阶

    这篇主要引申出Command结合MVVM模式在应用程序中的使用 我们要做出的效果是这样的 就是提供了一个简单的查询功能将结果绑定到DataGrid中,在前面的基础上,这个部分相对比较容易实现了 我们在 ...

  5. 三、Silverlight中使用MVVM(三)——进阶

    这篇主要引申出Command结合MVVM模式在应用程序中的使用 我们要做出的效果是这样的 就是提供了一个简单的查询功能将结果绑定到DataGrid中,在前面的基础上,这个部分相对比较容易实现了 我们在 ...

  6. C# WPF MVVM项目实战(进阶②)

    这篇文章还是在之前用Caliburn.Micro搭建好的框架上继续做的开发,今天主要是增加了一个用户窗体ImageProcessView,然后通过Treeview切换选择项之后在界面显示不同效果的图片 ...

  7. 最快让你上手ReactiveCocoa之进阶篇

    前言 由于时间的问题,暂且只更新这么多了,后续还会持续更新本文<最快让你上手ReactiveCocoa之进阶篇>,目前只是简短的介绍了些RAC核心的一些方法,后续还需要加上MVVM+Rea ...

  8. MVVM模式应用体会

    转自:http://www.cnblogs.com/626498301/archive/2011/04/08/2009404.html 进公司实习工作后,本人接触的第一个技术名语就是MVVM模式,从学 ...

  9. 走进Vue时代进阶篇(01):重构电商购物车模块

    前言 从这篇文章开始,我准备给大家分享一些关于Vue.js这门框架的技巧性系列文章,正好我们公司项目中也用到了Vue.所以,教是最好的学.进阶篇比较适合于二三线城市,还在小厂打拼的童鞋们.欢迎你们跟着 ...

随机推荐

  1. [HAOI 2017]八纵八横

    线段树分治+线形基. 线段树分治是个锤子?? 以时间轴构建线段树,把每个环以"对线段树产生影响的时间区间"的形式加入线段树即可. #include<bits/stdc++.h ...

  2. 网站搭建-云服务器是什么-云服务器ECS是什么

    学习上瘾了,本博客关闭,后期再总结整理.

  3. linux网卡驱动程序架构

    以cs89x0网卡驱动为例:

  4. pytest文档47-allure报告添加用例失败截图

    前言 使用 selenium 做 web 自动化的时候,很多小伙伴希望用例失败的时候能截图,把异常截图展示到allure报告里面. pytest 有个很好的钩子函数 pytest_runtest_ma ...

  5. C语言从1打印到100再打印到1该如何编写?我只服最后一种写法!

    我觉得这是一个送分题,奈何人才太多了,给出了各种古怪的写法,如果是做项目的话,我比骄建议一些正常的写法,就是大家都能看得懂的,不要搞什么花里胡哨,不过你要是交流的话,既然是交流,我不觉得要多正规,即使 ...

  6. 【Windows编程】入门篇——win 32窗口的hello word!

    ✍  Windows编程基础 1.Win 32应用程序基本类型 1)  控制台程序 不需要完善的windows窗口,可以使用DOS窗口方式显示 2)  Win 32窗口程序 包含窗口的程序,可以通过窗 ...

  7. PHP SPL标准库-数据结构

    SPL是用于解决典型问题的一组接口与类的集合. 双向链表 SplDoublyLinkedList SplStack SplQueue 双链表是一种重要的线性存储结构,对于双链表中的每个节点,不仅仅存储 ...

  8. 如何理解码分复用中的码分多址CDMA?

    如何理解CDMA? 推荐参考大神文章 https://blog.csdn.net/dog250/article/details/6420427 (码分多址(CDMA)的本质-正交之美) 首先我们先看& ...

  9. springboot入门系列(三):SpringBoot教程之RabbitMQ示例

    SpringBoot教程之RabbitMQ示例 SpringBoot框架已经提供了RabbitMQ的使用jar包,开发人员在使用RabbitMQ的时候只需要引用jar包简单的配置一下就可以使用Rabb ...

  10. Spring Boot与多数据源那点事儿~

    持续原创输出,点击上方蓝字关注我 目录 前言 写这篇文章的目的 什么是多数据源? 何时用到多数据源? 整合单一的数据源 整合Mybatis 多数据源如何整合? 什么是动态数据源? 数据源切换如何保证线 ...