1. 前言

WPF 事件的路由环境是 UI 组件树,先来看看这棵树。

1.1 Logical Tree 和 Visual Tree

WPF 中的树有两种,一颗是逻辑树,另一颗也是逻辑树。

开玩笑,WPF 不是鲁迅,另一颗是可视元素树。

  • 逻辑树的每个节点不是布局组件就是控件;
  • 而可视化树把逻辑树延伸到 Template;
  • 一般都是和 Logical Tree 打交道,如果到 Visual Tree,多半还是程序设计不良;
  • Logical Tree 查找元素用 LogicalTreeHelper 类的 static 方法实现;
  • Visual Tree 查找元素用 VisualTreeHelper 类的 static 方法实现;

路由事件被激发后沿着 Visual Tree 传递,因此 Template 里的控件能把消息送出来。

2. 事件

事件隐藏消息机制的很多细节;

拥有者、响应者、订阅关系;

A 订阅了 B,本质是让 B.Event 与 A.EventHandler 关联起来;

事件激发: B.Event 被调用,与之关联的 A.EventHandler 就会被调用。

// 拥有者:myButton,事件:myButton.Click,事件的响应者:窗体本身,处理器:this.myButton_Click
this.myButton.Click += new System.EventHandler(this.myButton_Click); void myButton_Click(object sender, EventArgs e) {}

缺点:

  • 动态生成一组相同的控件,每个控件的同一事件都是用同一个处理器来相应,需要显式书写事件订阅代码;
  • 控件的内部事件不能被外部订阅,当 UI 层级很多时,如果想让外层的容器订阅深层控件的某个事件就需要为每一层组件定义用于暴露内部事件的事件。

3. 路由事件

3.1 跟直接事件的区别

直接事件:发送者直接将事件消息发送给事件响应者,事件响应者使用其事件处理器方法对事件的发生做出响应、驱动程序逻辑按客户需求运行;

路由事件:拥有者和事件响应者没有直接显式的订阅关系,拥有者只负责激发事件,事件由谁响应它不知道,事件的响应者则安装有事件监听器,针对某类事件进行侦听,当有此类事件传递至此事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。

3.2 使用内置路由事件

// C# 添加路由事件处理器
this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked)); void ButtonClicked(object sender, RoutedEventArgs e) {} <!--xaml 添加路由事件处理器-->
<Grid x:Name="gridRoot" ButtonBase.Click="ButtonClicked"></Grid>

3.3 自定义路由事件

1)声明并注册路由事件;

2)为路由事件添加 CLR 事件包装;// 非必须

3)创建可以激发路由事件的方法;

public abstract class ButtonBase : ContentControl, ICommandSource
{
public static readonly RoutedEvent ClickEvent;
static ButtonBase()
{
ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
...
} public event RoutedEventHandler Click
{
add
{
AddHandler(ClickEvent, value);
}
remove
{
RemoveHandler(ClickEvent, value);
}
} protected virtual void OnClick()
{
RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this);
RaiseEvent(e);
CommandHelpers.ExecuteCommandSource(this);
}
3.3.1 注册路由事件入参
  1. 第 1 个参数:与依赖属性类似,需要拿去生成用于注册路由事件的 hash Code,微软建议与 RoutedEvent 变量的前缀和 CLR 事件包装器的名称一致,如例子中的 "Click";
  2. 第 2 个参数:路由事件策略, RoutingStrategy.Bubble/Tunnel/Direct,Bubble 为从事件的激发者向上级容器一层一层路由,Tunnel 的方向是反过来,Direct 是类似于直接事件的方式,直接将事件消息传达给事件处理器;
  3. 第 3 个参数:指定事件处理器的类型,从源代码可以看出,为指定的路由事件添加路由事件处理程序时,函数的调用顺序是:UIElment.AddHandler->EventHandlersStore.AddRoutedEventHandler->RoutedEvent.IsLegalHandler(检查路由事件处理程序的类型是否为路由事件注册时指定的处理器类型 或 RoutedEventHandler,否则抛出 ArgumentException);
  4. 第 4 个参数:指定路由事件的宿主是哪个类型,跟依赖属性类似,用于生成 hash Code;

例子:

// 自定义路由事件的数据,继承自 RoutedEventArgs(包含与路由事件相关联的状态信息和事件数据)
class ReportedEventArgs : RoutedEventArgs
{
public ReportedEventArgs(RoutedEvent routedEvent, object source) : base (routedEvent, source) {} public DateTime ClickTime { get; set; }
} class TimeButton : Button
{
public static readonly RoutedEvent ReportTimeEvent =
EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton)); public event RoutedEventHandler ReportTime
{
add { this.AddHandler(ReportTimeEvent, value); }
remove { this.RemoveHandler(ReportTimeEvent, value); }
} protected override void OnClick()
{
base.OnClick(); ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
} <StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler">
<ListDox x:Name""listBox" />
<local:TimeButton x:Name-"timeButton" Width="50" Height-"80" Content="报时" local:TimeButton.ReportTime="ReportTimeHandler"/>
</StackPanel> private void ReportTimeHandler(object sender, ReportTimeEventArgs e) { }
<StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler">
翻译成 C#:
Delegate dg = Delegate.CreateDelegate(typeof(EventHandler<ReportTimeEventArgs>), this, "ReportTimeHandler", false, true);
this.stackPanel_1.AddHandler(TimeButton.ReportTimeEvent, dg);

RoutedEventArgs 类具有一个 bool 类属性 Handled,一旦设置为 ture,就不会再往下传递,需要停止的时候就让它停止。

TextBox 的 TextChanged 事件、Binding 类的 SourceUpdated 事件也是路由事件。

4. RoutedEventArgs 的 Source 和 OriginalSource

Source 是 LogicalTree 上的消息源头;

OriginalSource 是 VisualTree 上的源头。

比如 一个 Window 包括一个 UserControl,UserControl 里面有个 Button,在 Window 中 UserControl 是 LogicalTree 的末端结点,e.Source 是那个 userControl,而窗体的 VisualTree 能看到内部结果,e.OriginalSource 是那个 button

5. 附加事件

5.1 非 UIElement 子类使用路由事件

class Student
{
public static readonly RoutedEvent NameChangedEvent =
EventManager.RegisterRoutedEvent("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student)); public string Name { get; set; }
} public partial class EventWindow : Window
{
public EventWindow()
{
InitializeComponent(); this.grid1.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(StudentNameChangedHandler));
} private void StudentNameChangedHandler(object sender, RoutedEventArgs e)
{
MessageBox.Show(string.Format("名称改成{0}", (e.OriginalSource as Student).Name));
} private void ButtonClick(object sender, RoutedEventArgs e)
{
Student stu = new Student() { Name = "OldName" };
stu.Name = "NewName"; RoutedEventArgs args = new RoutedEventArgs(Student.NameChangedEvent, stu);
this.button.RaiseEvent(args);
}
}

grid 监听 Student 的路由事件 NameChangedEvent;

Student 没有继承自 UIElement,没有 RaiseEvent 方法,因此借用其他 UIElement 元素调用 RaiseEvent 方法;

上面只是举例,一般我们用附加事件在 Binding、Mouse、Keyboard 这种全局的 Helper 类中。

5.2 例子 Binding

<Grid x:Name="grid1">
<Slider x:Name="slider" Width="300"/>
<TextBox x:Name="tb"
Text="{Binding Path=Value, ElementName=slider, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"/>
</Grid> this.grid1.AddHandler(Binding.TargetUpdatedEvent, new RoutedEventHandler(TargetChangedHandler));
this.grid1.AddHandler(Binding.SourceUpdatedEvent, new RoutedEventHandler(SourceChangedHandler)); private void TargetChangedHandler(object sender, RoutedEventArgs e) { }
private void SourceChangedHandler(object sender, RoutedEventArgs e) { }

5.3 例子 Keyboard

public EventWindow()
{
InitializeComponent(); Keyboard.AddKeyDownHandler(this, new KeyEventHandler(KeyDownHandler)); this.AddHandler(UIElement.KeyDownEvent, new RoutedEventHandler(KeyDownHandler2));
} private void KeyDownHandler(object sender, KeyEventArgs e)
{
if ((e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || e.KeyboardDevice.IsKeyDown(Key.RightCtrl)) && e.KeyboardDevice.IsKeyDown(Key.C))
{
}
} private void KeyDownHandler2(object sender, RoutedEventArgs e)
{
KeyEventArgs args = e as KeyEventArgs;
if ((args.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || args.KeyboardDevice.IsKeyDown(Key.RightCtrl)) && args.KeyboardDevice.IsKeyDown(Key.C))
{
}
}

WPF 基础 - 事件的更多相关文章

  1. WPF基础到企业应用系列6——布局全接触

    本文转自:http://knightswarrior.blog.51cto.com/1792698/365351 一. 摘要 首先很高兴这个系列能得到大家的关注和支持,这段时间一直在研究Windows ...

  2. WPF基础知识、界面布局及控件Binding(转)

    WPF是和WinForm对应的,而其核心是数据驱动事件,在开发中显示的是UI界面和逻辑关系相分离的一种开放语言.UI界面是在XAML语言环境下开发人员可以进行一些自主设计的前台界面,逻辑关系还是基于c ...

  3. WPF基础知识、界面布局及控件Binding

    WPF是和WinForm对应的,而其核心是数据驱动事件,在开发中显示的是UI界面和逻辑关系相分离的一种开放语言.UI界面是在XAML语言环境下开发人员可以进行一些自主设计的前台界面,逻辑关系还是基于c ...

  4. WPF 基础到企业应用系列索引

    转自:http://www.cnblogs.com/zenghongliang/archive/2010/07/09/1774141.html WPF 基础到企业应用系列索引 WPF 基础到企业应用系 ...

  5. WPF笔记(1.1 WPF基础)——Hello,WPF!

    原文:WPF笔记(1.1 WPF基础)--Hello,WPF! Example 1-1. Minimal C# WPF application// MyApp.csusing System;using ...

  6. 【深入浅出Linux网络编程】 “基础 -- 事件触发机制”

    回顾一下“"开篇 -- 知其然,知其所以然"”中的两段代码,第一段虽然只使用1个线程但却也只能处理一个socket,第二段虽然能处理成百上千个socket但却需要创建同等数量的线程 ...

  7. WPF 在事件中绑定命令(不可以在模版中绑定命令)

    其实这也不属于MVVMLight系列中的东东了,没兴趣的朋友可以跳过这篇文章,本文主要介绍如何在WPF中实现将命令绑定到事件中. 上一篇中我们介绍了MVVMLight中的命令的用法,那么仅仅知道命令是 ...

  8. WPF自学入门(三)WPF路由事件之内置路由事件

    有没有想过在.NET中已经有了事件机制,为什么在WPF中不直接使用.NET事件要加入路由事件来取代事件呢?最直观的原因就是典型的WPF应用程序使用很多元素关联和组合起来,是否还记得在WPF自学入门(一 ...

  9. WPF路由事件二:路由事件的三种策略

    一.什么是路由事件 路由事件是一种可以针对元素树中的多个侦听器而不是仅仅针对引发该事件的对象调用处理程序的事件.路由事件是一个CLR事件. 路由事件与一般事件的区别在于:路由事件是一种用于元素树的事件 ...

随机推荐

  1. Dapr微服务应用开发系列0:概述

    题记:Dapr是什么,Dapr包含什么,为什么要用Dapr. Dapr是什么 Dapr(Distributed Application Runtime),是微软Azure内部创新孵化团队的一个开源项目 ...

  2. redis键过期时间

    redis服务器中每个数据库都是一个redisDb,而redisDb实质上是一个字典的模型,数据库的每一个键都是一个字典的键值,对数据库的增删改查也就是对字典对象的增删改查. redis在维护带有过期 ...

  3. 2019牛客多校第九场B Quadratic equation(二次剩余定理)题解

    题意: 传送门 已知\(0 <= x <= y < p, p = 1e9 + 7\)且有 \((x+y) = b\mod p\) \((x\times y)=c\mod p\) 求解 ...

  4. JVM升华篇

    01 Garbage Collect(垃圾回收) 1.1 如何确定一个对象是垃圾? 要想进行垃圾回收,得先知道什么样的对象是垃圾. 1.1.1 引用计数法 对于某个对象而言,只要应用程序中持有该对象的 ...

  5. Kafka 博文索引

    博文索引 KafkaBroker 简析 KafkaConsumer 简析 KafkaProducer 简析 KafkaMirrorMaker 的不足以及一些改进 Kafka 简介 数据是系统的燃料,系 ...

  6. 010. NET5_命令参数读取+配置多种读取

    上节课遗留问题:上节脚本启动后,CSS样式丢失问题 解决办法:a.拷贝丢失的wwwroot目录:b. 给UesStaticFiles类指定读取wwwroot目录 静态文件读取 Nuget引入:Micr ...

  7. iPhone 如何查看 Wi-Fi 密码

    iPhone 如何查看 Wi-Fi 密码 shit, 需要安装第三方软件 refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问! 原创文 ...

  8. 如何使用 VuePress 搭建一个 element-ui 风格的文档网站

    如何使用 VuePress 搭建一个 element-ui 风格的文档网站 { "devDependencies": { "vuepress": "1 ...

  9. 最新 Apple iPhone 12 价格 All In One

    最新 Apple iPhone 12 价格 All In One 美版价格 Apple iPhone 12 mini $699 Apple iPhone 12 $799 Apple iPhone 12 ...

  10. js inheritance all in one

    js inheritance all in one prototype & proto constructor Object.definepropety Object.create() js ...