星空雅梦

WPF教程(十三)路由事件

属性系统在WPF中得到升级、进化为依赖属性。事件系统在WPF中也被升级进化成为路由事件(Routed Event),并在其基础上衍生出命令传递机制。路由事件是一种可以针对元素树中的多个侦听器而不是仅仅针对引发该事件的对象调用处理程序的事件。

路由事件与一般事件(CLR)的区别在于:路由事件是一种用于元素树的事件,当路由事件触发后,它可以向上或向下遍历可视树和逻辑树,他用一种简单而持久的方式在每个元素上触发,而不需要任何定制的代码(如果用传统的方式实现一个操作,执行整个事件的调用则需要执行代码将事件串联起来)。

(一)WPF内置路由事件

内置路由事件,主要知识点就是它的路由策略:1) 冒泡:由事件源向上传递一直到根元素。2) 直接:只有事件源才有机会响应事件。3) 隧道:从元素树的根部调用事件处理程序并依次向下深入直到事件源。一般情况下,WPF提供的输入事件都是以隧道/冒泡对实现的。隧道事件常常被称为Preview事件,不妨一一举例实现:

  1.  
    //冒泡事件,由发起元素向外扩散
  2.  
    <Grid>
  3.  
    <Border Height="50" Width="250" BorderBrush="Gray" BorderThickness="1" MouseUp="Border_MouseUp">
  4.  
    <StackPanel Background="LightGray" Orientation="Horizontal" MouseUp="StackPanel_MouseUp">
  5.  
    <TextBlock Name="YesTB" Width="50" MouseUp="TextBlock_MouseUp" Background="Blue" >Yes</TextBlock>
  6.  
    </StackPanel>
  7.  
    </Border>
  8.  
    </Grid>
  1.  
    private void Border_MouseUp(object sender, EventArgs e)
  2.  
    {
  3.  
    MessageBox.Show("Border");
  4.  
    //e.Handled = true;
  5.  
    }
  6.  
    private void StackPanel_MouseUp(object sender, EventArgs e)
  7.  
    {
  8.  
    MessageBox.Show("Panel");
  9.  
    //e.Handled = true;
  10.  
    }
  11.  
    private void TextBlock_MouseUp(object sender, EventArgs e)
  12.  
    {
  13.  
    MessageBox.Show("TextBlock");
  14.  
    //e.Handled = true;
  15.  
    }

点击TextBlock事件路由顺序依次为TextBlock_MouseUp——StackPanel_MouseUp——Border_MouseUp。现在我们再看看隧道路由:

把冒泡路由的xaml文件中的MouseUp改为PreviewMouseUp,同样点击TextBlock,路由顺序依次为Border_MouseUp——StackPanel_MouseUp——TextBlock_MouseUp,这里我们应该注意的是EventArgs:平时我们看到比较多的是路由事件一般都用RoutedEventArgs,当然有区别,RoutedEventArgs的一个实例提供了4个有用的属性:

Source——逻辑树中开始触发该事件的的元素。

OriginalSource——可视树中一开始触发该事件的元素。

Handled——布尔值,设置为true表示事件已处理,在这里停止。

RoutedEvent——真正的路由事件对象,(如Button.ClickEvent)当一个事件处理程序同时用于多个路由事件时,它可以有效地识别被出发的事件。

所以将下面这句添加到上程序肯定报错,注释掉上程序终止路由功能的e.Handled=true肯定也会报错。

MessageBox.Show(e.OriginalSource.ToString());

直接路由,它不通过元素树路由,仅在源元素发生,与.net事件类似,但其支持其他路由事件功能,例如类处理、System.Windows.EventTrigger 或 System.Windows.EventSetter。

(二)自定义路由事件

自定义路由事件的添加,形式比较像依赖属性的实现,共分为三个步骤:

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

2、为路由事件添加CLR事件包装;

3、调用元素的RaiseEvent(继承自UIElement类),创建激发路由事件的方法。

下面我们写个传递点击按钮时间的自定义路由事件,进行一步步进行分析: 首先定义用于承载时间消息的事件参数,主要充当在事件中传递的参数的载体,必须继承自EventArgs类:

  1.  
    class ReportTimeRoutedEventArgs : RoutedEventArgs
  2.  
    {
  3.  
    public ReportTimeRoutedEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { }
  4.  
    public DateTime ClickTime { get; set; }
  5.  
    }

在激发路由事件时,事件传递的参数就是ReportTimeEventArgs类对象,而该对象保存这按钮被点击的时间。看其构造函数的两个参数一个是路由事件的对象,另一个是该路由事件的宿主。 接下来,从Button继承一个类,分别按照上述三个步骤添加自定义的路由事件:

  1.  
    class MyButton : Button
  2.  
    {
  3.  
    //第一步:声明并注册【路由事件】
  4.  
    //EventManager.RegisterRoutedEvent(CLR事件包装器名称,路由事件冒泡策略,事件处理程序的类型,路由事件的所有者类类型)
  5.  
    public static readonly RoutedEvent OpenEvent = EventManager.RegisterRoutedEvent("Open", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeRoutedEventArgs>), typeof(MyButton));
  6.  
    //第二步:为路由事件添加CLR事件包装器
  7.  
    public event RoutedEventHandler Open
  8.  
    {
  9.  
    //为指定的路由事件添加路由事件处理程序,并将该处理程序添加到当前元素的处理程序集合中。
  10.  
    add { this.AddHandler(OpenEvent, value); }
  11.  
    remove { this.RemoveHandler(OpenEvent, value); }
  12.  
    }
  13.  
    //第三步:激发路由事件
  14.  
    protected override void OnClick()
  15.  
    {
  16.  
    base.OnClick();
  17.  
    //创建事件携带信息(RoutedEventArgs类实例),并和路由事件关联
  18.  
    ReportTimeRoutedEventArgs args = new ReportTimeRoutedEventArgs(OpenEvent, this);
  19.  
    args.ClickTime = DateTime.Now;
  20.  
    //调用元素的RaiseEvent方法(继承自UIElement类),引发路由事件
  21.  
    this.RaiseEvent(args);
  22.  
    }
  23.  
    }

来看声明和注册的路由事件,和依赖属性类似,该路由事件也是static的和readoly的,也就是说是全局范围内的对象,其注册函数包含四个参数,分别是:将会被封装成的CLR事件包装器名称;路由事件的传递方式,是向下逐级传递,还是向上逐级传递,还是有目标的一次性传递;事件处理器的类型;宿主类型。 接下来看CLR事件的包装器,不同于依赖属性的SetValue和GetValue,这里是利用add和Remove两个函数来给路由事件分配事件处理器。

在OnClick函数中,我们激发该路由事件。首先声明了一个传递路由事件参数的ReportTimeRoutedEventArgs 类对象,其后参数由路由事件和Button的this构成,将其ClickTime属性赋值为当前点击的时间,然后通过RaiseEvent这一虚函数激发该事件。RaiseEvent的参数是RoutedEventArgs类型对象,而在该对象中包含了这个全局的路由事件和事件宿主信息。当该事件在传递的路由节点中,如果配置了针对该事件的处理器,该节点就能从全局找到该事件并进行相应操作。搭建以下界面,每点击一次按钮,就会在ListBox上显示按钮被点击的事件:

  1.  
    <Grid Name="grid" local:MyButton.Open="TimeButton_ReportTime">
  2.  
    <StackPanel Name="stackpanel" local:MyButton.Open="TimeButton_ReportTime" >
  3.  
    <ListBox Width="400" Height="220" Name="list" />
  4.  
    <local:MyButton x:Name="timebutton" Width="40" Height="80" Content="tex" local:MyButton.Open="TimeButton_ReportTime" />
  5.  
    </StackPanel>
  6.  
    </Grid>

最后写上事件处理器,如下。为了便于理解和加深印象,我们可以在后天订阅事件处理器,把所有注释取消调试走一遍,就会发现整个实现原理。

  1.  
    public MainWindow()
  2.  
    {
  3.  
     
  4.  
    InitializeComponent();
  5.  
    //this.timebutton.Open += TimeButton_ReportTime;
  6.  
    //this.stackpanel.AddHandler(MyButton.OpenEvent, new RoutedEventHandler(timebutton_report));
  7.  
    //this.grid.AddHandler(MyButton.OpenEvent, new RoutedEventHandler(timebutton_report));
  8.  
    }
  9.  
    //事件处理器
  10.  
    private void TimeButton_ReportTime(object sender, ReportTimeRoutedEventArgs e)//注意参数
  11.  
    {
  12.  
    //ReportTimeRoutedEventArgs x = e as ReportTimeRoutedEventArgs;
  13.  
    list.Items.Add(e.ClickTime.ToLongTimeString() + (sender as FrameworkElement).Name);
  14.  
    //list.Items.Add(x.joke+x.ClickTime.ToLongTimeString() + (sender as FrameworkElement).Name);
  15.  
    }
  16.  
    //private void timebutton_report(object sender,RoutedEventArgs e)
  17.  
    //{
  18.  
    //list.Items.Add("hello wpf!");
  19.  
    //}

(三)附加事件
路由事件的宿主都是是Button、Grid等这些我们可以在界面上看得见的控件对象,而附加事件的宿主是Binding类、Mouse类、KeyBoard类这种无法在界面显示的类对象,比如一个文本框的改变,鼠标的按下,键盘的按下这些事件都是附加事件的例子。附加事件的提出就是为了让这种我们无法看见的类也可以通过路由事件同其他类对象进行交流。另外,附加事件的本质也是路由事件,下面就让我们看下实例:

设计一个名为Student2的类,如果Student2实例的Name属性值发生了变化就激发一个路由事件,我会使用弹窗来捕捉这个事件。

  1.  
    public class Student2
  2.  
    {
  3.  
    public int Id { get; set; }
  4.  
    public string Name { get; set; }
  5.  
    //第一步:声明并注册【路由事件】 宿主为Student2类对象
  6.  
    public static readonly RoutedEvent AEvent = EventManager.RegisterRoutedEvent
  7.  
    ("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student2));
  8.  
    //第二步:为附加事件添加CLR事件包装器,将处理器归进集合
  9.  
    public static void AddAEventHandler(DependencyObject d,RoutedEventHandler h)
  10.  
    {
  11.  
    UIElement e = d as UIElement;
  12.  
    if (e != null)
  13.  
    {
  14.  
    e.AddHandler(Student2.AEvent, h);
  15.  
    }
  16.  
    }
  17.  
    //将处理器移出集合
  18.  
    public static void RemoveAEventHandler(DependencyObject d, RoutedEventHandler h)
  19.  
    {
  20.  
    UIElement e = d as UIElement;
  21.  
    if (e != null)
  22.  
    {
  23.  
    e.RemoveHandler(Student2.AEvent, h);
  24.  
    }
  25.  
    }
  26.  
    }

可以看出,宿主并非控件,而是一个Student2的类,不懂之处可以仔细看注释。

  1.  
    public partial class MainWindow : Window
  2.  
    {
  3.  
    public Student2 stu;
  4.  
    public MainWindow()
  5.  
    {
  6.  
    InitializeComponent();
  7.  
    //效果等同下句,给gridMain添加路由事件和订阅处理器
  8.  
    this.gridMain.AddHandler(Student2.AEvent, new RoutedEventHandler(F1));
  9.  
    //Student2.AddAEventHandler(this.gridMain, new RoutedEventHandler(F1));
  10.  
    stu = new Student2()
  11.  
    {
  12.  
    Id = 101,
  13.  
    Name = "Tim"
  14.  
    };
  15.  
    //可做成Name输入control
  16.  
    CinName("Tom");
  17.  
    }
  18.  
    private void CinName(string Name)
  19.  
    {
  20.  
    //此处不是点击激发,是名字改变激发
  21.  
    string tt = Name;
  22.  
    //第三步:激发路由事件
  23.  
    //创建事件携带信息(RoutedEventArgs类实例),并和路由事件关联
  24.  
    if (tt != stu.Name)
  25.  
    {
  26.  
    RoutedEventArgs args = new RoutedEventArgs(Student2.AEvent, stu);
  27.  
    //借助Button激发
  28.  
    this.Button.RaiseEvent(args);
  29.  
    }
  30.  
    }
  31.  
    public void F1(object sender, RoutedEventArgs e)
  32.  
    {
  33.  
    MessageBox.Show((e.OriginalSource as Student2).Id.ToString());
  34.  
    }
  35.  
    }

Student2 不是UIElement的派生类,所以它不具有RaiseEvent这个方法,为了发送路由事件就不得不"借用"一下 Button 的RaiseEvent激发函数了。CinName函数就是来激发的条件,比如上面自定义路由事件激发是靠OnClick()点击就可激发,我们这个是靠Name变化。gridMain.AddHandler是给gridMain添加这个路由事件及处理器,这里是后台写法。

  1.  
    <Grid>
  2.  
    <Grid Name="gridMain" >
  3.  
    <Button Name="Button" Content="Button" HorizontalAlignment="Left" Margin="117,203,0,0" VerticalAlignment="Top" Width="75"/>
  4.  
    </Grid>
  5.  
    </Grid>

这里要注意的一点是,想要上层开始监听处理(该程序路由策略选的是Bubble),必须要将Button写在gridMain下层,否则无法开始路由。

还有个特别情况,当路由事件用Button的时候,发现其MouseDown、MouseUp毫无反应。经过一番资料查找,发现是控件在捕获了MouseDown等事件后,内部会将该事件的"Handled"设置为True,这个属性是用在事件路由中的,当某个控件得到一个RoutedEvent,就会检测Handled是否为true,为true则忽略该事件。Click事件,相当于将它们事件抑制(Supress)掉了,转换成了Click事件。所以,如果一定要使用这个事件的话,需要在初始化的函数里利用UIElement的AddHandler方法,显式的增加这个事件,如下:

  1.  
    //如果为 true,则将按以下方式注册处理程序:即使路由事件在其事件数据中标记为已处理,也会调用该处理程序;如果为 false,则使用默认条件注册处理程序,即当路由事件已标记为已处理时,将不调用该处理程序。
  2.  
    //默认值为 false。
  3.  
    *.AddHandler(Button.MouseDownEvent, new RoutedEventHandler(处理器函数), true);

还有一种方法就是使用相应的Preview事件。须注意隧道类型的事件是从根元素开始执行的。

(四)Source 与 OriginalSource

RoutedEventArgs 携带的是路由事件关联的状态信息和事件数据,Source 表示 LogicalTree 上消息的源头;OriginalSource 表示 VisualTree 的源头(路由事件的消息是在 VisualTree 上传递的,非逻辑树)。

(总结)

Winform支持的CLR事件,但是WPF支持的路由事件,有什么不同呢?同样的逻辑树嵌套包含,同样是双击Control控件后的处理器,同样是用CLR方式添加(+=),WPF只要点击嵌套里面一个控件即可路由群响应,但是Winform就做不到,点击哪个反应哪个,因而我们可以看到其明显弊端:

1.每一个消息都是从发送到响应的一个过程,当一个处理器要用多次,必须建立显式的点对点订阅关系(窗体对按钮事件的订阅,如果是再有一个按钮的话,就要再来一次订阅);

2.事件的宿主必须能够直接访问事件的响应者,不然无法建立订阅关系(如有两个组件,点击组件1的按钮,想让组件2响应事件,那么就让组件2向组件1的按钮暴露一个可以访问的事件,这样如果再多几个嵌套,会出现事件链,有暴露如果暴露不当就存在着威胁)。

平时看到的Click、MouseDown、MouseMove等,都不是事件,只是路由事件的包装器,真正的事件是RoutedEvent,处理器的方法都是RoutedEvent订阅的。

一个事件可以由多个事件处理器(处理函数)来处理(多播),一个事件处理器可以响应多个事件。

一个元素添加了事件侦听器,就不关心是谁触发了,事件可以由触发元素一层一层向外传递,或由最外层一直往里传递,而非由元素开始才向内传。

事件就是定义一个路由事件,然后元素侦听此事件就行,相当于多播。

原文地址:https://www.cnblogs.com/LiZhongZhongY/p/10870586.html