写在前面
本文一开始会给出一个使用WPF路由事件的实例,因为本文所有的表述都将基于该实例。而本文所给实例来自于《WPF自定义路由事件》一文,在《WPF自定义路由事件》一文中会对实例代码做详细说明,所以,大家在阅读本文实例代码期间若存在疑问,可以先去看看《WPF自定义路由事件》一文,看是否能从中获得你想要的解答。
本文实例
1 新建DetailReportEventArgs类,该类派生自RoutedEventArgs类,RoutedEventArgs类包含与路由事件相关的状态信息和事件数据。DetailReportEventArgs类中定义了属性EventTime与EventPublishr,EventTime属性记录时间的发生时间,EventPublishr属性记录事件的发布者。下面是DetailReportEventArgs类的完整代码。
//************************************************************ // // WPF路由事件示例代码 // // Author:三五月儿 // // Date:2014/08/31 // // http://blog.csdn.net/yl2isoft // //************************************************************ using System; using System.Windows; namespace WpfRoutedEventExp { public class DetailReportEventArgs : RoutedEventArgs { public DetailReportEventArgs(RoutedEvent routedEvent, object source) :base(routedEvent,source){} public DateTime EventTime { get; set; } public string EventPublisher { get; set; } } }
2 新建DetailReportButton类,该类派生自Button类,为该类添加路由事件DetailReportEvent。下面是DetailReportButton类的完整代码。
//************************************************************ // // WPF路由事件示例代码 // // Author:三五月儿 // // Date:2014/08/31 // // http://blog.csdn.net/yl2isoft // //************************************************************ using System; using System.Windows; using System.Windows.Controls; namespace WpfRoutedEventExp { public class DetailReportButton : Button { public static readonly RoutedEvent DetailReportEvent = EventManager.RegisterRoutedEvent("DetailReport",RoutingStrategy.Bubble,typeof(EventHandler<DetailReportEventArgs>),typeof(DetailReportButton)); public event RoutedEventHandler DetailReport { add { this.AddHandler(DetailReportEvent, value); } remove { this.RemoveHandler(DetailReportEvent, value); } } protected override void OnClick() { base.OnClick(); DetailReportEventArgs args = new DetailReportEventArgs(DetailReportEvent, this); args.EventPublisher = this.ToString(); args.EventTime = DateTime.Now; this.RaiseEvent(args); } } }
3 下面给出使用DetailReportButton 类的完整代码,包括画面代码以及画面后端代码两部分。
程序画面代码:
<Window x:Class="WpfRoutedEventExp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfRoutedEventExp" Title="MainWindow" Height="350" Width="525"> <Grid x:Name="Grid_FirstLayer" > <Grid x:Name="Grid_SecondLayer" > <Grid x:Name="Grid_ThirdLayer" > <local:DetailReportButton x:Name="Button_Confirm" Width="100" Height="100" Content="Click Me" Margin="142,111,250,100" /> </Grid> </Grid> </Grid> </Window>
程序画面的后端代码:
//************************************************************ // // WPF路由事件示例代码 // // Author:三五月儿 // // Date:2014/08/31 // // http://blog.csdn.net/yl2isoft // //************************************************************ using System.Windows; namespace WpfRoutedEventExp { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.Grid_ThirdLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1)); this.Grid_SecondLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1)); this.Grid_FirstLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1)); this.Button_Confirm.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1)); } private void Button_Clicked1(object sender, RoutedEventArgs e) { FrameworkElement ele = sender as FrameworkElement; DetailReportEventArgs dre = e as DetailReportEventArgs; MessageBox.Show(dre.EventPublisher + "-->" + ele.Name + ";" + dre.EventTime); } } }
4 执行程序,得到下图所示效果。
图1 程序运行效果图
结果显示:单击画面按钮,触发路由事件,路由事件的消息便从事件的触发者开始向它的上级容器控件一层一层的往外传,直到最外层的容器控件为止。
你我的约定
因为下面的说明中会反复修改实例代码,现在给出约定:
每一次修改在说明完相关的知识点后都将回退到修改前的状态。
我的新闻发布会
下面将以问答的方式来总结我在使用WPF路由事件时遇到或想到的几个小问题。
Q1 :除了使用AddHandler方法外,还有什么方法可以将想监听的事件与事件的处理器关联起来?
可以在XAML代码中将监听的事件与事件的处理器关联起来。按照下面所给方法修改实例代码,同样可以使用自定义的路由事件。
(1)注释掉以下四行代码。
this.Grid_ThirdLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1)); this.Grid_SecondLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1)); this.Grid_FirstLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1)); this.Button_Confirm.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));
(2)在XAML代码中将监听的事件与事件的处理器关联起来,具体操作见下图说明。
图2 在XAML中将监听的事件与事件处理器关联
(3)图2显示,此时使用的事件处理器不再是Button_Clicked1,而是Button_Clicked2,下面给出Button_Clicked2的代码。
private void Button_Clicked2(object sender, DetailReportEventArgs e) { FrameworkElement ele = sender as FrameworkElement; MessageBox.Show(e.EventPublisher + "-->" + ele.Name + ";" + e.EventTime); }
将Button_Clicked2与Button_Clicked1进行对比,发现两者的本质区别就是:第二个参数的类型不相同。
Button_Clicked2的第二个参数的类型为DetailReportEventArgs,而Button_Clicked1的第二个参数的类型为RoutedEventArgs。
继续Q:造成这种区别的原因又是什么呢?
那是因为在使用AddHandler方法时,传入的事件处理器必须满足RoutedEventHandler的定义,而RoutedEventHandler的定义如下所示:
public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)
很显然,RoutedEventHandler本质上就是一个委托,而与该委托能够匹配的方法必须满足:
第一个参数为object类型,第二个参数为RoutedEventArgs类型,且不具有返回值。
所以实例中使用了满足该委托要求的Button_Clicked1。
接着阐述,Button_Clicked2存在的原因。
查看自定义路由事件的定义代码:
public static readonly RoutedEvent DetailReportEvent = EventManager.RegisterRoutedEvent("DetailReport",RoutingStrategy.Bubble,typeof(EventHandler<DetailReportEventArgs>),typeof(DetailReportButton));
从定义中可以找到路由事件要求的事件处理器的类型,为EventHandler<DetailReportEventArgs>。
同样可以查看EventHandler<DetailReportEventArgs>的定义,如下所示:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
将其中的TEventArgs替换成DetailReportEventArgs,得到以下定义代码:
public delegate void EventHandler<DetailReportEventArgs>(object sender, DetailReportEventArgs e);
看到这个定义,我想大家也就知道了Button_Clicked2存在的原因了吧。
Q2: 既然为路由事件增加了CLR事件包装,那么,我们是否可以通过“+=”与“-=”符号来操作路由事件?
当然可以。
下面将通过修改示例代码进行演示来说明这一点。
注释掉实例中一切将事件与事件处理器关联起来的代码(画面代码以及画面后端代码中均要这么做)。
在画面后端代码中增加以下代码:
this.Button_Confirm.DetailReport += Button_Clicked1;
运行程序,点击按钮,得到以下消息框:
图3 使用“+=”订阅事件后的运行效果图
事件处理器被正确执行,此时的路由事件与普通的CLR事件相同,直接将消息传递给绑定的事件处理器。
接着,再增加以下代码:
this.Button_Confirm.DetailReport -= Button_Clicked1;
再次运行程序,点击按钮,这下子,就“呵呵”了,我想你懂的。
Q3 :同一容器中的控件之间能够相互监听路由事件的消息吗?
按照下面的描述修改示例代码。
(1)在DetailReportButton按钮前增加名为Button_SameLayer的按钮。
<Grid x:Name="Grid_ThirdLayer" > <Button x:Name="Button_SameLayer"/> <local:DetailReportButton x:Name="Button_Confirm" Width="100" Height="100" Content="Click Me" Margin="142,111,250,100" /> </Grid>
(2)使Button_SameLayer监听DetailReportButton的事件DetailReportEvent。
this.Button_SameLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));
(3)运行程序,发现Button_SameLayer并没有监听到路由事件的到来。
由此可见,同一容器中的控件之间无法相互监听路由事件的消息。
Q4:如何让一个路由事件传递至某一个节点处不再继续传递?
在事件处理方法Button_Clicked1中,增加以下代码:
if (ele == this.Grid_SecondLayer) { e.Handled = true; }
这样一来,当消息传递至Grid_SecondLayer控件后将不会继续传递下去。
因为,“e.Handled= true”的意思是说:该路由事件已经被处理了,不需要再继续传递下去。
Q5: RoutedEventArgs的Source与OriginalSource的区别是什么?
为了阐述这个主题,需要对实例做相对较大的一次变动。
(1)新建用户控件MyUserControl,MyUserControl不完成任何有意义的工作,MyUserControl控件的XAML代码如下所示:
<UserControl x:Class="WpfRoutedEventExp.MyUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Grid x:Name="Grid_FirstLayerInUserControl"> <Grid x:Name="Grid_SecondLayerInUserControl"> <Button x:Name="Button_ThirdLayerInUserControl" Width="100" Height="100" Content="Hello,Kitty!"/> </Grid> </Grid> </UserControl>
代码对应的设计视图如下图所示:
图4 自定义控件的设计图
(2)在实例代码中使用我们自定义的用户控件,并为其指定名称为myUserControl。
<Grid x:Name="Grid_ThirdLayer" > <local:DetailReportButton x:Name="Button_Confirm" Width="100" Height="100" Content="Click Me" Margin="142,111,250,100" /> <local:MyUserControl x:Name="myUserControl" Width="100" Height="100" HorizontalAlignment="Left" Margin="285,110,0,100"/> </Grid>
自定义控件myUserControl位于容器Grid_ThirdLayer之中,Button_Confirm按钮之后。
图5 使用自定义的控件
(2)去掉所有的事件监听。
(3)使Grid_FirstLayer监听Button的Click事件。
this.Grid_FirstLayer.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button_Clicked1));
(4)修改事件处理器方法Button_Clicked1,在方法中输出事件的Source与OriginalSource属性值。
private void Button_Clicked1(object sender, RoutedEventArgs e) { MessageBox.Show(e.Source + ";" + e.OriginalSource ); }
(5)点击“Hello,Kitty!”按钮(“Hello,Kitty!”按钮位于自定义控件myUserControl中),得到以下结果:
图6 Source与OriginalSource的区别
从结果可以看到:e.Source的值为MyUserControl(即自定义控件),而e.OriginalSource的值为Button(即“Hello,Kitty!”按钮)。
Source与OriginalSource均为事件源。只不过Source表示的是LogicalTree上的消息源头,而OriginalSource表示的是VisualTree上的源头。
Button.Click路由事件是从自定义控件内部名为Button_ThirdLayerInUserControl的Button控件发出的,在主窗口中,myUserControl是LogicalTree的末端节点,所以e.Source就是MyUserControl;而窗体的VisualTree则包含了MyUserControl的内部结构,所以e.OriginalSource将返回Button_ThirdLayerInUserControl按钮。
写在后面
本文未完,以后待续...