路由事件

事件和委托参见C#事件和委托
路由事件是WPF中特有的,独特的事件,允许事件在元素树中进行路由。

传统的事件模式,事件的触发者和调用者是同一个,因为订阅者实际上也只是添加了自己的方法到发布者身上。而路由事件,发布者也就是事件的拥有者只管发布,事件由谁处理不归它管,和订阅者之间没有显式的订阅关系

路由:“起点与终点间有若干个中转站,从起点出发后经过每个中转站时要做出选择,最终以正确(比如最短或者最快)的路径到达终点。” 路由描述的就是这样的一个过程。
路由事件,是指事件的拥有者和响应者不必建立订阅关系,拥有者只管激发事件,响应者通过在自身设置事件监听器去监听对应的事件,并可以决定事件是否继续传播,如果说原始事件是两个人窃窃私语的话,那路由事件就是一队人挨个传话。当事件响应者通过事件监听器监听到某个事件的发生,通过事件携带的参数可以获取到事件的来源,从而做出判断该事件是否是自己关心的某个控件激发的,如果是,可以处理并停止事件的传播,如果不是,则放行不予理睬。
例如,可以将图像放在 Button的内部,这会有效地扩展按钮的可视化树。 但是,添加的图像不能破坏按钮的命中测试行为,它需要在用户单击图像像素时做出响应。

事件类型

WPF支持三种路由事件类型:冒泡(Bubbling)隧道(Tunneling)直接(Direct)

  • 冒泡事件(Bubbling Events):事件从事件源开始向父元素传播,直到根元素或被处理。(最常见的,也是默认的,如果没有显式声明事件被处理,还是会继续传播)
    例如,ButtonClick事件是一个冒泡事件。当按钮被点击时,事件会沿着元素树从按钮向上传播,直到到达根元素或被处理。

    <Button name="Button1" Click="Button_Click">Click Me</Button>
    

    等效于

    Button1.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(Button_Click));
    //或者 如果实现了CLR包装器
    //Button1.Click += Button_Click
    
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Button clicked!");
    }
    
  • 隧道事件(Tunneling Events):事件从根元素开始向子元素传播,直到事件源。隧道事件通常以“Preview”前缀命名,例如PreviewMouseDown。
    例如,PreviewKeyDown事件是一个隧道事件,当按下键盘按键时,事件会从根元素开始向下传播到事件源。

    <Window PreviewKeyDown="Window_PreviewKeyDown">
        <Grid>
            <Button>Click Me</Button>
        </Grid>
    </Window>
    
    private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        MessageBox.Show("Key pressed!");
    }
    

    很多事件即有隧道事件的版本,又有冒泡事件的版本,例如PreviewKeyDown 和KeyDown,称为一对事件,这对事件共享相同的事件参数,如KeyEventArgs,路由传播过程是先隧道传播,从父节点一直到子节点,然后再冒泡从子节点传播回父事件。如果在隧道传播时,就已经声明为已处理,不仅隧道传播会停止,冒泡传播也不会生效。

  • 直接事件(Direct Events):事件只在事件源元素上引发,不进行路由传播,类似于传统的.NET事件

为什么要这样设计

1.支持更合理的事件处理控制,不需要在子控件上单独处理事件,可以在父控件中处理子控件的事件。
比如常规窗体上的最大化,最小化,关闭按钮,如果使用常规的事件处理,需要在按钮上处理事件,但是通过路由事件,我们可以在父控件窗体上处理,提高代码的可维护性
2.可以配置传播路径,支持在某处进行处理或拦截事件,如果不处理可以一直传播,比较灵活,这样运行支持复杂的UI结构和自定义结构

3.同一类事件可以统一处理

<Border Height="30" Width="200" BorderBrush="Gray" BorderThickness="1">
    <StackPanel Background="LightBlue" Orientation="Horizontal" Button.Click="YesNoCancelButton_Click">
        <Button Name="YesButton">Yes</Button>
        <Button Name="NoButton">No</Button>
        <Button Name="CancelButton">Cancel</Button>
    </StackPanel>
</Border>

这三个按钮中的每一个都是潜在的 Click 事件源。 单击其中一个按钮时,它会引发 Click 事件,从按钮浮升到根元素。 Button和 Border元素没有附加事件处理程序,但 StackPanel有。
如果相关父元素上没有默认实现该事件,那么必须显式指定事件,比如上面的StackPanel 本身没有实现按钮的点击事件 ,所以要加Button.Click="YesNoCancelButton_Click"

private void YesNoCancelButton_Click(object sender, RoutedEventArgs e)
{
    FrameworkElement sourceFrameworkElement = e.Source as FrameworkElement;
    switch (sourceFrameworkElement.Name)
    {
        case "YesButton":
            // YesButton logic.
            break;
        case "NoButton":
            // NoButton logic.
            break;
        case "CancelButton":
            // CancelButton logic.
            break;
    }
    e.Handled = true;
}

最初引发路由事件的元素在事件处理程序参数中被标识为 RoutedEventArgs.Source(也就是上面的button)。
事件侦听器是附加和调用了事件处理程序的元素,它在事件处理程序参数中标识为 sender(上面的StackPanel)

路由事件支持在事件路由路线上的元素之间交换事件信息,因为每个侦听器都可以访问事件数据的同一实例。 如果事件数据中的某个元素更改了某些内容,则该更改对事件路由中的后续元素可见。

4.支持EventTrigger和EventSetter

自定义路由事件

  1. 注册路由事件 ,使用 EventManager.RegisterRoutedEvent
  2. 添加事件包装器,注意包装器名称一般和路由事件一致
  3. 添加触发
public class CustomButton : Button
{
    // Register a custom routed event using the Bubble routing strategy.
    public static readonly RoutedEvent ConditionalClickEvent = EventManager.RegisterRoutedEvent(
        name: "ConditionalClick",
        routingStrategy: RoutingStrategy.Bubble,
        handlerType: typeof(RoutedEventHandler),
        ownerType: typeof(CustomButton));

    // Provide CLR accessors for assigning an event handler.
    public event RoutedEventHandler ConditionalClick
    {
        add { AddHandler(ConditionalClickEvent, value); }
        remove { RemoveHandler(ConditionalClickEvent, value); }
    }

    void RaiseCustomRoutedEvent()
    {
        // Create a RoutedEventArgs instance.
        RoutedEventArgs routedEventArgs = new(routedEvent: ConditionalClickEvent);

        // Raise the event, which will bubble up through the element tree.
        RaiseEvent(routedEventArgs);
    }

    // For demo purposes, we use the Click event as a trigger.
    protected override void OnClick()
    {
        // Some condition combined with the Click event will trigger the ConditionalClick event.
        if (DateTime.Now > new DateTime())
            RaiseCustomRoutedEvent();

        // Call the base class OnClick() method so Click event subscribers are notified.
        base.OnClick();
    }
}

在XAML中使用自定义控件并监听事件。

<Window x:Class="CodeSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:custom="clr-namespace:WpfControl;assembly=WpfControlLibrary"
        Title="How to create a custom routed event" Height="100" Width="300">

    <StackPanel Name="StackPanel1" custom:CustomButton.ConditionalClick="Handler_ConditionalClick">
        <custom:CustomButton
            Name="customButton"
            ConditionalClick="Handler_ConditionalClick"
            Content="Click to trigger a custom routed event"
            Background="LightGray">
        </custom:CustomButton>
    </StackPanel>
</Window>

在对应cs代码中处理事件。

// The ConditionalClick event handler.
private void Handler_ConditionalClick(object sender, RoutedEventArgs e)
{
    string senderName = ((FrameworkElement)sender).Name;
    string sourceName = ((FrameworkElement)e.Source).Name;

    Debug.WriteLine($"Routed event handler attached to {senderName}, " +
        $"triggered by the ConditionalClick routed event raised on {sourceName}.");
}

// Debug output when CustomButton is clicked:
// Routed event handler attached to CustomButton,
//     triggered by the ConditionalClick routed event raised on CustomButton.
// Routed event handler attached to StackPanel1,
//     triggered by the ConditionalClick routed event raised on CustomButton.

RoutedEventArgs 是事件数据的基类,用于传递事件的相关信息。它包含事件的路由信息和其他数据

RaiseEvent 方法用于触发事件的实际传播。它会使用传递的 RoutedEventArgs 实例,并且根据 RoutedEventArgs 中的 RoutedEvent 来决定是哪个事件被触发。RaiseEvent 方法会根据事件的路由策略(在这里是 RoutingStrategy.Bubble)沿着控件树传播事件。

常见的路由事件
  • Mouse Events:如MouseDownMouseUpMouseMovePreviewMouseDownPreviewMouseUpPreviewMouseMove
  • Keyboard Events:如KeyDownKeyUpPreviewKeyDownPreviewKeyUp
  • Touch Events:如TouchDownTouchUpTouchMovePreviewTouchDownPreviewTouchUpPreviewTouchMove
事件传播

将事件声明为已处理来停止事件传播

你可以在事件处理程序中将RoutedEventArgsHandled属性设置为true,来停止事件的进一步传播。

private void Button_Click(object sender, RoutedEventArgs e)
{
    e.Handled = true;
    MessageBox.Show("Button clicked!");
}

如果想要处理已经处理的事件(一般情况下不会),那么可以使用 AddHandler(RoutedEvent, Delegate, Boolean)重载后台代码,将 handledEventsToo 参数设置为 true

底层原理

路由事件的底层实现基于 WPF 的 RoutedEvent 系统,这是一种能够在控件层级之间传递的事件。每个控件(如 UIElement)都支持注册、触发和处理路由事件。
路由事件的传播通过事件管理器 EventManager 进行处理,事件会沿着视觉树(VisualTree)或者逻辑树(LogicalTree)传播,因此支持父子元素之间的事件传递。
注册路由事件类似于注册依赖属性,会设置一个哈希表存储事件的信息,存储起来,使得 WPF 知道哪些事件是路由事件、如何传播它们、以及它们的路由策略(冒泡或隧道)。
事件的传播是由 WPF 的事件系统在控件树中自动管理的,当子控件触发一个路由事件(比如点击按钮时的 Button.Click 事件),WPF 的事件系统会将该事件注册到事件源(子控件)上,并开始执行事件路由的过程。
事件从事件源控件(例如按钮)开始传播。UIElement 的 RaiseEvent 方法会检测事件的路由策略,并决定如何沿着控件树传播事件。
事件会沿着控件的路径向上或者向下传播。每个控件在传播过程中会调用 OnEvent 方法,以便处理该事件。
如果某个父控件或中间控件的 OnEvent 方法处理了事件(例如调用了 AddHandler),该控件的事件处理程序会被执行。事件传播会在找到第一个处理程序后停止,除非事件设置为继续传播。

附加事件

附加事件(Attached Event)是一种特殊的路由事件,它允许事件由没有直接处理该事件的元素来声明和处理。这种事件类型通常用于使得子元素或关联的元素能够处理事件,即使它们并未在事件的原始触发元素的对象树中。

附加事件类似于附加属性。附加属性允许你在不是该属性定义类的对象上设置该属性值。附加事件则是让一个对象能够处理由其他对象引发的事件,或者说一个事件可以由没有直接关系的元素处理。

例如,WPF 中的 Button 类定义了一个 Click 事件,通常只能由 Button 自己或者其继承类处理。然而,附加事件的机制允许其他类(例如 GridStackPanel)声明自己对 ButtonClick 事件感兴趣。

2. 使用场景

附加事件通常用于以下场景:

  • 事件冒泡和隧道:当你想让父级容器或者祖先元素处理某个子元素的事件时,附加事件特别有用。举个例子,如果你有一个父级容器需要对其所有子按钮的点击做出反应,你可以在父级容器中处理 Button.Click 事件。
  • 在XAML中声明事件处理:附加事件可以在 XAML 中声明,允许你在布局中直接定义事件处理逻辑。

3. 创建和使用附加事件

创建附加事件时通常需要两部分:

  1. 注册事件:使用 EventManager.RegisterRoutedEvent 方法注册事件。(这里和路由事件一致)
  2. 声明包装器:附加事件不需要继承特点基类,因为WPF知道可以在附加事件的UIElement实例上调用UIElement.RemoveHandler或者UIElement.AddHandler方法(这里包装器是两个静态方法,和路由事件不一致)
// 在一个类中定义附加事件
public static class MyElement
{
    // 定义一个路由事件
    public static readonly RoutedEvent MyAttachedEvent = 
        EventManager.RegisterRoutedEvent(
            "MyAttached", 
            RoutingStrategy.Bubble, 
            typeof(RoutedEventHandler), 
            typeof(MyElement)
        );

    // 提供添加事件处理器的方法
    public static void AddMyAttachedHandler(DependencyObject d, RoutedEventHandler handler)
    {
        if (d is UIElement element)
        {
            element.AddHandler(MyAttachedEvent, handler);
        }
    }

    // 提供移除事件处理器的方法
    public static void RemoveMyAttachedHandler(DependencyObject d, RoutedEventHandler handler)
    {
        if (d is UIElement element)
        {
            element.RemoveHandler(MyAttachedEvent, handler);
        }
    }

    // 一个方法,用于触发事件,需要在别的
    public static void RaiseMyAttachedEvent(UIElement element)
    {
        element.RaiseEvent(new RoutedEventArgs(MyAttachedEvent));
    }
}

在XAML中使用:

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp"
        Title="MainWindow" Height="350" Width="525">
    <Grid local:MyElement.MyAttached="Grid_MyAttached">
        <!-- 其他子元素 -->
    </Grid>
</Window>

在 C# 代码中,你可以处理事件:

private void Grid_MyAttached(object sender, RoutedEventArgs e)
{
    MessageBox.Show("附加事件触发!");
}
    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}");
        }
 <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);
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 理解路由事件 路由事件是一种可以针对元素树中的多个侦听器而不是仅仅针对引发该事件的对象调用处理程序的事件,也就是说...
    东南有大树阅读 512评论 0 1
  • 注:本文出现的所有代码为了简单明了均省略了很多细节部分,只注重原理,直接复制粘贴运行得不到对应的结果。 WPF的数...
    quchangTJU阅读 4,251评论 0 4
  • 本文介绍1.路由策略2.冒泡事件3.隧道事件4.直接事件5.自定义事件 路由策略 WPF路由事件的策略分为以下三种...
    Elvis523阅读 949评论 0 1
  • 二、使用 2.1 例子 2.2 路由类型 冒泡路由(从下到上) 隧道路由(从上到下) 直接路由 2.3 添加处理程...
    落地成佛阅读 397评论 0 0
  • 事件 事件的前身是消息(在窗体点左键会生成一条携带参数的消息加入windows待处理消息队列,当windows处理...
    李霖弢阅读 994评论 0 0