我前面一篇博客讲了自定义窗体设计器,其实功能太简单,主要想阐述的是底层原理(虽然我不保证VS IDE设计器确实是那样去实现的)。编程讲究的是刨根问底,刨到祖坟最好,这篇或者可能以后几篇博客我想说一下VS IDE中的窗体设计器,虽说不能面面俱到,但也能让大家知道个大概。初学者可能阅读起来有些困难。

其实回头一看,我之前的好几篇博客倒是跟窗体设计器有些关系,当时写的时候也没有想到说为了照顾以后要说的内容,算是凑巧,这其中包括系列(九)、系列(八)、系列(七)。我总结了一下,了解窗体设计器主要搞懂三个部分:

  • “容器-组件-服务”模型;
  • 设计时(Design_Time)和运行时(Run_Time);
  • 代码自动生成。

这篇主要讲第一个,也就是本文标题说的“容器-组件-服务”模型。下面才是正文:

“容器-组件-服务”模型平时很少用到,不过窗体设计器中就用到过,不知道诸位看官对“容器”、“组件”、“服务”的了解程度如何,今儿我都讲一遍:

容器:

我们平时经常碰到过所谓的容器,比如Panel控件,它可以存放Button控件,那么它就可以说是一个容器,再比如ArrayList可以存放int数据,那么它也可以说是一个容器(暂且称为物理容器吧),但是,今天讲到的“容器”跟这些都不一样,前两个可以说是物理容器,有很明显的“把一个东西放进另外一个东西内部”的意思,属于“物理包含”,而今儿咱们讨论的容器属于“逻辑包含”,也就是说不需要将元素放到它内部,元素与容器之间只需要存在某一个关系就行。见图1,

两个容器传数据 容器之间如何通讯_System

图1

如图,一个元素可以包含在一个物理容器中,但同时也可以包含在一个逻辑容器中(通过某一种关系关联),比如元素1,当然,也可以有元素不存在任何容器中,比如元素3。

在.net中将直接或间接实现了System.ComponentModel.IContainer接口的类称之为“容器”,它与它元素之间的关系为“逻辑包含”。另外,它的元素有严格的规定,必须是“组件”(.net中将直接或者间接实现了System.ComponentModel.IComponent接口的类称之为组件,详细参考后面讲到的组件)。IContainer接口的源码为:

两个容器传数据 容器之间如何通讯_自定义_02

两个容器传数据 容器之间如何通讯_自定义_03

View Code

1 public interface IContainer : IDisposable
 2 {
 3     // Methods
 4     void Add(IComponent component);
 5     void Add(IComponent component, string name);
 6     void Remove(IComponent component);
 7 
 8     // Properties
 9     ComponentCollection Components { get; }
10 }

可以看出,只要是容器,必须有“添加”和“删除”组件的功能,另外,只要是容器,都必须遵守“Dispose”模式(有关Dispose模式,在这里我就不再讲了,它是一种安全管理系统资源的方式)。初看起来,虽然这里讲到的容器跟其他所谓的容器好像没什么两样,重要的不同接下来会说到。

组件:

其实,系列(七)中我已经说到了组件的概念,组件其实就是一个类,一个特殊的类,再总结一遍:

  • 将直接或者间接实现了System.ComponentModel.IComponent接口的类称之为“组件”;
  • 组件是一个类,实现了IComponent接口的特殊的类;
  • 组件一定是类,但类不一定是组件;
  • 在使用类似VS IDE这样的开发工具时,如果一个类型是“组件”,那么它就有可能出现在ToolBox中,也就是说,组件支持编程可视化。(至于为什么,下一篇博客能讲到);
  • 组件需要遵循“Dispose”模式,因为它实现了IComponent接口,而IComponent接口又实现了IDisposable接口。

以上是之前得出来的结论,今天,我们再来看一下System.ComponentModel.IComponent接口的源码,稍后又可以总结出几个结论:

两个容器传数据 容器之间如何通讯_自定义_02

两个容器传数据 容器之间如何通讯_自定义_03

View Code

1 public interface IComponent : IDisposable
2 {
3     // Events
4     event EventHandler Disposed;
5 
6     // Properties
7     ISite Site { get; set; }
8 }

如你所见,IComponent接口确实实现了IDisposable接口,所以正如系列(七)中总结的一样:组件需遵循“Dispose”模式,除此之外,就一个Disposed事件,这个很容易就知道,在组件dispose时候触发的事件,还有一个ISite接口成员,这个对我们来说比较陌生,看一下ISite接口的源码(System.ComponentModel.ISite):

两个容器传数据 容器之间如何通讯_自定义_02

两个容器传数据 容器之间如何通讯_自定义_03

View Code

1 public interface ISite : IServiceProvider
2 {
3     // Properties
4     IComponent Component { get; }
5     IContainer Container { get; }
6     bool DesignMode { get; }
7     string Name { get; set; }
8 }

我们先不考虑IServiceProvider接口和DesignMode属性(以后再说),通过Component属性和Container属性,我们就应该很容易的知道ISite的作用其实就是关联一个组件和一个容器,将一个组件和一个容器关联起来。

因为IComponent中就有这么一个ISite,再根据前面讲“容器”的时候说到,容器中只能存放组件,而且是通过某一种关系关联起来的,那么,我们就可以断言,他们之间的关联就是通过ISite维持的,也就是说,容器可以通过ISite与它内部的组件通信,反过来,组件也可以使用ISite与它所在的容器通信,因此,得出结论如下:

  •  容器中存放组件,容器和组件之间可以进行通讯,以ISite为桥梁;
  •  将1扩展一下,既然容器和组件通过ISite为桥梁能通信,那么同一个容器中的组件之间肯定也是可以通讯的(容器作为中转)。如图2:

两个容器传数据 容器之间如何通讯_两个容器传数据_08

图2

  •  既然IContainer实现了IDisposable接口,IComponent接口也实现了IDisposable接口,再加上容器中是存放组件的,因此,我们可以认为,容器还有更重要的一个功能,那就是统一负责它其中的组件的资源释放。当容器Dispose的时候,里面所有的组件都跟着Dispose。

ISite专业术语称之为“站点”,当一个组件(逻辑意义上)放到一个容器中时,组件的ISite就会被赋值(站点化),之后,组件就和容器关联上了。再补充一句,.net中已经为我们默认实现了System.ComponentModel.IComponent接口和System.ComponentModel.IContainer接口,分别为System.ComponentModel.Component和System.ComponentModel.Container,我需要说明的是,它们都只是默认实现,实现了最基础最简单的部分,比如Dispose方法,IContainer.Add()、IContainer.Remove以及接下来要讲到的GetService方法(这个在后面讲“服务”的时候会说到),如果你需要功能更加详细的更加强大的容器或者组件,请从Container类或者Component派生出的新的容器或者组件。我再送上一张图,贴近实际地解释一下“容器”和“组件”的关系,如图3:

两个容器传数据 容器之间如何通讯_控件_09

图3

如图所示,Form是一个物理容器,存放着Label和TextBox控件,由于上图虚线框中,所有元素都属于“组件”(IComponent->Component->Control,具体原因请参考系列(七)),所以图中容器可以包含虚线框中的所有控件。稍微提前透漏一下,我们在winform中的Form1.Designer.cs文件中,经常看见一行:

两个容器传数据 容器之间如何通讯_自定义_02

两个容器传数据 容器之间如何通讯_自定义_03

View Code

1 private System.ComponentModel.IContainer components = null;

和InitializeComponent方法中有一行:

两个容器传数据 容器之间如何通讯_自定义_02

两个容器传数据 容器之间如何通讯_自定义_03

View Code

1 components = new System.ComponentModel.Container();

components就是一个默认容器,一般用于存放窗体中的Timer、ImageList、ErroProvider等类似组件,注意,窗体上其他控件不在容器范围内。components主要负责本窗体内所有组件的资源释放,因此,你可以在窗体的Dispose(bool disposing)方法中看见:

components.Dispose();  //释放本窗体中所有组件资源

我们再来思考一个问题,容器中存放组件,那么,组件内部可不可以存在容器呢?也就是说,容器包含组件,组件中是否可以再存在容器成员?比如一个组件类似如下:

两个容器传数据 容器之间如何通讯_自定义_02

两个容器传数据 容器之间如何通讯_自定义_03

View Code

1 class MyComponent:Component
 2 {
 3     private Container _container = null;
 4     public MyComponent()
 5     {
 6         _container = new Container();
 7         System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
 8         timer.Tick+=… //注册事件
 9         timer.Start();
10         _container.Add(timer); //向容器中添加Timer组件
11     }
12     //其他代码
13 }

在组件内部,存在一个Container数据成员(当然可能存在很多个),然后向其中添加其他组件(比如Timer),设想一下,如果把这个动作循环下去,也就是说,容器包含组件,组件内部又有一个容器,这个容器又包含一些组件,各个组件内部又包含容器…如此循环下去,有点绕啊,不过这个可以看成“树形图”,如图4:

两个容器传数据 容器之间如何通讯_两个容器传数据_16

图4

如上图,包含层数只有4层,理论上可包含无数层,实质上,我们在编写一个组件的时候,如果内部需要用到更多的其他组件,为了更好的管理各个组件的资源,我们一般都会将这些组件放到一个容器中,再将这个容器方法父组件中,那么,父组件的代码就应该是这样子了(强烈建议诸位看官熟悉“Dispose”模式)

两个容器传数据 容器之间如何通讯_自定义_02

两个容器传数据 容器之间如何通讯_自定义_03

View Code

1 class MyComponent:Component
 2 {
 3     private Container _container = null;
 4     public MyComponent()
 5     {
 6         _container = new Container();
 7         System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
 8         timer.Tick+=… //注册事件
 9         timer.Start();
10         _container.Add(timer); //向容器中添加Timer组件
11     }
12     //其他代码
13     // ---- 以下是新加内容
14     protected override void Dispose(bool disposing)
15 {
16      If(disposing) //释放托管资源
17      {
18           _container.Dispose(); //容器会调用容器中组件的Dispose,依次向下
19      }
20      base.Dispose(disposing); //重要
21 }
22 }

因此,容器和组件嵌套多少层都无所谓,只要你记得在组件dispose的时候,负责内部容器的dispose就ok。

到目前为止,我们已经总结出“容器”和“组件”相结合的两大好处:

  • 容器可以负责很多组件的资源释放;
  • 容器可以协助容器内部各个组件之间的相互通讯。

我们已经了解了第1条,至于怎么实现组件之间相互通讯,首先要知道“服务”的概念,接下来“服务”登场。

服务:

 “服务”,就是“帮助”的意思,某人为你提供服务,就是某人为你提供帮助,今天要说的这个“服务”也是这个意思,组件跟容器通讯,或者组件与同一容器中其他组件通讯靠的就是“服务”,“服务”由容器默认提供,各个组件可以获取容器的服务,每个服务负责一项(或者几项紧密相关)的任务。说得直白一点就是,打个比方,你去镇政府咨询一些农村政策,政府部门会派相关的代表为你提供解答,那么这些代表就为你提供服务,每个代表负责一项事情,比如你咨询农业税相关事情,专门有负责农业税的代表为你提供帮助,如果你咨询贫困户名额的事情,专门有负责贫困户审核的代表为你提供帮助,那么这里,镇政府就是“容器”,你就是“组件”,代表就是“服务”,很明显,镇政府只是逻辑上包含你。将来哪一天,天朝公平公正原则大型实施,允许你(组件)委派代表(服务)去镇政府(容器)工作,也就是说,组件有时候也是可以向容器提供“自定义服务”的,按照自己规定的方式提供服务,当然,你(组件)不可能委派一个代表(服务)去镇政府(容器)当镇长或者书记吧,也就是说,容器中有的服务是不可替代的,因为有些服务是整个模式中的核心,必须由容器开发者默认提供,如果随便交给使用者(Coder)去替换掉,容器可能就乱掉,不能正常工作,就像书记镇长这样的职位,想必只能由上一级委派吧。有点乱,来一张图来说明一下问题,见图5

两个容器传数据 容器之间如何通讯_自定义_19

图5

如图,“我”这个组件可能有一些背景,可以向镇政府(容器)添加自定义服务。图中,既然你我他三个组件都能与容器通讯,那么,你我他三个组件相互之间定可以相互通讯了,容器的“中转”加上“服务”,足以让各个组件之间进行互相沟通。

接下来,我说一下“服务”的种类,这个没有官方分类,只是我自己归纳的:

  • 主动型。组件从容器取得“服务”后,可以使用这个服务进行一些操作(对容器或者其他组件),体现为:
Service s = GetService(serviceType); //组件从容器获取指定服务
       s.DoSomething(); // 主动操作
       s.Event += new EventHandler(…); //监听一些事件
       s.Property = “12345”; //给…赋值
       string str = s.Property; //取值
  • 被动型。“被动型服务”一般属于“自定义服务”,由组件提供给容器,容器在需要的时候会使用该服务,体现为:

       MyService s = new MyService(); //新建自定义服务

       container.AddService(s); // 向容器添加自定义服务,供容器使用,有点类似注册事件

以上就是“容器-组件-服务”模式,想必说得有点乱,或者太抽象,几乎看不大懂,我做了一个demo,提供源码下载,模拟一个从外部接收数据,显示在主界面的程序,有三个接收模块,每个模块都是一个组件,同时主界面可以随时控制“开始”和“停止”,大概结构图如下,如图6

两个容器传数据 容器之间如何通讯_System_20

图6

如上图,显示窗体、接收模块1、2、3均为组件,显示窗体同时属于“控件”,将他们4个放进(逻辑地)一个容器(Container),由Container负责他们之间的通讯,如“接收模块—>数据—>显示窗体”、“显示窗体—>控制命令—>接收模块”。Demo虽小,完全的可以划分为5个部分,四个组件和一个容器。开发组件时,只要知道服务接口,每人(每小组)单独负责一个组件的开发,四个组件在开发过程中,完全没有任何耦合的地方。提供一张效果图,如图7:

两个容器传数据 容器之间如何通讯_两个容器传数据_21

图7

其中,可以自定义服务,来控制消息格式化。xp .net3.5运行通过,源码下载地址:

为了方便,3个接收数据的组件我放在了一个项目中,其实应该都单独分开。另外,编译后请不要用鼠标将3个组件拖进窗体中,因为这样的话,设计器就会生成将组件添加到默认容器中(System.ComponentModel.IContainer components),最好手动写代码。

题后话:

写到这儿,其实我已经意识到好多地方说漏了,没办法,我觉得这个东西说起来确实太多太复杂,比如组件在什么时候跟容器关联的?也就是说什么时候组件的ISite被赋值?其实这个东西只能查看.net源码,System.ComponentModel.Container源码中有答案,就是在组件被加到容器中的那一刻,组件就被站点化,即Container.Add(IComponent component)和Container.Add(IComponent component,string name)这两个方法中,另外,没有被站点化(ISite没被赋值)的组件,永远获取不了服务(因为根本没人给它提供),至于组件是怎么获取服务的,诸位看官请查看System.ComponentModel.Component源码。最后,一个组件可以不存在于任何容器内,比如Button控件,它一般只存在于父控件之中。

个人觉得写代码还是讲究一个“深入浅出”,不能钻那深,结果越陷越纠结,最后只知道内部原理,而且还是懵懵懂懂的,跳出来,却已经不知道整个大概的流程。“深入”,比如说.net初学者可能感觉System.Windows.Forms.Timer中的Tick事件处理程序中为啥不需要解决跨线程访问控件?因为给人感觉不就是Timer内部新开辟了一个线程吗?其实你追其源码,刨其祖坟,会发现它不是通过线程实现的,而是通过Windows中WM_TIMER消息去实现的,而window消息处理一般都是在ui线程中,根本就没有跨线程。“浅出”,你比方说,我们现在在研究窗体设计器原理,到时候研究得差不多的时候,你得总结一下窗体设计器中的每一个宏观上的具体操作,内部又是怎么去实现的,要一一对应起来,这样你才不会搞糊涂。

最后希望本文对您有所帮助,O(∩_∩)O~,文中代码有些是在word中写进去的,恐怕有些错误。如果感觉本文对您有帮助,请点一下右下角的“推荐”或者给点建议也是可以滴,O(∩_∩)O~。