一、消息驱动与直接事件模型

​ 事件的前身是消息(Message)。Windows 是消息驱动的系统,运行其上的程序也遵循这个原则。消息的本质就是一条数据,这条消息里面包含着消息的类别,必要的时候还记载着一些消息参数。例如:当你在按下鼠标左键时,一条名为 WM_LBUTTONDOWN 的消息被生成并加入到 Windows 待处理的消息队列中。当 Windows 处理到这条消息时,会把消息发送给单击的窗体,窗体会按照自己的算法来响应这条消息。以上过程,被称为消息驱动

​ 随着微软面向对象平台开发日趋成熟,微软把消息驱动包装成了更容易让人理解的事件模型。事件模型隐藏了消息的消息驱动的许多细节,让程序开发变得简单,繁琐的消息驱动机制在事件模型中简化为以下 3 个关键点:

  1. 事件的拥有者:即消息的发送者。事件的宿主可以在某些条件下激发它拥有的事件。

  2. 事件的响应者:即消息的接收者、处理者。事件接收者通过使用其事件处理器(Evenet Handler)对事件做出响应。

  3. 事件的订阅关系:事件的拥有者可以随时激发事件,但事件发生后会不会得到响应,需要看这个事件是否被订阅。

​ 在上述事件模型中,事件的响应者通过订阅关系直接关联到事件拥有者的事件上,这种直接事件模型的不完美之处在于事件的拥有者和响应者必须通过事件订阅建立“专线”联系,即必须显式的建立点对点订阅关系,也就意味着事件的宿主必须能够直接访问事件的响应者,不然,无法建立订阅关系。

​ 为了解决直接事件模型的缺点,.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 添加的事件。

三、自定义路由事件

​ 创建自定义路由事件分为以下三个步骤:

  1. 声明并注册路由事件
  2. 为路由事件添加 CLR 事件包装
  3. 创建可以激发路由事件的方法

​ 例如:我们可以自定义一个 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 之路由事件和附加事件(六)的更多相关文章

  1. WPF自定义路由事件(二)

    WPF中的路由事件 as U know,和以前Windows消息事件区别不再多讲,这篇博文中,将首先回顾下WPF内置的路由事件的用法,然后在此基础上自定义一个路由事件. 1.WPF内置路由事件 WPF ...

  2. WPF - 善用路由事件

    原文:WPF - 善用路由事件 在原来的公司中,编写自定义控件是常常遇到的任务.但这些控件常常拥有一个不怎么好的特点:无论是内部还是外部都没有使用路由事件.那我们应该怎样宰自定义控件开发中使用路由事件 ...

  3. 细说WPF自定义路由事件

    WPF中的路由事件 as U know,和以前Windows消息事件区别不再多讲,这篇博文中,将首先回顾下WPF内置的路由事件的用法,然后在此基础上自定义一个路由事件. 1.WPF内置路由事件   W ...

  4. WPF之路由事件的理解

    博客园上讲解路由事件的文章很多,在此转其中之一供学习参考: https://www.cnblogs.com/zhili/p/WPFRouteEvent.html 网上流传的文章中都对冒泡进行了说明,但 ...

  5. WPF知识点全攻略10- 路由事件

    路由事件是WPF不得不提,不得不会系列又一 先来看一下他的定义: 功能定义:路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件. 实现定义:路由事件是一个 C ...

  6. WPF的路由事件、冒泡事件、隧道事件(预览事件)

    本文摘要: 1:什么是路由事件: 2:中断事件路由: 3:自定义路由事件: 4:为什么需要自定义路由事件: 5:什么是冒泡事件和预览事件(隧道事件): 1:什么是路由事件 WPF中的事件为路由事件,所 ...

  7. 【WPF】路由事件

    总结WPF中的路由事件,我将学到的内容分为四部分来逐渐掌握 第一部分:wpf中内置的路由事件 以Button的Click事件来说明内置路由事件的使用 XAML代码: <Window x:Clas ...

  8. 迟到的 WPF 学习 —— 路由事件

    1. 理解路由事件:WPF 通过事件路由(event routing)概念增强了传统的事件执行的能力和范围,允许源自某个元素的事件由另一个元素引发,例如,事件路由允许工具栏上的一个按钮点击的事件在被代 ...

  9. 深入浅出WPF——附加事件(Attached Event)

    3.3 事件也附加——深入浅出附加事件 WPF事件系统中还有一种事件被称为附加事件(Attached Event),简言之,它就是路由事件.“那为什么还要起个新名字呢?”你可能会问. “身无彩凤双飞翼 ...

随机推荐

  1. 安装MySQL数据库(在Windows下通过zip压缩包安装)

    安装MySQL 这里建议大家使用压缩版,安装快,方便.不复杂. 软件下载 mysql5.7 64位下载地址: https://dev.mysql.com/get/Downloads/MySQL-5.7 ...

  2. 【Git】3、创建Git版本库、配置Git仓库用户邮箱信息

    初识Git 文章目录 初识Git 1.创建Git版本库 认识.git 2.基础配置 2.1.查看配置信息 2.2.配置昵称邮箱信息 2.3.修改配置信息 1.通过命令行 2.通过修改配置文件. 修改全 ...

  3. yum -y install gnuplot

    [root@test~]# yum -y install gnuplotLoaded plugins: fastestmirrorLoading mirror speeds from cached h ...

  4. 【Linux】fstab中 每个字段代表的含义

      默认情况下,fstab中已经有了当前的分区配置,内容可能类似: # <file system> <mount point> <type> <options ...

  5. 【Linux】一个网卡部署多个内网ip

    1.用root权限的用户登录CENTOS,进入network-scripts文件夹下(本步骤可以省略,与二步骤一起完成): shell命令:cd /ect/sysconfig/network-scri ...

  6. ctfhub技能树—sql注入—字符型注入

    打开靶机 查看页面信息 查询回显位 查询数据库名(查询所有数据库名:select group_concat(schema_name) from information_schema.schemata) ...

  7. 词嵌入之GloVe

    什么是GloVe GloVe(Global Vectors for Word Representation)是一个基于全局词频统计(count-based & overall statisti ...

  8. Java中的深浅拷贝问题,你清楚吗?

    一.前言 拷贝这个词想必大家都很熟悉,在工作中经常需要拷贝一份文件作为副本.拷贝的好处也很明显,相较于新建来说,可以节省很大的工作量.在Java中,同样存在拷贝这个概念,拷贝的意义也是可以节省创建对象 ...

  9. GlusterFS分布式存储系统复制集更换故障Brick操作记录

    场景: GlusterFS 3节点的复制集,由于磁盘故障,其中一个复制集需要重装系统,所以需要重装glusterfs并将该节点加入glusterfs集群 一. 安装GlusterFS 首先在重装系统节 ...

  10. Numpy的一些学习记录

    Numpy的一些记录 产生numpy.array的方式 import numpy as np arr1 = np.array([1, 2, 3]) print(arr1) arr2 = np.zero ...