当前位置: 移动技术网 > IT编程>开发语言>.net > 浅析MVP模式中V-P交互问题及案例分享

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

2017年12月12日  | 移动技术网IT编程  | 我要评论

表达志向的诗句,粮油招商,csol自雷脚本

在差不多两年的时间内,我们项目组几十来号人都扑在一个项目上面。这是一个基于微软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出很多相关的资料来,所以在这里就再多做介绍了。



二、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按钮进行保存。整个操作界面如下图所示:


首先,我们创建实体类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);
         }
     }
 }

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网