WPF高級教程(七)路由事件

介紹

與依賴項屬性一樣,路由事件是WPF對於傳統.NET事件的升級,使得事件擁有更強的傳播能力。

定義,註冊和包裝

// 我們來看一個Click事件定義的例子
public abstract class ButtonBase : ContentControl
{
    // 定義路由事件
    public static readonly RoutedEvent ClickEvent;
    
    // 註冊路由事件
    static ButtonBase()
    {
        ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
    }
    
    // 普通事件包裝路由事件
    public event RoutedEventHandler Click
    {
        add
        {
            // AddHandler 和 RemoveHandler都是再FrameworkElement中定義的
            base.AddHandler(ButtonBase.ClickEvent, value);
        }
        remove
        {
            base.RemoveHandler(ButtonBase.ClickEvent, value);
        }
    }
}

共享

通過上面的定義我們可以看到,與依賴項屬性一樣,路由事件也是靜態定義,包裝爲普通事件使用,那麼我們自然就可以推測我們可以像依賴項屬性一樣,將別的類的路由事件作爲己用,這裏我們需要使用 RoutedEvent.AddOwner()方法。

// UIElement類添加Mouse類的MouseUp事件
UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement));

使用

路由事件的引發

使用RaiseEvent方法引發路由事件

RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent, this);
base.RaiseEvent(e);

路由事件的處理

監聽事件

// 在xaml中直接處理
<button Click="cmdOK_Click">OK</Button>
// 後臺代碼連接事件
img.MouseUp += new MouseButtonEventHandler(img_MouseUp);
// 甚至可以簡化代碼
img.MouseUp += img_MouseUp;
// 直接調用AddHandler方法,不通過事件包裝器綁定
img.AddHandler(Image.MouseUpEvent, new MouseButtonEventHandler(image_MouseUp));
// 由於我們之前講過的,這是一個共享事件,UIElement Image Mouse中的MouseUp方法都是共享的,也可以AddHandler到UIElement中處理,這兩種是等價的。唯一的區別就是不寫Image不太容易看出MouseUp是由Image引發的
img.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(image_MouseUp));

事件處理程序都有一個Args的參數,Args都繼承自RoutedEventArgs,包含下面的屬性
在這裏插入圖片描述

斷開路由事件

斷開路由事件不能在xaml中實現,必須使用代碼 -=

// 使用-=運算符
img.MouseUp -= img_MouseUp;
// 直接使用RemoveHandler方法
img.RemoveHandler(Image.MouseUpEvent, new MouseButtonEventHandler(image_MouseUp));

路由事件的分類

分類描述

  • 直接路由事件 起源於一個元素,不會傳遞給下一個元素
  • 冒泡路由事件 沿着元素樹向上傳遞(MouseUp事件),如果不處理的話,一直傳遞到元素樹最上層元素
  • 隧道路由事件 沿着元素樹向下傳遞(KeyDown事件),隧道事件爲提前處理事件提供了機會,比如PreviewKeyDown事件

設置事件種類

在使用EventManager.RegistEvent方法註冊一個事件的時候需要傳遞一個RoutingStrategy的枚舉值,設置事件的種類。

1. 冒泡路由事件

xaml代碼:

<Window x:Class="Charles.WPF.View.TestBubblingEvent"
        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:Charles.WPF.View"
        mc:Ignorable="d"
        Title="TestBubblingEvent" Height="359" Width="329" MouseUp="SomethingClick">
    <Grid>
        <Grid Margin="3" MouseUp="SomethingClick">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="Auto"></RowDefinition>
                <RowDefinition Height="Auto"></RowDefinition>
            </Grid.RowDefinitions>
            <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left"
                   Background="AliceBlue" BorderBrush="Black" BorderThickness="1" MouseUp="SomethingClick">
                <StackPanel MouseUp="SomethingClick">
                    <TextBlock Margin="3" MouseUp="SomethingClick">Image and text label</TextBlock>
                    <Image Source="/Image/1.jpg" Stretch="None" MouseUp="SomethingClick" Width="30" Height="30"></Image>
                    <TextBlock Margin="3" MouseUp="SomethingClick">Courtesy of the StackPanel.</TextBlock>
                </StackPanel>
            </Label>
            <ListBox Grid.Row="1" Margin="5" Name="lstMessage"></ListBox>
            <CheckBox Grid.Row="2" Margin="5" Padding="3" HorizontalAlignment="Right"
                      Name="chk_Handle">Handle first event</CheckBox>
            <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right" Name="cmd_Clear" Click="cmd_Clear_Click">
                Clear List
            </Button>
        </Grid>
    </Grid>
</Window>

後臺代碼:

public partial class TestBubblingEvent : Window
{
    protected int eventCount = 0;
    public TestBubblingEvent()
    {
        InitializeComponent();
    }

    private void SomethingClick(object sender, MouseButtonEventArgs e)
    {
        eventCount++;
        string message = "#" + eventCount.ToString() + ":\r\n" +
            " Sender: " + sender.ToString() + ":\r\n" +
            " Source: " + e.Source + ":\r\n" +
            " Original Source: " + e.OriginalSource;
        lstMessage.Items.Add(message);
        e.Handled = (bool)chk_Handle.IsChecked;
    }

    private void cmd_Clear_Click(object sender, RoutedEventArgs e)
    {
        lstMessage.Items.Clear();
    }
}

這個例子說明了冒泡事件的事件傳遞順序,點擊圖片的時候,事件由Image觸發,層層向上傳遞,不碰到e.Handled = True不會終止傳遞。

一些冒泡事件的技巧:

  • Sender是當前事件觸發的控件,Source是觸發源
  • 事件的處理使用 MouseButtonEventArgs 和 RoutedEventArgs都是可以的
  • 我們也監聽了窗口的MouseUp事件,這就讓我們在窗口的任意空白位置點擊之後都會觸發MouseUp事件,但是我們發現,點擊按鈕的時候不觸發窗口的MouseUp事件,而是觸發Button的Click事件,這是因爲Button的源代碼中掛起了MouseUp事件,並且引發了一個更高級的Click事件
  • WinForm中,大多數控件都擁有Click事件,在WPF中,只有少數控件擁有Click事件
  • 有一種方法可以監聽到Handled爲True的事件,雖然這是不推薦的,但是是可以實現的,通過傳遞最後一個參數true,可以接收到掛起的事件(不推薦)
    cmdClear.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(cmdClear_MouseUp), true);
    
  • 上面我們講到,大部分控件都沒有Click事件,那Click事件如何冒泡呢?事實上,Click事件支持冒泡,但是處理Click事件需要一些特殊的技巧
    // StatckPanel 沒有Click但是要監聽裏面Button的Click
    // 用下面的方法是不行的
    <StackPanel Click="DoSomething">
        <Button/>
        <Button/>
        <Button/>
    </StackPanel>
    
    這樣寫會報錯,因爲StackPanel並沒有Click事件,這時候我們想要監聽所有Button的Click事件,需要用到一個附件事件的技巧
    <StackPanel Button.Click="DoSomething">
        <Button/>
        <Button/>
        <Button/>
    </StackPanel>
    
    使用類名加上事件可以獲取StackPanel中Button的Click事件,這就是附加屬性的使用。在代碼中,需要注意不能使用 += 的方法進行事件處理方法的綁定,因爲+=默認就綁定到StackPanel上了,需要使用AndHandler方法
    // StackPanel 的名字爲 pnlButtons
    pnlButtons.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));
    
    要確定是StackPanel中哪個按鈕引發了事件,可以使用下面的方法
    // 方法1. 使用控件的x:Name屬性
    private void DoSomething(object sender, RoutedEventArgs args)
    {
        // cmd1是按鈕控件1的Name屬性
        if(sender == cmd1)
        {
            // 觸發的是第一個按鈕
        }
        else if(sender == cmd1)
        ...
    }
    // 方法2. 給按鈕添加Tag
    <Button Tag="first button"/>
    
    object tag = ((FrameworkElement)sender).Tag;
    

2. 隧道路由事件

  • 隧道路由事件的工作方式和冒泡路由事件相同,但是方向相反
  • 隧道路由事件都是以Preview開頭的事件
  • 隧道路由事件和冒泡路由事件公用同一個RoutedEventArgs,所以如果把隧道路由事件標記爲已處理,冒泡路由事件就不會觸發,這個屬性很適合用於做預處理
  • 隧道事件總是在冒泡事件之前觸發
  • 從下圖可以看到,事件的觸發先下去後上來,所以在任意中間元素中讓e.Handler=true則冒泡事件都不會觸發
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章