WPF 之路由事件和附加事件(六)
一、消息驱动与直接事件模型
事件的前身是消息(Message)。Windows 是消息驱动的系统,运行其上的程序也遵循这个原则。消息的本质就是一条数据,这条消息里面包含着消息的类别,必要的时候还记载着一些消息参数。例如:当你在按下鼠标左键时,一条名为 WM_LBUTTONDOWN 的消息被生成并加入到 Windows 待处理的消息队列中。当 Windows 处理到这条消息时,会把消息发送给单击的窗体,窗体会按照自己的算法来响应这条消息。以上过程,被称为消息驱动。
随着微软面向对象平台开发日趋成熟,微软把消息驱动包装成了更容易让人理解的事件模型。事件模型隐藏了消息的消息驱动的许多细节,让程序开发变得简单,繁琐的消息驱动机制在事件模型中简化为以下 3 个关键点:
事件的拥有者:即消息的发送者。事件的宿主可以在某些条件下激发它拥有的事件。
事件的响应者:即消息的接收者、处理者。事件接收者通过使用其事件处理器(Evenet Handler)对事件做出响应。
事件的订阅关系:事件的拥有者可以随时激发事件,但事件发生后会不会得到响应,需要看这个事件是否被订阅。

在上述事件模型中,事件的响应者通过订阅关系直接关联到事件拥有者的事件上,这种直接事件模型的不完美之处在于事件的拥有者和响应者必须通过事件订阅建立“专线”联系,即必须显式的建立点对点订阅关系,也就意味着事件的宿主必须能够直接访问事件的响应者,不然,无法建立订阅关系。
为了解决直接事件模型的缺点,.NET 推出了路由事件。
二、路由事件(Routed Event)
路由事件中,事件的拥有者和事件的响应者之间没有直接显式的订阅关系,事件的拥有者只负责激发事件,事件将由谁响应它并不知道,事件的响应者则安装有事件侦听器,针对某类事件进行侦听,当有此类事件传递至此时,就使用事件处理器响应并决定是否继续传递。
WPF 中大多数事件都是可路由事件。我们以 Button 的 Click 事件来说明路由事件的用法:
// 声明一个事件处理器
private void ButtonClicked(object sender, RoutedEventArgs e)
{
MessageBox.Show($"由事件的拥有者 {(e.OriginalSource as FrameworkElement).Name} 发起,并由事件的响应者 {(sender as FrameworkElement).Name}响应!");
}
<!--Grid 捕获从内部“飘出”的 Button 单击事件并通过方法 ButtonClicked 来响应-->
<Grid x:Name="GridRoot" Button.Click="ButtonClicked">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Canvas x:Name="CanvasLeft" Grid.Column="0" Margin="20">
<Button x:Name="ButtonLeft" Width="200" Height="100" Content="Left"></Button>
</Canvas>
<Canvas x:Name="CanvasRight" Grid.Column="1" Margin="20">
<Button x:Name="ButtonRight" Width="200" Height="100" Content="Right"></Button>
</Canvas>
</Grid>
当我们点击不同的按钮时,显示不同的内容,具体如下:

在上述的例子中,我们也可以在 Canvas 控件上添加针对按钮的路由事件,此时点击 Button 后,就会依次触发 Canvas 和 Grid 添加的事件。
三、自定义路由事件
创建自定义路由事件分为以下三个步骤:
- 声明并注册路由事件
- 为路由事件添加 CLR 事件包装
- 创建可以激发路由事件的方法
例如:我们可以自定义一个 Button 类型,可以让事件消息携带被单击时的时间。首先,我们自定义一个事件参数:
// 用于承载时间消息的消息参数
public class ReportTimeRoutedEventArgs : RoutedEventArgs
{
public ReportTimeRoutedEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source)
{
}
public DateTime ClickTime { get; set; }
}
然后,我们自定义一个 Button 类,并添加自定义路由事件:
public class DyButton : Button
{
// 声明和注册路由事件
public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime",
RoutingStrategy.Direct, typeof(EventHandler<ReportTimeRoutedEventArgs>), typeof(DyButton));
// 将路由事件包装成 CLR 事件
public event RoutedEventHandler ReportTime
{
add { this.AddHandler(ReportTimeEvent, value); }
remove { this.RemoveHandler(ReportTimeEvent, value); }
}
// 激发路由事件,借用 Click 事件激发
protected override void OnClick()
{
base.OnClick();
var rtArgs = new ReportTimeRoutedEventArgs(ReportTimeEvent, this) { ClickTime = DateTime.Now };
this.RaiseEvent(rtArgs);
}
}
最后,在 XAML 上绑定此事件:
// 声明一个事件处理器
private void DyButtonClicked(object sender, ReportTimeRoutedEventArgs e)
{
this.lst1.Items.Add(
$"由事件的拥有者 {(e.OriginalSource as FrameworkElement).Name} 在 {e.ClickTime.ToString("HH:mm:ss.fff")} 发起,并由事件的响应者 {(sender as FrameworkElement).Name} 响应!");
}
<Grid x:Name="g1" local:DyButton.ReportTime="DyButtonClicked">
<Grid x:Name="g2" local:DyButton.ReportTime="DyButtonClicked">
<StackPanel x:Name="s1" local:DyButton.ReportTime="DyButtonClicked">
<ListBox x:Name="lst1" Margin="20" Height="200" local:DyButton.ReportTime="DyButtonClicked"></ListBox>
<local:DyButton x:Name="ButtonDwayne" Margin="20" Height="100" Content="OK" ></local:DyButton>
</StackPanel>
</Grid>
</Grid>
当我们点击 “OK” 按钮时,可以看到如下两种效果,同一个路由事件,展示的路由路径不一样,是由注册路由事件的路由策略不同造成的,当我们使用冒泡策略(RoutingStrategy.Bubble)时和隧道策略(RoutingStrategy.Tunnel)时,效果分别如下图所示,可以清晰的看到冒泡策略使事件从内向外,而隧道策略使事件从外向内传递:

注意:我们知道路由事件是沿着 Visual Tree 进行传递的,路由事件的传递参数 RoutedEventArgs 有两个属性:Source 和 OriginalSource,都表示事件消息的源头。两者的区别在于, Source 表示 Local Tree 上的消息源头,OriginalSource 表示 Visual Tree 上的消息源头(Local Tree 和 Visual Tree 可阅读:WPF 中的逻辑树(Logical Tree)与可视化元素树(Visual Tree))。
四、附加事件(Attached Event)
路由事件的宿主全是拥有可视化实体的界面元素,附加事件需借助界面元素去与其他对象进行沟通。例如:设计一个 Student 类,当 Name 属性发生变化时,激发一个路由事件,同时界面捕获到这个事件,具体实现如下:
首先,我们声明一个 Student 类,并注册和声明路由事件:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
// 声明和注册路由事件(由于 Student 类 UIElment 类的派生类,因此不具备 AddHandler 和 RemoveHandler 方法,需要手动实现)
public static readonly RoutedEvent NameChanedEvent = EventManager.RegisterRoutedEvent("NameChaned",
RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));
// 为界面元素添加路由事件
public static void AddNameChangedHandler(DependencyObject sender, RoutedEventHandler e)
{
UIElement ui= sender as UIElement;
if (ui != null)
{
ui.AddHandler(Student.NameChanedEvent,e);
}
}
// 为界面元素移除路由事件
public static void RemoveNameChangedHandler(DependencyObject sender, RoutedEventHandler e)
{
UIElement ui = sender as UIElement;
if (ui != null)
{
ui.RemoveHandler(Student.NameChanedEvent, e);
}
}
}
其次,我们添加一个事件处理器:
// 声明一个事件处理器
private void NameChanged(object sender, RoutedEventArgs e)
{
var s = (e.OriginalSource as Student);
MessageBox.Show($"{s.Id},{s.Name}");
}
然后,我们在 UI 元素上添加对 NameChanged 事件的侦听与触发:
<Grid local:Student.NameChanged="NameChanged">
<Button x:Name="Button1" Click="ButtonBase_OnClick"></Button>
</Grid>
// 触发 NameChanged 事件
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Student s = new Student() {Name = "Tim", Id = 0,};
RoutedEventArgs reArgs = new RoutedEventArgs(Student.NameChanedEvent, s);
this.Button1.RaiseEvent(reArgs);
}
最后,当我们点击按钮,触发 NameChanged 事件后,输出结果如下所示:
0,Tim
WPF 之路由事件和附加事件(六)的更多相关文章
- WPF自定义路由事件(二)
WPF中的路由事件 as U know,和以前Windows消息事件区别不再多讲,这篇博文中,将首先回顾下WPF内置的路由事件的用法,然后在此基础上自定义一个路由事件. 1.WPF内置路由事件 WPF ...
- WPF - 善用路由事件
原文:WPF - 善用路由事件 在原来的公司中,编写自定义控件是常常遇到的任务.但这些控件常常拥有一个不怎么好的特点:无论是内部还是外部都没有使用路由事件.那我们应该怎样宰自定义控件开发中使用路由事件 ...
- 细说WPF自定义路由事件
WPF中的路由事件 as U know,和以前Windows消息事件区别不再多讲,这篇博文中,将首先回顾下WPF内置的路由事件的用法,然后在此基础上自定义一个路由事件. 1.WPF内置路由事件 W ...
- WPF之路由事件的理解
博客园上讲解路由事件的文章很多,在此转其中之一供学习参考: https://www.cnblogs.com/zhili/p/WPFRouteEvent.html 网上流传的文章中都对冒泡进行了说明,但 ...
- WPF知识点全攻略10- 路由事件
路由事件是WPF不得不提,不得不会系列又一 先来看一下他的定义: 功能定义:路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件. 实现定义:路由事件是一个 C ...
- WPF的路由事件、冒泡事件、隧道事件(预览事件)
本文摘要: 1:什么是路由事件: 2:中断事件路由: 3:自定义路由事件: 4:为什么需要自定义路由事件: 5:什么是冒泡事件和预览事件(隧道事件): 1:什么是路由事件 WPF中的事件为路由事件,所 ...
- 【WPF】路由事件
总结WPF中的路由事件,我将学到的内容分为四部分来逐渐掌握 第一部分:wpf中内置的路由事件 以Button的Click事件来说明内置路由事件的使用 XAML代码: <Window x:Clas ...
- 迟到的 WPF 学习 —— 路由事件
1. 理解路由事件:WPF 通过事件路由(event routing)概念增强了传统的事件执行的能力和范围,允许源自某个元素的事件由另一个元素引发,例如,事件路由允许工具栏上的一个按钮点击的事件在被代 ...
- 深入浅出WPF——附加事件(Attached Event)
3.3 事件也附加——深入浅出附加事件 WPF事件系统中还有一种事件被称为附加事件(Attached Event),简言之,它就是路由事件.“那为什么还要起个新名字呢?”你可能会问. “身无彩凤双飞翼 ...
随机推荐
- Linux 用户操作之用户管理 (用户增删改操作)
目录 添加用户 删除用户 修改用户 切换用户 配置用户密码 查看配置文件 cat /etc/pwsswd 添加用户 可选项 -c comment 指定一段注释性描述. -d 目录 指定用户主目录,如果 ...
- 浏览器performance工具介绍及内存问题表现与监控内存的几种方式
一.GC的目的 为了实现内存空间的良性循环,performance提供多种监控方式监控内存 分析内存相关信息 当代码出现问题的时候及时定位到出现问题的代码块, 提高执行效率. preforcemanc ...
- golang语言初体验
Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型.编译型语言.Go 语言语法与 C 相近,但功能上 ...
- 解决ROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'creat table study_record( id int(11) not null
之前一直用的好好的,突然就出现了这个错误: ERROR 1064 (42000): You have an error in your SQL syntax; check the manual tha ...
- ASP.NET Core错误处理中间件[3]: 异常处理器
DeveloperExceptionPageMiddleware中间件错误页面可以呈现抛出的异常和当前请求上下文的详细信息,以辅助开发人员更好地进行纠错诊断工作.ExceptionHandlerMid ...
- SAP GUI用颜色区分不同的系统
对于经常打开多个窗口的SAP用户,有时候可能同时登录了生产机.测试机和开发机,为了避免误操作,比如在测试要执行的操作,结果在生产机做了,结果可想而知. 虽然可以通过右下角查看再去判断,但是总是没有通过 ...
- 1.8V转5V电平转换芯片,1.8V转5V的电源芯片
1.8V是一个比较低的电压,在电压供电电压中,1.8V电压的过于小了,在一些电子模块或者MCU中,无法达到供电电压,和稳压作用,PW5100就是可以在1.8V转5V的电平转换电路和芯片,最大可提供50 ...
- Python 中 lru_cache 的使用和实现
在计算机软件领域,缓存(Cache)指的是将部分数据存储在内存中,以便下次能够更快地访问这些数据,这也是一个典型的用空间换时间的例子.一般用于缓存的内存空间是固定的,当有更多的数据需要缓存的时候,需要 ...
- MYSQL面试题-索引
MYSQL面试题-索引 引自B站up编程不良人:https://www.bilibili.com/video/BV19y4y127h4 一.什么是索引? 官方定义:索引是一种帮助mysql提高查询效率 ...
- 【pytest】(十)fixture参数化-巧用params和ids优雅的创建测试数据
我们都知道参数化. 比如我要测试一个查询接口/test/get_goods_list,这个接口可以查询到商品的信息. 在请求中,我可以根据请参数goods_status的不同传值,可以查询到对应状态的 ...