1. 前言
WPF 事件的路由环境是 UI 组件树,先来看看这棵树。
1.1 Logical Tree 和 Visual Tree
WPF 中的树有两种,一颗是逻辑树,另一颗也是逻辑树。
开玩笑,WPF 不是鲁迅,另一颗是可视元素树。
- 逻辑树的每个节点不是布局组件就是控件;
- 而可视化树把逻辑树延伸到 Template;
- 一般都是和 Logical Tree 打交道,如果到 Visual Tree,多半还是程序设计不良;
- Logical Tree 查找元素用 LogicalTreeHelper 类的 static 方法实现;
- Visual Tree 查找元素用 VisualTreeHelper 类的 static 方法实现;
路由事件被激发后沿着 Visual Tree 传递,因此 Template 里的控件能把消息送出来。
2. 事件
事件隐藏消息机制的很多细节;
拥有者、响应者、订阅关系;
A 订阅了 B,本质是让 B.Event 与 A.EventHandler 关联起来;
事件激发: B.Event 被调用,与之关联的 A.EventHandler 就会被调用。
// 拥有者:myButton,事件:myButton.Click,事件的响应者:窗体本身,处理器:this.myButton_Click
this.myButton.Click += new System.EventHandler(this.myButton_Click);
void myButton_Click(object sender, EventArgs e) {}
缺点:
- 动态生成一组相同的控件,每个控件的同一事件都是用同一个处理器来相应,需要显式书写事件订阅代码;
- 控件的内部事件不能被外部订阅,当 UI 层级很多时,如果想让外层的容器订阅深层控件的某个事件就需要为每一层组件定义用于暴露内部事件的事件。
3. 路由事件
3.1 跟直接事件的区别
直接事件:发送者直接将事件消息发送给事件响应者,事件响应者使用其事件处理器方法对事件的发生做出响应、驱动程序逻辑按客户需求运行;
路由事件:拥有者和事件响应者没有直接显式的订阅关系,拥有者只负责激发事件,事件由谁响应它不知道,事件的响应者则安装有事件监听器,针对某类事件进行侦听,当有此类事件传递至此事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。
3.2 使用内置路由事件
// C# 添加路由事件处理器
this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
void ButtonClicked(object sender, RoutedEventArgs e) {}
<!--xaml 添加路由事件处理器-->
<Grid x:Name="gridRoot" ButtonBase.Click="ButtonClicked"></Grid>
3.3 自定义路由事件
1)声明并注册路由事件;
2)为路由事件添加 CLR 事件包装;// 非必须
3)创建可以激发路由事件的方法;
public abstract class ButtonBase : ContentControl, ICommandSource
{
public static readonly RoutedEvent ClickEvent;
static ButtonBase()
{
ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
...
}
public event RoutedEventHandler Click
{
add
{
AddHandler(ClickEvent, value);
}
remove
{
RemoveHandler(ClickEvent, value);
}
}
protected virtual void OnClick()
{
RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this);
RaiseEvent(e);
CommandHelpers.ExecuteCommandSource(this);
}
3.3.1 注册路由事件入参
- 第 1 个参数:与依赖属性类似,需要拿去生成用于注册路由事件的 hash Code,微软建议与 RoutedEvent 变量的前缀和 CLR 事件包装器的名称一致,如例子中的 "Click";
- 第 2 个参数:路由事件策略, RoutingStrategy.Bubble/Tunnel/Direct,Bubble 为从事件的激发者向上级容器一层一层路由,Tunnel 的方向是反过来,Direct 是类似于直接事件的方式,直接将事件消息传达给事件处理器;
- 第 3 个参数:指定事件处理器的类型,从源代码可以看出,为指定的路由事件添加路由事件处理程序时,函数的调用顺序是:UIElment.AddHandler->EventHandlersStore.AddRoutedEventHandler->RoutedEvent.IsLegalHandler(检查路由事件处理程序的类型是否为路由事件注册时指定的处理器类型 或 RoutedEventHandler,否则抛出 ArgumentException);
- 第 4 个参数:指定路由事件的宿主是哪个类型,跟依赖属性类似,用于生成 hash Code;
例子:
// 自定义路由事件的数据,继承自 RoutedEventArgs(包含与路由事件相关联的状态信息和事件数据)
class ReportedEventArgs : RoutedEventArgs
{
public ReportedEventArgs(RoutedEvent routedEvent, object source) : base (routedEvent, source) {}
public DateTime ClickTime { get; set; }
}
class TimeButton : Button
{
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); }
}
protected override void OnClick()
{
base.OnClick();
ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
}
<StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler">
<ListDox x:Name""listBox" />
<local:TimeButton x:Name-"timeButton" Width="50" Height-"80" Content="报时" local:TimeButton.ReportTime="ReportTimeHandler"/>
</StackPanel>
private void ReportTimeHandler(object sender, ReportTimeEventArgs e) { }
<StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler">
翻译成 C#:
Delegate dg = Delegate.CreateDelegate(typeof(EventHandler<ReportTimeEventArgs>), this, "ReportTimeHandler", false, true);
this.stackPanel_1.AddHandler(TimeButton.ReportTimeEvent, dg);
RoutedEventArgs 类具有一个 bool 类属性 Handled,一旦设置为 ture,就不会再往下传递,需要停止的时候就让它停止。
TextBox 的 TextChanged 事件、Binding 类的 SourceUpdated 事件也是路由事件。
4. RoutedEventArgs 的 Source 和 OriginalSource
Source 是 LogicalTree 上的消息源头;
OriginalSource 是 VisualTree 上的源头。
比如 一个 Window 包括一个 UserControl,UserControl 里面有个 Button,在 Window 中 UserControl 是 LogicalTree 的末端结点,e.Source 是那个 userControl,而窗体的 VisualTree 能看到内部结果,e.OriginalSource 是那个 button
5. 附加事件
5.1 非 UIElement 子类使用路由事件
class Student
{
public static readonly RoutedEvent NameChangedEvent =
EventManager.RegisterRoutedEvent("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));
public string Name { get; set; }
}
public partial class EventWindow : Window
{
public EventWindow()
{
InitializeComponent();
this.grid1.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(StudentNameChangedHandler));
}
private void StudentNameChangedHandler(object sender, RoutedEventArgs e)
{
MessageBox.Show(string.Format("名称改成{0}", (e.OriginalSource as Student).Name));
}
private void ButtonClick(object sender, RoutedEventArgs e)
{
Student stu = new Student() { Name = "OldName" };
stu.Name = "NewName";
RoutedEventArgs args = new RoutedEventArgs(Student.NameChangedEvent, stu);
this.button.RaiseEvent(args);
}
}
grid 监听 Student 的路由事件 NameChangedEvent;
Student 没有继承自 UIElement,没有 RaiseEvent 方法,因此借用其他 UIElement 元素调用 RaiseEvent 方法;
上面只是举例,一般我们用附加事件在 Binding、Mouse、Keyboard 这种全局的 Helper 类中。
5.2 例子 Binding
<Grid x:Name="grid1">
<Slider x:Name="slider" Width="300"/>
<TextBox x:Name="tb"
Text="{Binding Path=Value, ElementName=slider, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"/>
</Grid>
this.grid1.AddHandler(Binding.TargetUpdatedEvent, new RoutedEventHandler(TargetChangedHandler));
this.grid1.AddHandler(Binding.SourceUpdatedEvent, new RoutedEventHandler(SourceChangedHandler));
private void TargetChangedHandler(object sender, RoutedEventArgs e) { }
private void SourceChangedHandler(object sender, RoutedEventArgs e) { }
5.3 例子 Keyboard
public EventWindow()
{
InitializeComponent();
Keyboard.AddKeyDownHandler(this, new KeyEventHandler(KeyDownHandler));
this.AddHandler(UIElement.KeyDownEvent, new RoutedEventHandler(KeyDownHandler2));
}
private void KeyDownHandler(object sender, KeyEventArgs e)
{
if ((e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || e.KeyboardDevice.IsKeyDown(Key.RightCtrl)) && e.KeyboardDevice.IsKeyDown(Key.C))
{
}
}
private void KeyDownHandler2(object sender, RoutedEventArgs e)
{
KeyEventArgs args = e as KeyEventArgs;
if ((args.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || args.KeyboardDevice.IsKeyDown(Key.RightCtrl)) && args.KeyboardDevice.IsKeyDown(Key.C))
{
}
}