WPF事件学习
一、WPF中的事件
~~~~ 事件在WPF中升级成了路由事件(route event),这种机制在很大程度上减少了对程序员的束缚,使得程序设计与实现变得更加灵活,模块之间的耦合度(coupling degree)进一步降低(耦合度简单说就是不同模块间的关联度,因为不同模块间不需要指名道姓的列出绑定/关联,这种语句不需要了button.click += button_click)。

二、WPF中的树形结构
~~~~ 路由事件有路由两字。
~~~~ 路由,可以通俗地理解为路径由来(或者在链路中传递经过的路径),路由器就是这样,记录在网络中传递的经由点信息,供消息包在节点间传递使用。那既然WPF中有路由事件,那必然有路由的路径,那路由事件是在怎样的链路中传递呢?
~~~~ 如果你观察过WPF界面对应的XAML代码,你就会发现它的UI布局是具有树形结构的(将这种标签式的代码折叠后,可以很清楚地看到它的结构)。现在把布局和控件组成的UI看成一棵树,当树上的某个节点(即对应一个控件或者布局)触发某个事件,程序员就可以选择以一定的方式响应该事件,使该事件被恰当地处理。你可以把事件看成一只蚂蚁🐜,让它往树上或者树根爬,而每经过一个树枝的分叉点就会把消息带给这个分叉点。


~~~~ WPF的路由链路是这棵UI组件树,我们稍微地来研究下这棵树。
~~~~ WPF中有两种“树”:一种叫逻辑树(Logical Tree),一种叫可视元素树(Visual Tree)。两种树区别是啥?其实很简单,逻辑树完全是由布局组件(Layout) 和控件(Controls)构成,它的每个节点不是布局组件就是控件。而可视元素树是什么呢?如果把一片树叶放大观察🕵,你会发现这片叶子的脉络也像一棵树一样,由叶柄向上分出许多分叉。在WPF中的逻辑树中,充当叶子的一般都是控件,如果我们把这些控件放大观察,会发现它们本身也是由更细微的组件(这些组件不是控件,而是一些可视化的组件,派生自Visual类)组成的树。
三、事件的来龙去脉
~~~~ 事件的前身是消息(Message)。Windows是由消息驱动的操作系统,运行在上面的程序都遵照这个机制。消息的本质就是一条数据,它记录着消息的类别,必要时还会带一些参数(类似通信中的协议头)。比如,你在窗体上按下鼠标左键,这时就产生了一条名为WM_LBUTTONDOWN的消息,并加入到了Windows待处理的消息队列中(大多数情况下,Windows的消息队列不会有太多消息在排队,消息会立刻被处理,如果你的计算机很慢很忙,那可能消息会被延时处理,这就是操作系统常见的延迟反应)。当Windows处理到这条消息时会把消息发送到你单击的窗体,窗体会用自己的一套算法来响应这个消息,这个算法就是Windows API开发中常说的消息处理函数。消息处理函数是一个多级嵌套的switch结构,进入这个switch结构的消息会被分流最终到达某个末端分支,在这个分支中会有一个程序员编写的函数被调用。例如对于鼠标左键按下这个消息,程序员可能会编写一个函数来查看它所携带的参数(即鼠标单击处的X、Y坐标,如果你有用WinAPI开发过桌面程序,应该了解过这点),然后决定是把它们显示出来还是在这个点上绘制图形等。也有一些消息是不带参数的,比如按钮被单击的消息,程序员通常不关心鼠标点在按钮的什么位置。
~~~~ 上面叙述的过程就是消息触发算法逻辑的过程,又称为消息驱动。这样一个过程对想入门Windows开发的人来说门槛太高(我只是想点击按钮,执行一个函数,却要在响应对象中判断消息类型,再一层层决定分发给谁,对于一个初学者来讲直接就晕了),而且对于大型的Windows程序来说开发和维护成本也不低。因此随着微软面向对象开发平台的日趋成熟,微软把消息机制封装成了更容易让人理解的事件模型。
~~~~ 事件模型隐藏了消息机制的许多细节(本质上还是消息机制这点不会变),让程序开发变得更简单。繁琐的消息驱动机制在事件模型中被简化为了3个关键点:
- 事件的拥有者:即消息的发送者。事件的宿主可以在某些条件下触发它所拥有的事件,即事件被触发。事件被触发后消息就会被发送。
- 事件的响应者:即消息的接收者、处理者。事件接收者使用事件处理器(Event Handler)对事件做出响应。
- 事件的订阅关系(或者说绑定、关联):事件的拥有者可以随时触发事件,但事件发生后能否得到响应要看有没有事件的响应者,或者说要看这个事件是否被关注。如果对象A关注了对象B的某个事件是否发生,则称A订阅了B的事件。更进一步讲,事件实际上上一个用event关键字修饰的委托(Delegate)类型的成员变量,事件处理器则是一个函数,说A订阅了B的事件,本质上就是让B.Event和A.Eventhandler关联起来。所谓事件触发就是B.Event被调用,这时与其关联的A.EventHandler就会被调用。

~~~~ 该模型中,事件的响应者通过订阅关系直接关联在事件拥有者的事件上,为了与WPF路由事件模型区分开,该模型称为直接事件模型或者CLR事件模型。
~~~~ 现在看一个例子。新建一个Windows Form项目,在窗体上放置一个按钮命名为myButton。双击按钮,VS会自动为我们创建myButton的Click事件处理器(myButton_Click方法)并跳转到其中。这时,一个完整的直接事件模型就实现了,让我们识别一下事件模型的几个关键部分: - 事件的拥有者:myButton
- 事件:myButton.Click
- 事件的响应者:窗体本身
- 事件处理器:this.myButton_Click
- 订阅关系绑定:可以在Form1.Designer.cs文件中找到的一句代码是
this.myButton.Click += new System.EventHandler(this.myButton_Click);// 此句就是确立订阅关系的代码
~~~~ 我们实现myButton_Click方法的代码如下:
private void myButton_Click(object sender, EventArgs e)
{
if(sender is Button)
{
MessageBox.Show(sender as Button).Name;
}
}
~~~~ 运行结果是:

~~~~ 这说明在直接事件模型中(CLR事件模型),事件的拥有者就是消息的发送者。
~~~~ 这个例子是直接事件模型最简单的应用,实际上,只要支持事件的委托和影响事件的方法在签名上保持一致(即参数列表和返回值一致),则一个事件就可以由多个事件处理器来响应(该事件发生后,有多个事件处理函数,即多播事件)。当然一个事件处理器也可以用来响应多个事件。
~~~~ 直接事件模型是传统.Net开发中对象相互协同、沟通信息的主要手段,与消息通信相比它很大程度上简化了程序的开发,然而它并不完美。它的不完美之处在于事件的响应者与事件的拥有者之间必须建立事件订阅这个“专线联系”。这样至少有两个弊端:
- 每对消息都是“发送-响应”关系,必须建立显式的点对点订阅关系。
- 事件的宿主必须能够直接访问事件的响应者,不然无法建立订阅关系。
Notes:这两种弊端会在以下情况显露出来:
-
程序运行时,你需要在容器内动态生成一组相同控件,每个控件的同一个事件都使用同一个事件处理器来响应,面对这种情况,我们在动态生成控件的同时就需要显式书写订阅代码。

-
用户控件的内部事件不能被外界订阅,必须为用户控件定义新的事件用以向外界暴露内部事件。当模块划分很细的时候,UI组件层级会很多,如果想让外层的容器订阅内层控件的某个事件,则需要为每一层组件定义用以暴露事件的事件。

~~~~ 路由事件的出现很好地解决了上述两种情况中出现的问题。
四、路由事件
~~~~ 为了降低事件订阅带来的耦合度和代码量,WPF推出了路由事件机制。路由事件与直接事件的区别在于,直接事件触发时,发送者直接将消息通过事件订阅交给事件响应者,事件响应者使用其事件处理器方法对事件的发生做出响应、使得程序逻辑按客户需求运行;而路由事件的事件拥有者和事件响应者之间则没有显式的订阅关系,事件的拥有者只负责触发事件,事件由谁来响应它并不知道,事件的响应者则安装有事件侦听器,针对某类事件进行侦听,当有此类事件在链路中传递至此,事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。举个例子,在可视化树上有一个按钮控件,当它被单击后就相当于它喊了一声“我被单击了”,这样一个Button.Click事件就开始在可视化树传播,当事件经过某个节点时,如果这个节点没有安装用于侦听Button.Click的侦听器,那么它就会无视这个事件,让它继续传播;如果某个节点安装了针对Button.Click的侦听器,那么它的事件处理器就会被调用(侦听者并不关心具体上哪个Button的Click事件被传来,即任何一个传来的Button.Click事件都会被侦听到),在事件处理器程序内程序员可以查看路由事件原始的出发点是哪个控件、上一站是哪里(是不是和路由器间的节点传递很像),还可以决定事件传递到此为止还是继续传递----路由事件就是靠这样“口耳相传”的办法将消息传给“关心”它的控件。
~~~~ 顺便说一句,尽管WPF推出了路由事件机制,但它仍然支持传统的直接事件模型。接下来聊聊路由事件的使用。


3739

被折叠的 条评论
为什么被折叠?



