WPF原理剖析——路由事件

一、路由事件与传统事件
传统事件的触发者和处理者是紧密相连的,而路由事件则不是,路由事件允许一个元素的事件有另外的元素触发。
也即就是说路由事件的拥有者和响应者之间没有显示的订阅关系。事件的拥有者只负责激发事件,事件将由谁处理他们并不知道(可以只引发事件没有响应,但是如果这个在传统事件里面的话是会产生异常的。),

1、事件触发者

复制代码
//这个类负责事件的定义和触发
    class MailManager
    {
        //第二步:定义事件成员
        public event EventHandler<NewMailEventArgs> NewMail;
        //第三步:定义一个负责引发事件的方法通知已经登记的对象
        protected virtual void OnNewMail(NewMailEventArgs e)
        {
           // if (NewMail != null)//如果我们没有在引发事件的时候订阅事件,会产生异常(未将对象的引用设置到实例)
                NewMail(this, e);
        }
        //第四步:将输入转化为期望事件,这里其实是对第三步的一个包装
        public void RaiseEvent(string from, string to, string subject)
        {
            NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
            OnNewMail(e);
        }
    }
复制代码

2、事件响应者

复制代码
 class Program
    {
        static void Main(string[] args)
        {
            MailManager mailManager = new MailManager();
            //订阅事件
            //mailManager.NewMail += new EventHandler<NewMailEventArgs> (mailManager_NewMail);//假设我们在引发事件之前没有订阅事件(相当于实例化一个事件)
            //调用方法引发事件
            mailManager.RaiseEvent("a", "b", "事件邮件");
            Console.ReadKey();
        }
        //处理事件
        static void mailManager_NewMail(object sender, NewMailEventArgs e)
        {
            Console.WriteLine("收到一封主题为{0},是从{1}到{2}", e.Subject, e.From, e.To);
        }
    }
复制代码

大家可以看到我在编写传统事件的时候在事件响应者中去掉了事件的订阅,然后我直接引发了事件,这样就会产生一个“未将对象引用设置到对象的实例”,因为我们的事件是一个用Event修饰委托,而委托又会在编译的时候生成一个类。当我们没有事件订阅者的时候相当于,我们声明了一个类,但是没有创建他的实例,就开始调用它的实例方法。
例如我们有一个类A,里面包含一个方法Add(),

public A a;//声明类A
//a=new A();//这里我们直接省去这一步(即在事件过程中缺少事件的订阅)
a.Add();//这里我们调用Add方法的时候就会产生异常

通过上面的例子大家肯定明白了路由事件的触发者和响应者之间没有显示的订阅关系,这句话。

二、路由事件的注册(例子来源深入浅出WPF)
第一步:创建一个继承至RoutedEventArgs类的路由事件参数类,以便传递给事件响应着一些参数信息;如果我们没有什么其他额外信息需要传递的话,我们可以直接使用RoutedEventArgs这个路由事件参数基类。

public class ReportTimeEventArgs:RoutedEventArgs
    {
        public DateTime ClickTime { get; set; }
        public ReportTimeEventArgs(RoutedEvent routedEvent, object source)
            : base(routedEvent, source) { }
    }

第二步:注册路由事件,并且添加普通事件包装器和事件激发方法。

复制代码
/// <summary>
    /// 创建一个报时按钮
    /// </summary>
   public 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);}
        }
        //激发路由事件,借用Click事件的激发方法
        protected override void OnClick()
        {
             base.OnClick();
            ReportTimeEventArgs args=new ReportTimeEventArgs (ReportTimeEvent,this);//这个事件参数初始化的时候包含的参数提供了事件的来源
            args.ClickTime=DateTime.Now;
            this.RaiseEvent(args);//激发事件,这里和普通事件不同(普通事件激发是有事件包装器激发的)
        }
    }
复制代码

Note:
 1、路由事件注册方法RegisterRoutedEvent方法和注册依赖属性的方法很类似,他需要4个参数,普通事件包装器的名字、路由事件策略、事件处理类型、事件拥有者;
 2、事件包装器,这里的目的主要是为了适应以前学习.net同学的方便这样我们就可以使用+=或者-=,我们其实可以直接调用AddHandler或者RemoveHandler方法来添加和删除事件;
 3、激发事件方法调用的是RaiseEvent(),这个方法是在UIElement类中定义的,即所有UI元素都有这个方法。

三、路由事件的策略
 1、Bubble(冒泡模式):事件从自己激发一直传递到根元素;
 2、Tunnel(隧道模式):事件从根元素传递到自己;
 3、Direct(直接模式):和传统事件一样。

复制代码
<Window Name="wimdow1">
   <Grid x:Name="grid1"  local:TimeButton.ReportTime="ReportTimeHandler">
        <Grid x:Name="grid2"  local:TimeButton.ReportTime="ReportTimeHandler">
            <Grid x:Name="grid3"  local:TimeButton.ReportTime="ReportTimeHandler">
                <StackPanel x:Name="stackPanel"  local:TimeButton.ReportTime="ReportTimeHandler">
                    <ListBox x:Name="listBox"/>
                    <local:TimeButton x:Name="timeButton" Width="80" Height="80" Content="报时" local:TimeButton.ReportTime="ReportTimeHandler" ></local:TimeButton>
                </StackPanel>
            </Grid>
        </Grid>
    </Grid>
</Window>
复制代码

上面代码我为每一个空间都设置了对TimeButton事件的侦听;所以当事件在上面传递的时候,都可以处理。下面是三种模式的输出结果:

Bubble(冒泡)
   

Tunnel(隧道)

  

Direct(直接)

四、路由事件中的几个注意字段
sender:这个是事件处理器中方法的触发源控件,他是当前控件而不是事件的最开始的发生源控件;
OriginalSource:这个是VisualTree树上的最开始发生源控件;
Source:这个是LogicTree树上的最开始发生源的控件。

五、附加事件

其实在上面我们就使用了附加事件,例如我们在给除TimeButton控件之外的空间都附加上了TimeButton控件的ReportTime事件;这样子的一个好处可以是我们统一处理一些控件的事件,例如我们一个stackpanel包含了多个button控件;这时候我们就可以将他们的click事件统一由stackpanel控件来处理,但是stackPanel又没有click事件,这时候我们就可以使用button.Click=""附加给stackpanel控件。其实这里也是WPF中路由事件的一种应用。我们不需要为每一个button控件都包含click事件的处理器,而可以统一由路由事件经过的visualtree上其他元素来处理。

原文地址:https://www.cnblogs.com/bruce1992/p/14152840.html