欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

浅析MVP模式中V-P交互问题及案例分享

程序员文章站 2024-02-22 22:43:58
在差不多两年的时间内,我们项目组几十来号人都扑在一个项目上面。这是一个基于微软scsf(smart client software factory)的项目,客户端是墨尔本一...

在差不多两年的时间内,我们项目组几十来号人都扑在一个项目上面。这是一个基于微软scsf(smart client software factory)的项目,客户端是墨尔本一家事业单位。前两周,我奉命负责对某个模块进行code review工作,在此期间,发现了一些问题,也有了一些想法。不过,有些想法可能还不是很成熟,不能完全保证其正确性,有机会写出来讨论一下。今天来说说关于mvp的一些想法。

一、简单讲讲mvp是什么玩意儿
如果从层次关系来讲,mvp属于presentation层的设计模式。对于一个ui模块来说,它的所有功能被分割为三个部分,分别通过model、view和presenter来承载。model、view和presenter相互协作,完成对最初数据的呈现和对用户操作的响应,它们具有各自的职责划分。model可以看成是模块的业务逻辑和数据的提供者;view专门负责数据可视化的呈现,和用户交互事件的相对应。一般地,view会实现一个相应的接口;presenter是一般充当model和view的纽带。

mvp具有很多的变体,其中最为常用的一种变体成为passive view(被动视图)。对于passive view,model、view和presenter之间的关系如下图所示。view和modell之间不能直接交互,view通过presenter与model打交道。presenter接受view的ui请求,完成简单的ui处理逻辑,并调用model进行业务处理,并调用view将相应的结果反映出来。view直接依赖presenter,但是presenter间接依赖view,它直接依赖的是view实现的接口。关于mvp和passive view基本的常识性东西,不是本篇文章论述的重点,对此不清楚的读者相信可以google出很多相关的资料来,所以在这里就再多做介绍了。

浅析MVP模式中V-P交互问题及案例分享

二、passive view模式的基本特征总结

passive view,顾名思义,view是被动的。那么主动是谁呢?答案是presenter。对于presenter的主动性,我个人是这么理解的:

•presenter是整个mvp体系的控制中心,而不是单纯的处理view请求的人;
•view仅仅是用户交互请求的汇报者,对于响应用户交互相关的逻辑和流程,view不参与决策,真正的决策者是presenter;
•view向presenter发送用户交互请求应该采用这样的口吻:“我现在将用户交互请求发送给你,你看着办,需要我的时候我会协助你”,不应该是这样:“我现在处理用户交互请求了,我知道该怎么办,但是我需要你的支持,因为实现业务逻辑的model只信任你”;
•对于绑定到view上的数据,不应该是view从presenter上“拉”回来的,应该是presenter主动“推”给view的;
•view尽可能不维护数据状态,因为其本身仅仅实现单纯的、独立的ui操作;presenter才是整个体系的协调者,它根据处理用于交互的逻辑给view和model安排工作。

三、理想与现实的距离

上面对passive view mvp特征的罗列,我觉得是一种理想状态。是在大型项目中,尤其是项目的开发者自身并不完全理解mvp原理的情况下,要整体实现这样的一种理想状态是一件很难的事情。有人可能会说,在开发人员不了解mvp的情况下要求他们用好mvp,你这不是扯淡吗?实际上,在这里并不是说开发人员完全没有mvp关于关注点分离的概念,只是对mvp中的三元角色并没有非常清晰的界定(实际上也没有一个明确的规范对model、view和presenter具体的职责范围进行明确的划分),在开发的时候,会不自觉地受传统编程习惯的影响,将presenter单纯地当成是view调用model的中介。我经常这么说:如果以view为中心,将presenter当成是view和model的中间人,这也叫mvp模式,不过这里的p不是presenter,而是proxy,是model在view的代理而已。

从passive view中model、view和presenter三者之间的依赖关系来看,这个模型充分地给了开发者犯这样错误的机会。注意上面的图中view到presenter的箭头表明view是可以任意的调用presenter的。开发人员完全有可能将大部分ui处理逻辑写在view中,而presenter仅仅对model响应操作的简单调用。因为在我review的各种所谓的mvp编程方式中,有不少是这么写的。在很多情况下,甚至不用认真去分析具体的代码,从view和presenter中代码的行数就可以看出来,因为view的代码和presenter的代码都不在一个数量级。

我现在的一个目的是提出一种编程模式,杜绝开发人员将程序写成基于proxy的mvp,在我看来,唯一的办法就是尽量弱化(不可能剔除)view对presenter的依赖。实际上,对于mvp来说,view仅仅向presenter递交用户交互请求,仅此而已。如果我们将view对presenter的这点依赖关系实现在框架层次中,最终开发人员的编程来说就不需要这种依赖了。那么我就可以通过一定的编程技巧使view根本无法访问presenter,从而避免presenter成为proxy的可能的。

那么,如果在不能获得presenter的情况下,使view能够正常将请求递交给presenter呢?很简单,通过事件订阅机制就可以了,虽然view不可以获取到presenter,但是presenter却可以获取到view,让presenter订阅view的相关事件就可以的。

四、让view不再依赖presenter的编程模型

现在,我们就来如果通过一种简单的编程模式就能够让view对presenter的依赖完全地从中最终开发者的源代码中移除。为此,我们需要定义一系列的基类,首先我为所有的view创建基类viewbase,在这里我们直接用form作为view,而在scsf中view一般是通过usercontrol来表示的。viewbase定义如下,为了使view中不能调用presenter,我将其定义成私有字段。那么,如何让view和presenter之间建立起关联呢?在这里通过虚方法createpresenter,具体的view必须重写该方法,不然会抛出一个notimplementedexception异常。在构造函数中,调用该方法比用返回值为presenter赋值。

复制代码 代码如下:

 using system;
 using system.componentmodell;
 using system.windows.forms;
 namespace mvpdemo
 {
     public class viewbase: form
     {
         private object _presenter;

         public viewbase()
         {
             _presenter = this.createpresenter();
         }

         protected virtual object createpresenter()
         {
             if (licensemanager.currentcontext.usagemodel == licenseusagemodel.designtime)
             {
                 return null;
             }
             else
             {
                 throw new notimplementedexception(string.format("{0} must override the createpresenter method.", this.gettype().fullname));
             }
         }      
     }
 }

然后,我们也为所有的presenter创建基类presenter<iview>,泛型类型iview表示具体view实现的接口。表示view的同名只读属性在构造函数中赋值,赋值完成之后调用调用虚方法onviewset。具体的presenter可以重写该方法进行对view进行事件注册工作。但是需要注意的是,presenter的创建是在viewbase的构造函数中通过调用createpresenter方法实现,所以执行onviewset的时候,view本身还没有完全初始化,所以在此不能对view的控件进行操作。

复制代码 代码如下:

 namespace mvpdemo
 {
     public class presenter<iview>
     {
         public iview view { get; private set; }

         public presenter(iview view)
         {
             this.view = view;
             this.onviewset();
         }
         protected virtual void onviewset()
         { }
     }
 }

由于,presenter是通过接口的方式与view进行交互的。在这里,由于view通过form的形式体现,有时候我们要通过这个接口访问form的一些属性、方法和事件,需要将相应的成员定义在接口上面,比较麻烦。此时,我们可以选择将这些成员定义在一个接口中,具体view的接口继承该接口就可以了。在这里,我们相当是为所有的view接口创建了“基接口”。作为演示,我现在了form的三个事件成员定义在街口iviewbase中。

复制代码 代码如下:

 using system;
 using system.componentmodell;
 namespace mvpdemo
 {
    public interface iviewbase
     {
        event eventhandler load;
        event eventhandler closed;
        event canceleventhandler closing;
     }
 }

五、实例演示

上面我通过定义基类和接口为整个编程模型搭建了一个框架,现在我们通过一个具体的例子来介绍该编程模型的应用。我们采用的是一个简单的windows forms应用,模拟管理客户信息的场景,逻辑很简单:程序启动的时候显示出所有的客户端列表;用户选择某一客户端,将响应的信息显示在textbox中以供编辑;对客户端信息进行相应修改之后,点击ok按钮进行保存。整个操作界面如下图所示:


浅析MVP模式中V-P交互问题及案例分享

首先,我们创建实体类customer,简单起见,仅仅包含四个属性:id、firstname、lastname和address:

复制代码 代码如下:

 using system;
 namespace mvpdemo
 {
     public class customer: icloneable
     {
         public string id
         { get; set; }

         public string firstname
         { get; set; }

         public string lastname
         { get; set; }

         public string address
         { get; set; }      

         object icloneable.clone()
         {
             return this.clone();
         }

         public customer clone()
         {
             return new customer {
                 id          = this.id,
                 firstname   = this.firstname,
                 lastname    = this.lastname,
                 address     = this.address
             };
         }
     }
 }

然后,为了真实模拟mvp三种角色,特意创建一个customermodel类型,实际上在真实的应用中,并没有单独一个类型来表示model。customermodel维护客户列表,体统相关的查询和更新操作。customermodel定义如下:

复制代码 代码如下:

 using system.collections.generic;
 using system.linq;
 namespace mvpdemo
 {
     public class customermodel
     {
         private ilist<customer> _customers = new list<customer>{
             new customer{ id = "001", firstname = "san", lastname = "zhang", address="su zhou"},
             new customer{ id = "002", firstname = "si", lastname = "li", address="shang hai"}
         };

         public void updatecustomer(customer customer)
         {
             for (int i = 0; i < _customers.count; i++)
             {
                 if (_customers[i].id == customer.id)
                 {
                     _customers[i] = customer;
                     break;
                 }
             }
         }

         public customer getcustomerbyid(string id)
         {
             var customers = from customer in _customers
                             where customer.id == id
                             select customer.clone();
             return customers.toarray<customer>()[0];
         }

         public customer[] getallcustomers()
         {
             var customers = from customer in _customers
                             select customer.clone();
             return customers.toarray<customer>();
         }
     }
 }

接着,我们定义view的接口icustomerview。icustomerview定义了两个事件,customerselected在用户从gird中选择了某个条客户记录是触发,而customersaving则在用户完成编辑点击ok按钮视图提交修改时触发。icustomerview还定义了view必须完成的三个基本操作:绑定客户列表(listallcustomers);显示单个客户信息到textbox(displaycustomerinfo);保存后清空可编辑控件(clear)。

复制代码 代码如下:

 using system;
 namespace mvpdemo
 {
     public interface icustomerview : iviewbase
     {
         event eventhandler<customereventargs> customerselected;

         event eventhandler<customereventargs> customersaving;

         void listallcustomers(customer[] customers);

         void displaycustomerinfo(customer customer);

         void clear();
     }
 }

事件参数的类型customereventargs定义如下,两个属性customerid和customer分别代表客户id和具体的客户,它们分别用于上面提到的customerselected和customersaving事件。

复制代码 代码如下:

 using system;
 namespace mvpdemo
 {
     public class customereventargs : eventargs
     {
         public string customerid
         { get; set; }

         public customer customer
         { get; set; }
     }
 }

而具体的presenter定义在如下的customerpresenter类型中。在重写的onviewset方法中注册view的三个事件:load事件中调用model获取所有客户列表,并显示在view的grid上;customerselected事件中通过事件参数传递的客户id调用model获取相应的客户信息,显示在view的可编辑控件上;customersaving则通过事件参数传递的被更新过的客户信息,调用model提交更新。

复制代码 代码如下:

 using system.windows.forms;

 namespace mvpdemo
 {  
     public class customerpresenter: presenter<icustomerview>
     {
         public customermodel model
         { get; private set; }

         public customerpresenter(icustomerview view)
             : base(view)
         {
             this.model = new customermodel();
         }

         protected override void onviewset()
         {
             this.view.load += (sender, args) =>
                 {
                     customer[] customers = this.model.getallcustomers();
                     this.view.listallcustomers(customers);
                     this.view.clear();
                 };
             this.view.customerselected += (sender, args) =>
                 {
                     customer customer = this.model.getcustomerbyid(args.customerid);
                     this.view.displaycustomerinfo(customer);
                 };
             this.view.customersaving += (sender, args) =>
                 {
                     this.model.updatecustomer(args.customer);
                     customer[] customers = this.model.getallcustomers();
                     this.view.listallcustomers(customers);
                     this.view.clear();
                     messagebox.show("the customer has been successfully updated!", "successfully update", messageboxbuttons.ok, messageboxicon.information);
                 };
         }      
     }
 }

对于具体的view来说,仅仅需要实现icustomerview,并处理响应控件事件即可(主要是用户从grid中选择某个记录触发的rowheadermouseclick事件,以及点击ok的事件)。实际上不需要view亲自处理这些事件,而仅仅需要触发相应的事件,让事件订阅者(presenter)来处理就可以了。此外还需要重写createpresenter方法完成对customerpresenter的创建。customerview定义如下:

复制代码 代码如下:

 using system;
 using system.windows.forms;

 namespace mvpdemo
 {
     public partial class customerview : viewbase, icustomerview
     {
         public customerview()
         {
             initializecomponent();           
         }

         protected override object createpresenter()
         {
             return new customerpresenter(this);
         }

         #region icustomerview members

         public event eventhandler<customereventargs> customerselected;

         public event eventhandler<customereventargs> customersaving;

         public void listallcustomers(customer[] customers)
         {
             this.datagridviewcustomers.datasource = customers;
         }

         public void displaycustomerinfo(customer customer)
         {
             this.buttonok.enabled = true;
             this.textboxid.text = customer.id;
             this.textbox1stname.text = customer.firstname;
             this.textboxlastname.text = customer.lastname;
             this.textboxaddress.text = customer.address;
         }

         public void clear()
         {
             this.buttonok.enabled       = false;
             this.textbox1stname.text    = string.empty;
             this.textboxlastname.text   = string.empty;
             this.textboxaddress.text    = string.empty;
             this.textboxid.text         = string.empty;
         }

         #endregion

         protected virtual void oncustomerselected(string customerid)
         {
             var previousid = this.textboxid.text.trim();
             if (customerid == previousid)
             {
                 return;
             }
             if(null != this.customerselected)
             {
                 this.customerselected(this, new customereventargs{ customerid = customerid});
             }
         }

         protected virtual void oncustomersaving(customer customer)
         {
             if(null != this.customersaving)
             {
                 this.customersaving(this, new customereventargs{ customer = customer});
             }
         }

         private void datagridviewcustomers_rowheadermouseclick(object sender, datagridviewcellmouseeventargs e)
         {  
             var currentrow = this.datagridviewcustomers.rows[e.rowindex];
             var customerid = currentrow.cells[0].value.tostring();
             this.oncustomerselected(customerid);
         }

         private void buttonok_click(object sender, eventargs e)
         {
             var customer        = new customer();
             customer.id         = this.textboxid.text.trim();
             customer.firstname  = this.textbox1stname.text.trim();
             customer.lastname   = this.textboxlastname.text.trim();
             customer.address    = this.textboxaddress.text.trim();
             this.oncustomersaving(customer);
         }
     }
 }