从前面几篇文章可以了解到,Rx作为LINQ的一种扩展,极大地简化了异步编程。但Rx的用法不仅如此,由于其可高的扩展性,在其他很多方面也有所应用。
在前面例子中,我们使用代码和UI界面上的元素打交道,这种方式在传统的Winfom编程中很常见,但是在基于XAML构造的界面这种应用程序中,这样显得不是非常友好,XAML中声明式编程可以使得程序更加简洁,传统的方式没有利用到XAML中强大的绑定功能。之前,我们大量使用了诸如Observable.FromEvent这样的操作,然后来使用后台代码来设置控件的属性,这都是传统的编程方式。
当然,对于规模较小的程序来说,这种方式无可厚非。这种方式的最大的缺点在于,对于测试很不友好,要测试这样的应用程序很困难,我们需要创建UI控件并模拟输入,这样效率不高而且不可靠。另一个缺点是,这种方式使得代码高度耦合而且脆弱。针对这些问题,一种称为Model-View-ViewModel(MVVM)的设计模式逐渐发展起来。
结合MVVM模式和Rx类库,发展出了ReactiveUI这个MVVM框架。他能够使得应用程序可以管理,并能使用声明式、函数式的方式来表达复杂的对象间的交互。换句话说,ReactiveUI能够帮助我们描述属性之间是如何联系起来的,即使有些属性与异步方法调用有关。
1. MVVM模式
Model-View-ViewModel模式是充分利用XAML设计平台上的数据绑定功能而产生的一种设计模式。在该模式中,Model是用来表现应用程序的数据以及与界面独立的逻辑的核心对象。View就是UI控件及界面,比如窗体控件或者用户自定义控件。值得注意的是对于同一数据对象,可能有多种表现形式,比如对于同一数据源,有的视图用来显示统计或者全局的信息,有的显示每一项的详细信息。
一般我们很熟悉的是MVC模型,所以MVVM模式中的ViewModel是其特别的地方。从名字上看,ViewModel是一种针对视图的模型。可能有点不好理解。举个例子来说:在用户注册页面(View)中,一般有输入用户名,密码,重复输入密码这几个输入框。在这个视图中,用户名和密码显然是存在于Model中,但是 重复输入密码 这一项并不属于Model,它显然不应该存在于真实的数据模型中,该项只是用在View中。
在传统的以XAML为界面的程序中,开发者一般使用页面(View)的后台的代码来存储这个重复输入密码值,但是这样同样存在可测试性和紧耦合的问题。例如,如果我们要测试“只有当密码和重复密码输入的值匹配,提交按钮才可以使用”这样一个场景就变得有点困难。现在,我们将这个字段存在另一个称之为ViewModel的对象中,这个ViewModel对象只是一个普通的类,他并不需要继承自UI控件,我们可以将该对象看做是与View的交互逻辑。在我们的例子中,验证两次密码是否匹配以及在匹配时让提交按钮可用,这些逻辑都应该写在ViewModel对象中。对于每一个View,都应该有一个对应的ViewModel对象。
1.1 ViewModeld的理念
MVVM最强大的一方面在于它的目标是将一个命令(command)或者属性(property)是什么和如何执行分开来。ViewModel是对属性和命令的一种思考。在传统的基于Codebehind的用户交互框架中,开发者需要思考控件的事件和属性。当以这种方式编写代码时,意味着事件和相应的控件紧紧的联系在一起。使得测试变得困难,因为我们需要模拟出控件的动作才能测试控件对应的事件及功能是否正常。
当使用MVVM的ViewModels时,最重要的是将这两部分逻辑分开来。在View中决定了这些控件如何被触发,同时,控件对应的一些属性利用XAML的绑定技术和ViewModel绑定起来。
1.2 MVVM框架的作用
现在有很多开源的MVVM框架可以使用了,如MVVMLight、Prism,这些框架框架各有优点。但是他们都提供了实现MVVM模式的最基本要素。首先,这些框架为ViewModel对象提供了一个基类,当这些对象的属性在属性值发生改变时会得到通知,这个是通过实现INotifyPropertyChanged接口来完成的,这个接口很关键,因为他通知View需要更新绑定到界面上的数据。MVVM提供了处理命令的一套系统,当用户发出一些命令时它能够很好的处理。这是通过实现ICommand接口来实现的,这个接口通常包含在UI控件中。
2.ReactiveUI库
ReactiveUI类库是实现了MVVM模式的框架,他移除了一些Rx和用户界面进行交互的代码。ReactiveUI的核心思想是使开发者能够将属性变更以及事件转换为IObservable对象,然后在需要的时候使用IObservable对象将这些对象转换到属性中来。他的另一个核心目标是可以在ViewModel中相关属性发生变化时可以可执行相应的命令。虽然其他的框架也允许这么做,但是ReactiveUI会在依赖属性变更时自动的去更新结果,而不需要通过拉或者调用类似UpdateTheUI之类的方法。
2.1 核心类
ReactiveObject:它是ViewModel对象,该对象实现了INotifyPropertyChanged接口。除此之外,该对象也提供了一个称之为Changed的IObservable接口,允许其他对象来注册,从而使得该对象属性变更时能够得到通知。使用Rx中强大的操作符,我们还可以追踪到一些状态是如何改变的。
ReactiveValidateObject:该对象继承自ReactiveObject对象,它通过实现IDataErrorInfo接口,利用DataAnnotations来验证对象。因此属性的值可以使用一些限制标记,UI界面能够自动的在属性的值违反这些限制时显示出这些错误。
ObservableAsPropertyHelper
ReactiveCommand:该类实现了ICommand和IObservable接口,并且当Execute执行时OnNext方法就会被执行。该对象的CanExecute可以通过IObservable
ReactiveAsyncCommand:该对象继承自ReactiveCommand,并且封装了一种通用的模式。即“触发一步命令,然后将结果封送到dispather线程中”该对象也允许设置最大并行值。当达到最大值时,CanExecute方法返回false。
3.使用ReactiveObject实现ViewModels
和其他MVVM框架一样,ReactiveUI框架有一个对象来作为ViewModel类。该对象和基于传统的实现了ViewModel对象的MVVM框架如Foundation,Cliburn.Micro类似。但是最大的不同在于,ReactiveUI能够很容易的通过名为Changed的IObservable接口注册事件变化。在任何一个属性发生变化时,都会触发通知,客户端通常只需要关注感兴趣的一两个变化了的属性。使用ReactiveUI,可以通过WhenAny扩展方法很容易的获取这些属性值:
var newLoginVm = new NewUserLoginViewModel(); newLoginVm.WhenAny(x => x.User, x => x.Value) .Where(x => x.Name == "Bob") .Subscribe(x => MessageBox.Show("Bob is already a user!")); IObservable<bool> passwordIsValid = newLoginVm.WhenAny( x => x.Password, x => x.PasswordConfirm, (pass, passConf) => (pass.Value == passConf.Value));
WhenAny语法看起来过有点奇怪。方法中第一个参数是通过匿名方法定义的一系列属性。在上面的例子中,我们关心的是神马时候Password或者PasswordConfirm发生变化。最后一个参数和Zip操作符中的类似,他使用一个匿名方法来将两个结果结合起来,然后返回结果。当这两个属性中的任何一个发生变化时,方法就会执行,并以IObservable的形式返回执行结果,在上面的例子中就是passwordIsValid这个对象。
对于ReactiveObject,值得注意的是,属性必须明确的使用特定的语法进行定义。因为简单的get,set并没有实现INotifyPropertyChanged,从而不会通知ReactiveObject对象该属性发生了改变。唯一例外的就是,如果一个属性在构造器中初始化了,在以后的程序中不会发生改变。在ReactiveObject中,属性的命名也需要注意,用作属性的私有字段必须为属性名称前面加上下划线。下面的例子展示了如何使用ReactiveObject声明一个可读写的属性。
public class AppViewModel : ReactiveObject { int _SomeProp; public int SomeProp { get { return _SomeProp; } set { this.RaiseAndSetIfChanged(x => x.SomeProp, value); } } }
传统的实现IpropertyChangeNofity接口的实现方法如下:
public class AppViewModel : INotifyPropertyChanged { int _SomeProp; public int SomeProp { get { return _SomeProp; } set { if (_SomeProp == value) return; _SomeProp = value; RaisePropertyChanged("SomeProp"); } } public e