第八章深入浅出话事件

《深入浅出WPF》学习笔记整理
第八章

路由:起点和终点之间有若干中转,事件从起点出发后经过每个中转时要做出选择,以正确的路径到达终点。

WPF事件的路由环境是UI组件树:每个结点不是布局组件就是控件。

WPF中”树“有:一逻辑树(Logical Tree)、二可视元素树(Visual Tree)。

在Logcal Tree上导航或查找元素,可借助LogicalTreeHelper类的static方法。在Visual Tree上导航或查找元素,可借助VisualTreeHelper类的static方法。

事件模型中3个关键点:事件的拥有者、事件的响应者、事件的订阅关系。A关注B的某个事件是否发生,则称A订阅B的事件。也即:B.Event与A.EventHandler关联起来,而事件激发就是B.Event被调用,此时与之关联的A.EventHandler也会被调用。

直接事件

直接事件模型(CLR事件模型):CLR事件本质是一个用event关键字修饰的委托实例。是传统.NET开发中对象相互协同的主要手段。

例如:窗体上一个Button,双击,自动创建Button_Click方法并跳转其中。这样一个直接事件模型就实现了。

//Form.Designer.cs中订阅关系确立的代码
this.Button.Click+=new System.EventHandler(this.Button_Click);

弊端:

  • 每对消息是”发送——>接收“的专线联系。
  • 事件的宿主必须能够直接访问事件的响应者,不然无法建立订阅关系。
路由事件

事件的拥有者只负责激发消息,响应者通过事件侦听器对特定类型事件进行侦听。当有此类事件传递至此,响应者使用事件处理器来响应,并决定事件是否可以继续传递

使用WPF内置路由事件
<Window x:Class="WpfApplication1_8_27.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="200">
    <Grid x:Name="gridRoot" Background="Lime">
        <Grid x:Name="gridA" Margin="10" Background="Blue">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Canvas x:Name="canvasLeft" 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="10">
                <Button x:Name="buttonRight" Content="Right" Width="40" Height="100" Margin="10"/>
            </Canvas>
        </Grid>
    </Grid>
</Window>

image-20200827085216937image-20200827085430102

单击”buttonLeft“和"buttonRight",则Button.Click事件会沿上图路线传送。此时没有Button.Click事件的侦听对象,所以会一路上传。

安装gridRoot对Button.Click的侦听器:

namespace WpfApplication1_8_27
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
        }

        private void ButtonClicked(object sender,RoutedEventArgs e)//自己编写而非程序生成
        {
            MessageBox.Show((e.OriginalSource as FrameworkElement).Name);
        }
    }
}

程序运行后,单击"Left"按钮,弹出左边对话框。单击"Right"按钮,弹出右边对话框。

在这里插入图片描述

需要注意的是:路由事件的消息是从内部一层一层传递到最外层的gridRoot,由gridRoot元素交给ButtonClicked方法来处理。所以,ButtonClicked的参数sender实际上是gridRoot而不是被单击的Button。如果需要查看事件的源头则使用e.OriginalSource,且用as/is强制转换成正确的类型。

其中,上面的路由事件添加命令也可以在XAML中完成:

this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));//可替换为
<Grid x:Name="gridRoot" Background="Lime" Button.Click="ButtonClicked">
<!--或者-->
<Grid x:Name="gridRoot" Background="Lime" ButtonBase.Click="ButtonClicked">    

在拼写Button.Click、ButtonBase.Click时没有提示,直至拼写到”=“

自定义路由事件

创建步骤:

  • 声明并注册路由事件:声明一个public static readonly修饰的RoutedEvent类型字段,然后用EventManager的RegisterRoutedEvent方法注册。
  • 为路由事件添加CLR事件包装:把路由事件曝露得像个传统直接事件,使用add、remove替换get、set 。
  • 创建可以激发路由事件的方法:创建消息(RoutedEventArgs)并与路由事件关联,调用RaiseEvent方法发送出去。(传统CLR事件用Invoke激发)
public abstract class ButtonBase:ContentControl,ICommandSource
{
    //声明并注册路由事件
      public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));

        //为路由事件添加CLR事件包装
   public event RoutedEventHandler Click
    {
        add { this.AddHandler(ClickEvent, value); }
        remove { this.RemoveHandler(ClickEvent, value); }
    }
    //激发路由事件的方法,此方法在用户单击鼠标时会被Windows调用
    protected virtual void OnClick()
   {
       RoutedEventArgs newEvent = new RoutedEventArgs(ButtonBase.ClickEvent, this);
       this.RaiseEvent(newEvent);
   }
    //..
}

其中参数如下:

EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
  • 第一个为string类型,路由事件的名称。
  • 第二个为路由事件的策略,Bubble(冒泡式,由激发者向上级容器一层一层传递)、Tunnel(隧道式,UI树根向激发事件的控件移动)、Direct(直达式,模仿CLR直接事件)。
  • 第三个为指定事件处理器的类型,参数与事件处理器的返回值一致。
  • 第四个参数指明事件宿主类型。

示例:创建一个路由事件,用以报告事件发生的时间。

这个示例需要先写C#代码,后写XAML代码

namespace WpfApplication1_8_27
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>

    //创建一个ReportTimeEventArgs类的派生类,并添加ClickTime属性
    class ReportTimeEventArgs : RoutedEventArgs
    {
        //用以承载时间消息的事件参数
        public ReportTimeEventArgs(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));

        //CLR事件包装器
        public event RoutedEventHandler ReportTime
        {
            add { this.AddHandler(ReportTimeEvent, value); }
            remove { this.RemoveHandler(ReportTimeEvent, value); }
        }

        //激发路由事件,借用Click事件的激发方法
        protected override void OnClick()
        {
            base.OnClick(); //写完OnClick(),自动添加

            ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
            args.ClickTime = DateTime.Now;
            this.RaiseEvent(args);
        }
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        //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.listbox1.Items.Add(content);
        }
    }
}
<Window x:Class="WpfApplication1_8_27.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1_8_27"
        Title="MainWindow" Height="200" Width="200"
       local:TimeButton.ReportTime="ReportTimeHandler" >
    <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="stack_panel_1" local:TimeButton.ReportTime="ReportTimeHandler">
                    <ListBox x:Name="listbox1"/>
                    <local:TimeButton x:Name="timeButton" Width="80" Height="80" Content="报时" local:TimeButton.ReportTime="ReportTimeHandler"/>
                </StackPanel>
            </Grid>
        </Grid>
    </Grid>
</Window>

在UI界面上,以Window为根,套了三层Grid和一层StackPanel。在最里面的StackPanel中放置一个ListBox和一个TimeButton(刚创建的Button派生类),从最内层的TimeButton到最外层的Window都侦听着TimeButton.ReportTimeEvent这个路由事件。

TimeButton–>StackPanel–>Grid–>Grid–>Grid–>Window

image-20200827103855772

当把TimeReportEvent策略改为Tunnel时:

image-20200827104549733


如何让一个路由事件在某个结点不再继续传递?

  • 路由事件携带的事件参数必须是RoutedEventArgs类或者其派生类的实例。
  • RoutedEventArgs类具有一个bool类型的Handled,但其为True就表示路由事件已经被处理了,则该事件不再往下传递了。

修改上述示例中的ReportTimeEvent处理器,则路由事件经过grid_2后就被处理了,不再向下传递。

namespace WpfApplication1_8_27
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>

    //创建一个ReportTimeEventArgs类的派生类,并添加ClickTime属性
    class ReportTimeEventArgs : RoutedEventArgs
    {
        //用以承载时间消息的事件参数
        public ReportTimeEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) 
        { }
        public DateTime ClickTime { get; set; }
    }

    class TimeButton : Button
    {
        //声明和注册路由事件
        public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Tunnel, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton));

        //CLR事件包装器
        public event RoutedEventHandler ReportTime
        {
            add { this.AddHandler(ReportTimeEvent, value); }
            remove { this.RemoveHandler(ReportTimeEvent, value); }
        }

        //激发路由事件,借用Click事件的激发方法
        protected override void OnClick()
        {
            base.OnClick(); //写完OnClick(),自动添加

            ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
            args.ClickTime = DateTime.Now;
            this.RaiseEvent(args);
        }
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        //ReportTimeEvent路由事件处理器,此处添加了RoutedEventArgs的Handled=true的处理,让路由事件在grid_2之后不再传递
        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.listbox1.Items.Add(content);

            if(element==this.grid_2) //添加部分
            {
                e.Handled = true;  //添加部分
            }
        }
    }
}

程序运行结果如下:

image-20200827105453569

路由事件虽好,但不要滥用,事件该由谁来捕捉处理,传到这个地方就应该处理掉。

RoutedEventArgs的Source与OriginalSource

路由事件的消息是沿着VisualTree传递的,而路由事件的消息则包含在RoutedEventArgs实例中。

Source与OriginalSource都表示路由事件传递的起点,Source是LogicalTree上的消息源头,而OriginalSource是VisualTree上的源头。

在当前工程中添加一个窗体:

image-20200827111016348

没有复现出来。

附加事件Attached Event

附加事件就是路由事件,是对附加事件宿主的真实写照。

示例:设计一个名为Student的类,如果Student的Name属性值发生变化就激发一个路由事件,就使用界面元素来捕捉这个事件。

<Window x:Class="WpfApplication1_8_27_1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="200">
    <Grid x:Name="gridMain">
        <Button x:Name="button1" Content="OK" Width="80" Height="80" Click="Button_Click"/>
    </Grid>
</Window>
public class Student
{
    //声明并定义路由事件
    public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));
    public int Id { get; set; }
    public string Name { get; set; }
}

namespace WpfApplication1_8_27_1
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            //为外层Grid添加路由事件侦听器
            this.gridMain.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(this.StudentNameChangedHandler));
        }

        //Click事件处理器
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Student stu = new Student() { Id = 101, Name = "Tim" };
            stu.Name = "Tom";

            //准备事件消息并发送路由事件
            RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, stu);
            this.button1.RaiseEvent(arg);
        }

        //Grid捕捉到NameChangedEvent后的处理器
        private void StudentNameChangedHandler(object sender,RoutedEventArgs e)
        {
            MessageBox.Show((e.OriginalSource as Student).Id.ToString());
        }
    }
}

后台代码中,当界面上唯一的Button被单击后会触发Button_Click。应为Student不是UIElement的派生类,所以不具有RaiseEvent方法,需要借用Button的RaiseEvent。

在窗体构造器中为Grid元素添加了对Student.NameChangedEvent的侦听,这与添加路由事件的侦听没有区别。Grid捕捉到路由事件后会显示事件消息源Student的Id

image-20200827113513257

理论上Student类已经具有附加事件了,但官方规定要为其添加一个CLR包装艺编XAML编辑器识别并进行智能提示。但,Student类并非派生自UIElement,不具有AddHandler、RemoveHandler方法。

  • 为目标UI元素添加附加事件侦听器的包装器是一个Add *Handler的public static方法(事件的侦听者-DependencyObject,事件的处理器-RoutedEventHandler委托类型)
  • 解除UI元素对附加事件侦听的包装器是一个Remove*Handler的public static方法,星号为事件名称。
public class Student
{
    //声明并定义路由事件
    public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));
   
    //为界面元素添加路由事件侦听
    public static void AddNameChangedHandler(DependencyObject d,RoutedEventHandler h) //程序中发生改变的地方1
    {
        UIElement e = d as UIElement;
        if(e != null)
        {
            e.AddHandler(Student.NameChangedEvent, h);
        }
    }
    
    //移除侦听
    public static void RemoveNameChangedHandler(DependencyObject d,RoutedEventHandler h)
    {
        UIElement e = d as UIElement;
        if(e != null)
        {
            e.RemoveHandler(Student.NameChangedEvent, h);
        }
    }
    public int Id { get; set; }
    public string Name { get; set; }
}

namespace WpfApplication1_8_27_1
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            //为外层Grid添加路由事件侦听器
           // this.gridMain.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(this.StudentNameChangedHandler));
            Student.AddNameChangedHandler(this.gridMain, new RoutedEventHandler(this.StudentNameChangedHandler));  //程序中发生改变的地方2
        }

        //Click事件处理器
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Student stu = new Student() { Id = 101, Name = "Tim" };
            stu.Name = "Tom";

            //准备事件消息并发送路由事件
            RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, stu);
            this.button1.RaiseEvent(arg);
        }

        //Grid捕捉到NameChangedEvent后的处理器
        private void StudentNameChangedHandler(object sender,RoutedEventArgs e)
        {
            MessageBox.Show((e.OriginalSource as Student).Id.ToString());
        }
    }
}

image-20200827115421690

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值