(一)传统编程模型
在传统的窗体编程模型中,包括.NET、Winform、WPF和Silverlight,Visual Studio会为我们分别提供不同的项目模板,如下所示:
于是,我们得以创建项目如下(以Winform为例):
注意到,我们看到的Form1可视化界面是由Visual Studio为我们自动生成的,它实际上是由Form1.cs和Form1.Designer.cs两部分组成的,它们是同一个类Form1的两个部分。我们从来不会去修改Form1.Designer.cs,因为它是由Visual Studio来自动维护的。当我们触发一些事件时,比如说,双击窗体从而默认添加Form1的Load事件,这时,Visual Studio就会做两件事情:
1) 在Form1.cs中添加Form1_Load方法:
private void Form1_Load(object sender, EventArgs e)
{
}
2) 在Form1.Designer.cs中把Form1_Load方法添加到Form1的Load事件上,从而在Form加载的时候,会触发这个方法:
this.Load += new System.EventHandler(this.Form1_Load);
于是,我们所要做的工作简化为——只需在Form1_Load方法添加自己的逻辑就可以了,生产率得到大幅提升。但恰恰是这一优点,使得很多程序员只会拖拖控件,写写方法,而不晓得这其中的因果联系。这也同时验证了微软是在为懒人设计开发工具的理念。
我们举的例子是Winform的,但是逐一分析ASP.NET、WPF和Silverlight,你会发现,都是一样的。
在ASP.NET中,分为服务器和客户端两部分代码。说得俗一点,ASP.NET所要做的工作就是根据服务器端的.NET代码生成客户端的HTML代码。而服务器端的.NET代码,也采取了上述这种Event编程模式,如下所示:
比如说,在一个apsx页面添加一个Button:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Button ID="Button1" runat="server" onclick="Button1_Click" Text="Button" />
</div>
</form>
</body>
</html>
this.Load += Page_Load;
但可惜的是,就是找不到~~。
也许你会问WPF和Silverlight中的XAML是虾米,我认为哦,XAML的概念借鉴了HTML的思想,但是又有极大的不同。
为什么这么说呢?我们知道,二者都是标签语言,前者通过IE等浏览器而后者借助Blender等设计器,都可以达到所见即所得的效果,而区别又在哪里呢?
ASP.NET模型中,WebForm本身具有一棵控件树,在运行期它会把这棵树转换为一个HTML文件并显示;而WPF/Silverlight本身也有一棵控件树,通过序列化XAML来填充这棵树,最终会显示出这棵树。
二者区别就在于,HTML在ASP.NET中是显示的结果,而XAML在WPF/Silverlight中是源头。
我们在Visual Studio中建立一个WPF项目,会看到一个窗体对应Window1.xaml和Window1.xaml.cs两个文件。前者就是XAML了,而后者则用来放置那些Button1_Click、checkBox1_Checked方法的。
于是又有人要问了,控件的声明以及事件的绑定在哪里?不同于Winform的Form1.Designer.cs,在WPF中,放置这些代码的文件是一个名为Window1.g.i.cs的隐藏文件,它是readonly的,就是说,只允许Visual Studio自动修改,有兴趣的朋友可以在Window1.cs文件中Window1的构造函数里点击InitializeComponent方法,就会跳转到该隐藏文件(代码就不贴了)。
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
label1.Content = "Hello World";
}
private void checkBox1_Checked(object sender, RoutedEventArgs e)
{
label1.Content = "Open Seasam";
}
}
当Window1中控件之间的交互越来越多时,这种好处就越明显。我们将Window1称为中介者(Mediator)——就是说,窗体编程模型普遍采用了中介者模式,从ASP.NET、Winform、到WPF、Silverlight,无一不是如此。
关于中介者模式的介绍,请参见我的另一篇文章:《我也设计模式——Mediator》
(二)传统编程模式的缺点以及一些解决方案
关于Command模式的基本概念,请参见我的另一篇文章:《我也设计模式——Command》。
话说,就在Event编程模式大行其道的时候,一些显著的问题也摆在了我们面前:
1)虽然控件之间的交互完全都扔给Form窗体这个中介者了,但是我们发现,随着逻辑的越来越复杂,Form窗体中的代码也越来越多,动辄几千甚至上万。
2)随着单元测试的普及,越来越多的程序员要求在他们的窗体程序中对UI相关的逻辑进行测试,但是原有的Event编程模式把UI和逻辑混在了一起,这使得单元测试无法进行。
为此,我们通常会额外编写一个代理类,起个带有Helper后缀的名字。
比如上面那个点击Button改变Label内容的button1_Click方法,可以重构为:
public partial class Window1 : Window
{
private XXXHelper helper = new XXXHelper();
public Window1()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
string text = helper.GetContent();
label1.Content = text;
}
}
public class XXXHelper
{
public string GetContent()
{
return "Hello World";
}
}
所以说,MVP模式是最适合于在WPF/Silverlight中运用的了。
对于问题二,也就是如何在UI中做UnitTest,是近几年来程序界广泛讨论的一个话题。
我们究竟是要测试控件,还是要测试控件背后的数据?
这个问题进一步归结为:我们究竟是面向控件编程,还是面向数据编程?
貌似从编程伊始我们就面向控件编程,就是说,每次获得数据,就想法设法把这些数据分派到各个控件的各个属性上,而从来没有真正关心过数据。
但越来越多的实战经验告诉我们,应该把更多的精力放到数据本身上,而不是其在控件上的表现形式。
于是,我们引进了MVP模式,将UI一拆为二:界面(View)和数据(Model);二者之间通过Presenter来互通有无,就是说在View中触发事件引起Model的改变都封装成方法放在Presenter。
这样,我在HP的项目中,建立了这样的测试模型,单独对View做验收测试(AcceptTest),也就是自动化测试,利用UIA或者White框架;而单元测试则建立在Presenter的那些方法之上,用以测试Model中的数据。详细信息请关注这个系列的下两篇文章:《Prism中的 UnitTest》和《Prism中的AcceptTest》。
(三)Command应运而生
Command是和MVP模式相辅相成,因此,在阅读本章之前,建议读者参考这个系列的另一篇文章:《MVP模式的前世今生》。
就在传统Event编程模式满足不了我们复杂的业务逻辑时,MVP模式出现了,尤其是在WPF和Silverlight项目中,强大的数据绑定技术使MVP模式得以完美诠释。
但是,一个严峻的问题摆在了眼前:为了使View最简单,就像下面的代码一样(这是一个MVVM的例子):
public partial class ListItemContentView : UserControl
{
public ListItemContentView()
{
InitializeComponent();
}
public ListItemContentView(ListItemContentViewModel viewModel)
: this()
{
this.ViewModel = viewModel;
this.ViewModel.View = this;
}
public ListItemContentViewModel ViewModel
{
get
{
return this.DataContext as ListItemContentViewModel;
}
set
{
this.DataContext = value;
}
}
}
可以看到,View中没有任何事件绑定的方法,例如Button的Click事件,GridView的LoadRow事件(用于Silverlight中的数据逐行加载到GridView中)。那么这些事件都何去何从了呢?
大致分为三种情况:
1)一部分要使用AttachBehavior来自定义实现Command,比如说Button的Click事件,这是基于AttachBehavior 来实现的,Prism内部只提供了对Button的Click事件支持,其它的事件比如说TextBlock的Click事件,需要仿照Button的 Click事件来设计。
2)一部分被MVP模型的Model所消化,如GridView的SelectionChanged事件。
3)最后一部分,是实在不能转移的,就只好留在View中了,如该窗体的Loaded事件,这些事件大都有一个特性——它们都是基于控件的生命周期的。比如说窗体的Loaded事件,就是在窗体加载完成之后,别小看这个方法,只要没执行完,窗体中的数据和XAML就还都是未初始化过的,不能使用。这样的方法很多,但使用并不是很频繁,也就偶尔会用到,所以不必担心它们残留在View中而带来的一些麻烦:视觉不爽啦、没有重构彻底啦。要知道把事件转换为 Command是一个度的问题,过分设计往往会导致性能下降、开发周期变长等诸多问题。
接下来的2篇文章《AttachedBehavior技术详解》和《包氏波动思想》将会分别介绍前两种情况。
这里面蕴含着OO编程中组合(Composite)的思想。
后来,随着“依赖反转”理念的深入,我们也可以进一步重构如下:
public partial class Window1 : Window
{
private XXXHelper helper = new XXXHelper();
public Window1()
{
InitializeComponent();
helper.Window1 = this;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
helper.UpdateContent();
}
public void UpdateContent(string content)
{
label1.Content = content;
}
}
public class XXXHelper
{
public Window1 Window1 { get; set; }
public void UpdateContent()
{
Window1.UpdateContent("Hello World");
}
}
在新的重构中,我们在XXXHelper中也添加了一个对Window1的引用,这样就可以直接通过XXXHelper来改变Label的内容了。这可以被认为是MVP模式的前身,但就像辛亥革命那样,重构并不是很彻底,因为仍然要手动控制Label的显示内容。如果能只操作数据,UI就会自动跟着发生改变,这该有多好?遗憾的是,传统Winform和ASP.NET中的数据绑定做不到这一点,只有WPF和Silverlight中的绑定技术才能完美的诠释这一理念。