表现层设计模式
一、理论
1 MVC:模型-视图-控制器
模型:
指应用程序中,业务逻辑入口点对象。模型中包括:应用程序状态、视图展示的数据、响应用户请求的操作、执行控制器请求的操作
控制器:
由视图触发执行某个操作,对模型进行修改。
使用MVC意味着要创建视图,控制器和业务层
2 MVP:
目前一般不会直接用MVP,而使用它的两个变体:SC(Supervising Controller)
和PV(Passive View)。
1)SC:
Presenter:
处理输入响应,操纵视图以完成更复杂的视图逻辑,同步视图和模型。
当UI变化时,会发出抛出一个事件,致使Controller中相应的方法被调用,这个方法会处理请求并更新模型。视图会观察模型的变化并更新。
SC模式把一部分UI处理逻辑放到视图层,例如显示样式等。
2)PV:
Presenter:
响应用户事件,更新视图,负责UI处理逻辑,包括UI的呈现样式等。
当UI变化时,控制器更新模型和视图。
3. PM
模型:
PM中的模型不是业务层,而是包含多个属性的类,专门服务于视图层,含有展示视图所需的所有数据。
视图:
视图是UI元素的集合,UI元素绑定到模型属性上。用户触发的事件都将发送给展示器。
模型更新后,展示器控制视图更新。
视图持有对展示器的引用,模型通过展示器暴露给视图,视图不会暴露出任何接口。
展示器:
接收视图请求,调用表现层或业务逻辑层。
展示器持有模型对象的引用,并且暴露公开的方法和属性为视图提供数据。
二、代码示例
视图界面
每种方法的UI呈现都是相同的,不同的是接口,展示器等
1MVP-PV
视图接口
public interface IView { string Tips { set; }//对应TextBox控件 string Detail { set; get; }//对应RichTextBox控件 string SelectedItem { set; get; }//对应ComboBox控件被选择元素 List<string> Items { set; }//对应ComboBox控件 }
视图接口的实现
public partial class Form_MVP_PV : Form,IView { Presenter prt; public Form_MVP_PV() { InitializeComponent(); } public string Tips { set { this.Invoke(new Action(() => { this.tbxPV.Text = value; })); } } string IView.Detail { get { return this.rtbxPV.Text; } set { this.Invoke(new Action(() => { this.rtbxPV.Text += value; })); } } private void btnExe_Click(object sender, EventArgs e) { btnExe.Enabled = false; btnExe.Text = "正在执行"; this.cbxPv.Enabled = false; Task.Factory.StartNew(() => { prt.Colculate(); btnExe.Enabled = true; btnExe.Text = "开始"; this.cbxPv.Enabled = true; }); } private void Form_MVP_PV_Load(object sender, EventArgs e) { prt = new Presenter(this); prt.Initialize(); } public string SelectedItem { get { Control.CheckForIllegalCrossThreadCalls = false; return this.cbxPv.SelectedItem.ToString(); } set { this.cbxPv.SelectedItem = value; } } public List<string> Items { set { this.Invoke(new Action(() => { this.cbxPv.Items.AddRange(value.ToArray()); })); } }
展示器Presenter
public class Presenter { IView iView; public Presenter(IView view) { this.iView = view; } public void Initialize() { iView.Items = new List<string> { "first","second","thrid" }; iView.SelectedItem = "first"; } public void Colculate() { for (int i = 1; i < 11; i++) { iView.Tips = string.Format("第{0}组,共{1}个-执行完{2}-正在计算第{3}个", iView.SelectedItem, 10, i - 1, i); Thread.Sleep(1000);//具体工作,此处以挂起进程代替 string msg = string.Format("计算到第{0}"+Environment.NewLine,i); iView.Detail = msg; } iView.Tips = "全部完成"; } }
说明:
1)Presenter对Model的调用没有体现,一般来讲Model是业务层,这里为了体现PV的设计宗旨,即将视图和展示器分离,所以省略了Presenter对业务层调用。
2)你会发现在属性SelectedItem的get方法中加了Control.CheckForIllegalCrossThreadCalls = false;这行代码,目的是从不是创建cbxPv这个控件的线程访问它,那么哪些线程会访问它呢?一个自然就是创建此空间的线程,另一个就是private void btnExe_Click(object sender, EventArgs e)方法中所创建的一个线程。在此方法中创建线程是为了能够异步执行长时间计算任务,同时将任务生成的阶段性结果异步地展示到UI上。
3)你会发现private void btnExe_Click(object sender, EventArgs e)方法中包含了UI控件的部分显示逻辑,这似乎违背了PV设计的宗旨,但是这样的实现方式简便、直观、易于控制。下面为了将这段UI控件显示逻辑从视图挪走,放到Presenter中,代码修改如下:
首先,在IView中添加如下代码
bool BtnEnable { set; } string BtnText { set; } bool CheckBoxEnable { set; }
变为:
public interface IView { string Tips { set; }//对应TextBox控件 string Detail { set; get; }//对应RichTextBox控件 string SelectedItem { set; get; }//对应ComboBox控件被选择元素 List<string> Items { set; }//对应ComboBox控件 bool BtnEnable { set; } string BtnText { set; } bool CheckBoxEnable { set; } }
在接口实现(Form_MVP_PV类)中实现新添加的属性:
public bool BtnEnable { set { Control.CheckForIllegalCrossThreadCalls = false; this.btnExe.Enabled = value; } } public string BtnText { set { Control.CheckForIllegalCrossThreadCalls = false; this.btnExe.Text = value; } } public bool CheckBoxEnable { set { Control.CheckForIllegalCrossThreadCalls = false; this.cbxPv.Enabled = value; } }
注掉btnExe_Click方法中关于UI显示逻辑的带码,变为:
private void btnExe_Click(object sender, EventArgs e) { //btnExe.Enabled = false; //btnExe.Text = "正在执行"; //this.cbxPv.Enabled = false; Task.Factory.StartNew(() => { prt.Colculate(); //btnExe.Enabled = true; //btnExe.Text = "开始"; //this.cbxPv.Enabled = true; }); }
至此完成修改Presenter中的Colculate()方法,变为:
public void Colculate() { iView.BtnEnable = false; iView.CheckBoxEnable = false; iView.BtnText = "正在执行..."; for (int i = 1; i < 11; i++) { iView.Tips = string.Format("第{0}组,共{1}个-执行完{2}-正在计算第{3}个", iView.SelectedItem, 100, i - 1, i); Thread.Sleep(1000);//具体工作,此处以挂起进程代替 string msg = string.Format("计算到第{0}"+Environment.NewLine,i); iView.Detail = msg; } iView.Tips = "全部完成"; iView.BtnEnable = true; iView.BtnText = "执行"; iView.CheckBoxEnable = true; }
可以看到,为了将上面注掉的UI显示逻辑代码从视图层挪走,添加的代码量是注掉的代码的几倍。
2 MVP-SC
视图接口
public interface IView { void UpdateUI(Model model);//更新执行过程信息 string GetSelecteditem();//获得选择的元素 void AddItems(IEnumerable<string> set);//初始化添加元素 void Begin(Model model);//开始执行计算的时候,更新UI显示 void Complete(Model model);//结束执行计算的时候,更新UI显示 }
视图接口实现
public partial class Form_MVP_SC : Form,IView { Presenter prt; public Form_MVP_SC() { InitializeComponent(); } private void btnExeSC_Click(object sender, EventArgs e) { Task.Factory.StartNew(() => { prt.Colculate(); }); } private void Form_MVP_SC_Load(object sender, EventArgs e) { prt = new Presenter(this); prt.Initialize(); } public void UpdateUI(Model model) { this.Invoke(new Action(() => { this.tbxSC.Text = string.Format("第{0}组,共{1}个-执行完{2}-正在计算第{3}个", this.cbxSC.SelectedItem.ToString(), model.AllCount, model.DoingIndex - 1, model.DoingIndex); this.rtbxSC.Text += string.Format("计算到第{0}" + Environment.NewLine, model.DoingIndex); })); } public string GetSelecteditem() { Control.CheckForIllegalCrossThreadCalls = false; return this.cbxSC.SelectedItem.ToString(); } public void AddItems(IEnumerable<string> set) { this.cbxSC.Items.AddRange(set.ToArray()); this.cbxSC.SelectedIndex = 0; } public void Begin(Model model) { if (!model.Complete) { Control.CheckForIllegalCrossThreadCalls = false; this.cbxSC.Enabled = false; this.btnExeSC.Enabled = false; this.btnExeSC.Text = "正在执行..."; } } public void Complete(Model model) { if (model.Complete) { Control.CheckForIllegalCrossThreadCalls = false; this.cbxSC.Enabled = true; this.btnExeSC.Enabled = true; this.btnExeSC.Text = "执行"; } } }
展示器-Presenter
public class Presenter { IView iView; public Presenter(IView view) { this.iView = view; } public void Initialize() { iView.AddItems(new List<string> { "first", "second", "third" }); } public void Colculate() { Model vm = new Model(); iView.Begin(vm); for (int i = 1; i < 11; i++) { vm.AllCount = 100; string selectedItem = iView.GetSelecteditem(); //为了展示,从视图获取的数据,这里将DoingIndex修改为 switch (selectedItem) { case "first": vm.DoingIndex = i + 0; break; case "second": vm.DoingIndex = i + 1; break; case "third": vm.DoingIndex = i + 2; break; } Thread.Sleep(1000);//具体工作,此处以挂起进程代替 iView.UpdateUI(vm); } vm.Complete = true; iView.Complete(vm); } }
说明:
1)可以看到,Presenter中不包括UI展示细节,仅仅包含简单的UI处理逻辑,即:开始计算,计算过程中,计算任务完成以后调用了不同的方法来展示UI。
2)视图接口不包含任何属性,只有对UI进行控制的方法。展示器向接口传递Model数据,并且通过接口GetSelecteditem方法获得更新后的视图模型数据。
3 PM模式
在给出正式的PM模式之前,给出一个不标准的PM例子。
PM模式中强调UI控件绑定到模型属性上,但下面的例子,有点违背这一定义。
视图类:
public partial class Form_PM : Form { Presenter pt; public Form_PM() { InitializeComponent(); } private void Form_PM_Load(object sender, EventArgs e) { pt = new Presenter(); pt.UpdateUI += UpdateUI; this.cbxSC.Items.AddRange(pt.GetAllItem().ToArray()); this.cbxSC.SelectedIndex = 0; } private void btnExePM_Click(object sender, EventArgs e) { cbxSC.Enabled = false; btnExePM.Enabled = false; btnExePM.Text = "正在执行..."; Task.Factory.StartNew(() => { pt.Colculate(); }); } private void UpdateUI() { this.Invoke(new Action(() => { if (pt.vm.Complete) { cbxSC.Enabled = true; btnExePM.Enabled = true; btnExePM.Text = "执行"; this.tbxPM.Text = "全部完成"; } else { this.tbxPM.Text = string.Format("{3}组,共{0}个-执行完{1}-正在计算第{2}个", pt.vm.AllCount, pt.vm.CompleteIndex, pt.vm.CompleteIndex + 1, this.cbxSC.SelectedItem.ToString()); } this.rtbxPM.Text += string.Format("计算完第{0}" + Environment.NewLine, pt.vm.CompleteIndex); })); } private void cbxSC_SelectedIndexChanged(object sender, EventArgs e) { pt.Group = this.cbxSC.SelectedItem.ToString(); } }
展示器:
public class Presenter { public Model vm {set;get;} public string Group { set; get; } public Action UpdateUI; public Presenter() { vm = new Model(); } public void Colculate() { vm.Complete = false; vm.AllCount = 10; for (int i = 1; i < 11; i++) { //为了展示,从视图获取的数据,这里将DoingIndex修改为 switch (Group) { case "first": vm.CompleteIndex = i+ 0; break; case "second": vm.CompleteIndex = i+ 1; break; case "third": vm.CompleteIndex = i+ 2; break; } Thread.Sleep(1000);//具体工作,此处以挂起进程代替 if (i == vm.AllCount) { vm.Complete = true; } UpdateUI(); } } public List<string> GetAllItem() { return new List<string> { "first","second","thrid" }; } }
模型:
public class Model { public int AllCount { set; get; } public int CompleteIndex { set; get; } public bool Complete { set; get; } public List<string> AllItems { set; get; } }
说明:
1)展示器持有Model对象的引用并且Model对象作为展示器的公共属性暴露给视图,视图持有展示器的引用。
视图通过调用展示器的属性vm(Model类型) 和GetAllItem方法获得数据。
值得注意的是,展示器另一个公有字段UpdateUI的类型为Action,这里使用委托的目的是,当执行public void Colculate()方法时,每更新一次模型,展示器都能控制视图使用更新后的模型数据刷新视图UI
2)模型不含有方法,只有属性
3)视图层包含了一部分UI呈现逻辑,展示器没有将其完全包含,这样做的好处和MVP-SC模式是一样的。
此外,视图会更新展示器的公共属性Group。Group实际对应着视图层的ComboBox控件。这里似乎有两个模型,一个是视图展示数据用的模型,一个是展示器更新业务层数据用的模型。两者可以合二为一。
下面我们将UI逻辑完全挪到展示器中去,要实现这一目标,视图、模型、展示器都有调整。
视图
public partial class Form_PM : Form { Presenters pt; public Form_PM() { InitializeComponent(); } private void Form_PM_Load(object sender, EventArgs e) { pt = new Presenters(); pt.UpdateUI += UpdateUI; pt.Begin += Begin; pt.Complete += Complete; this.cbxSC.Items.AddRange(pt.GetAllItem().ToArray()); this.cbxSC.SelectedIndex = 0; } private void btnExePM_Click(object sender, EventArgs e) { Task.Factory.StartNew(() => { pt.Colculate(); }); } private void Begin() { this.Invoke(new Action(() => { this.cbxSC.Enabled = false; this.btnExePM.Enabled = false; this.btnExePM.Text = "正在执行..."; })); } private void UpdateUI() { this.Invoke(new Action(() => { this.tbxPM.Text = pt.vm.Tip; this.rtbxPM.Text += pt.vm.Detil;//string.Format("计算完第{0}" + Environment.NewLine, pt.vm.CompleteIndex); })); } private void Complete() { this.Invoke(new Action(() => { this.cbxSC.Enabled = true; this.btnExePM.Enabled = true; this.btnExePM.Text = "执行"; this.tbxPM.Text = "全部完成"; })); } private void cbxSC_SelectedIndexChanged(object sender, EventArgs e) { pt.vm.SelectedItem = this.cbxSC.SelectedItem.ToString(); } }
模型:
public class Models { public string Tip { set; get; } public string Detil { set; get; } public string SelectedItem { set; get; } public List<string> AllItems { set; get; } }
展示器:
public class Presenters { public Models vm {set;get;} public Action UpdateUI; public Action Begin; public Action Complete; public Presenters() { vm = new Models(); } public void Colculate() { Begin(); for (int i = 1; i < 11; i++) { //为了展示,从视图获取的数据,这里将DoingIndex修改为 int vs = 0; switch (vm.SelectedItem) { case "first": vs = i + 0; break; case "second": vs = i + 1; break; case "third": vs = i + 2; break; } vm.Tip = string.Format("{0}组,共{1}个-执行完{2}-正在计算第{3}个", vm.SelectedItem, 10, i, i+1); vm.Detil = string.Format("计算完第{0}" + Environment.NewLine, vs); Thread.Sleep(1000);//具体工作,此处以挂起进程代替 UpdateUI(); } Complete(); } public List<string> GetAllItem() { return new List<string> { "first","second","thrid" }; } }
主要的变化有:
1)关于模型。模型中的属性绝大部分都可简单地绑定到视图层控件上。
2)关于展示器。展示器全部的UI显示逻辑都被挪到了展示器中,为完成这种设计,添加了三个类型都为Action的字段,分别代表了任务开始,执行过程中,任务完成。
3)关于视图。视图中的UI逻辑都被挪到了展示器中,只留下UI控件和模型的绑定实现
4)关于视图和展示器的关联。使用多播委托来控制UI的刷新。
----------------------------------------------------------------------------------------
转载与引用请注明出处。
时间仓促,水平有限,如有不当之处,欢迎指正。