从Event折腾到Command

(一)传统编程模型

      在传统的窗体编程模型中,包括ASP.NET、Winform、WPF和Silverlight,Visual Studio会为我们分别提供不同的项目模板,如下所示:

      image

      于是,我们得以创建项目如下(以Winform为例):

      clip_image004

      注意到,我们看到的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>


      双击该Button,会产生如下图所示代码:

      clip_image006

      是不是和Winform编程模型很相像。唯一的不同就是,在ASP.NET中找不到Button1_Click方法是如何绑定Button的Click事件的,也找不到Page_Load方法是如何绑定到页面的Load事件的。

      在ASP.NET 1.1的年代哦,因为那时候还没有partial class的概念,所以对Button的定义以及把Button1_Click方法添加到Button的Click事件上,是和Button1_Click方法定义在同一个类文件中的。

      但是在ASP.NET 2.0以及更高版本里,这些信息都看不到了,也许ASP.NET 的设计人员觉得这些信息犹如鸡肋,便不再公之于众了。

      我猜想哦,对于Button以及其Click事件 ,因为我们之前已经在aspx文件中声明了这个控件,如下所示: 

   <asp:Button ID="Button1" runat="server" onclick="Button1_Click" Text="Button" />

      所以,ASP.NET引擎会将其序列化为一个Button对象,并为其添加Button1_Click方法,也就是自动生成以下两行代码:          

      Button button1 = GetButtonFromASPX();
      button1.Click 
+= button1_Click;

      其中,GetButtonFromASPX方法就是从aspx页面序列化得到一个Button对象。

      对于aspx页面中的控件,倒还可以这么解释,但是,对于aspx页面本身的Page_Load方法是如何绑定到Load事件的,我就百思不得其解了,按理说,应该存在这样一条语句:       

      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方法的。

      clip_image008

      于是又有人要问了,控件的声明以及事件的绑定在哪里?不同于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";
        }

    }

      以上这些基于Event的窗体开发模式,从Visual Studio 2002延续到2008(甚至2010也如是),可以说深入人心了。这其中蕴含着深刻的设计模式思想,分析如下。

      我们知道,窗体中的Event是基于观察者模式的,而在窗体中同时要管理多个Event,比如说上面那个WPF的例子,我们在一个WPF程序中的Window1中添加一个Button、一个Checkbox和一个Label。当点击Button的时候,Label会显示为Hello World,当选中Checkbox的时候,Label会显示为Open Seasam。

      代码如下所示:       

<CheckBox Height="16" Margin="34,82,124,0" Name="checkBox1" VerticalAlignment="Top" Checked="checkBox1_Checked" >CheckBox</CheckBox>
<Label Margin="33,120,125,114" Name="label1">Label</Label>

      相应的后台方法:

      1)隐藏在Window1.g.i.cs中的代码: 

this.button1.Click += new System.Windows.RoutedEventHandler(this.button1_Click);
this.checkBox1 = ((System.Windows.Controls.CheckBox)(target));

      2)Window1.cs中相应的方法: 

        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,虽然在窗体编程模型中将其称为容器(承载Button、Checkbox和Label这些控件),但是,Window1作为一个类,只是保持了对上述这些控件的引用,并且管理着与控件事件相关的方法(比如说button1_Click)。这样做的好处是,当Button的Click事件触发时,不必再和Label直接建立观察者模式,而是通知Window1,由Window1来间接操作Label(也就是button1_Click方法)。

      当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";
        }

    }

      这里面蕴含着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 getset; }

        
public void UpdateContent()
        
{
            Window1.UpdateContent(
"Hello World");
        }

    }

      在新的重构中,我们在XXXHelper中也添加了一个对Window1的引用,这样就可以直接通过XXXHelper来改变Label的内容了。这可以被认为是MVP模式的前身,但就像辛亥革命那样,重构并不是很彻底,因为仍然要手动控制Label的显示内容。如果能只操作数据,UI就会自动跟着发生改变,这该有多好?遗憾的是,传统Winform和ASP.NET中的数据绑定做不到这一点,只有WPF和Silverlight中的绑定技术才能完美的诠释这一理念。

      所以说,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技术详解》和《包氏波动思想》将会分别介绍前两种情况。

posted @ 2009-10-10 00:04  包建强  Views(7100)  Comments(18Edit  收藏  举报