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. Kubernets二进制安装(19)之集群平滑升级

    在实际生产环境中,部署好的集群稳定就行了,但是,如果需要使用到新的功能或当前版本出现了严重的漏洞,都建议做升级,本教程是将node节点从v1.15.10版本平滑升级到v1.15.12版本,如果升级到相 ...

  2. 操作系统 part5

    1.线程安全 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用.不会出现数据不一致或者数据污染. 线程不安全就 ...

  3. Linux 驱动框架---platform驱动框架

    Linux系统的驱动框架主要就是三个主要部分组成,驱动.总线.设备.现在常见的嵌入式SOC已经不是单纯的CPU的概念了,它们都会在片上集成很多外设电路,这些外设都挂接在SOC内部的总线上,不同与IIC ...

  4. TypeScript enum 枚举实现原理

    TypeScript enum 枚举实现原理 反向映射 https://www.typescriptlang.org/docs/handbook/enums.html enum Direction { ...

  5. js useful skills blogs

    js useful skills blogs blogs https://davidwalsh.name/tutorials/javascript https://www.ruanyifeng.com ...

  6. Koa & node.js

    KOA https://github.com/koajs/koa https://koajs.com/ $ nvm install 7 # node.js 7 + $ nvm install 10 $ ...

  7. Cookie 政策

    Cookie 政策 合规/隐私协议 https://www.synology.cn/zh-cn/company/legal/cookie_policy Cookie Cookie 政策 生效日期:20 ...

  8. import script module

    import script module .mjs <script type="module"> import {addTextToBody} from './util ...

  9. uniapp 创建简单的tabs

    tabs组件 <template> <view class="tabs"> <view class="bar" :style=&q ...

  10. js 脏检测

    参考 基础知识 <!DOCTYPE html> <html lang="en"> <head> <meta charset="U ...