表现层设计模式

一、理论

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 条评论
登录 后参与评论

相关文章

来自专栏王肖的UT

GLSL-变量和类型

1274
来自专栏挖掘大数据

处理海量数据的10种常见方法

本文将介绍10种处理海量数据问题的常见方法,也可以说是对海量数据的处理方法进行一个简单的总结,希望对你有帮助。

20610
来自专栏xcywt

编译到底做了什么(***.c -> ***.o的过程)

 (第一次写博客,好激动的说.......) 我们知道,一个程序由源代码到可执行文件往往由这几步构成: 预处理(Prepressing)-> 编译(Compil...

1895
来自专栏决胜机器学习

有趣的算法(三)——Hash算法

有趣的算法(三)——Hash算法 (原创内容,转载请注明来源,谢谢) 一、Hash算法 近期看到用hash实现基于hash的简单的小型数据库(传统大型数据...

3347
来自专栏程序猿DD

Java中的即时编译(Just-in-time compilation)

作者:知秋 原文:http://t.cn/RYLPEMc 像其他一些编程语言一样,Java通常也被称为“编译语言”。但有时你可能会感到困惑,尤其是当有人告诉你J...

1965
来自专栏点滴积累

geotrellis使用(三十九)COG 写入更新

前言 前面介绍过了如何在 ETL 的时候更新 Layer,使得能够在大数据量的时候完成 ETL 操作,同时前两篇文章也介绍了 COG 以及如何在 Geotrel...

37312
来自专栏前端侠2.0

oracle 两表关联时,年月条件的写法引起的巨大性能的差异

需求是要比较最近两个月的值,进行数据检验!所以我用自关联,来将两个月的数据放到一行上,然后进行比较!

782
来自专栏草根专栏

使用 C# (.NET Core) 实现模板方法模式 (Template Method Pattern)

本文的概念内容来自深入浅出设计模式一书. 项目需求 有一家咖啡店, 供应咖啡和茶, 它们的工序如下: ? 咖啡: ? 茶: ? 可以看到咖啡和茶的制作工序是差不...

3334
来自专栏DOTNET

asp.net web api 异常捕获

1 向客户端发送错误消息 使用throw new HttpResponseException()向客户端抛出错误信息。 HttpResponseExceptio...

44112
来自专栏noteless

[零]java8 函数式编程入门官方文档中文版 java.util.stream 中文版 流处理的相关概念

https://docs.oracle.com/javase/8/docs/api/

631

扫码关注云+社区