就像属性系统在WPF中得到升级、进化为依赖属性一样,事件系统在WPF中也被升级——进化为路由事件(Routed Event),并在其基础上衍生出命令传递机制。
从Windows API开发到传统的.NET开发,消息的传递都是直接模式的,即消息直接由发送者交给接收者。WPF把这种直接消息模型升级为可传递的消息模型——前面我们已经知道WPF的UI是由布局组件和控件构成的树形结构,当这棵树上的某个节点激发出某个事件时,程序员可以选择传统的直接事件模式让响应者来响应,也可以把WPF的路由事件看成一只小蚂蚁,它可以从树的基部向顶部目标爬行,每路过一个树枝的分叉点就会把消息带给这个分叉点。
WPF中有两种树:一种叫逻辑树(Logical Tree);一种可视元素树(Visual Tree)。前面我们见过的所有树形结构都是Logical Tree,Logical Tree最显著的特点就是它完全由布局组件和控件组成,换句话说就是它的每个结点不是布局组件就是控件。
如果想在Logical Tree上导航或查找元素,可以借助LogicalTreeHelper类的static方法来实现:
(1)BringIntoView:把选定元素带进用户可视区域,经常用于个滚动的视图。
(2)FindLogicalNode:按给定名称(Name属性值)查找元素,包括子级树上的元素。
(3)GetChildren:获取所有直接子级元素。
(4)GetParent:获取直接父级元素。
如果现在Visual Tree上导航或查找元素,则可借助VisualTreeHelper类的static方法来实现。
WPF的UI可以表示为Logical Tree和Visual Tree,那么当一个路由事件被激发后是沿着Visual Tree传递的,只有这样,藏在Template里的控件才能把消息送出来。
事件的前身是消息(Message)。Windows是消息驱动的操作系统,运行其上的程序也遵照这个运行机制运行。消息的本质就是一条数据,这条数据里记载着消息的类别,必要的时候还记载一些消息参数。
事件模型隐藏了消息机制的很多细节,让程序的开发变为简单。烦琐的消息驱动机制在事件模型中简化为3个关键点:
(1)事件的拥有者:即消息的发送者。事件的宿主可以在某些条件下激发它拥有的事件,即事件被触发。事件被触发则消息被发送。
(2)事件的响应者:即消息的接收者、处理者。事件接收者使用其事件处理器(Event Handler)对事件做出响应。
(3)事件的订阅关系:事件的拥有者可以随时激发事件,但事件发生后会不会得到响应要看有没有事件的响应者,或者说要看这个事件是否被关注。如果对象A关注对象B的某个事件是否发生,则称A订阅了B的事件。
在这种模型里,事件的响应者通过订阅关系直接关联在事件拥有者的事件上,为了与WPF的路由事件模型区分开,就把这种事件模型称为直接事件模型或者CLR事件模型。
直接事件模型是传统.NET开发中对象间相互协作、沟通信息的主要手段,它在很大程度上简化了程序的开发。然而直接事件模型并不完美,它的不完美之处就在于事件的响应者与事件的拥有者之间必须建立事件订阅这个“专线联系”。这样至少会有两个弊端:
(1)每对消息是“发送->响应”关系,必须建立显示的点对点订阅关系。
(2)事件的宿主必须能够直接访问事件的响应值。不然无法建立订阅关系。
直接事件模型的弱点会在下面两种情况中显露出来:
(1)程序运行期在容器中动态生成一组相同的控件,每个控件的同一个事件都使用同一个事件处理器来响应,面对这种情况,我们在动态生成控件的同时就需要显式书写事件订阅代码。
(2)用户控件的内部事件不能被外界所订阅,必须为用户控件定义新的事件用以向外界暴露内部事件。当模块划分很细的时候,UI组件的层级会很多,如果想让很外层的容器订阅深层控件的某个事件就需要为每一层组件定义用于暴露内部事件的事件、形成事件链。
路由事件的出现很好解决了上述两种情况中出现的问题。
为了降低有事件订阅带来的耦合度和代码量,WPF推出了路由事件机制。路由事件与直接事件的区别在于,直接事件激发时,发送者直接将消息通过事件订阅交送给事件相应者,事件响应者使用其事件处理方法对事件的发生做出响应、驱动程序逻辑按客户需求运行;路由事件的事件拥有者和事件响应者之间没有直接显式的订阅关系,事件的拥有者只负责激发事件,事件将有谁响应它并不知道,事件的响应者则安装有事件侦听器,针对某类事件进行侦听,当有此类事件传递至此时事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。
尽管WPF推出了路由事件机制,但它仍然支持传统的直接事件模型。
1:使用WPF内置路由事件
使用AddHandler方法把想监听的事件与事件的处理器关联起来。
AddHandler方法源自UIElement类,也就是说UI控件都具有这个方法。WPF的事件系统了与属性系统类似的“静态字段->包装器”的策略。也就是说,路由事件本身是一个RoutedEvent类型的静态成员变量。
路由事件是从内部一层一层传递出来最后到达最外层的。如果想查看事件源头,使用e.OriginalSource,使用它的时候需要使用as/is操作符或者强制类型转换把它识别或转换正确的类型。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(buttonClicked));
}
private void buttonClicked(object sender,RoutedEventArgs e)
{
MessageBox.Show((e.OriginalSource as FrameworkElement).Name);
}
}
2:自定义路由事件
为了方便程序中对象之间的通信常需要我们自己定义一些路由事件。
创建自定义路由事件大体可以分为三个步骤:
(1)声明并注册路由事件。
(2)为路由事件添加CLR事件包装。
(3)创建可以激发路由事件的方法。
定义路由事件与定义依赖属性的手法极为相似——为你的类声明一个由public static readonly修饰的RoutedEvent类型字段,然后使用EventManager类的RegisterRoutedEvent方法进行注册。
为路由事件添加CLR事件包装是为了把路由事件暴露的像一个传统的直接事件,如果不关注底层实现,程序员不会感觉到它与传统直接事件的区别。,仍然可以使用操作符(+=)为事件添加处理器和使用操作符(-=)移除不再使用的事件处理器。为路由事件添加CLR事件包装的代码与使用CLR属性包装依赖属性的代码格式亦非常详尽,只是关键字get和set被替换为add和remove。当使用操作符(+=)添加对路由事件的侦听处理时,add分支的代码会被调用;当使用操作符(-=)移除对此事件的侦听处理时,remove分支的代码会被调用——CLR事件只是“看上去像”一个直接事件,本质上不过是在当前元素上调用AddHandler和RemoveHandler而已。
激发路由事件很简单,首先创建需要让事件携带消息并把它与路由事件关联,然后调用元素的RaiseEvent方法把事件发送出去。
最重要的是了解EventManager.RegisterRoutedEvent方法的四个参数。
第一个参数为string类型,被称为路由事件的名称。
第二个参数为路由事件的策略。
(1)Bubble,冒泡式:路由事件由事件的激发者发出向它的上级容器一层一层路由,直至最外层容器。
(2)Tunnel,隧道式:事件的路由方向正好与Bubble策略相反,是由UI树的树根向事件激发控件移动。
(3)Direct,直达式:模仿CLR直接事件,直接将事件消息送达事件处理器。
第三个参数用于指定事件处理器类型。事件处理器的返回值类型和参数列表必须与此参数指定的委托保持一致,不然会导致在编译时抛出异常。
第四个参数用于指明路由事件的宿主是那个类型。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfApplication1
{
///
/// MainWindow.xaml 的交互逻辑
///
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ReportTimeHandler(object sender,ReportTimeEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
string timeStr = e.ClickTime.ToLongTimeString();
string content = string.Format("{0}到达{1}", timeStr, element.Name);
this.listBox.Items.Add(content);
}
}
class ReportTimeEventArgs:RoutedEventArgs
{
public ReportTimeEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { }
public DateTime ClickTime { get; set; }
}
class TimeButton : Button
{
//声明和注册路由事件
public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent
("ReportTime",RoutingStrategy.Bubble, typeof(EventHandler),typeof(TimeButton));
//CLR 事件包装器
public event RoutedEventHandler ReportTime
{
add { this.AddHandler(ReportTimeEvent, value); }
remove { this.RemoveHandler(ReportTimeEvent, value); }
}
protected override void OnClick()
{
base.OnClick();
ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
}
}
3:RoutedEventArgs的Source与OriginalSource
路由事件是沿着VisualTree传递的。VisualTree与LogicalTree的区别就在于:LogicalTree的叶子节点是构成用户界面的控件,而VisualTree要连控件中的细微结构也算上。
我们说“路由事件在VisualTree上传递”,本意上是说“路由事件的消息在VisualTree上传递”,而路由事件的消息则包含在RoutedEventArgs实例中。RoutedEventArgs有两个属性Source和OriginalSource,这两个属性都表示路由事件传递的起点,只不过Source表示的是LogicalTree上消息的源头,而OriginalSource则表示VisualTree上的源头。
4:附加事件
在WPF事件系统中还有一种事件被称为附加事件,它就是路由事件。
附加事件的种类:
(1)Binding类:SourceUpdated事件、TargetUpdated事件。
(2)Mouse类:MouseEnter事件、MouseLeave事件、MouseDown事件、MouseUp事件等。
(3)Keyboard类:KeyDown事件、KeyUp事件等。
路由事件的宿主都是拥有可视化实体的界面元素,而附加事件则不具备显示在用户界面上的能力。
UIElement类是路由事件宿主与附加事件宿主的分水岭,不单是因为从Element类开始才具备在界面上显示的能力,还因为RaiseEvent、AddHandler和RemoveHandler这些方法也在定义在UIElement类中。因此,如果在一个非UIElement派生类中注册了路由事件,则这个类的实例既不能自己激发(Raise)此路由事件也无法自己侦听此路由事件,只能把这个事件的激发“附着”在一个具有RaiseEvent方法的对象上,借助这个对象的RaiseEvent方法把事件发送出去;事件的监听任务也只能交给别的对象去做。
参考教材书:深入浅出WPF 刘铁猛 著