6.1 从玉蜂说起,回顾.NET事件模型

下图暗含.NET的事件模型,小龙女是事件的发布者,她发布了事件“我在绝情谷底”。老顽童和黄蓉是事件的订阅者,不过老顽童并没有处理该事件,而黄蓉处理了事件,隐约能猜出其中的含义。至于杨过,他没有订阅事件。而玉蜂正是传递信息的事件,事件、事件的发布者和事件的订阅者构成了.NET事件模型的 3 个角色。

在.NET中,一个事件用关键字 event来表示。如下方代码所示。

第6章 路由事件——绝情谷底玉蜂飞 - 图1

  1. public delegate void WhiteBee(string param);//声明了玉蜂的委托
  2. //小龙女类
  3. class XiaoLongnv
  4. {
  5. public event WhiteBee WhiteBeeEvent;//玉蜂事件
  6. public void OnFlyBee()
  7. {
  8. Console.WriteLine("小龙女在谷底日复一日地放着玉蜂,希望杨过有一天能看到……");
  9. WhiteBeeEvent(msg);
  10. }
  11. private string msg = "我在绝情谷底";
  12. }
  13. //老顽童类
  14. class LaoWantong
  15. {
  16. public void ProcessBeeLetter(string msg)
  17. {
  18. Console.WriteLine("老顽童:小蜜蜂、小蜜蜂,别跑!");
  19. }
  20. }
  21. //黄蓉类
  22. class Huangrong
  23. {
  24. public void ProcessBeeLetter(string msg)
  25. {
  26. Console.WriteLine("黄蓉:\"{0}\",莫非......", msg);
  27. }
  28. }
  29. //杨过类
  30. class YangGuo
  31. {
  32. public void ProcessBeeLetter(string msg)
  33. {
  34. Console.WriteLine("杨过:\"{0}\",我一定会找到她!".msg);
  35. }
  36. public void Sign()
  37. {
  38. Console.WriteLine("杨过叹息:龙儿,你在哪……");
  39. }
  40. }
  41. //这个Main方法在一个类里面
  42. static void Main(string[] args)
  43. {
  44. //第1步 人物介绍
  45. XiaoLongnv longnv = new XiaoLongnv();//小龙女
  46. LaoWantong wantong = new LaoWantong();//老顽童
  47. Huangrong rong = new Huangrong();//黄蓉
  48. YangGuo guo = new YangGuo();//杨过
  49. //第2步 订阅事件,唯独没有订阅杨过地ProcessBeeLetter
  50. longnv.WhiteBeeEvent += wantong.ProcessBeeLetter;
  51. longnv.WhiteBeeEvent += rong.ProcessBeeLetter;
  52. //longnv.WhiteBeeEvent += guo.ProcessBeeLetter;//杨过没有订阅小龙女的玉蜂事件
  53. //第3步 小龙女玉蜂传信
  54. longnv.OnFlyBee();
  55. //第4步 杨过叹息
  56. guo.Sign();
  57. }
  1. 不过这种事件看起来不像.NET的事件,于是改写为以下代码:
  1. //新增一个WhiteBeeEventArgs类
  2. public class WhiteBeeEventArgs : EventArgs
  3. {
  4. public readonly string _msg;
  5. public WhiteBeeEventArgs(string msg)
  6. {
  7. this._msg = msg;
  8. }
  9. }
  10. public delegate void WhiteBeeEventHandler(object sender, WhiteBeeEventArgs e);//声明了玉蜂的委托
  11. //小龙女类
  12. class XiaoLongnv
  13. {
  14. private string msg = "我在绝情谷底";
  15. public event WhiteBeeEventHandler WhiteBeeEvent;//玉蜂事件
  16. public void OnFlyBee()
  17. {
  18. Console.WriteLine("小龙女在谷底日复一日地放着玉蜂,希望杨过有一天能看到……");
  19. WhiteBeeEventArgs args = new WhiteBeeEventArgs(msg);
  20. WhiteBeeEvent(this,args);
  21. }
  22. }
  23. //老顽童类
  24. class LaoWantong
  25. {
  26. public void ProcessBeeLetter(object sender, WhiteBeeEventArgs e)
  27. {
  28. Console.WriteLine("老顽童:小蜜蜂、小蜜蜂,别跑!");
  29. }
  30. }
  31. //黄蓉类
  32. class Huangrong
  33. {
  34. public void ProcessBeeLetter(object sender, WhiteBeeEventArgs e)
  35. {
  36. Console.WriteLine("黄蓉:\"{0}\",莫非......", e._msg);
  37. }
  38. }
  39. //杨过类
  40. class YangGuo
  41. {
  42. public void ProcessBeeLetter(object sender, WhiteBeeEventArgs e)
  43. {
  44. //真的是龙儿吗?
  45. XiaoLongnv longnv = sender as XiaoLongnv;
  46. if(longnv != null)
  47. Console.WriteLine("杨过:\"{0}\",我一定会找到她!", e._msg);
  48. }
  49. public void Sign()
  50. {
  51. Console.WriteLine("杨过叹息:龙儿,你在哪……");
  52. }
  53. }
  54. //这个Main方法在一个类里面
  55. static void Main(string[] args)
  56. {
  57. //第1步 人物介绍
  58. XiaoLongnv longnv = new XiaoLongnv();//小龙女
  59. LaoWantong wantong = new LaoWantong();//老顽童
  60. Huangrong rong = new Huangrong();//黄蓉
  61. YangGuo guo = new YangGuo();//杨过
  62. //第2步 订阅事件,唯独没有订阅杨过的ProcessBeeLetter
  63. longnv.WhiteBeeEvent += wantong.ProcessBeeLetter;
  64. longnv.WhiteBeeEvent += rong.ProcessBeeLetter;
  65. //longnv.WhiteBeeEvent += guo.ProcessBeeLetter;//杨过没有订阅小龙女的玉蜂事件
  66. //第3步 小龙女玉蜂传信
  67. longnv.OnFlyBee();
  68. //第4步 杨过叹息
  69. guo.Sign();
  70. }
  1. 上述代码中的修改可以归纳为以下几点:

(1)委托类型的名称修改为以EventHandler结束,原型有一个void返回值并接受两个输入参数,即一个Object类型和一个EventArgs类型(或继承自EventArgs)。

修改前 public delegate void WhiteBee(string param);
修改后 public delegate void WhiteBeeEventHandler(object sender,WhiteBeeEventArgs e);
  1. 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;
}
}
  1. 这样修改不仅仅符合.NET规范,并且带来了更大的灵活性。例如,如果杨过收到了小龙女的玉蜂传信,则可以通过`sender`参数来判断是否真的是小龙女,甚至还可以了解更多有关的细节。
  1. 对于一个新手来说,这个例子才是真正适合我看的,因为比较简单,而且生动形象。
  2. 对于事件的发布者、事件的订阅者、事件处理器,三者的关系与程序执行的顺序有了点头绪。
  3. 这里梳理一下:
  4. 一、单独声明一个类来写事件参数(WhiteBeeEventArgs)类,这是一个类,继承自EventArgs。里面写要传递的参数。
  5. 顺便把相应的委托也跟这个类写在一起,取名叫WhiteBeeEventHandler,它需要两个参数。
  6. 二、然后是在老顽童类、黄蓉类、杨过类都声明相应的方法(ProcessBeeLetter),这个方法用来与委托对应。这个就是事件处理器吧。
  7. 三、在小龙女这个类里面public event WhiteBeeEventHandler whiteBeeEvent;这个语句我把event删掉了运行结果是一样的。或许事件与委托界限真的很模糊。
  8. 四、Main函数里面事件订阅部分,通过+=就可以实现“订阅”。一定是事件拥有者(小龙女)对象.事件 += 事件订阅者对象.对应的方法(事件处理器)
  9. 五、程序执行顺序:先进行好订阅操作,在执行OnFlyBee方法的时候,运行到WhiteBeeEvent(this,args)这句的时候,
  10. 会依次进入老顽童、黄蓉执行ProcessBeeLetter方法。

6.2 路由事件的定义

第6章 路由事件——绝情谷底玉蜂飞 - 图2

同依赖属性一样,路由事件也需要注册,不同的是使用 EventManager.RegisterRoutedEvent 方法。

同依赖属性,用户不会直接使用路由事件,而是使用传统的CLR事件。有两种方式关联事件及其处理函数,在代码中,仍然按照原来的方法关联和解除关联事件处理函数(+=和-=),如下代码所示:

  1. Button b2 = new Button();
  2. //关联事件及其处理函数
  3. b2.Click += new RoutedEventHandler(Onb2Click);
  4. //事件处理函数
  5. void Onb2Click(object sender, RoutedEventArgs e)
  6. {
  7. //logic to handle the Click event
  8. }
  1. ![](https://cdn.nlark.com/yuque/0/2022/png/26289675/1652859977776-d185fb4f-b8e8-4963-aed7-bd4f38316446.png)

在XAML中,事件和处理函数这样关联:

<Button Click="OnbeClick">Button</Button>

传统的事件触发往往直接调用其委托(因为事件的本质是委托),而路由事件则是通过一个RaiseEvent方法触发,调用该方法后所有关联该事件的对象都会得到通知。在ButtonBase中即有代码:

第6章 路由事件——绝情谷底玉蜂飞 - 图3

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

6.3 路由事件的作用

第6章 路由事件——绝情谷底玉蜂飞 - 图4

第6章 路由事件——绝情谷底玉蜂飞 - 图5

第6章 路由事件——绝情谷底玉蜂飞 - 图6

6.4 路由事件

6.4.1 识别路由事件

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

第6章 路由事件——绝情谷底玉蜂飞 - 图7

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,表示该事件已处理,这样可以停止路由事件
  1. Handled属性就是改变路由事件旅行的“元凶”。一旦在某个事件处理函数中将 Handled的值设置为true,路由事件就停止传递。如:

第6章 路由事件——绝情谷底玉蜂飞 - 图8

一个事件被标记为已处理,事件处理函数则不可以处理该事件。但是也有例外,WPF还提供了一种机制,即使事件被标记为已处理,事件处理函数仍然可以处理,但是关联事件及其处理函数需要稍作处理。AddHandler重载了两个方法,其中之一如下所示,需要将第3个参数设置为true:

public void AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo)

换句话说,因事件标识为处理而终止只是一种假象,路由事件的旅行仍然在继续,只不过普通的事件处理函数无法处理它。

3、改变旅行策略因素之二——类和实例事件处理函数

事件处理函数有两种类型:一是前面所说的普通事件处理函数,称为“实例事件处理函数”(Instance Handlers);二是通过EventManager.RegisterClassHandler 方法将一个事件处理函数和一个类关联起来,这种事件处理函数,称为“类事件处理函数”(Class Handler),其优先权高于前者。也就是说事件在旅行时,会先光临类事件处理函数,然后再光临实例事件处理函数

4、路由事件的旅行图

第6章 路由事件——绝情谷底玉蜂飞 - 图9

第6章 路由事件——绝情谷底玉蜂飞 - 图10

第6章 路由事件——绝情谷底玉蜂飞 - 图11

第6章 路由事件——绝情谷底玉蜂飞 - 图12

第6章 路由事件——绝情谷底玉蜂飞 - 图13

6.5 路由事件示例

125页

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

第6章 路由事件——绝情谷底玉蜂飞 - 图14

(1)需要继承一个按钮类,然后自定义”CustomClickEvent”的路由事件。其路由策略为Bubble,如下代码所示:

  1. public class MySimpleButton : Button
  2. {
  3. static MySimpleButton()
  4. {
  5. }
  6. //创建和注册该事件,该事件路由策略为Bubble
  7. public static readonly RoutedEvent CustomClickEvent = EventManger.RegisterRoutedEvent("CustomClick", RoutingStrategy.Bubble,
  8. typeof(RoutedEventHandler), typeof(MySimpleButton));
  9. //CLR事件的包装器
  10. public event RoutedEventHandler CustomClick
  11. {
  12. add { AddHandler(CustomClickEvent, value);}
  13. remove{ RemoveHandler(CustomClickEvent, value);}
  14. }
  15. //触发CustomClickEvent
  16. void RaiseCustomClickEvent()
  17. {
  18. RoutedEventArgs newEventArgs = new RoutedEventArgs(MySimpleButton.CustomClickEvent);
  19. RaiseEvent(newEventArgs);
  20. }
  21. //OnClick触发CustomClickEvent
  22. protected override void OnClick()
  23. {
  24. RaiseCustomClickEvent();
  25. }
  26. }
  1. 2)设计一个应用程序的界面,为WindowGridMySimpleButton关联相应的事件处理函数,如下代码所示:
  1. <Window x:Class="MySimpleButtonTest.MainWindow"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6. xmlns:custom="clr-namespace:MySimpleButtonTest"
  7. mc:Ignorable="d"
  8. Title="Window1" Name="window1" Height="450" Width="800"
  9. custom:MySimpleButton.CustomClick="InsertList">
  10. <Grid Margin="3" custom:MySimpleButton.CustomClick="InsertList" Name="grid1">
  11. <Grid.RowDefinitions>
  12. <RowDefinition Height="Auto"></RowDefinition>
  13. <RowDefinition Height="*"></RowDefinition>
  14. <RowDefinition Height="Auto"></RowDefinition>
  15. <RowDefinition Height="Auto"></RowDefinition>
  16. </Grid.RowDefinitions>
  17. <custom:MySimpleButton x:Name="simpleBtn" CustomClick="InsertList">
  18. MySimpleButton
  19. </custom:MySimpleButton>
  20. <ListBox Margin="5" Name="lstMessages" Grid.Row="1"></ListBox>
  21. <CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox>
  22. <Button Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3" Click="cmdClear_Click">Clear List</Button>
  23. </Grid>
  24. </Window>
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Data;
  9. using System.Windows.Documents;
  10. using System.Windows.Input;
  11. using System.Windows.Media;
  12. using System.Windows.Media.Imaging;
  13. using System.Windows.Navigation;
  14. using System.Windows.Shapes;
  15. namespace MySimpleButtonTest
  16. {
  17. /// <summary>
  18. /// MainWindow.xaml 的交互逻辑
  19. /// </summary>
  20. public partial class MainWindow : Window
  21. {
  22. public MainWindow()
  23. {
  24. InitializeComponent();
  25. }
  26. protected int eventCounter = 0;
  27. private void InsertList(object sender, RoutedEventArgs e)
  28. {
  29. eventCounter++;
  30. string message = "#" + eventCounter.ToString() + ":\r\n" + "InsertList\r\n" + "Sender:" + sender.ToString() + "\r\n"
  31. + "Source:" + e.Source + "\r\n" + "Original Source:" + e.OriginalSource;
  32. lstMessages.Items.Add(message);
  33. e.Handled = (bool)chkHandle.IsChecked;
  34. }
  35. private void cmdClear_Click(object sender, RoutedEventArgs e)
  36. {
  37. eventCounter = 0;
  38. lstMessages.Items.Clear();
  39. }
  40. }
  41. }
  1. 3)添加特殊的事件处理函数,为MySimpleButton添加一个指定客栈——类事件处理函数CustomClickClassHandler(代码①)。为了通知外部窗口127