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

在差不多两年的时间内,我们项目组几十来号人都扑在一个项目上面。这是一个基于微软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);
         }
     }
 }

(0)

相关推荐

  • android开发中使用java观察者模式

    复制代码 代码如下: //观察者,需要用到观察者模式的类需实现此接口public interface Observer { void update(Object... objs);} //被观察者(一个抽象类,方便扩展)public abstract class Observable { public final ArrayList<Class<?>> obserList = new ArrayList<Class<?>>(); /** Attach Obs

  • ASP.NET小结之MVC, MVP, MVVM比较以及区别(二)

    上一篇得到大家的关注,非常感谢.由于自己对于这些模式的理解也是有限,对于这些模式的比较,是结合自己的理解,一些地方不一定准确,但是只有亮出自己的观点,才能抛砖引玉不是? 欢迎各位拍砖.:) 复制代码 代码如下: 阅读目录: 四. MVP模式 4.1 MVP的思想 4.2 UI界面接口化 4.3 Presenter -- Model和View之间的桥梁 4.4 MVP的代码结构和时序图 4.5 MVP模式总结 五. MVVM模式 5.1 MVVM模式的设计思想 5.2 MVVM模式结构图 六. M

  • 详解Android MVP开发模式

    本文主要讲解MVP开发模式以及具体实例. 一.简介 MVP(Model View Presenter)模式是著名的MVC(Model View Controller)模式的一个演化版本,目前它在Android应用开发中越来越重要了.初看起来我们会感觉增加了很多类接口代码看起来更加清晰. MVP模式可以分离显示层和逻辑层,所以功能接口如何工作与功能的展示可以实现分离,MVP模式理想化地可以实现同一份逻辑代码搭配不同的显示界面.不过MVP不是一个结构化的模式,它只是负责显示层而已,任何时候都可以在自

  • ASP.NET小结之MVC, MVP, MVVM比较以及区别(一)

    MVC, MVP和MVVM都是用来解决界面呈现和逻辑代码分离而出现的模式.以前只是对它们有部分的了解,没有深入的研究过,对于一些里面的概念和区别也是一知半解.现在一边查资料,并结合自己的理解,来谈一下对于这三种模式思想的理解,以及它们的区别.欢迎各位高手拍砖. 阅读目录: 复制代码 代码如下: 一. MVC, MVP, MVVM诞生的需求? 二. 一段典型的耦合代码 三. MVC模式 3.1 主动MVC 3.2 被动MVC 3.3 Web应用中的MVC框架 3.4 MVC总结 一,MVC, MV

  • Android开发之文件操作模式深入理解

    一.基本概念 复制代码 代码如下: // 上下文对象 private Context context; public FileService(Context context) { super(); this.context = context; } // 保存文件方法 public void save(String filename, String fileContent) throws Exception { FileOutputStream fos = context.openFileOut

  • Android开发中的MVC设计模式浅析

    Android开发中的MVC设计模式的理解 1. Android系统中分层的理解: (1).在Android的软件开发工作中,应用程序的开发人员主要是应用Android Application Framework层封装好的Api进行快速开发. (2).在Android框架的四个层次中,下层为上层服务,上层需要下层的支持,上层需要调用下层的服务. (3).这种分层的方式带来极大的稳定性.灵活性和可扩展性,使得不同层的开发人员可以按照规范专心特定层的开发. (4). Android的官方建议应用程序

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

    在差不多两年的时间内,我们项目组几十来号人都扑在一个项目上面.这是一个基于微软SCSF(Smart Client Software Factory)的项目,客户端是墨尔本一家事业单位.前两周,我奉命负责对某个模块进行Code Review工作,在此期间,发现了一些问题,也有了一些想法.不过,有些想法可能还不是很成熟,不能完全保证其正确性,有机会写出来讨论一下.今天来说说关于MVP的一些想法. 一.简单讲讲MVP是什么玩意儿如果从层次关系来讲,MVP属于Presentation层的设计模式.对于一

  • 详解MVP模式在Android开发中的应用

    一.MVP介绍  随着UI创建技术的功能日益增强,UI层也履行着越来越多的职责.为了更好地细分视图(View)与模型(Model)的功能,让View专注于处理数据的可视化以及与用户的交互,同时让Model只关系数据的处理,基于MVC概念的MVP(Model-View-Presenter)模式应运而生. 在MVP模式里通常包含4个要素: (1)View:负责绘制UI元素.与用户进行交互(在Android中体现为Activity); (2)View interface:需要View实现的接口,Vie

  • Android中mvp模式使用实例详解

    MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负 责显示.作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller. 在MVC里,View是可以直接访问

  • Android MVP模式ListView中嵌入checkBox的使用方法

    MVP模式 ListView中嵌入checkBox的使用 本文写的是一个小demo,如何在ListView中嵌入checkBox配合使用,本篇文章与前面的嵌入Button类似,同样的采用MVP模式的写代码,本次案例中会有几个小细节,我将会在案例中介绍. 程序基本框架如下: View层: MainActivity.java public class MainActivity extends AppCompatActivity implements ViewInter<MyBean>{ //Lis

  • 浅析在javascript中创建对象的各种模式

    最近在看<javascript高级程序设计>(第二版) javascript中对象的创建 •工厂模式 •构造函数模式 •原型模式 •结合构造函数和原型模式 •原型动态模式 面向对象的语言大都有一个类的概念,通过类可以创建多个具有相同方法和属性的对象.虽然从技术上讲,javascript是一门面向对象的语言,但是javascript没有类的概念,一切都是对象.任意一个对象都是某种引用类型的实例,都是通过已有的引用类型创建:引用类型可以是原生的,也可以是自定义的.原生的引用类型有:Object.A

  • Android MVP模式实战教程

    一.什么是MVP 在网上找了些资料,整理如下: MVP是模型(Model).视图(View).主持人(Presenter)的缩写,分别代表项目中3个不同的模块. 模型(Model):负责处理数据的加载或者存储,比如从网络或本地数据库获取数据等: 视图(View):负责界面数据的展示,与用户进行交互: 主持人(Presenter):相当于协调者,是模型与视图之间的桥梁,将模型与视图分离开来. 如下图所示,View与Model并不直接交互,而是使用Presenter作为View与Model之间的桥梁

  • 浅析Python字符串中的r和u的区别

    目录 1.r(R) 2.u(U) 补充 参考文献 Python中字符串前面我们经常看到加r(R)或u/(U)的前缀,而这两个符号是什么意思呢? 1.r(R) r意为raw,表示不包含转义字符的原生字符串.常见的转义字符包括下列几种: 转义字符 描述 \(在行尾时) 续航符 \\ 反斜杠符号 ' 单引号(字符串需要为""形式) " 双引号(需要字符串用需要为''形式) \b 退格(Backspace) \000 空 \n 换行 \v 纵向制表符 \t 横向制表符 下面是几个转义

  • Android开发:浅谈MVP模式应用与内存泄漏问题解决

    最近博主开始在项目中实践MVP模式,却意外发现内存泄漏比较严重,但却很少人谈到这个问题,促使了本文的发布,本文假设读者已了解MVP架构. MVP简介 M-Modle,数据,逻辑操作层,数据获取,数据持久化保存.比如网络操作,数据库操作 V-View,界面展示层,Android中的具体体现为Activity,Fragment P-Presenter,中介者,连接Modle,View层,同时持有modle引用和view接口引用 示例代码 Modle层操作 public class TestModle

  • 深入浅析Docker容器中的Patroni

    目录 创建镜像 文件结构 DockerFile entrypoint.sh function generatefile 构建镜像 运行镜像 总结 附图 上一篇文章向大家介绍了Repmgr的搭建过程,实现了自动切换,今天将向大家介绍,如何搭建容器下的Patroni集群环境,Patroni作为开箱即用PG高可用工具,越来越多的被各个厂商用于云环境下使用. patroni基本架构如图所示: etcd作为分布式注册中心.进行集群选主工作:vip-manager为主节点设置漂移IP:patroni负责引导

  • 浅析Go语言中数组的这些细节

    目录 Go语言基础二 len&cap 二维数组的遍历 数组的拷贝与传参 求数组所有元素之和 例题:数组元素匹配问题 今日总结 Go语言基础二 len&cap 书接上文,我们提到二维数组中的第二个维度的数组不能用...来表示,接下来我们要认识两个新的函数,它们分别是len和cap package main ​ func main() { a := [2]int{} println(len(a), cap(a)) } 由上方代码可知,我们在main函数里面定义了一个a数组,长度为2,未进行初始

随机推荐