在差不多两年的时间内,我们项目组几十来号人都扑在一个项目上面。这是一个基于微软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的主动性,我个人是这么理解的:
"codetitle">复制代码 代码如下:
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);
}
}
}