WPF之事件
事件的前身是消息(Message)。Windows是消息驱动的操作系统,运行其上的程序也遵照这个机制运行。消息本质就是一条数据,这条数据记载着消息的类别,必要的时候还记载一些消息参数。比如鼠标左键点击窗体时,就会产生一条WM_LBUTTONDOWN消息并加入Windows待处理的消息队列中,当Windows处理到这条消息时就会把消息发送给你点击的窗体,窗体会用自己的一套算法来响应这个消息。
微软把消息机制进行封装推出了易于理解的事件模型,事件模型隐藏了消息机制的很多细节,使得程序开发变得简单。其中的三个关键点有:事件的拥有者、事件的响应者和事件的订阅关系。事件的响应者通过订阅关系直接关联在事件的拥有者的事件上。事件触发时,发送者直接将消息通过事件订阅交给事件响应者,事件响应者使用其事件处理方法对事件的发生做出响应,驱动程序逻辑按客户需求运行;事件简化了程序的开发,但也有不完美之处,如每对消息是“发送—响应”关系,必须建立显示的点对点订阅;事件的宿主必须能够直接访问事件的响应者,不然无法建立订阅关系。
为了降低由事件订阅带来的耦合度和代码量,WPF推出了路由事件机制。路由事件的事件拥有者和响应者之间没有直接显示的订阅关系,事件的拥有者只负责触发事件,事件将由谁响应它并不知道,事件的响应者安装有事件侦听器,当事件传递到事件响应者时就使用事件处理器来响应事件并决定事件是否继续传递。
创建自定义路由事件分为三步:
- 声明并注册路由事件;
- 为路由事件添加CLR事件包装;
- 创建可以触发事件的方法。
接下来我们创建一个报告事件发生时间的路由事件,事件参数代码如下:
public class ReportTimeEventArgs:RoutedEventArgs { public ReportTimeEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { } public DateTime ClickTime { get; set; } }
创建一个Button的派生类并添加路由事件,代码如下:
public class TimeButton:Button { //声明和注册路由事件 public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), 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); } }
路由事件的声明类似依赖属性,使用private static readonly修饰,然后使用RegisterRoutedEvent注册。
RegisterRoutedEvent中
第一个参数为string,指定路由事件的名称;
第二个参数被称为路由策略,WPF有三种路由策略:
Bubble:冒泡式,路由事件从事件触发者出发向它的上级容器一层一层传递,直至最外层容器;
Tunnel:隧道式,事件传递的方向与Bubble相反;
Direct:直达式,类似CLR直接事件。
第三个参数用于指定事件处理器的类型。
第四个参数指明路由事件的宿主(拥有者)是那个类型。这个类型和第一个参数共同参与底层算法产生这个路由事件的hash code并被注册到程序的路由事件列表中。
为路由事件添加CLR事件包装是为了把路由事件暴露得像一个传统的直接事件,如果不关注底层实现,程序员不会感觉到它与传统直接事件有什么区别,仍然可以使用操作符(+=)操作事件,其实现类似get/set,使用add/remove调用AddHandler和RemoveHandler。
路由事件的触发,使用的是RaiseEvent,与传统事件略有不同。
我们在UI中测试事件的传递,XAML代码如下:
<Window x:Class="WpfApplication6.MainWindow" x:Name="window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication6" Title="MainWindow" Height="350" Width="525" local:TimeButton.ReportTime="ReportTimeHandler"> <Grid x:Name="grid1" Background="Blue" Margin="10" local:TimeButton.ReportTime="ReportTimeHandler"> <Grid x:Name="grid2" Background="Green" Margin="10" local:TimeButton.ReportTime="ReportTimeHandler"> <StackPanel x:Name="panel1" local:TimeButton.ReportTime="ReportTimeHandler"> <ListBox x:Name="list1" Height="220" Margin="10"></ListBox> <local:TimeButton x:Name="tb1" Margin="5" local:TimeButton.ReportTime="ReportTimeHandler" Height="30" Content="报时"></local:TimeButton> </StackPanel> </Grid> </Grid> </Window>
界面逻辑树窗体windo1--表格grid1--表格grid2--Panel1--TimeButton tb1,控件都侦听类路由事件ReportTime,事件处理器为ReportTimeandler,其处理代码如下:
private void ReportTimeHandler(object sender, ReportTimeEventArgs e) { FrameworkElement element = sender as FrameworkElement; string t = e.ClickTime.ToLongTimeString(); string content = string.Format("{0}到达{1}", t, element.Name); this.list1.Items.Add(content); }
运行程序,单击按钮,ListBox显示类路由事件传递的过程,修改RoutingStrategy可更改其传递过程。
如果事件处理一次后无需再传递,只需要在事件的处理函数里设置e.Handled=ture即可。