1.CLR事件模型(直接事件模型)
事件的响应者通过订阅关系直接关联在事件拥有者的事件上。
弊端:
每对消息是“发送-->响应”关系,必须建立显示的点对点订阅关系。
事件的宿主必须能够直接访问事件的响应者,不然无法建立订阅关系。
2.路由事件模型
2.1 定义
路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件。2.2 路由事件与直接事件的区别
路由事件与直接事件的区别在于,直接事件激发时,发送者直接将消息通过事件订阅交送给事件响应者,事件响应者使用其事件处理器方法对事件的发生做出响应、驱动程序逻辑按客户需求运行;路由事件的事件拥有者和事件响应者之间没有直接显示的订阅关系,事件的拥有者只负责激发事件,事件将由谁响应它并不知道,事件的响应者则安装有事件侦听器,针对某类事件进行侦听,当有此类事件传递至此时事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。2.3 CLR事件和路由事件关联和解除关联事件处理函数的差别
传统的CLR事件+=的背后 | Delegate.Combine(…) |
路由事件+=的背后 | AddHandler(…) |
传统的CLR事件-=的背后 | Delegate.Remove(…) |
路由事件-=的背后 | RemoveHandler(…) |
3.路由事件实例
XAML代码:
<Window x:Class="WpfApplication21.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid x:Name="grd1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Border x:Name="bd1">
<StackPanel x:Name="panel1" Orientation="Vertical" Button.Click="panel1_btnclick">
<Button x:Name="btn1" Height="50" Content="按钮1" Click="btn1_click"/>
<Button x:Name="btn2" Height="50" Content="按钮2"/>
</StackPanel>
</Border>
</Grid>
</Window>
C#的代码:
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication21
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//btn2.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.btn2_click));
//支持CLR事件+=
btn2.Click += btn2_click;
bd1.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.bd1_btnclick));
grd1.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.grd1_btnclick));
}
private void grd1_btnclick(object sender, RoutedEventArgs e)
{
Debug.WriteLine(string.Format("事件响应者:{0},按钮名称:{1}", "grd1", (e.OriginalSource as Button).Name));
}
private void bd1_btnclick(object sender, RoutedEventArgs e)
{
Debug.WriteLine(string.Format("事件响应者:{0},按钮名称:{1}", "bd1", (e.OriginalSource as Button).Name));
//事件不再传递
e.Handled = true;
}
private void btn2_click(object sender, RoutedEventArgs e)
{
Debug.WriteLine(string.Format("事件响应者:{0},按钮名称:{1}", "btn2", (e.OriginalSource as Button).Name));
}
private void btn1_click(object sender, RoutedEventArgs e)
{
Debug.WriteLine(string.Format("事件响应者:{0},按钮名称:{1}", "btn1", (e.OriginalSource as Button).Name));
}
private void panel1_btnclick(object sender, RoutedEventArgs e)
{
Debug.WriteLine(string.Format("事件响应者:{0},按钮名称:{1}", "panel1", (e.OriginalSource as Button).Name));
}
}
}
程序界面:
输出:
事件响应者:btn1,按钮名称:btn1
事件响应者:panel1,按钮名称:btn1
事件响应者:bd1,按钮名称:btn1
事件响应者:btn2,按钮名称:btn2
事件响应者:panel1,按钮名称:btn2
事件响应者:bd1,按钮名称:btn2
4.识别路由事件
比如ButtonBase.Click事件,在MSDN中有如下说明:
5.路由策略和事件处理程序
5.1 路由策略
所谓路由策略就是事件触发遍历整棵元素树的方式,这些策略由RoutingStrategy枚举值提供。- 冒泡:针对事件源调用事件处理程序。 路由事件随后会路由到后续的父元素,直到到达元素树的根。 大多数路由事件都使用冒泡路由策略。 冒泡路由事件通常用来报告来自不同控件或其他 UI 元素的输入或状态变化。
- 直接:只有源元素本身才有机会调用处理程序以进行响应。 这与 Windows Forms用于事件的“路由”相似。 但是,与标准 CLR 事件不同的是,直接路由事件支持类处理而且可以由 EventSetter 和 EventTrigger 使用。
- 隧道:最初将在元素树的根处调用事件处理程序。 随后,路由事件将朝着路由事件的源节点元素(即引发路由事件的元素)方向,沿路由线路传播到后续的子元素。 在合成控件的过程中通常会使用或处理隧道路由事件,这样,就可以有意地禁止显示复合部件中的事件,或者将其替换为特定于整个控件的事件。 在 WPF 中提供的输入事件通常是以隧道/冒泡对实现的。 隧道事件有时又称作 Preview 事件,这是由隧道/冒泡对所使用的命名约定决定的。
5.2 路由处理程序
路由事件的事件处理程序有一个签名,它与通用.NET事件处理程序的模式匹配:第一个参数是一个System.Object对象,名为sender,第二个参数(一般命名为e)是一个派生自System.EventArgs的类。传递给事件处理程序的sender参数就是该处理程序被添加到的元素。参数e是RoutedEventArgs的一个实例(或者派生自RoutedEventArgs),RoutedEventArgs是EventArgs的一个子类,它提供了4个有用的属性:- Source——逻辑树中一开始触发该事件的元素。
- OriginalSource——可视树中一开始触发该事件的元素(例如,TextBlock或者标准Button元素的ButtonChrome子元素)。
- Handled——布尔值,设置为true表示标记事件为已处理,这就是用于停止Tunneling或Bubbling的标记。
- RoutedEvent——真正的路由事件对象(如Button.ClickEvent),当一个事件处理程序同时被用于多个路由事件时,它可以有效地识别被触发的事件。
6.路由事件的作用
- 路由事件侦听器和路由事件源不必在其层次结构中共享公用事件。 任何 UIElement 或 ContentElement 可以是任一路由事件的事件侦听器。
- 路由事件还可以用来通过元素树进行通信,因为事件的事件数据会永存到路由中的每个元素中。 一个元素可以更改事件数据中的某项内容,该更改将对于路由中的下一个元素可用。(使用方法参见源代码)
- 某些 WPF 样式和模板功能(如 EventSetter 和 EventTrigger)要求被引用的事件是路由事件。
- 路由事件支持类处理机制,类可以凭借该机制来指定静态方法,这些静态方法能够在任何已注册的实例程序访问路由事件之前,处理这些路由事件。 这在控件设计中非常有用,因为您的类可以强制执行事件驱动的类行为,以防它们在处理实例上的事件时被意外禁止。
7.类处理程序
添加类处理的两种方法:
- 重写原有的虚方法。
- 使用 EventManager 类 RegisterClassHandler 的实用工具方法直接添加类处理。(代码参考第8点自定义路由事件)。
8.自定义路由事件
为Label添加Click事件。
public class MyLabel : Label
{
static MyLabel()
{
EventManager.RegisterClassHandler(typeof(MyLabel), ClickEvent, new RoutedEventHandler(LocalClick));
}
//添加的类处理程序
private static void LocalClick(object sender, RoutedEventArgs e)
{
Debug.WriteLine("添加的类处理程序。");
}
//创建和注册Click事件,该事件的路由策略为Bubble
public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent("Click",
RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyLabel));
//CLR事件包装器
public event RoutedEventHandler Click
{
add { AddHandler(ClickEvent, value); }
remove { RemoveHandler(ClickEvent, value); }
}
//触发ClickEvent
void RaiseClickEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(MyLabel.ClickEvent);
RaiseEvent(newEventArgs);
}
//使用左击事件来触发ClickEvent
protected override void OnMouseLeftButtonUp(System.Windows.Input.MouseButtonEventArgs e)
{
e.Handled = true;
RaiseClickEvent();
base.OnMouseLeftButtonUp(e);
}
}
使用:
<Border x:Name="bd1" mycontrol:MyLabel.Click="bd1_mylblclick">
<StackPanel x:Name="panel1" Orientation="Vertical" Button.Click="panel1_btnclick">
<Button x:Name="btn1" Height="50" Content="按钮1" Click="btn1_click"/>
<Button x:Name="btn2" Height="50" Content="按钮2"/>
<mycontrol:MyLabel Height="50" Content="我有Click路由事件" />
</StackPanel>
</Border>
private void bd1_mylblclick(object sender,RoutedEventArgs e)
{
Debug.WriteLine("我被Click了.");
}
输出:
添加的类处理程序。
我被Click了。
9.附加事件
对于附加事件,很多书籍的说法都不太正确,包括《WPF揭秘》中把Button.Click做为附加事件来解说是不合理的。在《深入浅出WPF》这本书中写的就很好,与MSDN的附加事件概念是一致的。
9.1 附加事件概念
附加事件是路由事件。使用附加事件,可以将特定事件的处理程序添加到任意元素中。 处理事件的元素不必定义或继承附加事件,可能引发事件的对象和用来处理实例的目标也都不必将该事件定义为类成员或将其作为类成员来“拥有”。9.2 附加事件CLR包装
- 为目标UI元素添加附加事件侦听器的包装器是一个名为Add*Handler的public static方法,星号代表事件名称(与注册事件时的名称一致)。此方法包含两个参数:第一个是事件的侦听者(类型为DependencyObject),第二个是事件的处理器(RoutedEventHandler委托类型)。
- 解除UI元素对附加事件侦听的包装器是名为Remove*Handler的public static方法,参数与Add*Handler一致。
9.3 自定义附加事件
为Student添加NameChanged附加事件。
public class Student
{
public string Name { get; set; }
//声明并定义路由事件
public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent
("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));
//为界面元素添加路由事件侦听
public static void AddNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e != null)
{
e.AddHandler(Student.NameChangedEvent, h);
}
}
//移出侦听
public static void RemoveNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e != null)
{
e.RemoveHandler(Student.NameChangedEvent, h);
}
}
}
使用:
<!--附加事件-->
<TextBox x:Name="txtBox2" Grid.Column="2" Height="50" Text="{Binding stu.Name,NotifyOnSourceUpdated=True,UpdateSourceTrigger=PropertyChanged}"
local:Student.NameChanged="fjsjfun"
TextChanged="TextBox_TextChanged"
Binding.SourceUpdated="txtBox2_SourceUpdated"
/>
<!--SourceUpdated为Binding.SourceUpdated的别名,这个是Binding的附加事件-->
//附加事件方法
private void fjsjfun(object sender,RoutedEventArgs e)
{
Debug.WriteLine(((Student)e.OriginalSource).Name);
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
//准备事件消息并发送路由事件
RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, stu);
txtBox2.RaiseEvent(arg);
}
9.4 附加事件小结
由于RaiseEvent、AddHandler、RemoveHandler这些方法定义在UIElememt类中,如需要在非UIElememt派生类中注册路由事件,而这个类的实例既不能激发路由事件也不能侦听路由事件,此时只能依靠其他对象去激发和侦听事件,于是有了路由事件的另一种用法,即附加事件。路由事件的路由第一站是事件的激发者,附加事件的路由第一站是激发它的元素。
作者:FoolRabbit
出处:http://blog.csdn.net/rabbitsoft_1987
欢迎任何形式的转载,未经作者同意,请保留此段声明!