就像属性系统在WPF中得到升级、进化为依赖属性一样,事件系统在WPF中也被升级,从而进化成为——路由事件(Routed Event),并在其基础上衍生出命令传递机制。就让我们一起来领略这些新消息机制的风采吧!
1、近观WPF的树形结构。
路由(Route)一词的大意为:起点和终点之间有若干个中转站,从起点出发后经过每个中转站时都要进行选择,最终以正确(比如最短或者最快)的路径到达终点。我们知道,WPF的UI是由布局组件和控件构成的属树形结构。因此,当这棵树上的某个节点激发出某个事件的时候,程序员可以选择以传统的直接事件模型让响应者响应之,也可以让这个事件在UI组件树沿着一定的方向传递且路过多个中转点,并在这个路由中被恰当的处理。因为WPF事件的路由环境是UI组件树,因此我们有必要先研究一下这棵树。4
在WPF中有两种树,一个为逻辑树(Logical Tree),另一个叫可视化元素树(Visual Tree)。逻辑树最大的特点就是它完全由布局组件和控件构成(包括列表控件中的条目元素),简单点说,就是它的每个节点不是布局组件就是控件。相对的可视化元素树其实是对Logical Tree的一种细分。在Logical Tree中,节点是一般都是控件,Visual Tree就是有组成控件的更加细小的组件(他们不是控件,而是一些可视化组件,派生自Visual类)来构成的。
2、事件的来龙去脉
事件的前身是消息(Message)Windows就是消息驱动的操作系统。消息驱动对于一个刚刚入门的Windows开发人员来说门槛太高,随着微软面向对象平台的成熟,微软把之前的消息机制封装成更容易让人们理解的事件模型。整个消息机制在事件模型中被简化成三个特点:
- 事件的拥有者:即消息的发送者。事件的宿主可以在某些条件下激发它所拥有的事件,即事件被触发
- 事件的响应者:即消息的接收者、处理者。事件接收者使用其事件的处理器(Event Handel)对事件作出响应
- 事件的订阅关系:事件的拥有者可以随时的触发事件,但是事件发生之后会不会得到响应要看有没有事件的响应者,或者说要看这件事件有没有被关注
直接事件模型是传统.Net开发中对象之间相互协调、沟通信息的主要手段,它在很大程度上简化了程序的开发。然后直接事件模型并不完美,它的不完美之处就在于事件的响应者与事件的拥有者之间必须建立事件订阅这样一个关系。
直接事件模型的弱点在以下两种情况下会暴露出来:
(1)程序运行期在容器中动态生成一组相同的控件,每个控件的同一个事件都使用同一个事件处理器来响应。面对这种情况,我们在动态生成代码的同时就需要显示书写事件订阅代码。
(2)用户控件内部事件不能被外接所订阅,必须对用户控件定义新的事件向外界暴露内部事件。当模块划分很细的时候,UI组件的层级会很多,如果想让最外层的容器订阅深层控件的某个事件就需要为每一层组件定义用于暴露内部事件的事件,形成事件链。
路由事件的出现,很好的解决了上面提到的两个问题。
3、路由事件
为了降低由事件订阅带来的耦合度和代码量,WPF退出了路由事件机制。路由事件与直接事件的区别在于:直接事件被触发时,发送者直接将消息通过事件订阅交给事件响应者,事件响应者在作出相应的事件;路由事件则没有显示的订阅关系,事件的发送者只负责触发事件,至于是谁响应事件它并不关心,但是事件的响应者提前安装事件的监听器,针对某类事件进行监听,当有此类事件传过来的时候,事件响应者怎会进行响应事件并决定事件是否再被传递。
3.1 使用WPF内置的路由事件
此处我们以Button的Click事件来说明路由事件的使用。
首先进行如下布局:(XAML代码如下)
<Window x:Class="_01_使用WPF内置路由事件.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:_01_使用WPF内置路由事件" mc:Ignorable="d" Title="Routed" Height="200" Width="200"> <Grid> <Grid Name="gridRoot" Background="Lime"> <Grid Name="gridA" Margin="10" Background="Blue"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Canvas Name="canvasLeft" Grid.Column="0" Background="Red" Margin="10"> <Button Name="buttonLeft" Content="Left" Width="40" Height="100" Margin="10"></Button> </Canvas> <Canvas Name="canvasRight" Grid.Column="1" Background="Yellow" Margin="10"> <Button Name="buttonRight" Content="Right" Width="40" Height="100" Margin="10"></Button> </Canvas> </Grid> </Grid> </Grid> </Window>
其运行效果及Logical Tree结构如下:
当单击buttonLeft时,Button.Click事件就会沿着buttonLeft---CanvasLeft-----gridA-------gridRoot-----Window线路传递。因为目前还没有哪个节点侦听Click事件,所以单击按钮之后尽管事件向上传递却并没有接到响应。下面,我们让gridRoot安装针对Button.Click的事件侦听器。
方法很简单,就是在窗体的构造器中调用gridRoot的AddHandler方法把想侦听的事件和事件处理器关联起来:
public MainWindow() { InitializeComponent(); //为gridRoot安装针对Button.Click事件的监听器 this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked)); }
上面的代码让最外层的Grid(gridRoot)能够捕捉到从“内部”飘出来的按钮单击事件,捕捉到会用this.ButtonClicked方法来进行响应处理。ButtonClicked代码如下:
private void ButtonClicked(object sender,RoutedEventArgs e) { MessageBox.Show((e.OriginalSource as FrameworkElement).Name); }
这里有一点非常重要:因为路由事件(的消息)是从内部一层层传递出来最后到达最外层的gridRoot,并且由gridRoot元素把消息事件交给Button_Click方法来处理,所以传入Button_Click方法的参数obj实际上是gridRoot而不是被单击的Button,这与直接的传统事件有些不一样。如果想查看事件的源头(最初发起者)怎么办呢?答案是使用e.OriginalSource,使用它的时候需要是用as/is操作符或着强制类型把它识别/转换为正确的类型。
运行程序单击右边的按钮,效果如下:
上述为元素添加路由时间在XAML代码里面也可以完成,只需要把XAML代码改成这样即可:
<Grid x:Name="gridRoot" Background="Lime" Button.Click="Button_Click">
To Be Continue