为何要写这篇文章
笔者当前正在负责研究所中一个项目,这个项目基于.NET平台,初步拟采用C/S部署体系,因此选择了Windows Forms做为其UI。通过几此迭代,咱们发现了一个问题:虽然业务逻辑已经封装到Services层中,但诸多的UI逻辑仍然弥漫在各个事件Listener中,使得UI显得臃肿不堪,而且存在诸多重复性代码。另外,需求提供方说,根据实际须要,不排除将部署结构改成B/S的可能性,甚至可能会要求此系统同时支持C/S和B/S两种部署方式。那么,若是保持目前将UI逻辑编码到Windows Forms中的方式,到时这些UI逻辑将没法复用,修改部署方式的代价很大。html
为了解决以上两个问题,笔者和相关人员商量后,决定引入既有成熟模式,从新设计表示层的架构方式,并重构既有代码。架构
提到表示层(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的概念。post
表示层能够拆分为两个部分:User Interface(简称UI)和Presentation Logic(简称P Logic)。this
UI是系统与用户交互的界面性概念,它的职责有两个——接受用户的输入和向用户展现输出。UI应该是一个纯静态的概念,自己不该包含任何逻辑,而单纯是一个接受输入和展现输出的“外壳”。例如,一个不包含逻辑的Windows Form,一张不包含逻辑的页面,一个不包含逻辑的Flex界面,都属于UI。编码
P Logic是表示层应有的逻辑性内容。例如,某个文本内容不能为空,当某个事件发生时获取界面上哪些内容,这都属于P Logic。应该指出,P Logic应该是抽象于具体UI的,它的本质是逻辑,能够复用到任何与此逻辑相符的UI。spa
UI与P Logic之间的联系是事件,UI能够根据用户的动做触发各类事件,P Logic响应事件并执行相应的逻辑。P Logic对UI存在约束做用,P Logic规定一套UI契约,UI要根据契约实现,才能被相应的P Logic调用。设计
下图展现了UI与P Logic的结构及交互原理。3d
图一、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模式:
图二、MVP模式
图2清楚地展现了MVP模式的几个特色:
一、View和Model彻底解耦,二者不发生直接关联,经过Presenter进行通讯。
二、Presenter并非与具体的View耦合,而是和一个抽象的View Interface耦合,View Interface至关于一个契约,抽象出了对应View应实现的方法。只要实现了这个接口,任何View均可以与指定Presenter兼容,从而实现了P Logic的复用性和视图的无缝替换。
三、View在MVP里应该是一个“极瘦”的概念,最多也只能包含维护自身状态的逻辑,而其它逻辑都应实如今Presenter中。
总的来讲,使用MVP模式能够获得如下两个收益:
一、将UI和P Logic两个关注点分离,获得更干净和单一的代码结构。
二、实现了P Logic的复用以及View的无缝替换。
在.NET平台上实现MVP模式
这一节经过一个示例程序展现在.NET平台上实现MVP的一种实践方法。原本想经过我目前负责的实际项目中的代码片断做为Demo,但这样作存在两个问题:一是这样作可能会违反学校的保密守则,二是这个项目应用了许多其余框架和模式,如经过Unity实现依赖注入,经过PostSharp实现AOP来负责异常处理和事务管理等,经过NHibernate实现的ORM等等,这样若是读者不了解系统总体架构就很难彻底读懂代码片断,MVP模式不够突出。所以,我专门为这篇文章实现了一个Demo,其中的MVP实践方式与实际项目中是一致的,并且Demo规模小,排除了其余干扰,使得读者更容易理解其中的MVP实现方式。
这个简单的Demo运行效果以下:
图三、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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
using System; namespace MVPSimple.Model { /// <summary> /// 表示菜品类别的枚举类型 /// </summary> public enum FoodType { 主菜 = 1, 汤 = 2, 甜品 = 3, } /// <summary> /// 菜品的Data Transfer Object /// </summary> public class FoodDto { /// <summary> /// ID,标识字段 /// </summary> public Int32 ID { get;set; } /// <summary> /// 菜品名称 /// </summary> public String Name { get;set; } /// <summary> /// 菜品类型 /// </summary> public FoodType Type { get;set; } /// <summary> /// 菜品价格 /// </summary> public Double Price { get;set; } /// <summary> /// 点菜数量 /// </summary> public Int32 Amount { get;set; } } } |
FoodServices.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
using System; using System.Collections.Generic; namespace MVPSimple.Model { /// <summary> /// 菜品Services的Mock实现 /// </summary> public class FoodServices { private IList<FoodDto> foodList = new List<FoodDto>(); /// <summary> /// 默认构造函数,初始化各个菜品 /// </summary> public FoodServices() { this.foodList.Add( new FoodDto() { ID = 1, Name = "牛排", Price = 60.00, Type = FoodType.主菜, } ); this.foodList.Add( new FoodDto() { ID = 2, Name = "法式蜗牛", Price = 120.00, Type = FoodType.主菜, } ); this.foodList.Add( new FoodDto() { ID = 3, Name = "水果沙拉", Price = 58.00, Type = FoodType.甜品, } ); this.foodList.Add( new FoodDto() { ID = 4, Name = "奶油红菜汤", Price = 15.00, Type = FoodType.汤, } ); this.foodList.Add( new FoodDto() { ID = 5, Name = "杂拌汤", Price = 20.00, Type = FoodType.汤, } ); } /// <summary> /// 按照菜品名称获取菜品详细信息 /// </summary> /// <param name="foodName">菜品名称</param> /// <returns>含有指定菜品信息的DTO</returns> public FoodDto GetFoodDetailByName(String foodName) { foreach (FoodDto f in this.foodList) { if (f.Name.Equals(foodName)) { return f; } } return new FoodDto() { ID = 0 }; } } } |
第三步,经过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:
using System; using System.Collections.Generic; using MVPSimple.Model; namespace MVPSimple.Presenters { /// <summary> /// MainView的接口,全部MainView必须实现此接口,此接口暴露给Presenter /// </summary> public interface IMainView { /// <summary> /// View上的菜品名称 /// </summary> String foodName { get;set; } /// <summary> /// View上点菜数量 /// </summary> Int32 Amount { get;set; } /// <summary> /// 判断某一菜品是否已经存在于点菜列表中 /// </summary> /// <param name="foodName">菜品名称</param> /// <returns>结果</returns> bool IsExistInList(String foodName); /// <summary> /// 将某一菜品加入点菜列表 /// </summary> /// <param name="food">菜品DTO</param> void AddFoodToList(FoodDto food); /// <summary> /// 将某一已点菜品从列表中移除 /// </summary> /// <param name="foodName">欲移除的菜品名称</param> void RemoveFoodFromList(String foodName); /// <summary> /// View显示提示信息给用户 /// </summary> /// <param name="message">信息内容</param> void ShowMessage(String message); /// <summary> /// View显示确认信息并返回结果 /// </summary> /// <param name="message">信息内容</param> /// <returns>用户回答是肯定仍是取消。True - 肯定,False - 取消</returns> bool ShowConfirm(String message); } }
能够看到,IMainView抽象了如图3所示的界面,但又不包含任何与Windows Forms相耦合的元素,所以若是须要,之后彻底可使用Web Forms、WPF或SL等技术实现这个接口。
第四步,实现Presenter
上文说过,一个界面应该对应一个Presenter,这个Demo里只有一个界面,因此只有一个Presenter。Presenter仅于视图接口耦合,而并不和具体视图耦合,最好证据就是Presenter工程根本没有引用WinUI工程!代码以下:
MainPresenter.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
using System; using System.Collections.Generic; using MVPSimple.Model; namespace MVPSimple.Presenters { /// <summary> /// MainView的Presenter /// </summary> public class MainPresenter { /// <summary> /// 当前关联View /// </summary> public IMainView View { get;set; } /// <summary> /// 默认构造函数,初始化View /// </summary> /// <param name="view">MainView对象</param> public MainPresenter(IMainView view) { View = view; } #region Acitons /// <summary> /// Action:将所点菜品增长到点菜列表 /// </summary> public void AddFoodAction() { if (String.IsNullOrEmpty(View.foodName)) { View.ShowMessage("请选输入菜品名称"); return; } if (View.Amount <= 0) { View.ShowMessage("点菜的份数至少要是一份"); return; } if (View.IsExistInList(View.foodName)) { View.ShowMessage(String.Format("菜品【{0}】已经在您的菜单中", View.foodName)); return; } FoodServices foodServ = new FoodServices(); FoodDto food = foodServ.GetFoodDetailByName(View.foodName); if (food.ID == 0) { View.ShowMessage(String.Format("抱歉,本餐厅没有菜品【{0}】",View.foodName)); return; } View.AddFoodToList(food); } /// <summary> /// Action:从点菜列表移除某一菜品 /// </summary> /// <param name="foodName">被移除菜品的名称</param> public void RemoveFoodAction(String foodName) { if (View.ShowConfirm("肯定要删除吗?")) { View.RemoveFoodFromList(foodName); } } #endregion } } |
第五步,实现View
这里咱们使用Windows Forms实现View。若是朋友们有兴趣,彻底能够本身试着用Web或WPF实现如下视图,同时能够验证P Logic的可复用性和视图无缝替换,亲身体验一下MVP模式的威力。Winform的View代码以下。
frmMain.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
using System; using System.Windows.Forms; using MVPSimple.Model; using MVPSimple.Presenters; namespace MVPSimple.WinUI { /// <summary> /// MainView的Windows Forms实现 /// </summary> public partial class frmMain : Form, IMainView { /// <summary> /// 相关联的Presenter /// </summary> private MainPresenter presenter; /// <summary> /// 默认构造函数,初始化Presenter /// </summary> public frmMain() { InitializeComponent(); this.presenter = new MainPresenter(this); } #region IMainView Members /// <summary> /// View上的菜品名称 /// </summary> public String foodName { get {return this.tbFoodName.Text; } set {this.tbFoodName.Text = value; } } /// <summary> /// View上点菜数量 /// </summary> public Int32 Amount { get {return (Int32)this.tbAmount.Value; } set {this.tbAmount.Value = (Decimal)value; } } /// <summary> /// 判断某一菜品是否已经存在于点菜列表中 /// </summary> /// <param name="foodName">菜品名称</param> /// <returns>结果</returns> public bool IsExistInList(String foodName) { foreach (ListViewItem i in this.lvFoods.Items) { if (i.Text == foodName) { return true; } } return false; } /// <summary> /// 将某一菜品加入点菜列表 /// </summary> /// <param name="food">菜品DTO</param> public void AddFoodToList(FoodDto food) { ListViewItem item = new ListViewItem(); Double price = food.Price * (Double)this.tbAmount.Value; item.Text = food.Name; item.SubItems.Add(food.Type.ToString()); item.SubItems.Add(this.tbAmount.Value.ToString()); item.SubItems.Add(price.ToString()); this.lvFoods.Items.Add(item); } /// <summary> /// 将某一已点菜品从列表中移除 /// </summary> /// <param name="foodName">欲移除的菜品名称</param> public void RemoveFoodFromList(String foodName) { foreach (ListViewItem i in this.lvFoods.Items) { if (i.Text == foodName) { this.lvFoods.Items.Remove(i); } } } /// <summary> /// View显示提示信息给用户 /// </summary> /// <param name="message">信息内容</param> public void ShowMessage(String message) { MessageBox.Show(message,"信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); } /// <summary> /// View显示确认信息并返回结果 /// </summary> /// <param name="message">信息内容</param> /// <returns>用户回答是肯定仍是取消。True - 肯定,False - 取消</returns> public bool ShowConfirm(String message) { DialogResult result = MessageBox.Show(message, "确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question); return DialogResult.OK == result; } #endregion #region Event Listeners private void btnAdd_Click(object sender, EventArgs e) { this.presenter.AddFoodAction(); } private void miDeleteFood_Click(object sender, EventArgs e) { if (this.lvFoods.SelectedItems.Count != 0) { String foodName = this.lvFoods.SelectedItems[0].Text; this.presenter.RemoveFoodAction(foodName); } } #endregion } } |
能够看到,使用了MVP后,View的代码变的很是干净整洁,之前充斥着厚重表示逻辑的事件Listener方法变得“瘦”了许多。
完成以上几步后,就能够运行这个Demo看效果了。
总结
这篇文章首先讨论表示层的组成,说明User Interface和Presentation Logic是表示层的两个重要组成部分,并分别说明了二者的做用及交互方式。接着讨论了MVP模式。最后,经过一个Demo展现了在.NET平台上实现MVP的一种实践方式。应该说,MVP很相似简化了MVC,MVP不但能够分离关注、使得代码变得干净整洁、并实现P Logic的复用,并且实现起来比MVC在结构上要简单不少。MVP是一种模式,自己有诸多实现方式,本文只是介绍了笔者使用的一种实践,朋友们也能够在此基础上摸索本身的实践。
PS:
本文来讲比较通俗易懂,对于理解起来也相对容易,想对MVP有更多的了解,请关注个人下一篇文章 <.NET平台上的 MVP 模式再探(二)>