事件和委托
在典型的面向对象软件的一般流程中,代码段创建类的对象并在该对象上调用力法。在这种情况下,调用程序是主动代码,因为它们是调用方法的代码。而对象是被动的,因为只有当某种方法被调用时才会用上对象并执行某种动作。
然而,也可能存在相反的情况。对象可以执行一些任务并在执行过程中发生某些事情时通知调用程序。称这类事情为事件(event),对象的事件发布称为引发事件。
事件驱动处理对于.NET来说并不是什么技术,在事件驱动处理中,当有事件发生时,某些代码段会通知其他对事件感兴趣的代码段。当用户使用鼠标、敲击键盘或者移动窗口时,Windows用户接口层一直使用事件的形式通知Windows应用程序。当用户采取影响ActiveX控件的动作时,ActiveX控件就会引发事件至ActiveX控件容器。
为了在C#代码中激发、发布和预约事件更容易,C#语言提供了一些特殊的关键字。使用这些关键字允许C#类毫不费力地激发和处理事件。
1.定义委托
当设计C#类引发的事件时,需要决定其他代码段如何接收事件。其他代码段需要编写一种方法来接收和处理发布的事件。例如,假设类实现了一个Web服务器,并想在任何时间从Internet发来页面请求时激发一个事件。在类激发这个new request事件时,其他代码段执行某种动作,并且代码中应该包含一种方法,在激发事件时执行该方法。
类用户实现接受和处理事件的方法由C#中的概念——委托(delegate)定义。委托是一种“函数模板”,它描述了用户的事件处理程序必须有的结构。委托也是一个类,其中包含一个签名以及对方法的引用。就像一个函数指针,但是它又能包含对静态和实例方法的引用。对于实例方法来说,委托存储了对函数人口点的引用以及对对象的引用。委托定义了用户事件处理程序内该返回的内容以及应该具备的参数表。
要在C#中定义一个委托,使用下列语法:
delegate 事件处理程序的返回类型 委托标识符(事件处理程序的参数表)
如果在激发事件的类中声明委托,可以在委托前加上前缀public,protected,internal或者 private关键字,如下面的delegate定义示例所示:
public delegate void EvenNumberHandler(int Number);
在上述示例中,创建了一个称为EvenNumberHandler的公共委托(public delegate),它不返回任何值。该委托只定义了一个要传递给它的参数,该参数为int类型。委托标识符(这里是EvenNumberHandler)可以是任何名称,只要不与C#关键字的名称重复即可。
2.定义事件
为了阐述清楚事件的概念,我们以一个示例开始讲述。假设正驱车在路上并且仪表板上显示燃料不足的灯亮了。在这个过程中。汽缸中的传感器给计算机发出燃料快要耗尽的信号。然后,计算机又激发一个事件来点亮仪表板上的灯,这样司机就知道要购买更多的油了。用最简单的话来说,事件是计算机警告你发发生某种状况的一种方式。
使用C#的关键字event来定义类激发的事件。在C#中事件声明的最简单形式包含下列内容:
evet 事件类型 事件标识符 事件类型委托标识符匹配
以下面的Web服务器示例为例:
public delegate void NewRequestHandler(string URL);
public class WebSever
{
public event NewRequestHandler NewRequestEvent;
//...
}
上述示例声明了一个称为NewRequestHandler的委托。NewRequestHandler定义了一个委托,作为处理new request事件的方法的方法模板。任何处理new request事件的方法都必须遵循委托的调用规则:必须不返回任何数据,必须用一个字符串作为参数表。事件处理程序的实现可以拥有任意的方法名称,只要返回值和参数表符合委托模板的要求即可。
WebServer类定义了一个事件NewRequestEvent。该事件的类型为NewRequestHandle。这意味着,只有与该委托的调用规则相匹配的事件处理程序才可以用于处理NewRequestEvent事件。
3.安装事件
编写完事件处理程序之后,必须用new运算符创建一个它的实例并将它安装到激发事件的类中。创建一个事件处理程序new实例时,要用new运算符创建一个属于这种委托类型的变量,并且作为参数传递事件处理程序方法的名称。
以Web服务器的示例为例,对事件处理程序new实例的创建如下代码所示:
public void MyNewRequestHandler(string URL) { } NewRequestHandler HandlerInstance; HandlerInstance = new NewRequestHandler(MyNewRequestHandler);
在创建了事件处理程序的new实例之后,使用+=运算符将其添加到事件变量中:
NewRequestEvent += HandlerInstance;
上述语句连接了HandleInstance委托实例与NewRequestEvent事件,该委托支持MyNewRequestMethod方法。使用+=运算符,可以将任意多的委托实例与一个事件相连接。同理,可以使用-=运算符从事件连接中删除委托实例:
NewRequestEvent -= HandlerInstance;
上述语句解除了HandlerInstance委托实例与NewRequestEvent事件的连接。
4.激发事件
可以使用事件标识符(比如事件的名称)从类中激发事件.就像事件是一个方法一样。作为方法调用事件将激发该事件c在Web浏览器示例中,使用如下语句激发new request事件:
NewRequestEvent(strURLOfNewRequest);
事件激发调用中使用的参数必须与事件委托的参数表匹配。定义NewRequestEvent事件的委托接受一个字符串参数;因此,当从Web浏览器类中激发该事件时,必须提供一个字符串。
下面的例子说明了委托和事件的概念。其中实现了一个类,该类从0计数到100,并在计数过程中找到偶数时激发一个事件。
示例:
using System; public delegate void EvenNumberHandler(int Number); class Counter { public event EvenNumberHandler OnEvenNumber; public Counter() { OnEvenNumber = null; } public void CountTo100() { int CurrentNumber; for(CurrentNumber = 0; CurrentNumber <= 100;CurrentNumber++) { if(CurrentNumber % 2 == 0) { if(OnEvenNumber != null) { OnEventNumber(CurrentNumber); } } } } } classEvenNumberHandlerClass { public void EvenNumberFound(int EvenNumber) { Console.WriteLine(EvenNumber); } } class ClassMain { public static void Main() { Counter MyCounter = new Counter(); EvenNumberHandlerClass MyEvenNumberHandlerClass = new EvenNumberHandlerClass(); MyCounter.OnEvenNUmber += new EvenNumberHandler(MyEvenNumberHandlerClass.EvenNumberFound); } }
程序实现了三个类
● Counter类执行计数功能。其中实现了一个公共方法CountTo100()和一个公共事件 OnEvenNumber。OnEvenNumber事件的委托类型为EvenNumberHandler。
● EvenNumherHandlerClass类包含一个公共方法EvenNumberFound。该方法为Counter类 的OnEvenNumber事件的事件处理程序。它将作为参数提供的整数打印到控制台上。
● MainClass类包含应用程序的Main()方法。Main()方法创建类Counter的一个对象并将该对象命名为MyCounter。还创建了类EvenNumberHandlerC1ass的一个new对象,井调用了对象MyEvenNumberHandlerC1ass。Majn()方法调用MyCounter对象的CountTo100()方法,但是不是在将委托实例安装到Counter类中之前调用的。其中的代码创建了一个new委托实例,用它来管理MyEvenNumber。HandlerClass对象的EvenNumberFound方法,并使用+=运算法将其添加到MyCounter对象的0nEvenNumber事件中。
CountTol00方法的实现使用一个局部变量从0计数到100。在每一次计数循环中,代码都会检测数字是否是偶数,方法是看数字被2除后是否有余数。如果数字确实是偶数,代码就激发OnEvenNumber事件,将偶数作为参数提供,以便与事件委托的参数表匹配。
因为MyEvenNumherHandlerClass的EvenNumberFound方法是作为事件处理程序安装的,而且该方法将提供的参数打印到控制台上,所以编译并运行代码后,0到100之间的所有偶数都会打印到控制台上。
5.标准化事件的设计
尽管C#可以接受它编译的任何委托设计,但是.NET框架还是鼓励开发人员采用标准的委托设计方式。
最好委托设计使用两个参数;例如,SystemEventhandler委托:
● 对引发事件的对象的引用
● 包含与事件有关的数据的对象
第二个参数包含了所有事件数据,应该是某个类的一个对象.这个类由.NET的System.EventArgs类派生出来。
对上面代码进行修改,其中使用了这种标准的设计方式。
示例:
using System; public delegate void EvenNumberHandler(object Originator, OnEvenNumberEventArgs EventNumberEventArgs); class Counter { public event EvenNumberHandler OnEvenNumber; public Counter() { OnEvenNumber = null; } public void CountTo100() { int CurrentNumber; for(CurrentNumber = 0; CurrentNumber <= 100;CurrentNumber++) { if(CurrentNumber % 2 == 0) { if(OnEvenNumber != null) { OnEvenNumberEventArgs EventArguments; EventArguments = new OnEvenNumberEvenArgs(CurrentNumber); OnEventNumber(this,EventArguments); } } } } } public class OnEvenNumberEventArgs : EventArgs { private int EventNumber; public OnEvenNumberEventArgs(int EvenNumber) { this.EvenNumber = EvenNumber; } public int Number { get { return EventNumber; } } } class EvenNumberHandlerClass { public void EvenNumberFound(object Originator, OnEvenNumberEventArgs EvenNumberEventArgs) { Console.WriteLine(EvenNumberEventArgs.Number); } } Class MainClass { public static void Main() { Counter MyCounter = new Counter(); EvenNumberHandlerClass MyEvenNumberHandlerClass = new EvenNumberHandlerClass(); MyCounter.OnEvenNUmber += new EvenNumberHandler(MyEvenNumberHandlerClass.EvenNumberFound); MyCounter.CountTo100(); } }