Part I of Events in Asp.Net: Events in .Net

声明:本blog所有文章均为EagleFish在cnBlogs上的原创,欢迎转载,但请注明出处。

在介绍Asp.net中的事件之前,我们先对.net中的事件处理和实现机制做以简单介绍。

相信所有人对事件(Event)的概念都是很熟悉的。要实现一个事件系统,下列元素是必不可少的:
1.事件源  (sender)。
2.事件的接收方  (receiver)。//注:准确的说,应该是接收方的事件处理方法。
3.发送方所要发送给接收方的信息 (message)。
4.接收方在发送方注册/注销 (register/unregister)。
前三者都是静态数据结构,而注册是一个动态的过程,其实就是把接收方和它所关心事件的发送方连接起来,这种连接过程的结果其实也是一个数据结构,一般体现为发送方对象实例内部维持的一个动态列表,列表里的每一项都是一个接收方对象实例的事件处理方法(可理解为函数指针)。在事件发生的时候,发送方遍历此列表,并依次调用列表中的方法,这个过程就叫做事件的触发(fire)。

下面,我们就以这4个概念为着眼点,来解释一下.net中的事件体系(.net中事件的实现利用了.net中的委托机制,这是一种被编译器直接支持的类型安全的回调函数定义方法,对它的详细介绍超出了本文的范围)。先介绍一下一个非常简单的应用场景吧:有若干个button,位于一个Panel上,每一个button有不同的name。当任意一个button被push的时候,Panel都会接收到这个事件,并作出响应:打印被点击button的name。好,要实现这个场景,我们要做些什么呢?

一.我们先从最容易的开始,先构建一个消息类,让这个类去封装button的名字。按约定,所有传递给事件处理程序用于存放事件信息的类都必须继承自System.EventArgs:
事件消息类

二. 接下来我们要定义事件源了,让我们先看一下Button类的代码:
事件源
在程序中的注释里已经提到了,Button类中的Push,OnPush和PushMe是和事件相关的3个重要元素,让我们来一一认识它们:
1.public event EventHandler<ButtonPushEventArgs> Push;
  这里定义了一个名为Push的事件。event是一个特殊的关键字,用来提示编译器生成特定的IL代码(我们稍后就会看到),这些代码对.Net中的事件机制起到了关键作用,因为正是这些代码操作着发送方内部的动态列表。而后面的EventHandler<ButtonPushEventArgs>定义了事件的具体类型。注意,我们在看.net的一些资料的时候,经常会看到“事件类型”这类的说法,这里的事件类型和我们日常生活中的事件类型有很大差别,它定义的其实是“事件处理方法”的类型(我们生活中的事件类型恐怕和程序中的事件消息message在意义上更为贴近)。当然,事件源自己是不清楚事件处理方法的,它只清楚自己发出的消息,所以这里定义事件类型,其实只能是一个有确定参数(EventArgs)的委托(Delegate)类型,而msdn上对EventHandler泛型委托的定义印证了我们这个猜测:
public delegate void EventHandler<TEventArgs> (
 Object sender,
 TEventArgs e
) where TEventArgs : EventArgs
由于所有委托类型都继承自System.MulticastDelegate类,可以用委托类型对象内部的_invocationList变量很容易的形成委托链,而这个委托链,就是事件源内部最关键的数据结构,数据源正是靠这个链表来依次调用接收方的注册方法。那么对这个委托链的具体操作到底在哪儿呢?前面我们说过,当编译器看到event关键字的时候,会生成一些特定的IL代码,本程序中的相关IL代码如下:
public event EventHandler Push;生成的IL代码
看到上面的add_Push方法,大家对于数据源如何新增事件接收者就心里有数了。事件接收者的内部一定有一个以Object和ButtonPushEventArgs为类型的方法,这个方法将被添加到事件源的事件委托链上,最终在事件发生的时候完成这个方法的调用。
  可以在在这里多提一句的是,大家可以注意一下IL代码的第12行,委托链的增长是通过调用Delegate类的静态方法Combine方法把一个新的委托添加到已有委托链尾端的。而这个方法的两个参数分别是当前调用栈栈顶的两个元素,第一个参数是编译器根据event关键字为Button类生成的类型为EventHandler<>的成员变量Push(见IL代码第一行),而第二个参数就是add_Push方法的传入参数了,事件的接收者最终会调用这个方法,并将自己的处理方法传入。在方法第一次被调用的时候,Push是一个空引用,而根据Delegate.Combine()方法的说明,把一个空引用和一个委托实例a进行连接,则返回a。所以,Push是在第一次调用了add_Push之后才有值的(这也印证了OnPush()方法定义中的对if(temp==null)的判断)。
  细心的朋友可能会问,这个add_Push方法是在IL代码中才生成的,那在写C#代码的时候,事件接收者是通过什么方式把事件处理方法挂载到委托链上的呢?别急,我们有比直接调用add_Push简单得多的方法,等下面介绍Panel类的时候您就知道了。
2.OnPush()和PushMe()
  首先要说的是,其实我们并不是一定要把它们写成两个方法。比如说,在我们这个简单的应用里面,完全可以不定义OnPush()方法,直接将PushMe()定义成如下形式:
改变后的PushMe
但我们在实际的开发过程中,事件源很少是一个单一类型,而是一个类型体系。比如说Button类的下面可能又分为ImageButton和LinkedButton等,把委托链调用OnPush提出来放在基类里面,而具体的事件触发PushMe则放到每一个子类里面(每一个子类发送的消息可能有所不同),是我们更常采用的设计方式。
  具体在OnPush和PushMe的内部,代码都比较简单了。前面提到了委托链,接下来还要提供一个方法去触发这个委托链被调用的过程,OnPush完成的就是这样的功能,它里面就是对委托方法的调用。每一个委托方法被Invoke以后,都会自动去调用委托链下一节点的方法(这是.net在委托类型里内置的功能)。而PushMe是事件的触发位置,最重要的功能自然就是对事件消息的封装了(这也是为什么要把这个方法下放到各个子类的原因),封装完毕之后直接调用OnPush即可。

三.事件接收者
接收者要做的事情比事件源少多了,我们只需要把注意力集中在它如何把自己的事件处理方法挂载到事件源的委托链上即可(讲到现在,大家应该可以体会出了,对于事件的注册,是接收者主动;而对于事件的触发,则是事件源主动,是一个push模型)。先看一下Panel类的代码:
Panel类
在这个类中,WhenButtonPushed是Panel定义的事件处理方法,而它的参数也完全满足在Button中的事件设定。而Add方法是用来把Button实例添加到Panel实例中,在这里我们最感兴趣的肯定是b.Push+=this.WhenButtonPushed。你可能已经想到了,当编译器看到这样一句语句的时候,会生成对add_Push调用的IL代码,而事实正是如此,我们一起来看看Panel.Add()方法编译出的IL代码:
Panel.Add()的IL代码

看到代码段的第12行您一定会感到非常亲切,它正是我们想看到的。

好,到现在为止,我们已经把一个有关.net事件的例子的各个重要部分都写完了,现在只要几句简单的调用,我们就能看到Event为我们带来的效果了。最后给出完整程序如下(包括上面提到的每一部分代码,可运行):

完整示例

 




 

posted @ 2007-07-26 15:41  EagleFish(邢瑜琨)  阅读(1085)  评论(3编辑  收藏  举报