WPF 路由事件

一、什么是路由事件
1.功能定义
路由事件是一种可以针对 元素树 中的多个 侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件。
2. 实现定义
路由事件是由类的实例支持的 CLR 事件,由 WPF 事件系统进行处理。

二、路由策略
1.冒泡
调用事件源上的事件处理程序,随后会路由到后续的父级元素,直到到达元素树的根,其中路由方向是从应用程序引发事件的元素传到根,并在事件数据中报告为源。

2.预览
预览事件,也称为隧道事件,是路由事件,其中路由的方向从应用程序根传到引发事件的元素,并在事件数据中报告为源。

3.直接
只有源元素本身才有机会调用处理程序以进行响应。

三、路由事件的使用

1.定义一个路由事件

// 这是一个常规路由事件的写法
public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
    "Tap", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyButtonSimple));

// Provide CLR accessors for the event
public event RoutedEventHandler Tap
{
        add { AddHandler(TapEvent, value); }
        remove { RemoveHandler(TapEvent, value); }
}

// 这是一个附加路由事件的写法
internal class MyRoutedEvents
{
    public static readonly RoutedEvent NeedsCleaningEvent = EventManager.RegisterRoutedEvent("NeedsCleaning", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(MyRoutedEvents));

    public static void AddNeedsCleaningHandler(DependencyObject d, RoutedEventHandler handler)
    {
        UIElement uie = d as UIElement;
        if (uie != null)
        {
            uie.AddHandler(MyRoutedEvents.NeedsCleaningEvent, handler);
        }
    }
    public static void RemoveNeedsCleaningHandler(DependencyObject d, RoutedEventHandler handler)
    {
        UIElement uie = d as UIElement;
        if (uie != null)
        {
            uie.RemoveHandler(MyRoutedEvents.NeedsCleaningEvent, handler);
        }
    }
}

其实两者也没多大差别,路由事件只是对常规的路由事件做了一个简单的封装而已。

2.引发路由事件
凡是继承自 UIElement 的元素都可以引发一个路由事件。可以调用UIElement.RaiseEvent(RoutedEventArgs e)方法引发一个路由事件,举个栗子:

// 主函数
    internal class MainClass
    {
        [STAThread]
        private static void Main()
        {
            var application = new Application();
            application.Run(new MainWindow());
        }
    }

// 主窗口,接收冒泡的路由事件。
    internal class MainWindow : Window
    {
        public MainWindow()
        {
            Width = 400;
            Height = 647;
            WindowStartupLocation = WindowStartupLocation.CenterScreen;
            this.Loaded += MainWindow_Loaded;

            // 订阅路由事件。
            this.AddHandler(MyUserControl.TapEvent, new RoutedEventHandler((sender, arg) =>
            {
                MessageBox.Show("Receive event as MainWindow.");
            }));
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
             this.AddChild(new MyUserControl());
        }
    }

// 用户控件,用于触发路由事件
    internal sealed class MyUserControl : UserControl
    {
        // 这是一个常规路由事件的写法
        public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
            "Tap", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyUserControl));

        // Provide CLR accessors for the event
        public event RoutedEventHandler Tap
        {
            add => AddHandler(TapEvent, value);
            remove => RemoveHandler(TapEvent, value);
        }

        public MyUserControl()
        {
            var myButton = new Button
            {
                Content = "Toggle Event",
                Width = 200,
                Height = 20
            };
            myButton.Click += MyButton_Click;
            this.AddChild(myButton);

            // 自己也订阅一个Tap事件
            this.AddHandler(TapEvent, new RoutedEventHandler((s, args) =>
            {
                MessageBox.Show("Receive event as myself.");
            }));
        }

        private void MyButton_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            this.RaiseEvent(new RoutedEventArgs(TapEvent, this));
        }
    }

在这个例子钟,RoutingStrategy.Bubble 参数是指定这个事件的路由策略,点击 Toggle Event 按钮,首先弹出 “Receive event as myself.”,然后再弹出 “Receive event as MainWindow.”,如果将 Bubble 改为 Tunnel 则弹出对话框的顺序相反。

刚才的栗子是一个常规路由事件的例子,那现在举一个附加事件的例子吧!如下:


    // 这是一个附加路由事件的写法
    internal class MyRoutedEvents
    {
        public static readonly RoutedEvent NeedsCleaningEvent = EventManager.RegisterRoutedEvent("NeedsCleaning", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyRoutedEvents));

        public static void AddNeedsCleaningHandler(DependencyObject d, RoutedEventHandler handler)
        {
            UIElement uie = d as UIElement;
            if (uie != null)
            {
                uie.AddHandler(MyRoutedEvents.NeedsCleaningEvent, handler);
            }
        }
        public static void RemoveNeedsCleaningHandler(DependencyObject d, RoutedEventHandler handler)
        {
            UIElement uie = d as UIElement;
            if (uie != null)
            {
                uie.RemoveHandler(MyRoutedEvents.NeedsCleaningEvent, handler);
            }
        }
    }

// 用于触发事件的用户控件。
    internal sealed class MyUserControl : UserControl
    {
        public MyUserControl()
        {
            var myButton = new Button
            {
                Content = "Toggle Event",
                Width = 200,
                Height = 20
            };
            myButton.Click += MyButton_Click;
            this.AddChild(myButton);

            // 自己也订阅一个NeedsCleaning事件
            MyRoutedEvents.AddNeedsCleaningHandler(this, new RoutedEventHandler((s, args) =>
            {
                MessageBox.Show("Receive event as myself.");
            }));
        }

        private void MyButton_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            this.RaiseEvent(new RoutedEventArgs(MyRoutedEvents.NeedsCleaningEvent, this));
        }
    }
    
    // 主窗口,用于接收用户控件。
    internal class MainWindow : Window
    {
        public MainWindow()
        {
            Width = 400;
            Height = 647;
            WindowStartupLocation = WindowStartupLocation.CenterScreen;
            this.Loaded += MainWindow_Loaded;

            // 自己也订阅一个NeedsCleaning事件
            MyRoutedEvents.AddNeedsCleaningHandler(this, new RoutedEventHandler((s, args) =>
            {
                MessageBox.Show("Receive event as MainWindow.");
            }));
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
             this.AddChild(new MyUserControl());
        }
    }

    // 主函数
    internal class MainClass
    {
        [STAThread]
        private static void Main()
        {
            var application = new Application();
            application.Run(new MainWindow());
        }
    }    

同样,点击 Toggle Event 按钮,首先弹出 “Receive event as myself.”,然后再弹出 “Receive event as MainWindow.”,如果将 Bubble 改为 Tunnel 则弹出对话框的顺序相反。

四、路由事件实现原理

        // Constructor for a RoutedEvent (is internal 
        // to the EventManager and is onvoked when a new
        // RoutedEvent is registered)
        internal RoutedEvent(
            string name,
            RoutingStrategy routingStrategy,
            Type handlerType,
            Type ownerType)
        {
            _name = name;
            _routingStrategy = routingStrategy;
            _handlerType = handlerType;
            _ownerType = ownerType;

            _globalIndex = GlobalEventManager.GetNextAvailableGlobalIndex(this);
        }

        // _globalIndexToEventMap 是一个用于存储 RoutedEvent 的一个数组,路由事件的 _globalIndex 索引就是路由事件在 _globalIndexToEventMap 数组中的索引。
        internal static int GetNextAvailableGlobalIndex(object value)
        {
            int index;
            lock (Synchronized)
            {
                // Prevent GlobalIndex from overflow. RoutedEvents are meant to be static members and are to be registered 
                // only via static constructors. However there is no cheap way of ensuring this, without having to do a stack walk. Hence 
                // concievably people could register RoutedEvents via instance methods and therefore cause the GlobalIndex to 
                // overflow. This check will explicitly catch this error, instead of silently malfuntioning.
                if (_globalIndexToEventMap.Count >= Int32.MaxValue)
                {
                    throw new InvalidOperationException(SR.Get(SRID.TooManyRoutedEvents));
                }

                index = _globalIndexToEventMap.Add(value);
            }
            return index;
        }

路由事件通常都是静态的,存储在 GlobalEventManager 类型的 _globalIndexToEventMap 字段里。

posted @   RafaelLxf  阅读(273)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示