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),简言之,它就是路由事件.“那为什么还要起个新名字呢?”你可能会问. “身无彩凤双飞翼 ...
随机推荐
- docker 常用的容器命令
容器命令 # --name 给容器起名 # -p 端口映射 # -d 后台启动 # -it 交互模式启动 # 交互模式启动 # docker run -it 镜像名/id /bin/bash # do ...
- zabbix_server上的问题
不要写成127.0.0.1,要不然一直包zabbix agent没有启动.
- [Usaco2007 Jan]Balanced Lineup 飞盘比赛
题目描述 每天,农夫 John 的N(1 <= N <= 50,000)头牛总是按同一序列排队. 有一天, John 决定让一些牛们玩一场飞盘比赛. 他准备找一群在对列中为置连续的牛来进行 ...
- GStreamer环境搭建篇
GStreamer是一套强大的多媒体中间件系统,跟FFmpeg功能类似. 各个Linux发行版(Ubuntu,fedora),大都集成了GStreamer相关工具,而作为软件层次结构最上层的播放器,几 ...
- echarts图表X轴文字过长解决解决方案:根据文字长度自动旋转
Echarts 标签中文本内容太长的时候怎么办 ? 关于这个问题搜索一下,有很多解决方案.无非就是 省略(间隔显示).旋转文字方向.竖排展示 前面两种解决方案,就是echarts暴露的: { ax ...
- Obligations for calling close() on the iterable returned by a WSGI application
Graham Dumpleton: Obligations for calling close() on the iterable returned by a WSGI application. ht ...
- go语言rpc学习
rpc 就是 远程过程调用 指的是调用远端服务器上的程序的方法整个过程. rpc 理论 RPC技术在架构设计上有四部分组成,分别是:客户端.客户端存根.服务端.服务端存根. 客户端:服务调用发 ...
- Java Object类 和 String类 常见问答 6k字+总结
写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解.所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项 ...
- Java进阶专题(二十二) 从零开始搭建一个微服务架构系统 (上)
前言 "微服务"一词源于 Martin Fowler的名为 Microservices的,博文,可以在他的官方博客上找到http:/ /martinfowler . com/art ...
- Language Guide (proto3) | proto3 语言指南(八)未知字段和任意类型
未知字段和任意类型篇幅较少,因此将他们合并到本文进行描述. Unknown Fields - 未知字段 未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段.例如,当一个旧二进制代码解析 ...