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步 订阅事件,唯独没有订阅杨过地ProcessBeeLetter
longnv.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步 订阅事件,唯独没有订阅杨过的ProcessBeeLetter
longnv.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()
{
}
//创建和注册该事件,该事件路由策略为Bubble
public 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);}
}
//触发CustomClickEvent
void RaiseCustomClickEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(MySimpleButton.CustomClickEvent);
RaiseEvent(newEventArgs);
}
//OnClick触发CustomClickEvent
protected 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页