zoukankan      html  css  js  c++  java
  • 【转载】wpf学习笔记3

    事件也附加——深入浅出附加事件

    WPF事件系统中还有一种事件被称为附加事件(Attached Event),简言之,它就是路由事件。“那为什么还要起个新名字呢?”你可能会问。

    “身无彩凤双飞翼,心有灵犀一点通”,这就是对附加事件宿主的真实写照。怎么解释呢?让我们看看都有哪些类拥有附加事件:

    • Binding类:SourceUpdated事件,TargetUpdated事件
    • Mouse类:MouseEnter事件、MouseLeave事件、MouseDown事件、MouseUp事件,等等
    • Keyboard类:KeyDown事件、KeyUp事件,等等

    再对比一下那些拥有路由事件的类,诸如ButtonSliderTextBox……发现什么问题了吗?原来,路由事件的宿主都是些拥有可视化实体的界面元素,而附加事件则不具备显示在用户界面上的能力。也就是说,附加事件的宿主没有界面渲染功能这双“飞翼”,但一样可以使用附加事件这个“灵犀”与其他对象进行沟通。

    理解了附加事件的原理,让我们动手写一个例子。我想实现的逻辑是这样的:设计一个名为Student的类,如果Student实例的Name属性值发生了变化就激发一个路由事件,我会使用界面元素来捕捉这个事件。

    这个类的代码如下:

    1. public class Student 
    2.     // 声明并定义路由事件
    3.     public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent 
    4.         ("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student)); 
    5.     public int Id { get; set; } 
    6.     public string Name { get; set; }         

             设计一个简单的界面:

    1. <Window x:Class="WpfApplication1.Window1"
    2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Attached Event"
    4.         Height="200" Width="200">
    5.     <Grid x:Name="gridMain">
    6.         <Button x:Name="button1" Content="OK" Width="80" Height="80"
    7.                 Click="Button_Click" />
    8.     </Grid>
    9. </Window>

            其后台代码如下:

    1. public partial class Window1 : Window 
    2.     public Window1() 
    3.     { 
    4.         InitializeComponent(); 
    5.         // 为外层Grid添加路由事件侦听器
    6.         this.gridMain.AddHandler( 
    7.             Student.NameChangedEvent, 
    8.             new RoutedEventHandler(this.StudentNameChangedHandler)); 
    9.     } 
    10.     // Click事件处理器
    11.     private void Button_Click(object sender, RoutedEventArgs e) 
    12.     { 
    13.         Student stu = new Student() { Id = 101, Name = "Tim" }; 
    14.         stu.Name = "Tom"; 
    15.         //  准备事件消息并发送路由事件
    16.         RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, stu); 
    17.         this.button1.RaiseEvent(arg); 
    18.     } 
    19.     // Grid捕捉到NameChangedEvent后的处理器
    20.     private void StudentNameChangedHandler(object sender, RoutedEventArgs e) 
    21.     { 
    22.         MessageBox.Show((e.OriginalSource as Student).Id.ToString()); 
    23.     } 

    后台代码中,当界面上唯一的Button被点击后会触发Button_Click这个方法。有一点必须注意的是:因为Student不是UIElement的派生类,所以它不具有RaiseEvent这个方法,为了发送路由事件就不得不“借用”一下ButtonRaiseEvent方法了。在窗体的构造器中我为Grid元素添加了对Student.NameChangedEvnet的侦听——与添加对路由事件的侦听没有任何区别。Grid在捕捉到路由事件后会显示事件消息源(一个Student实例)的Id

    运行程序并点击按钮,效果如图:

    理论上现在的Student类已经算是具有一个附加事件了,但微软的官方文档约定要为这个附加事件添加一个CLR包装以便XAML编辑器识别并进行智能提示。可惜的是,Student类并非派生自UIElement,因此亦不具备AddHandlerRemoveHandler两个方法,所以不能使用CLR属性作为包装器(因为CLR属性包装器的addremove分支分别调用当前对象的AddHandlerRemoveHandler)。微软规定:

    • 为目标UI元素添加附加事件侦听器的包装器是一个名为Add*Handlerpublic static方法,星号代表事件名称(与注册事件时的名称一致)。此方法接收两个参数,第一个参数是事件的侦听者(类型为DependencyObject),第二个参数为事件的处理器(RoutedEventHandler委托类型)。
    • 解除UI元素对附加事件侦听的包装器是名为Remove*Handlerpublic static方法,星号亦为事件名称,参数与Add*Handler一致。

    按照规范,Student类被升级为这样:

    1. public class Student 
    2.     // 声明并定义路由事件
    3.     public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent 
    4.         ("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student)); 
    5.     // 为界面元素添加路由事件侦听
    6.     public static void AddNameChangedHandler(DependencyObject d, RoutedEventHandler h) 
    7.     { 
    8.         UIElement e = d as UIElement; 
    9.         if (e != null) 
    10.         { 
    11.             e.AddHandler(Student.NameChangedEvent, h); 
    12.         } 
    13.     } 
    14.     // 移除侦听
    15.     public static void RemoveNameChangedHandler(DependencyObject d, RoutedEventHandler h) 
    16.     { 
    17.         UIElement e = d as UIElement; 
    18.         if (e != null) 
    19.         { 
    20.             e.RemoveHandler(Student.NameChangedEvent, h); 
    21.         } 
    22.     } 
    23.     public int Id { get; set; } 
    24.     public string Name { get; set; } 

    原来的代码也需要做出相应的改动(只有添加事件侦听一处需要改动):

    1. public Window1() 
    2.     InitializeComponent(); 
    3.     // 为外层Grid添加路由事件侦听器
    4.     Student.AddNameChangedHandler( 
    5.         this.gridMain, 
    6.         new RoutedEventHandler(this.StudentNameChangedHandler)); 

    现在让我们仔细理解一下附加事件的“附加”。确切地说,UIElement类是路由事件宿主与附加事件宿主的分水岭,不单是因为从UIElement类开始才具备了在界面上显示的能力,还因为RaiseEventAddHanderRemoveHandler这些方法也定义在UIElement类中。因此,如果在一个非UIElement派生类中注册了路由事件,则这个类的实例即不能自己激发(Raise)此路由事件也无法自己侦听此路由事件,只能把这个事件的激发“附着”在某个具有RaiseEvent方法的对象上、借助这个对象的RaiseEvent方法把事件发送出去;事件的侦听任务也只能交给别的对象去做。总之,附加事件只能算是路由事件的一种用法而非一个新概念,说不定哪天微软就把附加事件这个概念撤消了。

    最后分享些实际工作中的经验。

    第一,像Button.Click这些路由事件,因为事件的宿主是界面元素、本身就是UI树上是一个结点,所以路由事件路由时的第一站就是事件的激发者。附加事件宿主不是UIElement的派生类,所以不能出现在UI树上的结点,而且附加事件的激发是借助UI元素实现的,因此,附加事件路由的第一站是激发它的元素。

    第二,实际上很少会把附加事件定义在Student这种与业务逻辑相关的类中,一般都是定义在像BindingMouseKeyboard这种全局的Helper类中。如果需要业务逻辑类的对象能发送出路由事件来怎么办?我们不是有Binding吗!如果程序架构设计的好(使用数据驱动UI),那么业务逻辑一定会使用Binding对象与UI元素关联,一旦与业务逻辑相关的对象实现了INotifyPropertyChanged接口并且Binding对象的NotifyOnSourceUpdated属性设为true,则Binding就会激发其SourceUpdated附加事件,此事件会在UI元素树上路由并被侦听者捕获。

    ========================================

    另附Apress之《Pro WPF in C# 2008》之原文,请大家通过甄别加深印象。亦请《Pro》一书的读者、译者加以注意。

    Attached Events

    The fancy label example is a fairly straightforward example of event bubbling because all the elements

    support the MouseUp event. However, many controls have their own more specialized

    events. The button is one example—it adds a Click event that isn’t defined by any base class.

    This introduces an interesting dilemma. Imagine you wrap a stack of buttons in a Stack-

    Panel. You want to handle all the button clicks in one event handler. The crude approach is to

    attach the Click event of each button to the same event handler. But the Click event supports

    event bubbling, which gives you a better option. You can handle all the button clicks by handling

    the Click event at a higher level (such as the containing StackPanel).

    Unfortunately, this apparently obvious code doesn’t work:

    <StackPanel Click="DoSomething" Margin="5">

    <Button Name="cmd1">Command 1</Button>

    <Button Name="cmd2">Command 2</Button>

    <Button Name="cmd3">Command 3</Button>

    ...

    </StackPanel>

    The problem is that the StackPanel doesn’t include a Click event, so this is interpreted by

    the XAML parser as an error. The solution is to use a different attached-event syntax in the

    form ClassName.EventName. Here’s the corrected example:

    <StackPanel

    Button.Click="DoSomething" Margin="5"> 实际上这是添加路由事件处理函数,而非附加事件

    <Button Name="cmd1">Command 1</Button>

    <Button Name="cmd2">Command 2</Button>

    <Button Name="cmd3">Command 3</Button>

    ...

    </StackPanel>

    Note

    The Click event is actually defined in the ButtonBase class and inherited by the Button class. If you

    attach an event handler to ButtonBase.Click, that event handler will be used when any ButtonBase-derived

    control is clicked (including the Button, RadioButton, and CheckBox classes). If you attach an event handler

    to Button.Click, it’s only used for Button objects.

    You can wire up an attached event in code, but you need to use the UIElement.

    AddHandler() method rather than the += operator syntax. Here’s an example (which assumes

    the StackPanel has been given the name pnlButtons):

    pnlButtons.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));

    In the DoSomething() event handler you have several options for determining which button

    fired the event. You can compare its text (which will cause problems for localization) or its

    name (which is fragile because you won’t catch mistyped names when you build the application).

    The best approach is to make sure each button has a Name property set in XAML, so

    that you can access the corresponding object through a field in your window class and compare

    that reference with the event sender. Here’s an example:

    private void DoSomething(object sender, RoutedEventArgs e)

    {

    if (sender == cmd1)

    { ... }

    else if (sender == cmd2)

    { ... }

    else if (sender == cmd3)

    { ... }

    }

    Another option is to simply send a piece of information along with the button that you

    can use in your code. For example, you could set the Tag property of each button, as shown

    here:

    <StackPanel Click="DoSomething" Margin="5">

    <Button Name="cmd1" Tag="The first button.">Command 1</Button>

    <Button Name="cmd2" Tag="The second button.">Command 2</Button>

    <Button Name="cmd3" Tag="The third button.">Command 3</Button>

    ...

    </StackPanel>

    You can then access the Tag property in your code:

    private void DoSomething(object sender, RoutedEventArgs e)

    {

    object tag = ((FrameworkElement)sender).Tag;

    MessageBox.Show((string)tag);

    }

  • 相关阅读:
    设计模式入门--设计模式原则(总纲)
    Chart图形 [GDI+] OWCChart统计图的封装类 (转载)
    Chart图形 [功能帮助类] Assistant创建显示图像的标签和文件 (转载)
    [GDI+] 生成缩略图的类文件SmallImage (转载)
    [Mime] 在c#程序中放音乐的帮助类 (转载)
    [访问系统] Api_Win32_Mac类工具包 (转载)
    [访问系统] C#计算机信息类ComputerInfo (转载)
    [Jquery] jQuery.cookie帮助类 (转载)
    [综合|基础|语法] 最新的皮肤帮助类 UI_Misc_Helper (转载)
    [JS] JavascriptHelp (转载)
  • 原文地址:https://www.cnblogs.com/fx2008/p/2423471.html
Copyright © 2011-2022 走看看