6.1 从玉蜂说起,回顾.NET事件模型
下图暗含.NET的事件模型,小龙女是事件的发布者,她发布了事件“我在绝情谷底”。老顽童和黄蓉是事件的订阅者,不过老顽童并没有处理该事件,而黄蓉处理了事件,隐约能猜出其中的含义。至于杨过,他没有订阅事件。而玉蜂正是传递信息的事件,事件、事件的发布者和事件的订阅者构成了.NET事件模型的 3 个角色。
在.NET中,一个事件用关键字 event来表示。如下方代码所示。

public delegate void WhiteBee(string param);//声明了玉蜂的委托//小龙女类class XiaoLongnv{public event WhiteBee WhiteBeeEvent;//玉蜂事件public void OnFlyBee(){Console.WriteLine("小龙女在谷底日复一日地放着玉蜂,希望杨过有一天能看到……");WhiteBeeEvent(msg);}private string msg = "我在绝情谷底";}//老顽童类class LaoWantong{public void ProcessBeeLetter(string msg){Console.WriteLine("老顽童:小蜜蜂、小蜜蜂,别跑!");}}//黄蓉类class Huangrong{public void ProcessBeeLetter(string msg){Console.WriteLine("黄蓉:\"{0}\",莫非......", msg);}}//杨过类class YangGuo{public void ProcessBeeLetter(string msg){Console.WriteLine("杨过:\"{0}\",我一定会找到她!".msg);}public void Sign(){Console.WriteLine("杨过叹息:龙儿,你在哪……");}}//这个Main方法在一个类里面static void Main(string[] args){//第1步 人物介绍XiaoLongnv longnv = new XiaoLongnv();//小龙女LaoWantong wantong = new LaoWantong();//老顽童Huangrong rong = new Huangrong();//黄蓉YangGuo guo = new YangGuo();//杨过//第2步 订阅事件,唯独没有订阅杨过地ProcessBeeLetterlongnv.WhiteBeeEvent += wantong.ProcessBeeLetter;longnv.WhiteBeeEvent += rong.ProcessBeeLetter;//longnv.WhiteBeeEvent += guo.ProcessBeeLetter;//杨过没有订阅小龙女的玉蜂事件//第3步 小龙女玉蜂传信longnv.OnFlyBee();//第4步 杨过叹息guo.Sign();}
不过这种事件看起来不像.NET的事件,于是改写为以下代码:
//新增一个WhiteBeeEventArgs类public class WhiteBeeEventArgs : EventArgs{public readonly string _msg;public WhiteBeeEventArgs(string msg){this._msg = msg;}}public delegate void WhiteBeeEventHandler(object sender, WhiteBeeEventArgs e);//声明了玉蜂的委托//小龙女类class XiaoLongnv{private string msg = "我在绝情谷底";public event WhiteBeeEventHandler WhiteBeeEvent;//玉蜂事件public void OnFlyBee(){Console.WriteLine("小龙女在谷底日复一日地放着玉蜂,希望杨过有一天能看到……");WhiteBeeEventArgs args = new WhiteBeeEventArgs(msg);WhiteBeeEvent(this,args);}}//老顽童类class LaoWantong{public void ProcessBeeLetter(object sender, WhiteBeeEventArgs e){Console.WriteLine("老顽童:小蜜蜂、小蜜蜂,别跑!");}}//黄蓉类class Huangrong{public void ProcessBeeLetter(object sender, WhiteBeeEventArgs e){Console.WriteLine("黄蓉:\"{0}\",莫非......", e._msg);}}//杨过类class YangGuo{public void ProcessBeeLetter(object sender, WhiteBeeEventArgs e){//真的是龙儿吗?XiaoLongnv longnv = sender as XiaoLongnv;if(longnv != null)Console.WriteLine("杨过:\"{0}\",我一定会找到她!", e._msg);}public void Sign(){Console.WriteLine("杨过叹息:龙儿,你在哪……");}}//这个Main方法在一个类里面static void Main(string[] args){//第1步 人物介绍XiaoLongnv longnv = new XiaoLongnv();//小龙女LaoWantong wantong = new LaoWantong();//老顽童Huangrong rong = new Huangrong();//黄蓉YangGuo guo = new YangGuo();//杨过//第2步 订阅事件,唯独没有订阅杨过的ProcessBeeLetterlongnv.WhiteBeeEvent += wantong.ProcessBeeLetter;longnv.WhiteBeeEvent += rong.ProcessBeeLetter;//longnv.WhiteBeeEvent += guo.ProcessBeeLetter;//杨过没有订阅小龙女的玉蜂事件//第3步 小龙女玉蜂传信longnv.OnFlyBee();//第4步 杨过叹息guo.Sign();}
上述代码中的修改可以归纳为以下几点:
(1)委托类型的名称修改为以EventHandler结束,原型有一个void返回值并接受两个输入参数,即一个Object类型和一个EventArgs类型(或继承自EventArgs)。
| 修改前 | public delegate void WhiteBee(string param); |
|---|---|
| 修改后 | public delegate void WhiteBeeEventHandler(object sender,WhiteBeeEventArgs e); |
(2)事件的命名为委托去掉`EventHandler`之后剩余的部分。
| 修改前 | public event WhiteBee WhiteBeeEvent; |
|---|---|
| 修改后 | public event WhiteBeeEventHandler WhiteBee; |
(3)继承自EventArgs的类型应该以EventArgs结尾。
| 修改前 | 无事件参数 |
|---|---|
| 修改后 | public class WhiteBeeEventArgs : EventArgs { public readonly string _msg; public WhiteBeeEventArgs(string msg) { this._msg = msg; } } |
这样修改不仅仅符合.NET规范,并且带来了更大的灵活性。例如,如果杨过收到了小龙女的玉蜂传信,则可以通过`sender`参数来判断是否真的是小龙女,甚至还可以了解更多有关的细节。
对于一个新手来说,这个例子才是真正适合我看的,因为比较简单,而且生动形象。对于事件的发布者、事件的订阅者、事件处理器,三者的关系与程序执行的顺序有了点头绪。这里梳理一下:一、单独声明一个类来写事件参数(WhiteBeeEventArgs)类,这是一个类,继承自EventArgs。里面写要传递的参数。顺便把相应的委托也跟这个类写在一起,取名叫WhiteBeeEventHandler,它需要两个参数。二、然后是在老顽童类、黄蓉类、杨过类都声明相应的方法(ProcessBeeLetter),这个方法用来与委托对应。这个就是事件处理器吧。三、在小龙女这个类里面public event WhiteBeeEventHandler whiteBeeEvent;这个语句我把event删掉了运行结果是一样的。或许事件与委托界限真的很模糊。四、Main函数里面事件订阅部分,通过+=就可以实现“订阅”。一定是事件拥有者(小龙女)对象.事件 += 事件订阅者对象.对应的方法(事件处理器)五、程序执行顺序:先进行好订阅操作,在执行OnFlyBee方法的时候,运行到WhiteBeeEvent(this,args)这句的时候,会依次进入老顽童、黄蓉执行ProcessBeeLetter方法。
6.2 路由事件的定义

同依赖属性一样,路由事件也需要注册,不同的是使用 EventManager.RegisterRoutedEvent 方法。
同依赖属性,用户不会直接使用路由事件,而是使用传统的CLR事件。有两种方式关联事件及其处理函数,在代码中,仍然按照原来的方法关联和解除关联事件处理函数(+=和-=),如下代码所示:
Button b2 = new Button();//关联事件及其处理函数b2.Click += new RoutedEventHandler(Onb2Click);//事件处理函数void Onb2Click(object sender, RoutedEventArgs e){//logic to handle the Click event}

在XAML中,事件和处理函数这样关联:
<Button Click="OnbeClick">Button</Button>
传统的事件触发往往直接调用其委托(因为事件的本质是委托),而路由事件则是通过一个RaiseEvent方法触发,调用该方法后所有关联该事件的对象都会得到通知。在ButtonBase中即有代码:

路由事件通过 EventManager.RegisterRoutedEvent 方法注册;通过 AddHandler 和 RemoveHandler 来关联和解除关联的事件处理器;通过 RaiseEvent方法来触发事件;通过传统的 CLR事件封装后供用户调用,使得用户如同传统的 CLR 事件一样使用路由事件。
6.3 路由事件的作用



6.4 路由事件
6.4.1 识别路由事件
和依赖属性一样,如果一个事件是路由事件,则在 MSDN 文档中会有路由事件(Routed Event Information)一节描述其路由信息。而普通的 CLR事件(如UIElement.IsVisibleChanged)则没有这一节信息。

6.4.2 路由事件的旅行
1、路由事件的旅行策略
路由事件的旅行当中,一般只出现两种角色:一是事件源,由其触发事件,是路由事件的起点;二是事件监听者,通常针对监听的事件有一个相应的事件处理函数。当路由事件经过事件监听者,就好比经过一个客栈,要做短暂的停留,有事件处理函数来处理该事件。
路由事件的策略有如下三种:
(1)Bubbling:事件从事件源出发一路上溯直到根节点,很多路由事件使用该策略。
(2)Direct:事件从事件源出发,围绕事件源转一圈结束。
(3)Tunneling:事件源触发事件后,事件从根节点出发下沉直到事件源。
上述三种策略都只能看作是路由事件的一个旅行计划,实际上当路由事件开始旅行的时候,由于事件监听者的干预,它的旅行计划会有所改变。
2、改变旅行策略因素之一——事件处理函数
一个最基本的路由事件处理函数的原型如下:
public delegate void RoutedEventHandler(Object sender, RoutedEventArgs e)
事件处理函数之间有微小差异,如鼠标事件的处理函数原型如下:
public delegate void MouseEventHandler(Object sender, MouseEventArgs e)
这种事件处理函数有如下两个特点:
(1)返回原型为 void。
(2)有两个参数,第1个是一个Object类型的对象,表示拥有该事件处理函数的对象;第2个是RoutedEventArgs或者是 RoutedEventArgs 的派生类,带有其路由事件的信息。
RoutedEventArgs结构包括4个成员变量:
| 名称 | 描述 |
|---|---|
| Source | 表明触发事件的源,如当键盘事件发生时,触发事件的源是当前获得焦点的对象;当鼠标事件发生时,触发事件的源是鼠标所在的最上层对象 |
| OriginalSource | 表明触发事件的源,一般来说OriginalSource和Source相同,区别在于Source表示逻辑树上的元素;OriginalSource是可视化树中的元素。如单击窗口的边框,Source为Window;OriginalSource为Border |
| RoutedEvent | 路由事件对象 |
| Handled | 布尔值,为true,表示该事件已处理,这样可以停止路由事件 |
Handled属性就是改变路由事件旅行的“元凶”。一旦在某个事件处理函数中将 Handled的值设置为true,路由事件就停止传递。如:

一个事件被标记为已处理,事件处理函数则不可以处理该事件。但是也有例外,WPF还提供了一种机制,即使事件被标记为已处理,事件处理函数仍然可以处理,但是关联事件及其处理函数需要稍作处理。AddHandler重载了两个方法,其中之一如下所示,需要将第3个参数设置为true:
public void AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo)
换句话说,因事件标识为处理而终止只是一种假象,路由事件的旅行仍然在继续,只不过普通的事件处理函数无法处理它。
3、改变旅行策略因素之二——类和实例事件处理函数
事件处理函数有两种类型:一是前面所说的普通事件处理函数,称为“实例事件处理函数”(Instance Handlers);二是通过EventManager.RegisterClassHandler 方法将一个事件处理函数和一个类关联起来,这种事件处理函数,称为“类事件处理函数”(Class Handler),其优先权高于前者。也就是说事件在旅行时,会先光临类事件处理函数,然后再光临实例事件处理函数。
4、路由事件的旅行图





6.5 路由事件示例
125页
自定义一个路由事件,名为”CustomClickEvent”。单击按钮时,这个事件就会触发为Window、Grid和Button装配不同的事件处理函数。然后单击按钮,观察路由事件的路由,如下图所示(根本看不清):

(1)需要继承一个按钮类,然后自定义”CustomClickEvent”的路由事件。其路由策略为Bubble,如下代码所示:
public class MySimpleButton : Button{static MySimpleButton(){}//创建和注册该事件,该事件路由策略为Bubblepublic static readonly RoutedEvent CustomClickEvent = EventManger.RegisterRoutedEvent("CustomClick", RoutingStrategy.Bubble,typeof(RoutedEventHandler), typeof(MySimpleButton));//CLR事件的包装器public event RoutedEventHandler CustomClick{add { AddHandler(CustomClickEvent, value);}remove{ RemoveHandler(CustomClickEvent, value);}}//触发CustomClickEventvoid RaiseCustomClickEvent(){RoutedEventArgs newEventArgs = new RoutedEventArgs(MySimpleButton.CustomClickEvent);RaiseEvent(newEventArgs);}//OnClick触发CustomClickEventprotected override void OnClick(){RaiseCustomClickEvent();}}
(2)设计一个应用程序的界面,为Window、Grid和MySimpleButton关联相应的事件处理函数,如下代码所示:
<Window x:Class="MySimpleButtonTest.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:custom="clr-namespace:MySimpleButtonTest"mc:Ignorable="d"Title="Window1" Name="window1" Height="450" Width="800"custom:MySimpleButton.CustomClick="InsertList"><Grid Margin="3" custom:MySimpleButton.CustomClick="InsertList" Name="grid1"><Grid.RowDefinitions><RowDefinition Height="Auto"></RowDefinition><RowDefinition Height="*"></RowDefinition><RowDefinition Height="Auto"></RowDefinition><RowDefinition Height="Auto"></RowDefinition></Grid.RowDefinitions><custom:MySimpleButton x:Name="simpleBtn" CustomClick="InsertList">MySimpleButton</custom:MySimpleButton><ListBox Margin="5" Name="lstMessages" Grid.Row="1"></ListBox><CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox><Button Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3" Click="cmdClear_Click">Clear List</Button></Grid></Window>
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Windows;using System.Windows.Controls;using System.Windows.Data;using System.Windows.Documents;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Imaging;using System.Windows.Navigation;using System.Windows.Shapes;namespace MySimpleButtonTest{/// <summary>/// MainWindow.xaml 的交互逻辑/// </summary>public partial class MainWindow : Window{public MainWindow(){InitializeComponent();}protected int eventCounter = 0;private void InsertList(object sender, RoutedEventArgs e){eventCounter++;string message = "#" + eventCounter.ToString() + ":\r\n" + "InsertList\r\n" + "Sender:" + sender.ToString() + "\r\n"+ "Source:" + e.Source + "\r\n" + "Original Source:" + e.OriginalSource;lstMessages.Items.Add(message);e.Handled = (bool)chkHandle.IsChecked;}private void cmdClear_Click(object sender, RoutedEventArgs e){eventCounter = 0;lstMessages.Items.Clear();}}}
(3)添加特殊的事件处理函数,为MySimpleButton添加一个指定客栈——类事件处理函数CustomClickClassHandler(代码①)。为了通知外部窗口127页
