迟到的 WPF 学习 —— 路由事件

1. 理解路由事件:WPF 通过事件路由(event routing)概念增强了传统的事件执行的能力和范围,允许源自某个元素的事件由另一个元素引发,例如,事件路由允许工具栏上的一个按钮点击的事件在被代码处理之前上传到工具栏,再由工具栏上传到所属窗体

2. 定义、注册和包装路由事件:和依赖性属性类似,它由只读的静态字段表示,在一个静态构造函数中注册,并通过一个标准的 .Net 事件定义进行包装。如 Button 的 Click 事件,该事件继承自抽象的 ButtonBase 基类

public abstract class ButtonBase:ContentControl...
{
//The event definition
public static readonly RoutedEvent ClickEvent; // event registration
static ButtonBase()
{
ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent(
"Click",RoutingStrategy.Bubble,typeof(RoutedEventHandler),typeof(ButtonBase));
...
} // The traditional event wrapper
public event RoutedEventHandler Click
{
add{
base.AddHandler(ButtonBase.ClickEvent,value);
}
remove{
base.RemoveHandler(ButtonBase.ClickEvent,value);
}
}
}

通过使用普通的事件定义进行包装,确保所有 .Net 的语言都能访问它。AddHandler 和 RemoveHandler 是在 FrameworkElement 中定义的,并被每个 WPF 元素继承。

3. 处理路由事件:从语法上,XAML 可以这样写

<Image Source="happy.jpg" Stretch="None" Name="img" MouseUp="img_MouseUp"/>

比较约定俗成的命名方式是“元素名_事件名”,如没有交互的极简事件也可以不命名元素

<Button Click="cmdOk_Click">OK</Button>

C# 还允许流线型的写法,隐式创建合适的委托对象

img.MouseUp += img_MouseUp;

上面的代码依赖事件包装器,事件包装器调用的是 UIElement.AddHandler() 方法,也可以直接调用直连事件

img.AddHandler(Image.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));

也可以使用“定义事件的类的名称”而不是用“引用事件的类的名称”,如改造成下面的写法

img.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));

两种写法在语法上是等效的,但第二种的缺点是,不能明确的指明 MouseUpEvent 是由 Image 提供的,在嵌套的元素中,可能会引起混论和遗忘,是代码难于理解。

想要断开事件,只能使用代码

img.MouseUp -= img_MouseUp;

或 UIElement.RemoveHandler() 方法

img.RemoveHandler(Image.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));

为同一事件多次连接相同的事件处理程序,在技术上是可行的,但属于编码错误,事件处理程序会被触发多次,即 += 就多加一次,-= 也只减一次

4. 事件路由:模拟一个场景,一个 Label 包含一个 StackPanel 面板,面板中包含两个 TextBlock 和一个 Image。现在,想要点击 Label 中的任意元素都触发同一个事件处理程序。当然可以为每一个元素的 MouseUp 和 MouseDown 都关联到相同的事件处理程序,但这将使代码看起来混乱不好维护,WPF 使用事件路由模型提供了更好的方法。

路由事件有三种方式实现:

直接路由事件(Direct event),起源于一个元素,不传递其它元素。

在包含的层次中向上传递的冒泡路由事件(Bubbling event),例如 MouseDown 就是一个泡泡事件,首先由改元素引发、再由父元素引发、最后被父元素的父元素引发,直至 WPF 元素树顶部。

在包含的层次中向下传递的隧道路由事件(Tunneling event),隧道事件在到达恰当的控件前为预览事件(或可能终止的事件)创造了机会,例如,PreviewKeyDown 可以截获是否按下了一个键,首先在窗口级别上,然后是容器,直到到达按下键时具有焦点的元素。

使用 EventManger.RegisterEvent 注册事件时,传递 RoutingStrategy 枚举值指示希望应用于事件的行为

5. 处理挂起的事件:当事件的 Handle 属性被置为 true 意味着通知其它侦听对象此事件被处理过了,有一个方法用于接收被处理过的事件,它必须使用前面介绍过的 AddHandler 方法的一个重载版本,将第三个 bool 类型参数设置为 true(举以下代码这个例子不太恰当,会有隐患,在此遵照原内容)

cmdClecr.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(cmdClear_MouseUp),true)

6. 附加事件:如果一个 StackPanel 里边有数个 Button 并且希望这些 Button 在点击时都能处理相同的事件,通常的想法是将所有 Button 的 Click 事件都关联到相同的处理程序,但在 WPF 中,可以通过在更高层次的元素上处理这个事件来解决,如下面

<StackPanel Click="DoSomething">
<Button Name="cmd1">Command 1</Button>
<Button Name="cmd2">Command 2</Button>
<Button Name="cmd3">Command 3</Button>
</StackPanel>

看起来写的很美,但实际不能运行,因为 StackPanel 木有 Click 事件,咋办?可以这样

<StackPanel Button.Click="DoSomething"/>

以“类名.事件名”的方式使用不同的关联事件的语法,解释一下,Click 是在 ButtonBase 中定义的,Button 继承自 ButtonBase。如果使用 ButtonBase.Click 的方式那么所有继承自 ButtonBase 的控件在单击时,都将使用“DoSomething”事件处理程序,Button.Click 更加精确。可以在代码中关联附加事件,但必须要用 AddHandler 而不是 +=,如(StackPanel 被命名为 pnlButtons)

pnlButtons.AddHandler(Button.Click,new RoutedEventHandler(DoSomething));

这会碰到一个问题,DoSomething 方法中如果想知道具体是谁引发了事件怎么办?可以依靠判断 sender 比如 sender == cmd1,但这需要为每个 Button 都设置 Name 属性,另一个选择是随按钮传递一个可在代码中使用的信息,比如为每个 Button 设置一个 Tag 属性

<StackPanel Click="DoSomething">
<Button Name="cmd1" Tag=“The first button”>Command 1</Button>
<Button Name="cmd2" Tag=“The second button”>Command 2</Button>
<Button Name="cmd3" Tag=“The third button”>Command 3</Button>
</StackPanel>

然后在 DoSomething 中这样

private void DoSomething(object sender,RoutedEventArgs e )
{
object tag = ((FrameworkElement)sender).Tag;
MessageBox.Show((string)tag);
}

7. 隧道路由:和冒泡类型,方向相反,在一个控件中引发的事件最先从顶层(通常是窗体)开始引发,然后是容器,最后到达实际引发的控件。隧道事件容易识别,它们都是以 Preview 开头,WPF 通常成对的定义冒泡和隧道,且隧道路由事件总是在冒泡路由事件前被引发。注意,如果隧道事件被标记为已处理,那么冒泡将不会被引发,这是因为两者共享一个 RoutedEventArg 对象的实例。

点击下载代码 —— 需 .Net Framework 4 或以上

上一篇:PDF.js Electron Viewer


下一篇:URLEncoder 和URLDecoder