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. 微型直流电机控制基本方法 L298N模块

    控制任务 让单个直流电机在L298N模块驱动下,完成制动.自由停车,正反转,加减速等基本动作 芯片模块及电路设计 图1 L298N芯片引脚 图2 L298N驱动模块 表1 L298N驱动模块的控制引脚 ...

  2. Linux系统编程 —共享内存之mmap

    共享内存概念 共享内存是通信效率最高的IPC方式,因为进程可以直接读写内存,而无需进行数据的拷备.但是它没有自带同步机制,需要配合信号量等方式来进行同步. 共享内存被创建以后,同一块物理内存被映射到了 ...

  3. SpringBoot整合Shiro+MD5+Salt+Redis实现认证和动态权限管理(上)----筑基中期

    写在前面 通过前几篇文章的学习,我们从大体上了解了shiro关于认证和授权方面的应用.在接下来的文章当中,我将通过一个demo,带领大家搭建一个SpringBoot整合Shiro的一个项目开发脚手架, ...

  4. httpd之ab压力测试

    安装软件 yum install -y httpd 参数说明:用法Usage: ab [options] [http[s]://]hostname[:port]/path用法:ab [选项] 地址 选 ...

  5. linux查看进程内存使用情况,以及将线程情况输出文件

    用jmap把进程内存使用情况dump到文件中,再用jhat分析查看.jmap进行dump命令格式如下: jmap -dump:format=b,file=/tmp/dump.dat 21711  -- ...

  6. IOS使用UITextView进行富文本编辑|纯干货

    看了好多blog介绍富文本编辑,有很多很好的开源项目,比如:YYText.FastTextView.ZSSRichTextEditor等等.本着学习的目的还是选择用UITextView来实现简单的富文 ...

  7. git克隆指定分支到本地

    我们每次使用命令 git clone https://xxx.com/android-app.git 默认 clone 的是这个仓库的 master 分支. 使用Git下载指定分支命令为:git cl ...

  8. chrome(谷歌)登录失败解决方案

    相信有很多小伙伴和我一样,同步chrome的收藏夹,这样也便于随时可以查看自己收藏的网址.但是同步文件,必须先要登录chrome账号,登录chrome账号时,总是会报黄页,或者一直加载不出来.接下来, ...

  9. 基于Python的接口自动化实战-基础篇之读写配置文件

    引言 在编写接口自动化测试脚本时,有时我们需要在代码中定义变量并给变量固定的赋值.为了统一管理和操作这些固定的变量,咱们一般会将这些固定的变量以一定规则配置到指定的配置文件中,后续需要用到这些变量和变 ...

  10. CentOS7克隆多个虚拟机

    VMware+centos7克隆虚拟机 步骤一:打开虚拟机,右键选中已经配置好的虚拟机,选择manage下面的clone选项.这里有一个需要注意的地方,就是虚拟机在启动或者挂起的状态下是不能clone ...