您的位置:首页 > 其它

.NET平台上的Model-View-Presenter模式实践

2011-07-14 15:57 363 查看
者当前正在负责研究所中一个项目,这个项目基于.NET平台,初步拟采用C/S部署体系,所以选择了Windows Forms作为其UI。经过几此迭代,我们发现了一个问题:虽然业务逻辑已经封装到Services层中,但诸多的UI逻辑仍然弥漫在各个事件 Listener中,使得UI显得臃肿不堪,并且存在诸多重复性代码。另外,需求提供方说,根据实际需要,不排除将部署结构改为B/S的可能性,甚至可能 会要求此系统同时支持C/S和B/S两种部署方式。那么,如果保持目前将UI逻辑编码到Windows Forms中的方式,到时这些UI逻辑将无法复用,修改部署方式的代价很大。 为了解决以上两个问题,笔者和相关人员商量后,决定引入既有成熟模式,重新设计表示层的架构方式,并重构既有代码。

提到表示层(Presentation Layer)的模式,我想大家脑海中第一个闪过的很可能是经典的MVC(Model-View-Controller)。我最初也准备使用MVC,但经过 分析和实验后,我发现MVC并不适合目前的情况,因为MVC的结构相对复杂,Model和View之间要实现一个Observer模式,并实现双向通信。 这样重构起来Services层也必须修改。我并不想修改Services层,而且我想将View和Model彻底隔离,因为我个人并不喜欢View和 Model直接通信的架构方式。最终,我选择了MVP(Model-View-Presenter)模式。

经过两天的重构和验证,目前已经将MVP正式引入项目的表示层,并且解决了上文提到的两个问题。在这期间,积累了少许关于在.NET平台上实践MVP的经验,在这里汇集成此文,和朋友们共享。

UI与P Logic

首先,我想先明确一下UI和P Logic的概念。

表示层可以拆分为两个部分:User Interface(简称UI)和Presentation Logic(简称P Logic)。

UI是系统与用户交互的界面性概念,它的职责有两个——接受用户的输入和向用户展示输出。UI应该是一个纯静态的概念,本身不应包含任何逻辑,而单纯是一个接受输入和展示输出的“外壳”。例如,一个不包含逻辑的Windows Form,一张不包含逻辑的页面,一个不包含逻辑的Flex界面,都属于UI。

P Logic是表示层应有的逻辑性内容。例如,某个文本内容不能为空,当某个事件发生时获取界面上哪些内容,这都属于P Logic。应该指出,P Logic应该是抽象于具体UI的,它的本质是逻辑,可以复用到任何与此逻辑相符的UI。

UI与P Logic之间的联系是事件,UI可以根据用户的动作触发各种事件,P Logic响应事件并执行相应的逻辑。P Logic对UI存在约束作用,P Logic规定一套UI契约,UI要根据契约实现,才能被相应的P Logic调用。

下图展示了UI与P Logic的结构及交互原理。



图1、UI与P Logic

Model-View-Presenter模式

MVP模式最早由Taligent的Mike Potel在《MVP: Model-View-Presenter The Taligent Programming Model for C++ and Java》(点击这里下载)一文中提出。MVP的提出主要是为了解决MVC模式中结构过于复杂和模型-视图耦合性过高的问题。MVP的核心思想是将UI分离成View,将P Logic分离成Presenter,而业务逻辑和领域相关逻辑都分离到Model中。View和Model完全解除耦合,不再像MVC中实现一个Observer模式,两者的通信则依靠Presenter进行。Presenter响应View接获的用户动作,并调用Model中的业务逻辑,最后将用户需要的信息返回给View。

下图直观表示了MVP模式:



图2、MVP模式

图2清楚地展示了MVP模式的几个特点:

1、View和Model完全解耦,两者不发生直接关联,通过Presenter进行通信。

2、Presenter并不是与具体的View耦合,而是和一个抽象的View Interface耦合,View Interface相当于一个契约,抽象出了对应View应实现的方法。只要实现了这个接口,任何View都可以与指定Presenter兼容,从而实现 了P Logic的复用性和视图的无缝替换。

3、View在MVP里应该是一个“极瘦”的概念,最多也只能包含维护自身状态的逻辑,而其它逻辑都应实现在Presenter中。

总的来说,使用MVP模式可以得到以下两个收益:

1、将UI和P Logic两个关注点分离,得到更干净和单一的代码结构。

2、实现了P Logic的复用以及View的无缝替换。

在.NET平台上实现MVP模式

这一节通过一个示例程序展示在.NET平台上实现MVP的一种实践方法。本来想通过我目前负责的实际项目中的代码片段作为Demo,但这样做存在两个问 题:一是这样做可能会违反学校的保密守则,二是这个项目应用了许多其他框架和模式,如通过Unity实现依赖注入,通过PostSharp实现AOP来负 责异常处理和事务管理等,通过NHibernate实现的ORM等等,这样如果读者不了解系统整体架构就很难完全读懂代码片段,MVP模式不够突出。因 此,我专门为这篇文章实现了一个Demo,其中的MVP实践方式与实际项目中是一致的,而且Demo规模小,排除了其他干扰,使得读者更容易理解其中的 MVP实现方式。

这个简单的Demo运行效果如下:



图3、Demo界面

这个Demo的功能如下:这是一个简单的点餐软件。系统中存有餐厅所有菜品的信息,客户只需在界面右侧输入菜品名称和数量,单击“添加”按钮,菜品就会被 添加到左侧点餐列表,并显示此菜品详细信息。如果所点菜品不存在则软件会给出提示。另外,在左侧已点餐品列表中右键单击某个条目,在弹出菜单中点击“删 除”,则可将此菜品从列表删除。

下面分步骤介绍应用了MVP模式的实现方式。

第一步,解决方法及工程结构

这个Demo共有三个工程,MVPSimple.Model为Mock方式实现的Services,作为 Model;MVPSimple.Presenters为Presenter工程,其中包括Presenter和View Interface;MVPSimple.WinUI为View的Windows Forms实现。

第二步,构建Mock方式的Services

因为重点在于表示层,所以这里的Services使用了Mock方式,并没有包含真正的业务领域逻辑。其中MVPSimple.Model工程里两个文件的代码如下:

FoodDto.cs:

01
using
System;
02
03
namespace
MVPSimple.Model
04
{
05
/// <summary>
06
/// 表示菜品类别的枚举类型
07
/// </summary>
08
public
enum
FoodType
09
{
10
主菜 = 1,
11
汤 = 2,
12
甜品 = 3,
13
}
14
15
/// <summary>
16
/// 菜品的Data Transfer Object
17
/// </summary>
18
public
class
FoodDto
19
{
20
/// <summary>
21
/// ID,标识字段
22
/// </summary>
23
public
Int32 ID {
get
;
set
;}
24
25
/// <summary>
26
/// 菜品名称
27
/// </summary>
28
public
String Name {
get
;
set
;}
29
 
30
/// <summary>
31
/// 菜品类型
32
/// </summary>
33
public
FoodType Type {
get
;
set
;}
34
35
/// <summary>
36
/// 菜品价格
37
/// </summary>
38
public
Double Price {
get
;
set
;}
39
40
/// <summary>
41
/// 点菜数量
42
/// </summary>
43
public
Int32 Amount {
get
;
set
;}
44
}
45
}
FoodServices.cs:

01
using
System;
02
using
System.Collections.Generic;
03
04
namespace
MVPSimple.Model
05
{
06
/// <summary>
07
/// 菜品Services的Mock实现
08
/// </summary>
09
public
class
FoodServices
10
{
11
private
IList<FoodDto>foodList = 
new
List<FoodDto>();
12
13
/// <summary>
14
/// 默认构造函数,初始化各个菜品
15
/// </summary>
16
public
FoodServices()
17
{
18
this
.foodList.Add(
19
new
FoodDto()
20
{
21
ID = 1,
22
Name =
"牛排"
,
23
Price = 60.00,
24
Type = FoodType.主菜,
25
}
26
);
27
28
this
.foodList.Add(
29
new
FoodDto()
30
{
31
ID = 2,
32
Name =
"法式蜗牛"
,
33
Price = 120.00,
34
Type = FoodType.主菜,
35
}
36
);
37
38
this
.foodList.Add(
39
new
FoodDto()
40
{
41
ID = 3,
42
Name =
"水果沙拉"
,
43
Price = 58.00,
44
Type = FoodType.甜品,
45
}
46
);
47
48
this
.foodList.Add(
49
new
FoodDto()
50
{
51
ID = 4,
52
Name =
"奶油红菜汤"
,
53
Price = 15.00,
54
Type = FoodType.汤,
55
}
56
);
57
58
this
.foodList.Add(
59
new
FoodDto()
60
{
61
ID = 5,
62
Name =
"杂拌汤"
,
63
Price = 20.00,
64
Type = FoodType.汤,
65
}
66
);
67
}
68
69
/// <summary>
70
/// 按照菜品名称获取菜品详细信息
71
/// </summary>
72
/// <param name="foodName">菜品名称</param>
73
/// <returns>含有指定菜品信息的DTO</returns>
74
public
FoodDto GetFoodDetailByName(String foodName)
75
{
76
foreach
(FoodDto f
in
this
.foodList)
77
{
78
if
(f.Name.Equals(foodName))
79
{
80
return
f;
81
}
82
}
83
84
return
new
FoodDto() {ID = 0 };
85
}
86
}
87
}

第三步,通过View Interface规定View契约

如果想实现Presenter和View的交互和无缝替换,必须在它们之间规定一个契约。一般来说,每一张界面(注意是界面不是视图)都应该对应一个View接口,不过由于Demo只有一个页面,所以也只有一个View接口。

这里需要特别强调,View接口必须抽象于任何具体视图而服务于Presenter,所以,View接口中绝不能出现任何与具体视图相关的元素。例如,我 们的Demo中是使用Windows Forms作为视图实现,但View接口中绝不可出现与Windows Forms相耦合的元素,如返回一个Winform的TextBox。因为如果这样做的话,使用其他技术实现的View就无法实现这个接口了,如使用 Web Forms实现,而Web Forms是不可能返回一个Winform的TextBox的。

下面给出视图接口的代码。

IMainView.cs:

01
using
System;
02
using
System.Collections.Generic;
03
using
MVPSimple.Model;
04
05
namespace
MVPSimple.Presenters
06
{
07
/// <summary>
08
/// MainView的接口,所有MainView必须实现此接口,此接口暴露给Presenter
09
/// </summary>
10
public
interface
IMainView
11
{
12
/// <summary>
13
/// View上的菜品名称
14
/// </summary>
15
String foodName {
get
;
set
;}
16
17
/// <summary>
18
/// View上点菜数量
19
/// </summary>
20
Int32 Amount {
get
;
set
;}
21
22
/// <summary>
23
/// 判断某一菜品是否已经存在于点菜列表中
24
/// </summary>
25
/// <param name="foodName">菜品名称</param>
26
/// <returns>结果</returns>
27
bool
IsExistInList(String foodName);
28
29
/// <summary>
30
/// 将某一菜品加入点菜列表
31
/// </summary>
32
/// <param name="food">菜品DTO</param>
33
void
AddFoodToList(FoodDto food);
34
 
35
/// <summary>
36
/// 将某一已点菜品从列表中移除
37
/// </summary>
38
/// <param name="foodName">欲移除的菜品名称</param>
39
void
RemoveFoodFromList(String foodName);
40
41
/// <summary>
42
/// View显示提示信息给用户
43
/// </summary>
44
/// <param name="message">信息内容</param>
45
void
ShowMessage(String message);
46
47
/// <summary>
48
/// View显示确认信息并返回结果
49
/// </summary>
50
/// <param name="message">信息内容</param>
51
/// <returns>用户回答是确定还是取消。True - 确定,False - 取消</returns>
52
bool
ShowConfirm(String message);
53
}
54
}
可以看到,IMainView抽象了如图3所示的界面,但又不包含任何与Windows Forms相耦合的元素,因此如果需要,以后完全可以使用Web Forms、WPF或SL等技术实现这个接口。

第四步,实现Presenter

上文说过,一个界面应该对应一个Presenter,这个Demo里只有一个界面,所以只有一个Presenter。Presenter仅于视图接口耦合,而并不和具体视图耦合,最好证据就是Presenter工程根本没有引用WinUI工程!代码如下:

MainPresenter.cs:

01
using
System;
02
using
System.Collections.Generic;
03
using
MVPSimple.Model;
04
05
namespace
MVPSimple.Presenters
06
{
07
/// <summary>
08
/// MainView的Presenter
09
/// </summary>
10
public
class
MainPresenter
11
{
12
/// <summary>
13
/// 当前关联View
14
/// </summary>
15
public
IMainView View {
get
;
set
;}
16
17
/// <summary>
18
/// 默认构造函数,初始化View
19
/// </summary>
20
/// <param name="view">MainView对象</param>
21
public
MainPresenter(IMainView view)
22
{
23
View = view;
24
}
25
26
#region Acitons
27
28
/// <summary>
29
/// Action:将所点菜品增加到点菜列表
30
/// </summary>
31
public
void
AddFoodAction()
32
{
33
if
(String.IsNullOrEmpty(View.foodName))
34
{
35
View.ShowMessage(
"请选输入菜品名称"
);
36
return
;
37
}
38
if
(View.Amount <= 0)
39
{
40
View.ShowMessage(
"点菜的份数至少要是一份"
);
41
return
;
42
}
43
if
(View.IsExistInList(View.foodName))
44
{
45
View.ShowMessage(String.Format(
"菜品【{0}】已经在您的菜单中"
, View.foodName));
46
return
;
47
}
48
49
FoodServices foodServ =
new
FoodServices();
50
FoodDto food = foodServ.GetFoodDetailByName(View.foodName);
51
if
(food.ID == 0)
52
{
53
View.ShowMessage(String.Format(
"抱歉,本餐厅没有菜品【{0}】"
,View.foodName));
54
return
;
55
}
56
57
View.AddFoodToList(food);
58
}
59
60
/// <summary>
61
/// Action:从点菜列表移除某一菜品
62
/// </summary>
63
/// <param name="foodName">被移除菜品的名称</param>
64
public
void
RemoveFoodAction(String foodName)
65
{
66
if
(View.ShowConfirm(
"确定要删除吗?"
))
67
{
68
View.RemoveFoodFromList(foodName);
69
}
70
}
71
72
#endregion
73
}
74
}

第五步,实现View

这里我们使用Windows Forms实现View。如果朋友们有兴趣,完全可以自己试着用Web或WPF实现以下视图,同时可以验证P Logic的可复用性和视图无缝替换,亲身体验一下MVP模式的威力。Winform的View代码如下。

frmMain.cs:

001
using
System;
002
using
System.Windows.Forms;
003
using
MVPSimple.Model;
004
using
MVPSimple.Presenters;
005
006
namespace
MVPSimple.WinUI
007
{
008
/// <summary>
009
/// MainView的Windows Forms实现
010
/// </summary>
011
public
partial
class
frmMain : Form, IMainView
012
{
013
/// <summary>
014
/// 相关联的Presenter
015
/// </summary>
016
private
MainPresenter presenter;
017
018
/// <summary>
019
/// 默认构造函数,初始化Presenter
020
/// </summary>
021
public
frmMain()
022
{
023
InitializeComponent();
024
this
.presenter =
new
MainPresenter(
this
);
025
}
026
027
#region IMainView Members
028
029
/// <summary>
030
/// View上的菜品名称
031
/// </summary>
032
public
String foodName
033
{
034
get
{
return
this
.tbFoodName.Text;}
035
set
{
this
.tbFoodName.Text = value;}
036
}
037
038
/// <summary>
039
/// View上点菜数量
040
/// </summary>
041
public
Int32 Amount
042
{
043
get
{
return
(Int32)
this
.tbAmount.Value;}
044
set
{
this
.tbAmount.Value = (Decimal)value;}
045
}
046
047
/// <summary>
048
/// 判断某一菜品是否已经存在于点菜列表中
049
/// </summary>
050
/// <param name="foodName">菜品名称</param>
051
/// <returns>结果</returns>
052
public
bool
IsExistInList(String foodName)
053
{
054
foreach
(ListViewItem i
in
this
.lvFoods.Items)
055
{
056
if
(i.Text == foodName)
057
{
058
return
true
;
059
}
060
}
061
062
return
false
;
063
}
064
065
/// <summary>
066
/// 将某一菜品加入点菜列表
067
/// </summary>
068
/// <param name="food">菜品DTO</param>
069
public
void
AddFoodToList(FoodDto food)
070
{
071
ListViewItem item =
new
ListViewItem();
072
Double price = food.Price * (Double)
this
.tbAmount.Value;
073
074
item.Text = food.Name;
075
item.SubItems.Add(food.Type.ToString());
076
item.SubItems.Add(
this
.tbAmount.Value.ToString());
077
item.SubItems.Add(price.ToString());
078
this
.lvFoods.Items.Add(item);
079
}
080
081
/// <summary>
082
/// 将某一已点菜品从列表中移除
083
/// </summary>
084
/// <param name="foodName">欲移除的菜品名称</param>
085
public
void
RemoveFoodFromList(String foodName)
086
{
087
foreach
(ListViewItem i
in
this
.lvFoods.Items)
088
{
089
if
(i.Text == foodName)
090
{
091
this
.lvFoods.Items.Remove(i);
092
}
093
}
094
}
095
096
/// <summary>
097
/// View显示提示信息给用户
098
/// </summary>
099
/// <param name="message">信息内容</param>
100
public
void
ShowMessage(String message)
101
{
102
MessageBox.Show(message,
"信息"
, MessageBoxButtons.OK, MessageBoxIcon.Warning);
103
}
104
105
/// <summary>
106
/// View显示确认信息并返回结果
107
/// </summary>
108
/// <param name="message">信息内容</param>
109
/// <returns>用户回答是确定还是取消。True - 确定,False - 取消</returns>
110
public
bool
ShowConfirm(String message)
111
{
112
DialogResult result = MessageBox.Show(message,
"确认"
, MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
113
return
DialogResult.OK == result;
114
}
115
116
#endregion
117
118
#region Event Listeners
119
120
private
void
btnAdd_Click(
object
sender, EventArgs e)
121
{
122
this
.presenter.AddFoodAction();
123
}
124
125
private
void
miDeleteFood_Click(
object
sender, EventArgs e)
126
{
127
if
(
this
.lvFoods.SelectedItems.Count != 0)
128
{
129
String foodName =
this
.lvFoods.SelectedItems[0].Text;
130
this
.presenter.RemoveFoodAction(foodName);
131
}
132
}
133
134
#endregion
135
}
136
}
可以看到,使用了MVP后,View的代码变的非常干净整洁,以前充斥着厚重表示逻辑的事件Listener方法变得“瘦”了许多。

完成以上几步后,就可以运行这个Demo看效果了。

总结

这篇文章首先讨论表示层的组成,说明User Interface和Presentation Logic是表示层的两个重要组成部分,并分别说明了两者的作用及交互方式。接着讨论了MVP模式。最后,通过一个Demo展示了在.NET平台上实现 MVP的一种实践方式。应该说,MVP很类似简化了MVC,MVP不但可以分离关注、使得代码变得干净整洁、并实现P Logic的复用,而且实现起来比MVC在结构上要简单很多。MVP是一种模式,本身有诸多实现方式,本文只是介绍了笔者使用的一种实践,朋友们也可以在 此基础上摸索自己的实践。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: