路由事件2

Overview

VisualTree上的控件都可以订阅自己和它的子级控件的任意路由事件。由于订阅时,无需发布者引用,所以是一种模糊型批量订阅。只要子级控件是定义被订阅的路由事件的类及其派生类或者AddOwner了路由事件的实例,它发布路由事件都会导致执行事件处理程序,通过e.Source能够识别真正的发布者,可以根据e.Source的Name,Tag,Type,周边布局来分辨和过滤发布者,以执行不同的逻辑。

路由事件有4种使用模型

  1. 控件激发事件,沿着可视化树自顶向下或自下向顶传播,沿途的任何控件都可以侦听(AddHandler)。
  2. 上级控件侦听某个路由事件实例,任何子级控件发布此路由事件实例都会被侦听到。
  3. 将某个控件的某个路由事件转成命令
  4. Interaction.Triggers监听某个控件的某个路由事件,当该控件发生时,调用其他对象的方法或赋值属性。

定义路由事件的思路

先按照定义CLR事件的思路。先想好事件的名称,如Click,路由事件的名称就是ClickEvent。

下一步,public readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent();

最后一步,AddHandler和RemoveHandler提供CLR事件封装器。

订阅路由事件的思路

先确定订阅哪个控件的哪个事件。

再确定订阅者,订阅者可以是发布者本身,也可以是可视化树上的父级控件。

订阅者控件.AddHandler(被订阅的控件的类.静态路由实例,事件处理函数)。

事件处理函数,Sender是订阅者,事件的真正发布者是e.Source,因为会收到共用同一个RoutedEvent实例的所有控件发布的事件,所以在函数中注意根据e.Source过滤掉不关注的发布者。

路由事件

下面通过ButtonBase的路由事件Click的源码来入门路由事件的技术细节。

public abstract class ButtonBase : ContentControl
{
    public event RoutedEventHandler Click
    {
        add { AddHandler(ClickEvent, value); }
        remove { RemoveHandler(ClickEvent, value); }
    }

    public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent(
    "Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
}

为类定义一个路由事件,就是定义一个静态的RoutedEvent类型的字段。

public static readonly RoutedEvent声明一个静态只读的RoutedEvent字段,是固定形式。

ClickEvent是路由事件的名称,习惯以Event结尾。

RoutedEvent没有公开构造函数,只能通过EventManager.RegisterRoutedEvent返回实例。

public static RoutedEvent RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, Type handlerType, Type ownerType);

name

字符串类型,路由事件的标识符。在同一个类中,不能定义2个相同标识符的路由事件,必须保证其唯一性。路由事件名称去掉Event后缀作为name即可。该例中,ClickEvent去掉Event是Click.

routingStrategy

路由策略。枚举类型,Tunnel Bubble Direct

handlerType

路由事件允许的事件处理函数签名。签名必须是 void Function(object sender, RoutedEventArgs e)形式。

所以常用.NET内置的泛型委托EventHandler< RoutedEventArgs>。

如果路由事件不携带额外的参数,直接使用RoutedEventArgs即可,如果需要携带额外的参数,继承RoutedEventArgs实现自定义类即可,如MouseUpEvent的MouseRoutedEventArgs。

签名必须是 void Function(object sender, RoutedEventArgs e)的形式,所以常常使用C#内置的泛型委托EventHandle<RoutedEventArgs>来作为自定义路由事件的函数签名。路由事件参数e的类型必须是RoutedEventArgs或其派生类,无额外消息时使用RoutedEventArgs时,直接使用RoutedEventArgs就行了,有其他的消息传递,就需要继承RoutedEventArgs实现增加了额外信息属性的派生类,如MouseButtonEventArgs。

ownerType

定义此路由事件的类。该例中是ButtonBase。

相比传统的CLR事件,路由事件是静态的,且事件处理函数中的sender不是事件发布者,而是订阅者,这些都让我们的思维感到不适应很别扭。没办法,只能去适应它。

路由事件封装器的委托类型,和路由事件的handlerType要相同,名称是路由事件名称去掉Event后缀。

发布

按照CLR事件思维,发布事件的伪代码如下:

ClickEvent.Invoke(this,new RoutedEventArgs());

但实际上,路由事件是使用RaiseEvent方法发布的,RaiseEvent定义在UIElement,而几乎所有的WPF元素都派生自UIElement。

与传统CLR事件不同的是,CLR事件的Invoke有2个形参,第一个是事件的发布者,第二个是事件参数;但是路由事件的RaiseEvent只有1个参数RoutedEventArgs,不仅包含要携带的事件数据,事件的发布者也是参数的一个成员。

发布路由事件需要先准备好路由事件的事件参数RoutedEventArgs。RoutedEventArgs有2个成员RoutedEvent和OriginalSource被赋值完成即可被发布。RoutedEvent是发布的路由事件,OriginalSource是发布者。像传统CLR事件那样,事件只能在定义它的类中被发布,其他类只能+=订阅之而不能发布之,所以路由事件也是在它的宿主内部被发布的,所以下面的代码OriginalSource是this,也就是Button。构造函数形参第一个是路由事件,第二个是OriginalSource。

RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent,this);
this.RaiseEvent(e);

RoutedEventArgs的OriginalSource属性是事件的发布者引用,谁调用RaiseEvent()谁就是OriginalSource。按照观察者模式要求,只有定义路由事件的类本身可以发布路由事件,所以调用RaiseEvent()的引用一定和RoutedEventArgs的OriginalSource相同,而且是this.

[注:该结论对附加事件不成立。]

订阅

注册和登出路由事件的API同样定义在UIElement。

void AddHandler(RoutedEvent routedEvent, Delegate handler);
void RemoveHandler(RoutedEvent routedEvent, Delegate handler);

订阅的思路

摆在您面前一颗VisualTree,VisualTree上某个控件本身和其父级控件,皆可以订阅控件的某个路由事件。控件发布事件,会自顶向下或自下向顶传播,凡是订阅的控件都会执行其事件处理函数。

假设VisualTree上有个Button,现在想订阅它的Click事件。第一步,确定目标控件的类---Button。第二步,确定路由事件---ClickEvent。第三步,确定订阅者是本身还是父级控件,调用AddHandler即可。

下面是通过逻辑树无限套娃特性,实现带图标和文字的按钮(StackPanel的Background默认是null,要添加背景色,才能响应鼠标事件)。

现要实现,鼠标点击TextBlock,Image,StackPanel,Button的任意一个元素身上,都能响应点击事件。

<Button x:Name="Button">
    <StackPanel>
        <Image x:Name="Image" />
        <TextBlock x:Name="TextBlock" />
    </StackPanel>
</Button>

方式一

Button.AddHandler(Button.MouseUpEvent,Button_OnMouseUp);

方式二

Button.AddHandler(CheckBox.MouseUpEvent,Button_OnMouseUp);

CheckBox和Button两个类,使用的是同一个静态RoutedEvent实例,所以这样写也是OK的,即使XAML里面根本就没有CheckBox。

方式三

Button.AddHandler(TextBlock.MouseUpEvent,Button_OnMouseUp);

千万不要误认为只有点击到TextBlock才会响应鼠标事件,而Image,StackPanel,Button就不行。道理同方式二。

方式四

Button.Click += Button_OnMouseUp;

和方式一是完全一样的代码。

方式五

Button.AddHandler(MouseUpEvent,Button_OnMouseUp);

此行代码会放置到Window类的构造函数中执行,因为Window类也有MouseUpEvent,所以省去了类名。

方式六

<Button x:Name="Button" Button.MouseUp="Button_OnMouseUp">
    <StackPanel>
        <Image x:Name="Image" />
        <TextBlock x:Name="TextBlock" />
    </StackPanel>
</Button>

需要省略Event后缀。

方式七

<Button x:Name="Button" MouseUp="Button_OnMouseUp">
    <StackPanel>
        <Image x:Name="Image" />
        <TextBlock x:Name="TextBlock" />
    </StackPanel>
</Button>

如果订阅者本身就有此RoutedEvent实例,类名前缀可省去。

方式八

<Button x:Name="Button" TextBlock.MouseUp="Button_OnMouseUp">
    <StackPanel>
        <Image x:Name="Image" />
        <TextBlock x:Name="TextBlock" />
    </StackPanel>
</Button>

与方式三是同样的道理。

事件处理程序

sender

sender是路由事件的订阅者,是调用AddHandler的控件,不一定是事件的发布者,e.Source才一定是事件的发布者。

<Grid ButtonBase.Click="ClickHandler">
    <Button ButtonBase.Click="ClickHandler"/>
</Grid>
// 事件处理程序
private void Button_OnMouseUp(object sender, MouseButtonEventArgs e)
{
   
}

Grid和Button都订阅了Button.ClickEvent,且链接的都是同一个事件处理函数,点击Button,ClickHandler会执行2次,但这2次的sender不一样,第1次是Button,第2次是Grid。

MouseButtonEventArgs

OriginalSourceSource

一般在写路由事件处理函数时,才会去分辨Source和OriginalSource。以MouseUpEvent的事件处理程序为例讲解。

void Button_OnMouseUp(object sender, MouseButtonEventArgs e)
{
    RoutedEvent revent = e.RoutedEvent;
    object source = e.Source;
    object osource = e.OriginalSource;
}
  1. 先确定路由事件是谁
  2. 再确定路由事件定义在哪个类,谁调用RaiseEvent发布的此路由事件,谁就是OriginalSource
  3. 在可视化树上找到OriginalSource,判断OriginalSource是否在逻辑树上,若是,Source和OriginalSource相同;若否,OriginalSource一定是控件模版或数据模版或用户控件的内部子控件,OriginalSource的TemplateParent便是Source。

RoutedEvent

Handled

路由事件被控件激发后,沿着可视化树自顶向下或自下向顶传播,路径上的每个节点都可以订阅,如果某个节点的事件处理程序被设置成True,事件不会再向上或向下传播,响应链被中断。

下面以Button.ClickEvent讲解。

如果我们沿用CLR事件的概念,能轻松的理解下面的代码:Button类的Click事件类型成员,被注册事件处理函数Button_ClickHandler。

<Button Click="Button_ClickHandler"></Button>

但是下面的代码可能就难以理解了。TextChanged是TextBox的成员,应当在TextBox标签中进行注册,怎么写到Button标签了?!

<Button TextBox.TextChanged="TextBoxBase_OnTextChanged">
    <TextBox></TextBox>
</Button>

这就是WPF事件比CLR事件强大的地方。WPF事件可以像CLR事件那样使用,如第一处代码片段,但也增加了额外的功能:解耦合订阅功能&路由功能。

如果想为某个控件的某个事件添加事件处理程序,只需要拿到其背后的static-routedevent-field.

使用静态路由字段,用基类的还是派生类的,还是AddOwner得到的,都可以。

共享路由事件

解合的订阅内部事件

订阅子级控件的路由事件,如果本身也有该事件,可以通过WPF事件进行订阅,也可以通过,如果自己没有

附加路由事件

路由策略

路由事件的优点

逻辑树搭出的‘自定义控件’

  1. 用户控件的内部控件作为字段是private的。使用用户控件时,无法订阅到内部控件的事件。必须在用户控件定义一个匹配的事件封装,或者把内部字段的权限设置成公开。

发布事件的控件不必是定义在它身上的路由事件。

posted @ 2024-02-03 02:29  ValueLee  阅读(49)  评论(0)    收藏  举报