淺析MVP模式中V-P交互問題及案例分享
在差不多兩年的時(shí)間內(nèi),我們項(xiàng)目組幾十來號(hào)人都撲在一個(gè)項(xiàng)目上面。這是一個(gè)基于微軟SCSF(Smart Client Software Factory)的項(xiàng)目,客戶端是墨爾本一家事業(yè)單位。前兩周,我奉命負(fù)責(zé)對(duì)某個(gè)模塊進(jìn)行Code Review工作,在此期間,發(fā)現(xiàn)了一些問題,也有了一些想法。不過,有些想法可能還不是很成熟,不能完全保證其正確性,有機(jī)會(huì)寫出來討論一下。今天來說說關(guān)于MVP的一些想法。
一、簡單講講MVP是什么玩意兒
如果從層次關(guān)系來講,MVP屬于Presentation層的設(shè)計(jì)模式。對(duì)于一個(gè)UI模塊來說,它的所有功能被分割為三個(gè)部分,分別通過Model、View和Presenter來承載。Model、View和Presenter相互協(xié)作,完成對(duì)最初數(shù)據(jù)的呈現(xiàn)和對(duì)用戶操作的響應(yīng),它們具有各自的職責(zé)劃分。Model可以看成是模塊的業(yè)務(wù)邏輯和數(shù)據(jù)的提供者;View專門負(fù)責(zé)數(shù)據(jù)可視化的呈現(xiàn),和用戶交互事件的相對(duì)應(yīng)。一般地,View會(huì)實(shí)現(xiàn)一個(gè)相應(yīng)的接口;Presenter是一般充當(dāng)Model和View的紐帶。
MVP具有很多的變體,其中最為常用的一種變體成為Passive View(被動(dòng)視圖)。對(duì)于Passive View,Model、View和Presenter之間的關(guān)系如下圖所示。View和Modell之間不能直接交互,View通過Presenter與Model打交道。Presenter接受View的UI請(qǐng)求,完成簡單的UI處理邏輯,并調(diào)用Model進(jìn)行業(yè)務(wù)處理,并調(diào)用View將相應(yīng)的結(jié)果反映出來。View直接依賴Presenter,但是Presenter間接依賴View,它直接依賴的是View實(shí)現(xiàn)的接口。關(guān)于MVP和Passive View基本的常識(shí)性東西,不是本篇文章論述的重點(diǎn),對(duì)此不清楚的讀者相信可以Google出很多相關(guān)的資料來,所以在這里就再多做介紹了。
二、Passive View模式的基本特征總結(jié)
Passive View,顧名思義,View是被動(dòng)的。那么主動(dòng)是誰呢?答案是Presenter。對(duì)于Presenter的主動(dòng)性,我個(gè)人是這么理解的:
•Presenter是整個(gè)MVP體系的控制中心,而不是單純的處理View請(qǐng)求的人;
•View僅僅是用戶交互請(qǐng)求的匯報(bào)者,對(duì)于響應(yīng)用戶交互相關(guān)的邏輯和流程,View不參與決策,真正的決策者是Presenter;
•View向Presenter發(fā)送用戶交互請(qǐng)求應(yīng)該采用這樣的口吻:“我現(xiàn)在將用戶交互請(qǐng)求發(fā)送給你,你看著辦,需要我的時(shí)候我會(huì)協(xié)助你”,不應(yīng)該是這樣:“我現(xiàn)在處理用戶交互請(qǐng)求了,我知道該怎么辦,但是我需要你的支持,因?yàn)閷?shí)現(xiàn)業(yè)務(wù)邏輯的Model只信任你”;
•對(duì)于綁定到View上的數(shù)據(jù),不應(yīng)該是View從Presenter上“拉”回來的,應(yīng)該是Presenter主動(dòng)“推”給View的;
•View盡可能不維護(hù)數(shù)據(jù)狀態(tài),因?yàn)槠浔旧韮H僅實(shí)現(xiàn)單純的、獨(dú)立的UI操作;Presenter才是整個(gè)體系的協(xié)調(diào)者,它根據(jù)處理用于交互的邏輯給View和Model安排工作。
三、理想與現(xiàn)實(shí)的距離
上面對(duì)Passive View MVP特征的羅列,我覺得是一種理想狀態(tài)。是在大型項(xiàng)目中,尤其是項(xiàng)目的開發(fā)者自身并不完全理解MVP原理的情況下,要整體實(shí)現(xiàn)這樣的一種理想狀態(tài)是一件很難的事情。有人可能會(huì)說,在開發(fā)人員不了解MVP的情況下要求他們用好MVP,你這不是扯淡嗎?實(shí)際上,在這里并不是說開發(fā)人員完全沒有MVP關(guān)于關(guān)注點(diǎn)分離的概念,只是對(duì)MVP中的三元角色并沒有非常清晰的界定(實(shí)際上也沒有一個(gè)明確的規(guī)范對(duì)Model、View和Presenter具體的職責(zé)范圍進(jìn)行明確的劃分),在開發(fā)的時(shí)候,會(huì)不自覺地受傳統(tǒng)編程習(xí)慣的影響,將Presenter單純地當(dāng)成是View調(diào)用Model的中介。我經(jīng)常這么說:如果以View為中心,將Presenter當(dāng)成是View和Model的中間人,這也叫MVP模式,不過這里的P不是Presenter,而是Proxy,是Model在View的代理而已。
從Passive View中Model、View和Presenter三者之間的依賴關(guān)系來看,這個(gè)模型充分地給了開發(fā)者犯這樣錯(cuò)誤的機(jī)會(huì)。注意上面的圖中View到Presenter的箭頭表明View是可以任意的調(diào)用Presenter的。開發(fā)人員完全有可能將大部分UI處理邏輯寫在View中,而Presenter僅僅對(duì)Model響應(yīng)操作的簡單調(diào)用。因?yàn)樵谖襌eview的各種所謂的MVP編程方式中,有不少是這么寫的。在很多情況下,甚至不用認(rèn)真去分析具體的代碼,從View和Presenter中代碼的行數(shù)就可以看出來,因?yàn)閂iew的代碼和Presenter的代碼都不在一個(gè)數(shù)量級(jí)。
我現(xiàn)在的一個(gè)目的是提出一種編程模式,杜絕開發(fā)人員將程序?qū)懗苫赑roxy的MVP,在我看來,唯一的辦法就是盡量弱化(不可能剔除)View對(duì)Presenter的依賴。實(shí)際上,對(duì)于MVP來說,View僅僅向Presenter遞交用戶交互請(qǐng)求,僅此而已。如果我們將View對(duì)Presenter的這點(diǎn)依賴關(guān)系實(shí)現(xiàn)在框架層次中,最終開發(fā)人員的編程來說就不需要這種依賴了。那么我就可以通過一定的編程技巧使View根本無法訪問Presenter,從而避免Presenter成為Proxy的可能的。
那么,如果在不能獲得Presenter的情況下,使View能夠正常將請(qǐng)求遞交給Presenter呢?很簡單,通過事件訂閱機(jī)制就可以了,雖然View不可以獲取到Presenter,但是Presenter卻可以獲取到View,讓Presenter訂閱View的相關(guān)事件就可以的。
四、讓View不再依賴Presenter的編程模型
現(xiàn)在,我們就來如果通過一種簡單的編程模式就能夠讓View對(duì)Presenter的依賴完全地從中最終開發(fā)者的源代碼中移除。為此,我們需要定義一系列的基類,首先我為所有的View創(chuàng)建基類ViewBase,在這里我們直接用Form作為View,而在SCSF中View一般是通過UserControl來表示的。ViewBase定義如下,為了使View中不能調(diào)用Presenter,我將其定義成私有字段。那么,如何讓View和Presenter之間建立起關(guān)聯(lián)呢?在這里通過虛方法CreatePresenter,具體的View必須重寫該方法,不然會(huì)拋出一個(gè)NotImplementedException異常。在構(gòu)造函數(shù)中,調(diào)用該方法比用返回值為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創(chuàng)建基類Presenter<IView>,泛型類型IView表示具體View實(shí)現(xiàn)的接口。表示View的同名只讀屬性在構(gòu)造函數(shù)中賦值,賦值完成之后調(diào)用調(diào)用虛方法OnViewSet。具體的Presenter可以重寫該方法進(jìn)行對(duì)View進(jìn)行事件注冊(cè)工作。但是需要注意的是,Presenter的創(chuàng)建是在ViewBase的構(gòu)造函數(shù)中通過調(diào)用CreatePresenter方法實(shí)現(xiàn),所以執(zhí)行OnViewSet的時(shí)候,View本身還沒有完全初始化,所以在此不能對(duì)View的控件進(jìn)行操作。
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進(jìn)行交互的。在這里,由于View通過Form的形式體現(xiàn),有時(shí)候我們要通過這個(gè)接口訪問Form的一些屬性、方法和事件,需要將相應(yīng)的成員定義在接口上面,比較麻煩。此時(shí),我們可以選擇將這些成員定義在一個(gè)接口中,具體View的接口繼承該接口就可以了。在這里,我們相當(dāng)是為所有的View接口創(chuàng)建了“基接口”。作為演示,我現(xiàn)在了Form的三個(gè)事件成員定義在街口IViewBase中。
using System;
using System.ComponentModell;
namespace MVPDemo
{
public interface IViewBase
{
event EventHandler Load;
event EventHandler Closed;
event CancelEventHandler Closing;
}
}
五、實(shí)例演示
上面我通過定義基類和接口為整個(gè)編程模型搭建了一個(gè)框架,現(xiàn)在我們通過一個(gè)具體的例子來介紹該編程模型的應(yīng)用。我們采用的是一個(gè)簡單的Windows Forms應(yīng)用,模擬管理客戶信息的場(chǎng)景,邏輯很簡單:程序啟動(dòng)的時(shí)候顯示出所有的客戶端列表;用戶選擇某一客戶端,將響應(yīng)的信息顯示在TextBox中以供編輯;對(duì)客戶端信息進(jìn)行相應(yīng)修改之后,點(diǎn)擊OK按鈕進(jìn)行保存。整個(gè)操作界面如下圖所示:
首先,我們創(chuàng)建實(shí)體類Customer,簡單起見,僅僅包含四個(gè)屬性: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
};
}
}
}
然后,為了真實(shí)模擬MVP三種角色,特意創(chuàng)建一個(gè)CustomerModel類型,實(shí)際上在真實(shí)的應(yīng)用中,并沒有單獨(dú)一個(gè)類型來表示Model。CustomerModel維護(hù)客戶列表,體統(tǒng)相關(guān)的查詢和更新操作。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定義了兩個(gè)事件,CustomerSelected在用戶從Gird中選擇了某個(gè)條客戶記錄是觸發(fā),而CustomerSaving則在用戶完成編輯點(diǎn)擊OK按鈕視圖提交修改時(shí)觸發(fā)。ICustomerView還定義了View必須完成的三個(gè)基本操作:綁定客戶列表(ListAllCustomers);顯示單個(gè)客戶信息到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();
}
}
事件參數(shù)的類型CustomerEventArgs定義如下,兩個(gè)屬性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方法中注冊(cè)View的三個(gè)事件:Load事件中調(diào)用Model獲取所有客戶列表,并顯示在View的Grid上;CustomerSelected事件中通過事件參數(shù)傳遞的客戶ID調(diào)用Model獲取相應(yīng)的客戶信息,顯示在View的可編輯控件上;CustomerSaving則通過事件參數(shù)傳遞的被更新過的客戶信息,調(diào)用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);
};
}
}
}
對(duì)于具體的View來說,僅僅需要實(shí)現(xiàn)ICustomerView,并處理響應(yīng)控件事件即可(主要是用戶從Grid中選擇某個(gè)記錄觸發(fā)的RowHeaderMouseClick事件,以及點(diǎn)擊OK的事件)。實(shí)際上不需要View親自處理這些事件,而僅僅需要觸發(fā)相應(yīng)的事件,讓事件訂閱者(Presenter)來處理就可以了。此外還需要重寫CreatePresenter方法完成對(duì)CustomerPresenter的創(chuàng)建。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);
}
}
}
相關(guān)文章
asp.net 生成靜態(tài)時(shí)的過濾viewstate的實(shí)現(xiàn)方法
有時(shí)候我們?cè)谟胊sp.net生成靜態(tài)文件的時(shí)候,總會(huì)出現(xiàn)一些viewstate的字符,因?yàn)槭庆o態(tài)的不是aspx文件,所有沒必要留了,精簡代碼等原因,大家就需要看下面的方法了。2009-03-03.NET性能調(diào)優(yōu)之一:ANTS Performance Profiler的使用介紹
本系列文章主要會(huì)介紹一些.NET性能調(diào)優(yōu)的工具、Web性能優(yōu)化的規(guī)則(如YSlow)及方法等等內(nèi)容。成文前最不希望看到的就是園子里不間斷的“哪個(gè)語言好,哪個(gè)語言性能高”的爭(zhēng)論,不多說,真正的明白人都應(yīng)該知道這樣的爭(zhēng)論有沒有意義,希望我們能從實(shí)際性能優(yōu)化的角度去討論問題2013-01-01實(shí)現(xiàn).Net7下數(shù)據(jù)庫定時(shí)檢查的方法詳解
在軟件開發(fā)過程中,有時(shí)候我們需要定時(shí)地檢查數(shù)據(jù)庫中的數(shù)據(jù),并在發(fā)現(xiàn)新增數(shù)據(jù)時(shí)觸發(fā)一個(gè)動(dòng)作。為了實(shí)現(xiàn)這個(gè)需求,本文我們?cè)?.Net?7?下進(jìn)行一次簡單的演示。感興趣的可以了解一下2022-12-12ASP.NET Core奇淫技巧之動(dòng)態(tài)WebApi的實(shí)現(xiàn)
這篇文章主要介紹了ASP.NET Core奇淫技巧之動(dòng)態(tài)WebApi的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Entity?Framework管理一對(duì)二實(shí)體關(guān)系
本文詳細(xì)講解了Entity?Framework管理一對(duì)二實(shí)體關(guān)系的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03.NET?Core類庫項(xiàng)目中讀取appsettings.json配置的方法
ASP.NET?Core是一個(gè)全新的Web開發(fā)平臺(tái),微軟在它上面構(gòu)建了MVC、SingalR、GRPC、Orleans這樣廣泛使用的Web框架,今天通過本文給大家詳細(xì)介紹下.NET?Core讀取appsettings.json配置的方法,感興趣的朋友一起看看吧2022-03-03ASP.NET中UpdatePanel與jQuery同時(shí)使用所遇問題解決
在.NET中使用了UpdatePanel,里面的輸入框使用了jQuery的日歷選擇器,接下來介紹下兩者同時(shí)使用的一些細(xì)節(jié)及問題的解決方法,感興趣的各位可以參考下哈2013-03-03