每每谈到WPF的路由事件,我总是比较模糊的,因为我一般很少用,因为一般是用Binding来满足数据驱动界面的要求,要么就是通过路由命令来执行我想要做的方法,路由事件确实用得少,那么路由事件跟一般的事件的区别在哪里呢?如何使用呢?下面讲揭开其神秘的面纱:
首先说一说事件的历史,在windows操作系统上运行程序,都是消息驱动的,早期Windows API开发和MFC开发的时候可以直接看到各种消息,并且定义消息,到了VB和COM的时代,消息被封装成了事件,到了.NET时代也是事件,但是是直接式的,但了WPF时代又更灵活了,变成了路由式的了。那么到底什么是直接式,什么是路由式呢?
下面先直接切入主题,我们看一看路由事件到底是什么?直接的事件这里就不说了。
1 <Grid> 2 <Grid.ColumnDefinitions> 3 <ColumnDefinition></ColumnDefinition> 4 <ColumnDefinition></ColumnDefinition> 5 </Grid.ColumnDefinitions> 6 <Grid.RowDefinitions> 7 <RowDefinition></RowDefinition> 8 <RowDefinition></RowDefinition> 9 </Grid.RowDefinitions> 10 <Grid x:Name="Grid1" Grid.Row="0" Grid.Column="0" Button.Click ="ButtonClicked"> 11 <Grid.ColumnDefinitions> 12 <ColumnDefinition></ColumnDefinition> 13 <ColumnDefinition></ColumnDefinition> 14 </Grid.ColumnDefinitions> 15 <Grid x:Name="GridLeft" Grid.Column="0" Grid.Row="0"> 16 <Grid x:Name="GridLeftSub" Margin="5" Background="Blue"> 17 <Canvas x:Name="CanvasLeft" Margin="5" Background="Orange"> 18 <Button x:Name="ButtonLeft" Margin="5" Background="Red" Content="Left" Width="80" ></Button> 19 </Canvas> 20 </Grid> 21 </Grid> 22 <Grid x:Name="GridRight" Grid.Column="1" Grid.Row="0"> 23 <Grid x:Name="GridRightSub" Margin="5" Background="Blue"> 24 <Canvas x:Name="CanvasRight" Margin="5" Background="Orange"> 25 <Button x:Name="ButtonRight" Margin="5" Background="Red" Content="Right" Width="80" ></Button> 26 </Canvas> 27 </Grid> 28 </Grid> 29 </Grid> 30 </Grid>
如果点击ButtonLeft按钮,其Click事件会在其树上传递。
如果按照以前的思维,事件Click是Button的,那么谁负责响应呢?这里路由事件是给了外层的Grid来响应及订阅,能不能给其他的控件了,只要在这条路由通道树上都是可以的,那么也就是说,事件的应用者只负责激发事件,至于谁响应它自己并不知道,谁想响应谁就要去订阅事件,这个跟以前传统的事件是不一样了,为了区分以前传统的事件这里检查CLR事件,WPF的路由事件叫做路由事件。CLR事件的每对消息必须显示建立发送-响应关系。那么路由事件的优势到底在哪里?先不急,我们先自定义以下路由事件吧。
见代码:
1 public class ReportTimeEventArgs:RoutedEventArgs 2 { 3 public DateTime ClickTime{get;set;} 4 public ReportTimeEventArgs (RoutedEvent routedEvent,object source):base(routedEvent,source) 5 { 6 7 } 8 } 9 public class TimeButton : Button 10 { 11 public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", 12 RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton)); 13 public event RoutedEventHandler ReportTime 14 { 15 add { this.AddHandler(ReportTimeEvent, value); } 16 remove { this.RemoveHandler(ReportTimeEvent, value); } 17 } 18 protected override void OnClick() 19 { 20 base.OnClick(); 21 ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this); 22 args.ClickTime = DateTime.Now; 23 this.RaiseEvent(args); 24 } 25 }
自定义一个时间按钮,注册的时候使用了EventManager.RegisterRoutedEvent函数,
第一个参数是指定事件的名称,一般要和CLR事件的名称一致。
第二个参数指定的是路由的策略,有三种,如果是Bubble,就是从里向外路由。如果是Tunnel就是由外向里路由,如果是Direct就是直达式,跟CLR事件的方式一样。
第三个参数指定的是事件处理器的类型。这里容易搞错,也就是说将来订阅这个事件的参数及返回值类型要跟这里指定的类型一致。这里要特别注意。就这一点我深入讲一下,先看看这几个委托及参数的原型:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
public delegate void EventHandler(object sender, EventArgs e);
public class RoutedEventArgs : EventArgs
public class ReportTimeEventArgs:RoutedEventArgs
如果可以直接使用EventArgs,那么就可以直接使用EventHandler委托类型,如果不是那就要使用泛型委托了,这就是我以前容易犯错的一个地方。
第四个参数就是指定事件的拥有者是什么类型。
在注册以后,就得到了一个路由事件,这个路由事件是public static readonly修饰。
跟依赖属性一样,注册完后,要CLR包装,不同的地方是这里包装是用add, remove。
包装完了外界就可以通过这个CLR包装事件来进行订阅。
接下来就是如何触发这个事件,于是我们重载了OnClick方法,在这个方法里面RaiseEvent,这个就是触发事件。
然后再讲讲路由事件的参数,我们事件的源头到底是哪个呢?sender又是谁呢?下面做一个实验:
自定义了一个控件:
1 <UserControl x:Class="WPFRoutedEventDemo.UserControl1" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 > 5 <Border BorderBrush="Blue" BorderThickness="13" CornerRadius="5"> 6 <Button x:Name="innerButton" Content="OK"></Button> 7 </Border> 8 </UserControl>
然后在主窗体添加:
1 <Grid x:Name="gridFirst" Grid.Row="1" Button.Click ="ButtonClicked2"> 2 <Grid x:Name="gridSecond"> 3 <local:UserControl1 x:Name="btn1" Margin="30"></local:UserControl1> 4 </Grid> 5 </Grid>
事件响应函数:
1 private void ButtonClicked2(object sender, RoutedEventArgs e) 2 { 3 string originSourceMsg = string.Format("OriginalSource is {0}, Name is:{1}", e.OriginalSource, (e.OriginalSource as FrameworkElement).Name); 4 string sourceMsg = string.Format("Source is {0}, Name is:{1}", e.Source, (e.Source as FrameworkElement).Name); 5 string senderMsg = string.Format("sender is {0}", sender.GetType()); 6 MessageBox.Show(originSourceMsg +" " + sourceMsg + " " + senderMsg); 7 }
发现得到的结果如下:
我们发现如果调用e.OriginalSource,得到的就是VisualTree上的源头,如果调用的是e.Source得到的就是LogicalTree上的源头,
另外sender表示谁订阅.
还有一个就是e.Handled,如果设置为true,表示整个事件不在路由下去了.
到目前为止,我们知道了路由事件是如何创建的,如何控制路由,以及与传统事件的一个区别。那我们再看看一个特例,我们看看按钮有哪些事件,当然这里只是简单讲下,因为以前犯过迷糊:
首先我给一个按钮添加了三个事件处理程序,分别是PreviewMouseDown, Click, MouseDown;
1 private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e) 2 { 3 MessageBox.Show("Button PreviewMouseDownEvent"); 4 } 5 6 private void Button_MouseDown(object sender, MouseButtonEventArgs e) 7 { 8 MessageBox.Show("Button MouseDownEvent"); 9 } 10 11 private void Button_Click(object sender, RoutedEventArgs e) 12 { 13 MessageBox.Show("Button ClickEvent"); 14 }
然后我点击,发现只有PreviewMouseDown得到了响应。
为什么呢?
首先讲下public void AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo);这个函数:
特别是最后一个参数handledEventsToo:
如果为 true,则将按以下方式注册处理程序:即使路由事件在其事件数据中标记为已处理,也会调用该处理程序;
如果为 false,则使用默认条件注册处理程序,即当路由事件被标记为已处理时,将不调用处理程序。默认值为false。
一般带Preview前缀的事件都是隧道型事件,不带前缀的都是冒泡型事件,但是据我的查看,这些其实是有出入了,肉查看了UIElement的源码,发现很多事件都是Direct类型,也不知道为什么?
带着这些疑问,我再次做以下实验,并解决了这些问题:
xaml代码:
<Grid x:Name="gridTest" Grid.Row="1" Grid.Column="1">
<Button x:Name="btnTest" Content="点我"></Button>
后台代码:
1 gridTest.AddHandler(Button.PreviewMouseDownEvent, new RoutedEventHandler(PreviewEventHandler), true); 2 gridTest.AddHandler(Button.PreviewMouseUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 3 gridTest.AddHandler(Button.PreviewMouseLeftButtonDownEvent, new RoutedEventHandler(PreviewEventHandler), true); 4 gridTest.AddHandler(Button.PreviewMouseLeftButtonUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 5 gridTest.AddHandler(Button.ClickEvent, new RoutedEventHandler(PreviewEventHandler), true); 6 gridTest.AddHandler(Button.MouseDownEvent, new RoutedEventHandler(PreviewEventHandler), true); 7 gridTest.AddHandler(Button.MouseUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 8 gridTest.AddHandler(Button.MouseLeftButtonUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 9 gridTest.AddHandler(Button.MouseLeftButtonDownEvent, new RoutedEventHandler(PreviewEventHandler), true); 10 11 btnTest.AddHandler(Button.PreviewMouseDownEvent, new RoutedEventHandler(PreviewEventHandler), true); 12 btnTest.AddHandler(Button.PreviewMouseUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 13 btnTest.AddHandler(Button.PreviewMouseLeftButtonDownEvent, new RoutedEventHandler(PreviewEventHandler), true); 14 btnTest.AddHandler(Button.PreviewMouseLeftButtonUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 15 btnTest.AddHandler(Button.ClickEvent, new RoutedEventHandler(PreviewEventHandler), true); 16 btnTest.AddHandler(Button.MouseDownEvent, new RoutedEventHandler(PreviewEventHandler), true); 17 btnTest.AddHandler(Button.MouseUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 18 btnTest.AddHandler(Button.MouseLeftButtonUpEvent, new RoutedEventHandler(PreviewEventHandler), true); 19 btnTest.AddHandler(Button.MouseLeftButtonDownEvent, new RoutedEventHandler(PreviewEventHandler), true);
事件响应代码:
1 private void PreviewEventHandler(object sender, RoutedEventArgs e) 2 { 3 Debug.WriteLine(string.Format(" {0:mm:ss} {1} sender:{2}", DateTime.Now, e.RoutedEvent.Name,sender)); 4 }
输出结果如下:
03:44 PreviewMouseLeftButtonDown sender:System.Windows.Controls.Grid
03:44 PreviewMouseDown sender:System.Windows.Controls.Grid
03:44 PreviewMouseLeftButtonDown sender:System.Windows.Controls.Button: 点我
03:44 PreviewMouseDown sender:System.Windows.Controls.Button: 点我
03:44 MouseLeftButtonDown sender:System.Windows.Controls.Button: 点我
03:44 MouseDown sender:System.Windows.Controls.Button: 点我
03:44 MouseLeftButtonDown sender:System.Windows.Controls.Grid
03:44 MouseDown sender:System.Windows.Controls.Grid
03:44 PreviewMouseLeftButtonUp sender:System.Windows.Controls.Grid
03:44 PreviewMouseUp sender:System.Windows.Controls.Grid
03:44 PreviewMouseLeftButtonUp sender:System.Windows.Controls.Button: 点我
03:44 PreviewMouseUp sender:System.Windows.Controls.Button: 点我
03:44 Click sender:System.Windows.Controls.Button: 点我
03:44 Click sender:System.Windows.Controls.Grid
03:44 MouseLeftButtonUp sender:System.Windows.Controls.Button: 点我
03:44 MouseUp sender:System.Windows.Controls.Button: 点我
03:44 MouseLeftButtonUp sender:System.Windows.Controls.Grid
03:44 MouseUp sender:System.Windows.Controls.Grid
看到结果我们再来一一分析原因:
哪些事件属于冒泡型:MouseLeftButtonDown, MouseDown,Click,MouseLeftButtonUp, MouseUp
哪些事件属于隧道型:PreviewMouseLeftButtonDown, PreviewMouseDown, PreviewMouseLeftButtonUp, PreviewMouseUp.
可以看出从实际情况上来看,确实带Preview的一般都是隧道型。
另外添加事件的时候,handledEventsToo设为True,并且Debug输出到输出窗口就能够把非preview的一些事件输出来,这个是为什么呢?
这个主要原因是在某些情况下,低级事件隐藏并转化为了高级事件,当我们把handledEventsToo设为True,就不会隐藏了,就能够输出来,而且也不能使用MessageBox,因为会阻塞消息循环,会导致后续的消息不会得到响应。
在这篇博客中有一点点涉及:
http://www.cnblogs.com/loveis715/archive/2012/04/10/2441513.html
Demo:
http://files.cnblogs.com/files/monkeyZhong/WPFRoutedEventDemo.zip