手机版

MVP模式下V-P交互分析及案例分享

时间:2021-11-07 来源:互联网 编辑:宝哥软件园 浏览:

在将近两年的时间里,我们项目组的几十个人全身心投入到一个项目中。这个项目基于微软SCSF(智能客户端软件工厂),客户端是墨尔本的一个机构。两周前,我奉命负责某个模块的代码审查。期间发现了一些问题,有了一些想法。但是有些想法可能还不够成熟,不能完全保证其正确性,所以有机会写出来讨论。今天就来说说关于MVP的一些想法。1.简单说说什么是MVP。如果涉及到层次关系,MVP属于表示层的设计模式。对于一个用户界面模块,它的所有功能被分为三个部分,分别由模型、视图和演示者承载。模型、视图和演示者相互协作,完成初始数据的呈现和对用户操作的响应,各有各的职责。模型可以看作是模块的业务逻辑和数据的提供者。View专门负责数据可视化的呈现,对应用户交互事件。一般情况下,View会实现相应的接口;演示者是模型和视图之间的链接。MVP有很多变体,其中最常用的变体是被动视图。对于被动视图,模型、视图和演示者之间的关系如下图所示。视图和模型之间没有直接的交互,视图通过演示者处理模型。演示者接受视图的用户界面请求,完成简单的用户界面处理逻辑,调用模型进行业务处理,并调用视图反映相应的结果。视图直接依赖于Presenter,而Presenter间接依赖于View,而View又直接依赖于View实现的接口。关于MVP和被动视图的基本常识不是本文的重点。不清楚的读者相信可以谷歌出很多相关信息,这里就介绍一下。

二、被动视图模式的基本特征总结被动视图,顾名思义,View就是被动的。那么谁是主动的呢?答案是Presenter。我个人对presenter倡议的理解是:Presenter是整个MVP系统的控制中心,而不是单纯处理View请求的人;视图只是用户交互请求的报告者。对于与用户交互相关的逻辑和流程,View不参与决策,真正的决策者是Presenter。View向Presenter发送用户交互请求时,应该采用以下语气:“我现在就把用户交互请求发送给你,你想怎么处理就怎么处理,需要我的时候我会协助你”,而不是这样:“我现在正在处理用户交互请求,我知道该怎么做,但是需要你的支持,因为实现业务逻辑的Model只信任你”;绑定到视图的数据不应由视图从演示者处“拉出”,而应由演示者主动“推”到视图。视图不尽可能维护数据状态,因为它只实现简单独立的UI操作;演示者是整个系统的协调者,它根据交互逻辑为视图和模型安排工作。3.从理想与现实的距离来看,我认为列举被动观MVP的特点是一种理想状态。但是在大型项目中,尤其是当项目开发人员本身并不完全了解MVP原理的时候,从整体上实现这样的理想状态是非常困难的。有人可能会说,在不知道的情况下,要求开发者好好使用MVP,这不是废话。其实这并不是说开发人员在MVP中完全没有关注点分离的概念,而是MVP中的三元角色没有明确的定义(其实也没有明确的规范来明确划分Model、View和Presenter的具体职责)。开发时,开发人员会不自觉地受到传统编程习惯的影响,简单地将Presenter视为通过View调用Model的中介。我经常这么说:如果你以View为中心,把Presenter当作View和Model之间的中介,这也叫MVP模式,但这里的P不是Presenter,而是Proxy,只是Model在View中的代理。根据被动视图中模型、视图和演示者之间的依赖关系,这种模型充分给了开发人员犯这种错误的机会。请注意,上图中从视图到演示者的箭头表示视图可以随意调用演示者。开发人员完全可以在View中编写大多数UI处理逻辑,而Presenter只是对Model响应操作的简单调用。因为我回顾的很多所谓的MVP编程方法都是这样写的。在很多情况下,甚至不需要仔细分析具体的代码,这可以从View和Presenter中代码的行数看出,因为View中的代码和Presenter中的代码不是同一个数量级的。我现在的目标之一是提出一个编程模型,以防止开发人员将程序编写为基于代理的MVP。在我看来,唯一的方法是最小化(而不是消除)视图对演示者的依赖。实际上,对于MVP,View只向Presenter提交用户交互请求,仅此而已。如果我们在框架级别实现视图对演示者的依赖,最终开发人员的编程将不需要这种依赖。然后我可以使用一些编程技巧,让View完全无法访问Presenter,从而避免Presenter成为Proxy的可能性。然后,如果您无法获得Presenter,如果View可以正常向Presenter提交请求,该怎么办?很简单,使用事件订阅机制就够了。虽然无法从“视图”中获得演示者,但演示者可以获得“视图”,因此演示者订阅与“视图”相关的事件就足够了。4.让视图不再依赖于演示者的编程模型。现在,让我们采用一个简单的编程模型,从最终开发人员的源代码中完全消除View对Presenter的依赖。为此,我们需要定义一系列基类。首先,我为所有视图创建一个基类视图库。这里,我们直接使用表单作为视图,而在SCSF,视图通常由用户控件表示。

视图库的定义如下。为了防止Presenter在View中被调用,我将其定义为私有字段。那么,如何在视图和演示者之间建立关系呢?在这里,通过虚拟方法CreatePresenter,具体视图必须重写此方法,否则将引发NotImplementedException异常。在构造函数中,调用此方法比用返回值向Presenter赋值更好。复制代码如下:使用系统;使用系统。ComponentModell使用系统。Windows .窗体;命名空间MVPDemo{公共类viewbase : Form { private object _ presenter;public ViewBase(){ _ presenter=this。create presenter();}受保护的虚拟对象create presenter(){ if(license manager。current context . USagemodel==LicenseUsageModel。Designtime) {返回null} else {抛出新的NotImplementedException(字符串。格式(“{0}”必须重写CreatePresenter方法。”,这个。GetType()。全名);}}}}然后,我们还为所有演示者创建了基类Presenter IView,泛型IView表示由特定View实现的接口。与视图同名的只读属性在构造函数中赋值,赋值完成后调用视图集上的虚拟方法。特定演示者可以覆盖此方法来注册视图事件。但是需要注意的是,Presenter是通过调用ViewBase的构造函数中的CreatePresenter方法创建的,所以在执行OnViewSet的时候,视图本身还没有完全初始化,所以无法在这里操作View的控件。复制代码如下:命名空间MVP演示{ public class presenter iview { public iview view { get;私有集;}公共演示者(IView视图){这。视图=视图;这个。onview set();}在Viewset () {}}}上受保护的虚拟空间,因为Presenter通过界面与视图交互。这里,由于View体现在Form的Form中,有时我们需要通过这个接口来访问Form的一些属性、方法和事件,在接口上定义对应的成员比较麻烦。此时,我们可以选择在一个接口中定义这些成员,特定View的接口可以继承这个接口。这里,我们已经为所有视图接口创建了“基本接口”。作为演示,我现在知道表单的三个事件成员是在街角的IViewBase中定义的。复制代码如下:使用系统;使用系统。ComponentModell命名空间MVPDemo{公共接口IViewBase {事件事件处理程序Load事件事件处理程序已关闭;事件取消处理程序关闭;}} V .示例演示我通过定义基类和接口为整个编程模型构建了一个框架。现在我们通过一个具体的例子来介绍这个编程模型的应用。我们使用一个简单的Windows窗体应用程序来模拟管理客户信息的场景。逻辑很简单:程序启动时,显示所有客户端列表;用户选择一个客户端,在文本框中显示响应信息进行编辑;修改客户端信息后,单击确定保存。整个操作界面如下图所示:

首先,我们创建实体类客户,简单起见,仅仅包含四个属性:身份证、名字、姓氏和地址:复制代码代码如下:使用系统;命名空间MVPDemo{公共类Customer:独一无二{公共字符串Id { get设置;}公共字符串名字{ get设置;}公共字符串姓氏{ get设置;}公共字符串地址{ get设置;}对象不适合.克隆(){ 0返回此克隆();}公共客户克隆(){返回新客户{ Id=this .Id,FirstName=this .名字,姓氏=这个。姓氏,地址=这个。地址};} }}然后,为了真实模拟最有价值球员三种角色,特意创建一个CustomerModel类型,实际上在真实的应用中,并没有单独一个类型来表示型号。CustomerModel维护客户列表,体统相关的查询和更新操作CustomerModel。定义如下:复制代码代码如下:使用系统。集合。通用;使用系统Linq .命名空间MVPDemo{公共类CustomerModel { private ilist customers _ customers=新列表Customer { new Customer { Id=' 001 ',FirstName='San ',LastName='张',Address='Su zhou'},new Customer{ Id='002 ',FirstName='Si ',LastName='Li ',Address=' Shang Hai ' } } public void UpdateCustomer(Customer){ for(int I=0;我_客户。计数;I){ if(_ customers[I]} .Id==客户. id){ _ customers[I]=customer;打破;} } }公共客户GetCustomerById(字符串id){ var客户=来自客户in _ customers其中顾客.Id==id选择客户克隆();退货客户to array customer()[0];}公共客户[]GetAllCustomers(){ var customers=from Customer in _ customers选择客户克隆();退货客户to array customer();} }}接着,我们定义视角的接口ICustomerView。ICustomerView定义了两个事件,客户选择在用户从用带束中选择了某个条客户记录是触发,而客户体验则在用户完成编辑点击好按钮视图提交修改时触发客户观察还定义了视角必须完成的三个基本操作:绑定客户列表(listal customers);显示单个客户信息到文本框(DisplayCustomerInfo);保存后清空可编辑控件(清除).复制代码代码如下:使用系统;命名空间MVPDemo{公共接口ICustomerView : iview base { event handlercustomereventargs CustomerSelected;事件处理程序客户化;void ListAllCustomers(客户[]客户);void DisplayCustomerInfo(客户客户);void Clear();}}事件参数的类型客户报告定义如下,两个属性CustomerId和顾客分别代表客户身份和具体的客户,它们分别用于上面提到的客户选择和客户体验事件。复制代码代码如下:使用系统;命名空间MVPDemo{公共类CustomerEventArgs : EventArgs {公共字符串CustomerId { get设置;}公共客户客户{获取;设置;} }}而具体的提出者定义在如下的客户代表类型中。在重写的OnViewSet方法中注册视角的三个事件:加载事件中调用模型获取所有客户列表,并显示在视角的格子上;客户选择事件中通过事件参数传递的客户身份调用模型获取相应的客户信息,显示在视角的可编辑控件上;客户体验则通过事件参数传递的被更新过的客户信息,调用模型提交更新。

复制代码代码如下:使用系统窗户。窗体;命名空间MVPDemo{公共类客户演示ter : presentericcustomerview {公共客户模型{获取私有集;}公共客户演示者(ICustomerview视图):底座(视图){这个.model=new CustomerModel();}受保护的覆盖void OnViewSet() { this .查看。加载=(发件人,参数)={客户[]客户=此模型。GetAllcustomers();这个。查看。ListAllCustomers(客户);这个。查看。clear();};这个。视图。客户选择=(发件人,参数)={客户客户=这个。模型CustomerId);这个。视图。DisplayCustomerInfo(客户);};这个视图。客户生活=(发件人,参数)={这个.模型。更新客户(参数。客户);客户[]客户=这个模型。GetAllcustomers();这个。查看。ListAllCustomers(客户);这个。查看。clear();消息框。显示('客户已成功更新!','成功更新,消息框按钮。好的,消息框图标。信息);};} }}对于具体的视角来说,仅仅需要实现我客户观察,并处理响应控件事件即可(主要是用户从格子中选择某个记录触发的RowHeaderMouseClick事件,以及点击好的事件)。实际上不需要视角亲自处理这些事件,而仅仅需要触发相应的事件,让事件订阅者(演示者)来处理就可以了。此外还需要重写创建演示者方法完成对客户代表的创建客户视图。定义如下:复制代码代码如下:使用系统;使用系统窗户。窗体;命名空间MVPDemo{ public分部类CustomerView : ViewBase,ICustomerView { public customer view(){ InitializeComponent();}受保护的覆盖对象CreatePresenter(){ 0返回新的客户演示者(本);} #地区ICustomerView成员公共事件客户选择的事件处理程序自定义事件参数;公共事件事件处理程序自定义事件参数保存;公共作废清单客户(客户[]客户){此。datagridviewcustomers。数据源=客户;} public void DisplayCustomerInfo(客户客户){这个。按钮ok。enabled=truethis.textBoxId.Text=客户.id;这个。textbox 1s tname。文本=客户.名字这个。文本框姓氏。文本=客户.姓氏;这个。文本框地址。文本=客户.地址;} public void Clear(){ this。按钮ok。enabled=false这个。textbox 1s tname。文本=字符串.空的;这个。文本框姓氏。文本=字符串.空的;这个。文本框地址。文本=字符串.空的;this.textBoxId.Text=string .空的;} #所选客户的终端区域受保护虚拟空间(字符串customerId){ var previousId=this。文本框id。文字。trim();if(customerId==previousId){ return;} if(null!=这个客户选择的){这个.客户选择的(这是新的CustomerEventArgs { CustomerId=CustomerId });} }受保护的虚拟无效客户保存(客户客户){ if(null!=这个。客户保存){这个。客户保存(这是新客户{客户=客户});} } private void datagridview customers _ RowHeaderMouseClick(对象发送方,DataGridView CellMouseEventArgs e){ var current row=this。datagridview客户。行[e . RowIndex];var customerId=currentRow .单元格[0].价值。ToString();这个客户选择(客户标识);}私有作废按钮确定_点击(对象发送者,EventArgs e){ var Customer=new Customer();顾客id=这个。文本框id。文字。trim();顾客名字=这个。textbox 1s tname。文字。trim();顾客姓氏=这个。文本框姓氏。文字。trim();顾客地址=这个。文本框地址。文字。trim();这个. OnCustomerSaving(客户);} }}

版权声明:MVP模式下V-P交互分析及案例分享是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。