一、理论
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的刷新。
----------------------------------------------------------------------------------------
时间仓促,水平有限,如有不当之处,欢迎指正。