开发工具与关键技术:Visual Studio 2017
作者:邓崇富
撰写时间:2019 年 6 月 15 日
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
WPF系统中的大多数事件都是可路由事件,可路由事件在MSDN文档里会具有Routed Event Information一栏,使用者可以通过这一栏信息了解如何响应这一路由事件。
下面的例子通过Button的Clik事件来说明路由事件的使用。
XAML代码如下:
<Grid x:Name="gridA" Margin="10" Background="#FFD6CDCD">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Canvas x:Name="canvasl" Grid.Column="0" Background="Red" Margin="10">
<Button x:Name="buttonLeft" Content="Left" Width="40" Height="100" Margin="10"/>
</Canvas>
<Canvas x:Name="canvasRight" Grid.Column="1" Background="Yellow" Margin="20" Width="40" Height="100">
<Button x:Name="buttonRight" Content="Right" Width="40" Height="100"/>
</Canvas>
</Grid>
效果图如下:
当单击buttonLeft时,Button.Click事件就会沿着buttonLeft—canvasLeft—girdA—girdRoot—window这条路线向上传送:单击buttonRight,则Button.Click事件沿着buttonRight—canvasRight—gridA—gridRoot—Window路线传送。因为目前还没有那个控件侦听Button.Click事件,所以单击按钮后尽管事件向上传递却并没有受到响应。下面,让我们为gridRoot安装针对Button.Click事件的侦听器。
在窗体的构造器中调用girdRoot的AddHandler方法把想监听的事件与事件的处理器关联起来:
public Window16()
{
InitializeComponent();
this.griRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
}
AddHandler方法源自UIElement类,也就是说,所有UI控件都具有这个方法。书写AddHandler方法时你会发现它的第一个参数是Button.Click。原来,WPF事件系统也使用了与属性系统类试的“静态字段—包装器”的策略。也就是说,路由事件本身是一个RoutedEvent类型的静态成员变量(Button.ClickEvent),Button还有一个与之对应的Click事件(CLR包装)专门用于对外界暴露这个事件。可以效仿依赖属性, 把事件的CLR包装称为“CRL事件”。如此,就像每个依赖属性拥有自己的CRL属性包装一样,每隔路由事件都拥有自己的CRL事件。
代码如下:
public Window16()
{
InitializeComponent();
this.griRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
}
上面的代码让最外层的Grid(gridRoot)能够捕抓到从内部“飘”出来的按钮单击事件,捕捉到后会用this.ButtonClicked方法来进行响应处理。
ButtonClicked方法代码如下:
private void ButtonClicked(object sender,RoutedEventArgs e)
{
MessageBox.Show((e.OriginalSource as FrameworkElement).Name);
}
这里有一点非常重要:因为路由事件(的信息)是从内部一层一层传递出来最后到达最外层的gridRoot,并且由gridRoot元素将事件消息交给ButtonClicked方法来处理,所以传入ButtonClicked方法的参数sender实际上是gridRoot而不是被单击的Button,这与传统的直接事件不太一样。如果想了解事件的源头(最初发起者),可以使用e.OriginalSource,使用它的时候需要使用as/is操作符或者强制类型转换把它们识别/转换为正确的类型。
运行代码后,单击右边或左边的按钮,效果如下:
自定义路由事件:
为了方便程序中对象之间的通信常需要我们自定义一些路由事件,创建自定义路由事件大体有以下三个步骤:
- 声明并注册路由事件。
- 为路由事件添加CRL事件包装。
- 创建可以激发路由事件的方法。
定义路由事件与定义依赖属性的手法很相似。为你的类声明一个由public static readonly 修饰的RoutedEvent类型字段,然后使用EventManager类的RegisterRoutedEvent方法进行注册。
激发路由事件很简单,首先创建需要让事件携带的消息(RoutedEventArgs类的实例)并把它与路由事件关联,然后调用元素的RaiseEvent方法(继承自UIElement类)把事件发送出去。注意,这与激发传统直接事件的方法不同,传统直接事件的激发是通过调用CRL事件的Invoke方法实现的,而路由事件的激发与作为其包装器的CRL毫不相干。
了解创建自定义路由事件的步骤后,下面是完整的注册代码:
public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent
("Click".RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
最重要的是了解EventManager.RegisterRoutedEvent方法的四个参数。
第一个参数为string类型,被称为路由事件的名称,按微软的建议,这个字符串应该与RoutedEvent变量的前缀和CRL事件包装器的名字一致。
第二个参数称为路由事件的策略。WPF路由事件有一下3种路由策略:
- Bubble,冒泡式:路由事件由事件的激发者出发向它的上级容器一层一层路由,直至最外层容器(Window或者Page)。因为是由树的底端向顶端移动,而且从事件激发元素到UI树的树根只有确定的一条路径,所以这种策略被形象地命名为“冒泡式”。
- Tunnel,隧道式:事件的路由方向正好与Bubble策略相反,是由UI树的树根向事件激发控件移动,因为从UI树根向树底移动时有很多路径,但我们希望是由树根向激发事件的控件移动,这就好像在树根与目标控件之间挖掘了一条隧道,事件只能沿着隧道移动,所以称之为“隧道式”。
- Direct,直达式:模仿CRL直接事件,直接将事件消息送达事件处理器。
第三个参数用于指定事件处理器的类型。事件处理器的返回值类型和·参数列表必须与此参数指定的委托保持一致,否则会导致在编译时抛出异常。
第四个参数用于指明路由事件的宿主(拥有者)是那个类型。与依赖属性类似,这个类型和第一个参数共同参与一些底层算法且产生这个路由事件的Hash Code并且注册到程序的路由事件列表中。
为了让事件消息能携带按钮被点击时的时间,下面创建一个RoutedEventArgs类的派生类,并为其添加ClickTime属性:‘
class ReportTimeEventArgs:RoutedEventArgs
{
public ReportTimeEventArgs(RoutedEvent routedEvent,object source) : base(routedEvent, source){ }
public DateTime ClickTime { get; set; }
}
然后,再创建一个Button类的派生类并按前述步骤为其添加路由事件:
class TimeButton : Button
{
//声明和注册路由事件
public static readonly RoutedEvent reportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton));
//CRL事件包装器
public event RoutedEventHandler ReportTime
{
add { this.AddHandler(reportTimeEvent, value); }
remove { this.RemoveHandler(reportTimeEvent, value); }
}
//激发路由事件,借用Click事件的激发方法
protected override void OnClick()
{
base.OnClick();//保证Button原有功能正常使用、Click事件能被激发
ReportTimeEventArgs args = new ReportTimeEventArgs(reportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
//ReportTimeEvent路由事件处理器
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.ItemsAdd(content);
}
}
下面是XAML代码:
<Window x:Class="WPF练习.Window17"
<!--此处省略了引用的命名空间-->
local:TimeButton.ReportTime="ReportTimeHandler"
Title="Window17" Height="300" Width="300">
<Grid x:Name="grid_1" local:TimeButton.ReportTime ="ReportTimeHandler">
<Grid x:Name="grid_2" local:TimeButton.ReportTime ="ReportTimeHandler">
<Grid x:Name="grid_3" local:TimeButton.ReportTime ="ReportTimeHandler">
<StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime ="ReportTimeHandler">
<ListBox x:Name="listBox"/>
<local:TimeButton x:Name="timeButton" Width="80" Height="80"
Content="报时" local:TimeButton.ReportTime ="ReportTimeHandler"/>
</StackPanel>
</Grid>
</Grid>
</Grid>
</Window>
运行效果: