10.路由事件

  • 10.路由事件已关闭评论
  • 76 次浏览
  • A+
所属分类:.NET技术
摘要

先看一段代码:这个布局最终形成的逻辑树应该是下面这个样子在WPF的元素树中,若某一个元素引发了一个事件,那么这个事件会沿着整棵树进行传播,而开发者可以在事件传播的沿途进行侦听(有点像设立关卡打劫)。一旦侦听到这个事件,便可以执行事件的回调函数。当然,只有被声明为RoutedEvent路由事件才具备传播功能

先看一段代码:

<Window x:Class="HelloWorld.MainWindow"         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"         xmlns:local="clr-namespace:HelloWorld"          xmlns:controls="clr-namespace:HelloWorld.Controls"         xmlns:helper="clr-namespace:HelloWorld.MVVM"         mc:Ignorable="d"          Title="WPF中文网 - www.wpfsoft.com" Height="350" Width="500">     <Window.DataContext>         <local:MainViewModel/>     </Window.DataContext>     <Border>         <Canvas>             <Button Content="确定" Width="80" Height="30" Canvas.Left="150" Canvas.Top="100"/>             <Button Content="取消" Width="80" Height="30" Canvas.Left="280" Canvas.Top="100"/>         </Canvas>     </Border> </Window>

这个布局最终形成的逻辑树应该是下面这个样子

<Window>     <Border>         <Canvas>             <Button/>             <Button/>         </Canvas>     </Border> </Window> 

在WPF的元素树中,若某一个元素引发了一个事件,那么这个事件会沿着整棵树进行传播,而开发者可以在事件传播的沿途进行侦听(有点像设立关卡打劫)。一旦侦听到这个事件,便可以执行事件的回调函数。当然,只有被声明为RoutedEvent路由事件才具备传播功能

隧道事件:

我们以上面的代码为例,假如用户单击了确定按钮,此时,从整个窗体的视角来看,窗体会说,哎呀,你把我给单击了,且单击的是我其中的确定按钮,于是,这个单击事件首先会经历第一个关卡——Window对象。如果开发者恰好订阅了Window对象的单击事件,那首先被执行的就是Window窗体的单击事件回调函数。紧接着单击事件会经历第二道关卡——Border对象,第三道关卡——Canvas对象,直到确定按钮为止。可见这个单击事件一路经历了4个控件,它们分别是Window->Border->Canvas->Button,开发者可以在这4个控件上去订阅单击事件。像这个从根目录一直路由到事件源对象的路由事件,我们称为隧道事件(预览事件),这类事件都是Preview单词开头

冒泡事件:

但是,从Button按钮的视角,用户肯定是先单击的我呀,我这里才是事件源,事件应该就像小孩向水中投石之后,平静的水面会泛起一圈圈的涟漪,最终消失在岸边——即最外层的Window窗体对象。那么,此时的事件路由方向就反过来了,Button->Canvas->Border->Window。像这种从事件源一直路由到元素树根的路由事件,我们称为冒泡事件。

直接事件:

除了这两种事件,WPF还支持直接事件,即只有事件源才能响应触发的事件,我们称为直接事件。三种事件由定义事件时通过RoutingStrategy 枚举进行标识。

public enum RoutingStrategy {     //     // 摘要:     //     路由事件使用隧道策略,以便事件实例通过树向下路由(从根到源元素)。     Tunnel = 0,     //     // 摘要:     //     路由事件使用冒泡策略,以便事件实例通过树向上路由(从事件元素到根)。     Bubble = 1,     //     // 摘要:     //     路由事件不通过元素树路由,但支持其他路由的事件功能。     Direct = 2 }

我们为Window、Border、Canvas、Button的PreviewMouseUp隧道事件都订阅了回调函数,然后F5调试运行,并单击确定按钮,观察输出结果如下:

Window对象的隧道事件PreviewMouseUp被触发 Border对象的隧道事件PreviewMouseUp被触发 Canvas对象的隧道事件PreviewMouseUp被触发 Button确定按钮的隧道事件PreviewMouseUp被触发

击的确定Button按钮,但是首先被触发的却是元素树的根元素Window对象,最后才是Button对象

 

我们为Window、Border、Canvas、Button的MouseUp冒泡事件都订阅了回调函数,然后F5调试运行,并单击确定按钮,观察输出结果如下

Canvas对象的冒泡事件MouseUp被触发 Border对象的冒泡事件MouseUp被触发 Window对象的冒泡事件MouseUp被触发

总结:第一点,从输出结果看,隧道事件是从根元素路由到事件源,冒泡事件是从事件源路由到根元素。第二点,如果同时订阅了隧道事件和冒泡事件,那么两条路由路线都将执行。谁先谁后?隧道事件先完成路由

 

路由事件实践:

注册一个路由事件,是向WPF的事件系统注册的,由静态类EventManager提供注册服务,具体则交给RegisterRoutedEvent方法成员完成。

public static readonly RoutedEvent 路由事件名称 = EventManager.RegisterRoutedEvent(             name: "路由事件名称",             routingStrategy: 冒泡事件/隧道事件/直接事件,             handlerType: 路由事件委托的反射实例,             ownerType: 路由事件拥有者的反射实例;

当一个路由事件注册到WPF的事件系统之后,还需要利用event关键词对其进行包装,包装成普通事件的样子,方便开发者调用。

/// <summary> /// 通过event包装成普通事件的外观 /// </summary> public event RoutedEventHandler 普通事件名称 {     add { AddHandler(路由事件名称, value); }     remove { RemoveHandler(路由事件名称, value); } }

最后,寻找合适的时候,利用UIElement基类中的RaiseEvent,触发这个路由事件即可。这样就完成了路由事件的注册流程。

<UserControl x:Class="HelloWorld.Controls.Widget"              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"               xmlns:d="http://schemas.microsoft.com/expression/blend/2008"               xmlns:local="clr-namespace:HelloWorld.Controls"              mc:Ignorable="d"               x:Name="userControl"              FontSize="30"              Foreground="#666666"              BorderBrush="#8CDDCD"              d:DesignHeight="450"               d:DesignWidth="800">     <Border BorderBrush="{Binding ElementName=userControl,Path=BorderBrush}">         <Border.Style>             <Style TargetType="Border">                 <Setter Property="Padding" Value="10"/>                 <Setter Property="Background" Value="White"/>                 <Setter Property="BorderBrush" Value="#8CDDCD"/>                 <Setter Property="BorderThickness" Value="0 3 0 0"/>                 <Setter Property="Margin" Value="0"/>                 <Style.Triggers>                     <Trigger Property="IsMouseOver" Value="True">                         <Setter Property="Background" Value="#F7F9F9"/>                     </Trigger>                 </Style.Triggers>             </Style>         </Border.Style>         <Grid>             <Grid.ColumnDefinitions>                 <ColumnDefinition/>                 <ColumnDefinition Width="auto"/>             </Grid.ColumnDefinitions>             <Grid.RowDefinitions>                 <RowDefinition/>                 <RowDefinition/>             </Grid.RowDefinitions>             <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Value,StringFormat={}{0:C}}"                         Foreground="{Binding ElementName=userControl,Path=Foreground}"                         FontSize="{Binding ElementName=userControl,Path=FontSize}" />             <TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Title}"                         Foreground="{Binding ElementName=userControl,Path=Foreground}"                         FontSize="14" TextWrapping="Wrap"/>             <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Icon}"                         Foreground="{Binding ElementName=userControl,Path=BorderBrush}"                         FontSize="26" Grid.RowSpan="2" VerticalAlignment="Center"/>         </Grid>         </Border> </UserControl>

Widget自定义控件的后端代码

/// <summary> /// Widget.xaml 的交互逻辑 /// </summary> public partial class Widget : UserControl {       public Widget()     {         InitializeComponent();           DataContext = this;     }       /// <summary>     /// 注册RoutedEvent路由事件     /// </summary>     public static readonly RoutedEvent CompletedEvent = EventManager.RegisterRoutedEvent(         name: "CompletedEvent",         routingStrategy: RoutingStrategy.Bubble,         handlerType: typeof(RoutedEventHandler),         ownerType: typeof(Widget));       /// <summary>     /// 通过event包装成普通事件的外观     /// </summary>     public event RoutedEventHandler Completed     {         add { AddHandler(CompletedEvent, value); }         remove { RemoveHandler(CompletedEvent, value); }     }       /// <summary>     /// 触发路由事件     /// </summary>     void RaiseRoutedEvent()     {         RoutedEventArgs routedEventArgs = new RoutedEventArgs(CompletedEvent, this);         RaiseEvent(routedEventArgs);     }         public string Icon     {         get { return (string)GetValue(IconProperty); }         set { SetValue(IconProperty, value); }     }       public static readonly DependencyProperty IconProperty =         DependencyProperty.Register("Icon", typeof(string), typeof(Widget),              new PropertyMetadata(""));         public string Title     {         get { return (string)GetValue(TitleProperty); }         set { SetValue(TitleProperty, value); }     }       public static readonly DependencyProperty TitleProperty =         DependencyProperty.Register("Title", typeof(string), typeof(Widget),              new PropertyMetadata("请输入标题"));       /// <summary>     /// 销售目标     /// </summary>     public double Target     {         get { return (double)GetValue(TargetProperty); }         set { SetValue(TargetProperty, value); }     }       public static readonly DependencyProperty TargetProperty =         DependencyProperty.Register("Target", typeof(double), typeof(Widget),              new PropertyMetadata(0.0));      //依赖属性     public double Value     {         get { return (double)GetValue(ValueProperty); }         set { SetValue(ValueProperty, value); }     }  /*当Value大于Target时,触发Completed事件*/     public static readonly DependencyProperty ValueProperty =         DependencyProperty.Register("Value", typeof(double), typeof(Widget),             new PropertyMetadata(0.0,new PropertyChangedCallback(OnValuePropertyChangedCallback)));       private static void OnValuePropertyChangedCallback(DependencyObject d,          DependencyPropertyChangedEventArgs e)     {         if(d is Widget control && e.NewValue is double value )         {             if (value >= control.Target && control.Target != 0)//当业绩大于100万美元时             {                 control.Icon = "";                                     control.RaiseRoutedEvent();//触发路由事件,完成销售目标             }             else             {                 control.Icon = "";             }         }     } }

我们在ValueProperty 的回调函数中去判断销售业务,如果完成销售目标,就引发完成事件。接下来演示怎么使用这个控件。首先在主窗体中实例化Widget,并将其Value绑定到一个Slider的Value

<Window x:Class="HelloWorld.MainWindow"         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"         xmlns:local="clr-namespace:HelloWorld"          xmlns:controls="clr-namespace:HelloWorld.Controls"         xmlns:helper="clr-namespace:HelloWorld.MVVM"         mc:Ignorable="d" Background="Lavender"         MouseUp="Window_MouseUp" PreviewMouseUp="Window_PreviewMouseUp"                 Title="WPF中文网 - www.wpfsoft.com" Height="350" Width="500">     <Window.DataContext>         <local:MainViewModel/>     </Window.DataContext>     <Border MouseUp="Border_MouseUp" PreviewMouseUp="Border_PreviewMouseUp"              Background="Transparent">         <Canvas MouseUp="Canvas_MouseUp" PreviewMouseUp="Canvas_PreviewMouseUp"                  Background="Transparent">                          <controls:Widget Value="{Binding ElementName=slider,Path=Value}"                               Target="1000000"                              Completed="Widget_Completed"                              Title="第四季度北美市场总销售额统计"                               Canvas.Left="116" Canvas.Top="17"                               Height="103" Width="269"/>                          <Slider x:Name="slider" Value="0" Maximum="2000000"                      Canvas.Left="116" Canvas.Top="138" Width="269"/>                          <ListBox x:Name="listBox" Height="133"                       Canvas.Left="116" Canvas.Top="167" Width="269"/>         </Canvas>     </Border> </Window>

当用户拖动Slider时,Widget的Value值就跟着改变,并判断当前销售业绩与目标业绩,我们像订阅普通事件一样,Completed="Widget_Completed"表示订阅完成事件。

private void Widget_Completed(object sender, RoutedEventArgs e) {     Widget widget = sender as Widget;     listBox.Items.Insert(0, $"完成目标销售额:{widget.Value}"); }

CompletedEvent路由事件在注册时被注册成RoutingStrategy.Bubble类型,即冒泡事件,如果想注册成隧道事件,建议取名为:PreviewCompletedEvent,并使用PreviewCompleted进行包装。