zoukankan      html  css  js  c++  java
  • 从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技术详解》和《包氏波动思想》将会分别介绍前两种情况。

  • 相关阅读:
    this is test,,this is only test!!!
    js设置鼠标悬停改变背景色
    免费数据恢复软件
    ORA-00600: 内部错误代码
    js控制只能输入数字和控制小数的位数
    Eclipse换背景色
    记JavaScript的入门学习(二)
    html+css基础篇
    记JavaScript的入门学习(一)
    前端之Photoshop切片
  • 原文地址:https://www.cnblogs.com/Jax/p/1579961.html
Copyright © 2011-2022 走看看