路由事件实战
UIElement类为键盘、鼠标和手写输入设备定义了许多路由事件,它们之中大多数是上升式(bubbling)事件,但其中也有许多事件拥有一个下降式的副本。下降式事件可以很容易识别出来,因为根据习惯,它们都包含Preview前缀,并且习惯上在其上升式副本被触发之前立即得到触发。例如,PreviewMouseMove是一个下降式事件,它在MouseMove上升式事件之前被触发。
隐藏在这一对事件之后的思想是给予元素一种有效取消或修改即将发生的事件的机会。一般地,WPF内建元素仅对上升式事件起作用(在同时定义了上升式和下降式事件的时候),即确保下降式事件的“preview”前缀名副其实。例如,设想我们想要实现一个限制输入内容为一个确定的模式或正字表达式的文本框。如果我们处理TextBox的KeyDown事件,我们最好的做法是将已经显示在TextBox中的文本清除。但是如果我们处理TextBox的PreviewKeyDown事件,那么我们可以将它标记为“已处理”,这样不仅终止了事件向下传递,而且终止了KeyDown事件触发的事件源向上传递。对于这种情形,TextBox永远都不会接收到KeyDown的通知,继而当前的字符永远不会被显示出来。
下面的例子描述一个简单的上升式事件的用法:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AboutDialog" MouseRightButtonDown="Window_MouseRightButtonDown"
Title="WPF揭秘" SizeToContent="WidthAndHeight" Background="OrangeRed">
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF揭秘(版本3.0)
</Label>
<Label>(C)2006 SAMS 出版集团</Label>
<Label>安装的章节:</Label>
<ListBox>
<ListBoxItem>第一章</ListBoxItem>
<ListBoxItem>第二章</ListBoxItem>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button MinWidth="75" Margin="10">Help</Button>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>您已经成功注册了本产品。</StatusBar>
</StackPanel>
</Window>
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls;
public partial class AboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
}
private void AboutDialog_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
this.Title = "Source = " + e.Source.GetType().Name + ", OriginalSource = " +
e.OriginalSource.GetType().Name + " @ " + e.Timestamp;
Control source = e.Source as Control;
if (source.BorderThickness != new Thickness(5))
{
source.BorderThickness = new Thickness(5);
source.BorderBrush = Brushes.Black;
}
else source.BorderThickness = new Thickness(0);
}
}
无论何时右键单击事件上传到Window对象,AboutDialog_MouseRightButtonDown事件处理方法会执行两个动作:(1)在Window的标题栏显示事件的有关信息;(2)添加(随后移除)一个粗黑边框围绕在右键单击的逻辑树元素外面。值得注意的是,当我们右键单击Label时,它的Source是当前Label的引用,而其OriginalSource则是其视觉子元素TextBlock的引用。
如果我们运行这个示例并且在任意元素上单击右键,我们将会注意到两种有趣的行为:
(1)当我们右键单击任意ListBoxItem的时候,Window都不会接收到MouseRightButtonDown事件。这是因为ListBoxItem内部处理了这个事件,这与MouseLeftButton事件实现了项目选择一样,都是终止了事件上传。
(2)右键单击Button时,Window可以接收到MouseRightButtonDown事件,但是为按钮设置Border属性不会有变化。这是因为按钮的默认视觉树不像Window、Label、ListBox、ListBoxItem和StatusBar,树中根本就没有Border元素。
尽管将RoutedEventArgs的Handled属性设置为true似乎终止事件向上或向下传递,但通过程序代码借助AddHandler的重载就可以打破这个规则。如下例:
public AboutDialog()
{
InitializeComponent();
this.AddHandler(Window.MouseRightButtonDownEvent,
new MouseButtonEventHandler(AboutDialog_MouseRightButtonDown), true);
}
将第三个参数设置为true,AboutDialog_MouseRightButtonDown事件处理方法就可以在右键单击ListBoxItem时为其添加边框了。
我们应当在任何可能的时候避免处理已处理完成的事件,因为事件应该在它发生的处理方法中得到处理。为Preview版本的事件附加一个事件处理器是更好的选择。
终止事件上/下传递不切实际,更加正确的说法是,事件上/下传递在路由事件被标记为已处理的时候仍然会继续,但是事件处理器方法在默认情况下只能接收到未处理的事件。