1、定义、注册和封装路由事件

在自己定义一个依赖属性之前,首先,我们得学习下WPF框架中是怎么去定义的,然后按照WPF框架中定义的方式去试着自己定义一个依赖属性。由于Button按钮的Click事件是继承于ButtonBase.aspx)基类的,直接查看ButtonBase中Click事件的定义。路由事件的定义与依赖属性的定义类似,路由事件由只读的静态字段表示,在一个静态构造函数通过EventManager.RegisterRoutedEvent.aspx)函数注册,并且通过一个.NET事件定义进行包装。

  1. [Localizability(LocalizationCategory.Button), DefaultEvent("Click")]
  2. public abstract class ButtonBase : ContentControl, ICommandSource
  3. {
  4. // 事件定义
  5. public static readonly RoutedEvent ClickEvent;
  6. // 事件注册
  7. static ButtonBase()
  8. {
  9. ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
  10. CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonBase), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ButtonBase.OnCommandChanged)));
  11. .......
  12. }
  13. // 传统事件包装
  14. public event RoutedEventHandler Click
  15. {
  16. add
  17. {
  18. base.AddHandler(ClickEvent, value);
  19. }
  20. remove
  21. {
  22. base.RemoveHandler(ClickEvent, value);
  23. }
  24. }
  25. .......
  26. }

2、共享路由事件

与依赖属性一样,可以在类之间共享路由事件的定义。即实现路由事件的继承。例如UIElement.aspx)类和ContentElement类都使用了MouseUp事件,但MouseUp事件是由System.Windows.Input.Mouse.aspx)类定义的。UIElement类和ContentElement类只是通过RouteEvent.AddOwner.aspx)方法重用了MouseUp事件。可以在UIElement类的静态构造函数找到下面的代码:

  1. static UIElement()
  2. {
  3. _typeofThis = typeof(UIElement);
  4. PreviewMouseUpEvent = Mouse.PreviewMouseUpEvent.AddOwner(_typeofThis);
  5. MouseUpEvent = Mouse.MouseUpEvent.AddOwner(_typeofThis);
  6. }

3、引发和处理路由事件

尽管路由事件通过传统的.NET事件进行包装,但路由事件并不是通过.NET事件触发的,而是使用RaiseEvent方法触发事件,所有元素都从UIElement类继承了该方法。下面代码是具体ButtonBase类中触发路由事件的代码:

  1. protected virtual void OnClick()
  2. {
  3. RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this);
  4. base.RaiseEvent(e);// 通过RaiseEvent方法触发路由事件
  5. CommandHelpers.ExecuteCommandSource(this);
  6. }

对于路由事件的处理,与原来WinForm方式一样,你可以在XAML中直接连接一个事件处理程序,具体实现代码如下所示:

  1. <TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest">
  2. text label
  3. </TextBlock>
  4. // 后台cs代码
  5. private void SomethingClick(object sender, MouseButtonEventArgs e)
  6. {
  7. }

还可以通过后台代码的方式连接事件处理程序,

  1. tbxTest.MouseUp += new MouseButtonEventHandler(SomethingClick);
  2. // 或者省略委托类型
  3. tbxTest.MouseUp += SomethingClick;

4、事件路由

 路由事件的特殊性在于其传递性,WPF中的路由事件分为三种。

  • 与普通的.NET事件类似的直接路由事件(Direct event)。它源自一个元素,并且不传递给其他元素。例如,MouseEnter事件(当鼠标移动到一个元素上面时触发)就是一个直接路由事件。
  • 在包含层次中向上传递的冒泡路由事件(Bubbling event)。例如,MouseDown事件就是一个冒泡路由事件。它首先被单击的元素触发,接下来就是该元素的父元素触发,依此类推,直到WPF到达元素树的顶部为止。
  • 在包含层次中向下传递的隧道路由事件(Tunneling event)。例如PreviewKeyDown就是一个隧道路由事件。在一个窗口上按下某个键,首先是窗口,然后是更具体的容器,直到到达按下键时具有焦点的元素。

    5、冒泡路由事件

    1. <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue"
    2. BorderBrush="Black" BorderThickness="2" MouseUp="SomethingClick">
    3. <StackPanel MouseUp="SomethingClick">
    4. <TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest">
    5. Image and text label
    6. </TextBlock>
    7. <Image Source="pack://application:,,,/BubbleLabelClick;component/face.png" Stretch="None" MouseUp="SomethingClick"/>
    8. <TextBlock Margin="3" MouseUp="SomethingClick">
    9. Courtest for the StackPanel
    10. </TextBlock>
    11. </StackPanel>
    12. </Label>

MouseUp事件由下向上传递了5级,直到窗口级别结束。将RoutedEventArgs.Handled属性设置为true,表示事件已被处理,且该事件将终止向上冒泡

6、隧道路由事件

  1. <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue"
  2. BorderBrush="Black" BorderThickness="2" PreviewKeyDown="SomeKeyPressed">
  3. <StackPanel>
  4. <TextBlock Margin="3" PreviewKeyDown="SomeKeyPressed">
  5. Image and text label
  6. </TextBlock>
  7. <Image Source="face.png" Stretch="None" PreviewMouseUp="SomeKeyPressed"/>
  8. <DockPanel Margin="0,5,0,0" PreviewKeyDown="SomeKeyPressed">
  9. <TextBlock Margin="3"
  10. PreviewKeyDown="SomeKeyPressed">
  11. Type here:
  12. </TextBlock>
  13. <TextBox PreviewKeyDown="SomeKeyPressed" KeyDown="SomeKeyPressed"></TextBox>
  14. </DockPanel>
  15. </StackPanel>
  16. </Label>

隧道路由事件与冒泡路由事件的工作方式一样,只是方向相反。隧道路由事件都是以单词Preview开头。并且,WPF一般都成对地定义冒泡路由事件和隧道路由事件。这意味着如果发现一个冒泡的MouseUp事件,则对应的PreviewMouseUp就是一个隧道路由事件。另外,隧道路由事件总是在冒泡路由事件之前被触发。

7、附加事件

因为所有元素都支持MouseUp和PreviewKeyDown事件。然而,许多控件都有它们自己特殊的事件。例如按钮的的Click事件,其他任何类都有定义该事件。假设有这样一个场景,StackPanel面板中包含了一堆按钮,并且希望在一个事件处理程序中处理所有这些按钮的单击事件。首先想到的办法就是将每个按钮的Click事件关联到同一个事件处理程序。但是Click事件支持事件冒泡,从而有一种更好的解决办法。可以在更高层次元素来关联Click事件来处理所有按钮的单击事件,具体的XAML代码实现如下所示:

  1. <Window x:Class="AttachClickEvent.MainWindow"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. Title="MainWindow" Height="350" Width="525">
  5. <StackPanel Margin="3" Button.Click="DoSomething">
  6. <Button Name="btn1">Button 1</Button>
  7. <Button Name="btn2">Button 2</Button>
  8. <Button Name="btn3">Button 3</Button>
  9. </StackPanel>
  10. </Window>
  11. ================================================
  12. // 也可以在代码中关联附加事件,但是需要使用UIElement.AddHandle方法,而不能使用+=运算符的方式
  13. // StackPanel面板命名为ButtonsPanel
  14. ButtonsPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoSomething));

8、WPF事件生命周期

FrameworkElement类实现了ISupportInitialize.aspx)接口,该接口提供了两个用于控制初始化过程的方法。第一个是BeginInit.aspx)方法,在实例化元素后立即调用该方法。BeginInit方法被调用之后,XAML解析器设置所有元素的属性并添加内容。第二个是EndInit方法,当初始化完成后,该方法被调用。此时引发Initialized.aspx)事件。更准确地说,XAML解析器负责调用BeginInit方法和EndInit方法。
  当创建窗口时,每个元素分支都以自下而上的方式被初始化。这意味着位于深层的嵌套元素在它们容器之前先被初始化。当引发初始化事件时,可以确保元素树中当前元素以下的元素已经全部完成了初始化。但是,包含当前元素的容器还没有初始化,而且也不能假设窗口的其他部分也已经完成初始化了。在每个元素都完成初始化之后,还需要在它们的容器中进行布局、应用样式,如果需要的话还会进行数据绑定。
  一旦初始化过程完成后,就会引发Loaded事件。Loaded事件和Initialized事件的发生过程相反。意思就是说,包含所有元素的窗口首先引发Loaded事件,然后才是更深层次的嵌套元素。当所有元素都引发了Loaded事件之后,窗口就变得可见了,并且元素都已被呈现
第五章-路由事件 - 图1