表现层设计模式

一、理论

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的刷新。

----------------------------------------------------------------------------------------

时间仓促,水平有限,如有不当之处,欢迎指正。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小鄧子的技术博客专栏

All RxJava - 为Retrofit添加重试

在我们的日常开发中离不开I/O操作,尤其是网络请求,但并不是所有的请求都是可信赖的,因此我们必须为APP添加请求重试功能。

701
来自专栏草根专栏

从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD

Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-...

3736
来自专栏Hellovass 的博客

优雅地烘焙 Retrofit

将构造 Retrofit 时所需要的材料隔离开来,利用依赖倒置这个原则,优雅地烘烤出美味的 Retrofit 实例。

713
来自专栏陈本布衣

Spring基础篇——Spring容器和应用上下文理解

上文说到,有了Spring之后,通过依赖注入的方式,我们的业务代码不用自己管理关联对象的生命周期。业务代码只需要按照业务本身的流程,走啊走啊,走到哪里,需要另...

3717
来自专栏圣杰的专栏

Windbg分析高内存占用问题

最近产品发布大版本补丁更新,一商超客户升级后,反馈系统经常奔溃,导致超市的收银系统无法正常收银,现场排队付款的顾客更是抱怨声声。为了缓解现场的情况, 客户都是手...

602
来自专栏水击三千

Android Geocoder(位置解析)

Android中提供GPS定位服务,同时开发者可以对获得的位置信息进行解析,可以获得位置的详细信息。 1.gps定位 在Eclipse中建立android应用程...

24210
来自专栏肖蕾的博客

使用Retrofit打印请求日志,过滤改变服务器返回结果,直接获取String字符串直接获取字符串手动解析查看Retrofit请求网络日志自定义Interceptor实现过滤改变请求返回的数据(可使用

1092
来自专栏张善友的专栏

发布一个日期选择控件(ASPNET2.0)

The Coolest DHTML Calendar,这是一个在GPL下发布的JS日历程序,具有极高的可配置性,包括外观样式、显示格式、显示内容等等。默认程序...

1909
来自专栏Java架构师学习

Zookeeper-watcher机制源码分析(二)

其大致流程如下   ① 通过传入的path(节点路径)从watchTable获取相应的watcher集合,进入②

691
来自专栏Android中高级开发

Android开发之漫漫长途 XI——从I到X的小结

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索...

792

扫描关注云+社区