Fork me on GitHub
从Event折腾到Command

(一)传统编程模型

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

从Event折腾到Command

 

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

从Event折腾到Command

 

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

从Event折腾到Command

 

  于是又有人要问了,控件的声明以及事件的绑定在哪里?不同于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中的绑定技术才能完美的诠释这一理念。

posted on 2010-07-10 19:16  HackerVirus  阅读(331)  评论(0编辑  收藏  举报