Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1482376
  • 博文数量: 465
  • 博客积分: 8915
  • 博客等级: 中将
  • 技术积分: 6365
  • 用 户 组: 普通用户
  • 注册时间: 2010-07-30 15:05
文章分类

全部博文(465)

文章存档

2017年(33)

2016年(2)

2015年(4)

2014年(29)

2013年(71)

2012年(148)

2011年(178)

分类: 架构设计与优化

2013-02-26 18:52:01

MVC的变体

通过采用MVC模式,我们可以将可视化UI元素的呈现、UI处理逻辑和业务逻辑分别定义在View、Controller和Model中,但是对于三者之间的交互,MVC并没有进行严格的限制。最为典型的就是允许View和Model绕开Controller进行直接交互,View可以通过调用Model获取需要呈现给用户的数据,Model也可以直接通知View让其感知到状态的变化。当我们将MVC应用于具体的项目开发中,不论是基于GUI的桌面应用还是基于Web UI的Web应用,如果不对Model、View和Controller之间的交互进行更为严格的限制,我们编写的程序可能比自治视图更加难以维护。

今天我们将MVC视为一种模式(Pattern),但是作为MVC最初提出者的Trygve M. H. Reenskau却将MVC视为一种范例(Paradigm),这可以从它在Applications Programming in Smalltalk-80(TM):How to use Model-View-Controller (MVC)中对MVC的描述可以看出来:In the MVC paradigm the user input, the modeling of the external world, and the visual feedback to the user are explicitly separated and handled by three types of object, each specialized for its task.

模式和范例的区别在于前者可以直接应用到具体的应用上,而后者则仅仅提供一些基本的指导方针。在我看来MVC是一个很宽泛的概念,任何基于Model、View和Controller对UI应用进行分解的设计都可以成为MVC。当我们采用MVC的思想来设计UI应用的时候,应该根据开发框架(比如Windows Forms、WPF和Web Forms)的特点对Model、View和Controller的界限以及相互之间的交互设置一个更为严格的规则。

在软件设计的发展历程中出现了一些MVC的变体(Varation),它们遵循定义在MVC中的基本原则,我们现在来简单地讨论一些常用的MVC变体。

MVP

MVP是一种广泛使用的UI架构模式,适用于基于事件驱动的应用框架,比如ASP.NET Web Forms和Windows Forms应用。MVP中的M和V分别对应于MVC的Model和View,而P(Presenter)则自然代替了MVC中的Controller。但是MVP并非仅仅体现在从Controller到Presenter的转换,更多地体现在Model、View和Presenter之间的交互上。

MVC模式中元素之间“混乱”的交互主要体现在允许View和Model绕开Controller进行单独“交流”,这在MVP模式中得到了彻底解决。如图1-2所示,能够与Model直接进行交互的仅限于Presenter,View只能通过Presenter间接地调用Model。Model的独立性在这里得到了真正的体现,它不仅仅与可视化元素的呈现(View)无关,与UI处理逻辑(Presenter)也无关。使用MVP的应用是用户驱动的而非Model驱动的,所以Model不需要主动通知View以提醒状态发生了改变。

 

图1-2  Model-View-Presenter之间的交互

MVP不仅仅避免了View和Model之间的耦合,更进一步地降低了Presenter对View的依赖。如图1-2所示,Presenter依赖的是一个抽象化的View,即View实现的接口IView,这带来的最直接的好处就是使定义在Presenter中的UI处理逻辑变得易于测试。由于Presenter对View的依赖行为定义在接口IView中,我们只需要Mock一个实现了该接口的View就能对Presenter进行测试。

构成MVP三要素之间的交互体现在两个方面,即View/Presenter和Presenter/Model。Presenter和Model之间的交互很清晰,仅仅体现在Presenter对Model的单向调用。而View和Presenter之间该采用怎样的交互方式是整个MVP的核心,MVP针对关注点分离的初衷能否体现在具体的应用中很大程度上取决于两者之间的交互方式是否正确。按照View和Presenter之间的交互方式以及View本身的职责范围,Martin Folwer将MVP可分为PV(Passive View)和SC(Supervising Controller)两种模式。

PV与SC

解决View难以测试的最好的办法就是让它无需测试,如果View不需要测试,其先决条件就是让它尽可能不涉及到UI处理逻辑,这就是PV模式目的所在。顾名思义,PV(Passive View)是一个被动的View,包含其中的针对UI元素(比如控件)的操作不是由View自身主动来控制,而被动地交给Presenter来操控。

如果我们纯粹地采用PV模式来设计View,意味着我们需要将View中的UI元素通过属性的形式暴露出来。具体来说,当我们在为View定义接口的时候,需要定义基于UI元素的属性使Presenter可以对View进行细粒度操作,但这并不意味着我们直接将View上的控件暴露出来。举个简单的例子,假设我们开发的HR系统中具有如图1-3所示的一个Web页面,我们通过它可以获取某个部门的员工列表。

 

图1-3  员工查询页面

现在通过ASP.NET Web Forms应用来设计这个页面,我们来讨论一下如果采用PV模式,View的接口该如何定义。对于Presenter来说,View供它操作的控件有两个,一个是包含所有部门列表的DropDownList,另一个则是显示员工列表的GridView。在页面加载的时候,Presenter将部门列表绑定在DropDownList上,与此同时包含所有员工的列表被绑定到GridView。当用户选择某个部门并点击“查询”按钮后,View将包含筛选部门在内的查询请求转发给Presenter,后者筛选出相应的员工列表之后将其绑定到GridView。

如果我们为该View定义一个接口IEmployeeSearchView,我们不能按照所示的代码将上述这两个控件直接以属性的形式暴露出来。针对具体控件类型的数据绑定属于View的内部细节(比如说针对部门列表的显示,我们可以选择DropDownList也可以选择ListBox),不能体现在表示用于抽象View的接口中。另外,理想情况下定义在Presenter中的UI处理逻辑应该是与具体的技术平台无关的,如果在接口中涉及控件类型,这无疑将Presenter也与具体的技术平台绑定在了一起。

public interface IEmployeeSearchView

{

    DropDownList             Departments { get;}

    GridView                 Employees { get; }

}

正确的接口和实现该接口的View(一个Web页面)应该采用如下的定义方式。Presenter通过对属性Departments和Employees赋值进而实现对相应DropDownList和GridView的数据绑定,通过属性SelectedDepartment得到用户选择的筛选部门。为了尽可能让接口只暴露必需的信息,我们特意将对属性的读/写作了控制。

public interface IEmployeeSearchView

{

    IEnumerable        Departments { set; }

    string                     SelectedDepartment { get; }

    IEnumerable          Employees { set; }

}

 

public partial class EmployeeSearchView: Page, IEmployeeSearchView

{

    //其他成员

    public IEnumerable Departments

    {

        set

        {

            this.DropDownListDepartments.DataSource = value;

            this.DropDownListDepartments.DataBind();

        }

    }

 

    public string SelectedDepartment

    {

        get { return this.DropDownListDepartments.SelectedValue;}

    }

 

    public IEnumerable Employees

    {          

        set

        {

            this.GridViewEmployees.DataSource = value;

            this.GridViewEmployees.DataBind();

        }

    }

}

PV模式将所有的UI处理逻辑全部定义在Presenter上,意味着所有的UI处理逻辑都可以被测试,所以从可测试性的角度来这是一种不错的选择,但是它要求将View中可供操作的UI元素定义在对应的接口中,对于一些复杂的富客户端(Rich Client)View来说,接口成员将会变得很多,这无疑会提升编程所需的代码量。从另一方面来看,由于Presenter需要在控件级别对View进行细粒度的控制,这无疑会提供Presenter本身的复杂度,往往会使原本简单的逻辑复杂化,在这种情况下我们往往采用SC模式。

在SC模式下,为了降低Presenter的复杂度,我们将诸如数据绑定和格式化这样简单的UI处理逻辑转移到View中,这些处理逻辑会体现在View实现的接口中。尽管View从Presenter中接管了部分UI处理逻辑,但是Presenter依然是整个三角关系的驱动者,View被动的地位依然没有改变。对于用户作用在View上的交互操作,View本身并不进行响应,而是直接将交互请求转发给Presenter,后者在独立完成相应的处理流程(可能涉及针对Model的调用)之后会驱动View或者创建新的View作为对用户交互操作的响应。

View和Presenter交互的规则(针对SC模式)

View和Presenter之间的交互是整个MVP的核心,能否正确地应用MVP模式来架构我们的应用主要取决于能否正确地处理View和Presenter两者之间的关系。在由Model、View和Presenter组成的三角关系中,核心不是View而是Presenter,Presenter不是View调用Model的中介,而是最终决定如何响应用户交互行为的决策者。

打个比方,View是Presenter委派到前端的客户代理,而作为客户的自然就是最终的用户。对于以鼠标/键盘操作体现的交互请求应该如何处理,作为代理的View并没有决策权,所以它会将请求汇报给委托人Presenter。View向Presenter发送用户交互请求应该采用这样的口吻:“我现在将用户交互请求发送给你,你看着办,需要我的时候我会协助你”,而不应该是这样:“我现在处理用户交互请求了,我知道该怎么办,但是我需要你的支持,因为实现业务逻辑的Model只信任你”。

对于Presenter处理用户交互请求的流程,如果中间环节需要涉及到Model,它会直接发起对Model的调用。如果需要View的参与(比如需要将Model最新的状态反应在View上),Presenter会驱动View完成相应的工作。

对于绑定到View上的数据,不应该是View从Presenter上“拉”回来的,应该是Presenter主动“推”给View的。从消息流(或者消息交换模式)的角度来讲,不论是View向Presenter完成针对用户交互请求的通知,还是Presenter在进行交互请求处理过程中驱动View完成相应的UI操作,都是单向(One-Way)的。反应在应用编程接口的定义上就意味着不论是定义在Presenter中被View调用的方法,还是定义在IView接口中被Presenter调用的方法最好都没有返回值。如果不采用方法调用的形式,我们也可以通过事件注册的方式实现View和Presenter的交互,事件机制体现的消息流无疑是单向的。

View本身仅仅实现单纯的、独立的UI处理逻辑,它处理的数据应该是Presenter实时推送给它的,所以View尽可能不维护数据状态。定义在IView的接口最好只包含方法,而避免属性的定义,Presenter所需的关于View的状态应该在接收到View发送的用户交互请求的时候一次得到,而不需要通过View的属性去获取。

实例演示:SC模式的应用(S101)

为了让读者对MVP模式,尤其是该模式下的View和Presenter之间的交互方式有一个深刻的认识,我们现在来做一个简单的实例演示。本实例采用上面提及的关于员工查询的场景,并且采用ASP.NET Web Forms来建立这个简单的应用,最终呈现出来的效果如图1-3所示。前面我们已经演示了采用PV模式下的IView应该如何定义,现在我们来看看SC模式下的IView有何不同。

先来看看表示员工信息的数据类型如何定义。我们通过具有如下定义的数据类型Employee来表示一个员工。简单起见,我们仅仅定义了表示员工基本信息(ID、姓名、性别、出生日期和部门)的5个属性。

public class Employee

{

    public string        Id { get; private set; }

    public string        Name { get; private set; }

    public string        Gender { get; private set; }

    public DateTime     BirthDate { get; private set; }

    public string        Department { get; private set; }

 

    public Employee(string id, string name, string gender,

        DateTime birthDate, string department)

    { 

        this.Id         = id;

        this.Name            = name;

        this.Gender     = gender;

        this.BirthDate       = birthDate;

        this.Department   = department;

    }

}

作为包含应用状态和状态操作行为的Model通过如下一个简单的EmployeeRepository类型来体现。如代码所示,表示所有员工列表的数据通过一个静态字段来维护,而GetEmployees返回指定部门的员工列表,如果没有指定筛选部门或者指定的部门字符为空,则直接返回所有的员工列表。

public class EmployeeRepository

{

    private static IList employees;

    static EmployeeRepository()

    {

        employees = new List();

        employees.Add(new Employee("001", "张三", "男",

            new DateTime(1981, 8, 24), "销售部"));

        employees.Add(new Employee("002", "李四", "女",

            new DateTime(1982, 7, 10), "人事部"));

        employees.Add(new Employee("003", "王五", "男",

            new DateTime(1981, 9, 21), "人事部"));

    }

 

    public IEnumerable GetEmployees(string department = "")

    {

        if (string.IsNullOrEmpty(department))

        {

            return employees;

        }

        return employees.Where(e => e.Department == department).ToArray();

    }

}

接下来我们来看作为View接口的IEmployeeSearchView的定义。如下面的代码片段所示,该接口定义了BindEmployees和BindDepartments两个方法,分别用于绑定基于部门列表的DropDownList和基于员工列表的GridView。除此之外,IEmployeeSearchView接口还定义了一个事件DepartmentSelected,该事件会在用户选择了筛选部门后点击“查询”按钮时触发。DepartmentSelected事件参数类型为自定义的DepartmentSelectedEventArgs,属性Department表示用户选择的部门。

public interface IEmployeeSearchView

{

    void     BindEmployees(IEnumerable employees);

    void     BindDepartments(IEnumerable departments);

    event    EventHandler DepartmentSelected;

}

 

public class DepartmentSelectedEventArgs : EventArgs

{

    public string Department { get; private set; }

    public DepartmentSelectedEventArgs(string department)

    {

        this.Department = department;

    }

}

作为MVP三角关系核心的Presenter通过EmployeeSearchPresenter表示。如下面的代码片段所示,表示View的只读属性类型为IEmployeeSearchView接口,而另一个只读属性Repository则表示作为Model的EmployeeRepository对象,两个属性均在构造函数中初始化。

public class EmployeeSearchPresenter

{

    public IemployeeSearchView View { get; private set; }

    public EmployeeRepository    Repository { get; private set; }

 

    public EmployeeSearchPresenter(IEmployeeSearchView view)

    {

        this.View                        = view;

        this.Repository              = new EmployeeRepository();

        this.View.DepartmentSelected += OnDepartmentSelected;

    }

 

    public void Initialize()

    {

        IEnumerable employees = this.Repository.GetEmployees();

        this.View.BindEmployees(employees);

        string[] departments =

            new string[] { "销售部", "采购部", "人事部", "IT部" };

        this.View.BindDepartments(departments);

    }

 

    protected void OnDepartmentSelected(object sender,

        DepartmentSelectedEventArgs args)

    {

        string department        = args.Department;

        var employees            = this.Repository.GetEmployees(department);

        this.View.BindEmployees(employees);

    }

}

在构造函数中我们注册了View的DepartmentSelected事件,作为事件处理器的OnDepartmentSelected方法通过调用Repository(即Model)得到了用户选择部门下的员工列表,返回的员工列表通过调用View的BindEmployees方法实现了在View上的数据绑定。在Initialize方法中,我们通过调用Repository获取所有员工的列表,并通过View的BindEmployees方法显示在界面上。作为筛选条件的部门列表通过调用View的BindDepartments方法绑定在View上。

最后我们来看看作为View的Web页面如何定义。如下所示的是作为页面主体部分的HTML,核心部分是一个用于绑定筛选部门列表的DropDownList和一个绑定员工列表的GridView。

   

        员工管理

       

    

    

       

           

               

                    选择查询部门:

                   

                        runat="server" />

                   

                        OnClick="ButtonSearch_Click" />

               

               

                    AutoGenerateColumns="false" Width="100%">

                   

                        

                       

                       

                            HeaderText="出生日期"

                            DataFormatString="{0:dd/MM/yyyy}" />

                       

                   

               

           

       

   

如下所示的是该Web页面的后台代码的定义,它实现了定义在IEmployeeSearchView接口的两个方法(BindEmployees和BindDepartments)和一个事件(DepartmentSelected)。表示Presenter的同名只读属性在构造函数中被初始化。在页面加载的时候(Page_Load方法)Presenter的Initialize方法被调用,而在“查询”按钮被点击的时候(ButtonSearch_Click)事件DepartmentSelected被触发。

public partial class Default : Page, IEmployeeSearchView

{

    public EmployeeSearchPresenter Presenter { get; private set; }

    public event EventHandler DepartmentSelected;

 

    public Default()

    {

        this.Presenter = new EmployeeSearchPresenter(this);

    }

 

    protected void Page_Load(object sender, EventArgs e)

    {

        if (!this.IsPostBack)

        {

            this.Presenter.Initialize();

        }

    }

 

    protected void ButtonSearch_Click(object sender, EventArgs e)

    {

        string department = this.DropDownListDepartments.SelectedValue;

        DepartmentSelectedEventArgs eventArgs =

            new DepartmentSelectedEventArgs(department);

        if (null != DepartmentSelected)

        {

            DepartmentSelected(this, eventArgs);

        }

    }

 

    public void BindEmployees(IEnumerable employees)

    {

        this.GridViewEmployees.DataSource = employees;

        this.GridViewEmployees.DataBind();

    }

 

    public void BindDepartments(IEnumerable departments)

    {

        this.DropDownListDepartments.DataSource = departments;

        this.DropDownListDepartments.DataBind();

    }

}

 

本文节选自《ASP.NET MVC 4 框架揭秘》

蒋金楠

电子工业出版社出版

阅读(1214) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~