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. JAVA学习线路:day01面向对象(继承、抽象类)

    所有的文档和源代码都开源在GitHub: https://github.com/kun213/DailyCode上了.希望我们可以一起加油,一起学习,一起交流. day01面向对象[继承.抽象类] 今 ...

  2. SCOI 2008 【奖励关】

    早上的考试一道都做不出,被教做人,心态爆炸ing...... 题目描述: 你正在玩你最喜欢的电子游戏,并且刚刚进入一个奖励关.在这个奖励关里,系统将依次随机抛出k次宝物,每次你都可以选择吃或者不吃(必 ...

  3. 极简 Node.js 入门 - 4.5 双工流

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  4. git-代码分支管理

    1. git代码分支管理     DEV SIT UAT PET PRE PRD PROD常见环境英文缩写含义 英文缩写 英文 中文 DEV development 开发 SIT System Int ...

  5. 用网桥和veth实现容器的桥接模式

    原理图如下 具体命令先不写了,有时间再写,主要还是用的上一篇说的知识.

  6. 从面试角度学完 Kafka

    Kafka 是一个优秀的分布式消息中间件,许多系统中都会使用到 Kafka 来做消息通信.对分布式消息系统的了解和使用几乎成为一个后台开发人员必备的技能.今天码哥字节就从常见的 Kafka 面试题入手 ...

  7. windows.h头文件中改变光标位置的函数——SetConsoleCursorPosition

    COORD 具体为 typedef struct COORD{ short X; short Y; } COORD,*PCOORD;     可以用来记录坐标. #include <iostre ...

  8. 源生代码和H5的交互 android:

    1: 默认的事情: Android 通过内置的UI控件WebView来加载网页.         网页是用一个网络地址来表示的:         其整个使用方法很简单如下:(android不关心实际的 ...

  9. 【树形DP】BZOJ 3829 Farmcraft

    题目内容 mhy住在一棵有n个点的树的1号结点上,每个结点上都有一个妹子i. mhy从自己家出发,去给每一个妹子都送一台电脑,每个妹子拿到电脑后就会开始安装zhx牌杀毒软件,第i个妹子安装时间为Ci. ...

  10. WebFlux快速上手

    一.新建项目 示例使用IDEA快速创建基于SpringBoot的工程. springboot 2.3.1 java 8 WebFlux 必须选用Reactive的库 POM 依赖 <depend ...