路由事件与直接事件(WinForm方式的事件)的区别在于:
案例:
<Grid x:Name="gridRoot">
<Grid x:Name="gridA">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
Grid.ColumnDefinitions>
<Canvas x:Name="canLeft" Grid.Column="0" >
<Button x:Name="btnLeft" Content="Left"/>
Canvas>
<Canvas x:Name="canRight" Grid.Column="1" >
<Button x:Name="btnRight" Content="Right"/>
Canvas>
Grid>
Grid>
逻辑树结构
当单击btnLeft时,Button.Click事件会沿着btnLeft-canLeft-gridA-gridRoot-Window路线传送,单击btnRight原理相同。
public MainWindow()
{
InitializeComponent();
//为gridRoot安装针对Button.Click事件的监听器
this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
}
//事件处理程序
private void ButtonClicked(object sender, RoutedEventArgs e)
{
//路由事件是一层层传出的,最后到达gridRoot,并由gridRoot将事件消息交给事件处理程序
//所以sender是gridRoot,而不是btnLeft或者btnRight,这点和传统的直接事件不同
//e.OriginalSource可以查看事件的最初发起者
MessageBox.Show((e.OriginalSource as FrameworkElement).Name);
}
在XAML实现,
自定义路由事件大致分为3个步骤:
案例:当点击按键时,报告事件发生的时间
//用于承载事件消息的时间参数
class ReportTimeEventArgs : RoutedEventArgs
{
public ReportTimeEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source)
{
}
public DateTime ClickTime { set; get; }
}
class TimeButton:Button
{
//声明和注册事件
//参数1:路由事件名称,和事件包装器的名称相同
//参数2:路由事件的策略,wpf路由事件的策略有3种
// Bubble:冒泡式,由激发者向上级容器一层一层传递直到UI树的根部,路径唯一
// Tunnel:隧道式,与Bubble策略相反,路径不唯一
// Direct: 直达式,直接将事件消息发送到事件处理方法
//参数3:事件类型
//参数4:事件拥有者
public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton));
//路由事件的包装器,固定写法
public event RoutedEventHandler ReportTime
{
add { this.AddHandler(ReportTimeEvent, value); }
remove { this.RemoveHandler(ReportTimeEvent, value); }
}
//激发路由事件,使用Click激发
protected override void OnClick()
{
base.OnClick();
ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
}
//ReportTimeEvent事件处理方法
private void ReportTimeHandle(object sender, ReportTimeEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
string timeStr = e.ClickTime.ToLongTimeString();
this.listBox.Items.Add($"{timeStr} 到达 {element.Name}");
}
<Grid x:Name="grid_1" local:TimeButton.ReportTime="ReportTimeHandle">
<Grid x:Name="grid_2" local:TimeButton.ReportTime="ReportTimeHandle">
<Grid x:Name="grid_3" local:TimeButton.ReportTime="ReportTimeHandle">
<StackPanel x:Name="stackPanel" local:TimeButton.ReportTime="ReportTimeHandle">
<ListBox x:Name="listBox"/>
<local:TimeButton x:Name="timeBtn" Content="报时" local:TimeButton.ReportTime="ReportTimeHandle"/>
StackPanel>
Grid>
Grid>
Grid>
如果传递到某个节点不再继续向下传递事件可以将RoutedEventArgs中的Handled属性设置为true,意思为“已经处理完成”。
//ReportTimeEvent事件处理方法
private void ReportTimeHandle(object sender, ReportTimeEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
string timeStr = e.ClickTime.ToLongTimeString();
this.listBox.Items.Add($"{timeStr} 到达 {element.Name}");
if (element == this.grid_2)
{
e.Handled = true;
}
}
我们常说的WPF树形结构通常指的是LogicalTree,而事件则是沿着VisualTree传递的,他俩的区别在于:LogicalTree的叶子结点构成了用户界面,而VisualTree要连控件中的细微结构也算上。如一个ListBox控件的细微结构由Border、ScrollViewer、Grid等等组成。
常见的附加事件
可以看出,路由事件的宿主都是拥有可视化实体的界面元素,而附加事件不具备显示在用户界面上的能力。
案例:设计一个Student类,如果其中的Name属性发生变化则激发一个路由事件,并用界面元素来捕捉。
<Grid x:Name="gird">
<Button x:Name="btn1" Content="OK" Click="Btn1_Click"/>
Grid>
class Student
{
//声明并定义路由事件
public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent("NamgeChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));
public int Id { set; get; }
public string Name { set; get; }
}
public MainWindow()
{
InitializeComponent();
//为gird添加路由事件监听器
this.gird.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(this.StudentNameChangedHandler));
}
//Grid的事件处理方法
private void StudentNameChangedHandler(object sender, RoutedEventArgs e)
{
MessageBox.Show((e.OriginalSource as Student).Id.ToString());
}
//非UIElement类没有RaiseEvent方法,所以要借用一个Button
private void Btn1_Click(object sender, RoutedEventArgs e)
{
Student student = new Student() { Id = 100, Name = "Tim" };
student.Name = "Tom";
//准备事件消息并发送路由事件
RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, student);
this.btn1.RaiseEvent(arg);
}
为附加事件增加包装器
//在Student类中增加
//为目标UI元素增加事件监听器的包装器
//参数1:事件监听者
//参数2:事件处理函数
public static void AddNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e != null)
{
e.AddHandler(Student.NameChangedEvent, h);
}
}
//为目标UI元素移除事件监听器的包装器
public static void RemoveNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e != null)
{
e.RemoveHandler(Student.NameChangedEvent, h);
}
}
这样可以将上面
this.gird.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(this.StudentNameChangedHandler));
改为
Student.AddNameChangedHandler(this.gird, new RoutedEventHandler(this.StudentNameChangedHandler));
,
或者删除上句将XMAL改为