WPF采取了路由事件機制,這樣事件可以在可視樹上層級傳遞。要知道 XAML 中控件都是由很多其他元素組合而成,比如我們單擊了 Button 內部的 TextBlock 元素,Button 依然可以可以接收到該事件並觸發 Button.Click。通常情況下,我們只是關心邏輯樹上的事件過程。
我們看看 Button Click 事件的實現。
public abstract class ButtonBase : ContentControl, ICommandSource
{
public static readonly RoutedEvent ClickEvent;
static ButtonBase()
{
ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ButtonBase));
......
}
public event RoutedEventHandler Click
{
add { base.AddHandler(ClickEvent, value); }
remove { base.RemoveHandler(ClickEvent, value); }
}
}
看上去很簡單,不是嗎?和依賴屬性有點類似。
注冊路由事件時,我們可以選擇不同的路由策略。
管道傳遞(Tunneling): 事件首先在根元素上觸發,然後向下層級傳遞,直到那個最初觸發事件的子元素。
冒泡(Bubbling): 事件從最初觸發事件的子元素向根元素層級往上傳遞。
直接(Direct): 事件僅在最初觸發事件的子元素上觸發。
Window1.xaml
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<Border MouseRightButtonDown="MouseRightButtonDown">
<StackPanel MouseRightButtonDown="MouseRightButtonDown">
<Button MouseRightButtonDown="MouseRightButtonDown">Test</Button>
</StackPanel>
</Border>
</Grid>
</Window>
Window1.xaml.cs
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show((sender as Label).Name);
}
}
在按鈕上單擊右鍵後,你會依次看到顯示 "Button"、"StackPanel"、"Border" 的三個對話框,顯然事件按照冒泡向根元素傳遞。
有一點需要注意,WPF 路由事件參數有個 Handled 屬性標記,一旦被某個程序標記為已處理,事件傳遞就會終止。測試一下。
public partial class Window1 : Window
{
private void MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show(sender.GetType().Name);
if (sender.GetType().Name == "StackPanel") e.Handled = true;
}
}
很有效,Border.MouseRightButtonDown 不在有效。嚴格來說,事件並沒有被終止,它依然會繼續傳遞個上級或下級的元素,只是 WPF 沒有觸發事件代碼而已。我們可以使用 AddHandler 方法重新注冊一個新的事件處理方法,使得可以繼續處理被終止的事件(注意: 如果事件沒有終止,這會導致兩次事件處理)。
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.border1.AddHandler(Border.MouseRightButtonDownEvent,
new MouseButtonEventHandler(MouseRightButtonDown), true);
}
private void MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show(sender.GetType().Name);
if (sender.GetType().Name == "StackPanel") e.Handled = true;
}
}
再運行試試,你會發現 Border.MouseRightButtonDown 被觸發了。
public void AddHandler(
RoutedEvent routedEvent,
Delegate handler,
bool handledEventsToo
)
handledEventsToo: 如果為 true,則將按以下方式注冊處理程序:即使路由事件在其事件數據中標記為已處理,也會調用該處理程序;如果為 false,則使用默認條件注冊處理程序,即當路由事件被標記為已處理時,將不調用處理程序。
通常情況下,WPF 控件會在管道事件的名稱前添加 Preview 前綴。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<Border PreviewMouseRightButtonDown="MouseRightButtonDown">
<StackPanel PreviewMouseRightButtonDown="MouseRightButtonDown">
<Button PreviewMouseRightButtonDown="MouseRightButtonDown">Test</Button>
</StackPanel>
</Border>
</Grid>
</Window>
這回的輸出結果正好跟前面的演示反過來,依次是 "Border"、"StackPanel"、"Button"。如果繼續保留事件終止代碼,那麼 Button.PreviewMouseRightButtonDown 就不再被觸發。
附加事件
和附加屬性類似,WPF 允許我們在一個沒有定義事件的元素上處理經管道或冒泡傳遞的路由事件。
Window1.xaml
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<StackPanel Button.Click="Click">
<Button>Test</Button>
</StackPanel>
</Grid>
</Window>
Window1.xaml.cs
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(e.Source.ToString());
}
}
輸出:
System.Windows.Controls.Button:Test
雖然 StackPanel 沒有 Click 事件,但我們依然捕獲了 Button 的點擊事件,還有就是注意附加屬性的寫法。不要想當然認為該示例表示只有 Button 類型的子元素才會觸發 Click 附加事件。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<StackPanel Button.Click="Click">
<Button>Test</Button>
<CheckBox>Check</CheckBox>
</StackPanel>
</Grid>
</Window>
很遺憾地告訴你,CheckBox 一樣觸發了 Click 附加事件。