Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9731292
  • 博文数量: 1227
  • 博客积分: 10026
  • 博客等级: 上将
  • 技术积分: 20273
  • 用 户 组: 普通用户
  • 注册时间: 2008-01-16 12:40
文章分类

全部博文(1227)

文章存档

2010年(1)

2008年(1226)

我的朋友

分类: C/C++

2008-04-01 09:18:04

下载本文示例代码
本文是《VC知识库在线杂志》.NET平台系列文章中的第六篇。该系列文章从第十期“.NET和C#”栏的两篇文章中引入委派的概念。讨论了在微软.NET构架中的应用程序如何声明、创建及使用委派。本文将介绍事件,它是委派应用得最多的场合。
  事件允许某个对象通知其它对象,有个特定的事情已经发生。例如,当一个按钮被按下,应用程序中的几个对象可能会接收到一个通知并执行某些动作。事件就是允许这种交互作用的类型成员。明确地说,定义一个事件成员意味着某个类型提供以下的事情:
  •  对象能在事件中注册它们的兴趣。
  • 对象能在事件中注销它们的兴趣。
  •  拥有事件的对象能维护一组注册对象并在某个特定事件发生时通知这些对象
   为了充分理解事件,我们先定义一种假想的使用事件的情形。假设你想要设计一个e-mail应用程序。当一个e-mail到达时,用户可能想要把这个e-mail消息转发到一个传真机上或者寻呼机上。在构造这个应用程序时,我首先设计一个类型,叫做MailManager,它接收到来的e-mail消息。MailManager将提供或者说暴露一个事件,叫MailMsg。其它类型(如传真和寻呼机)可能在这个事件中注册兴趣。接着,当MailManager接收到一个新的e-mail消息时,它将激活这个事件,把消息分发到每一个注册对象。然后,每个对象则按照自己期望的方式来处理这个消息。
    在应用程序初始化时,我要求只有一个MailManager实例,但可以实例化任何数目用户想要的传真类型和寻呼机类型。图一显示了应用程序的初始化过程以及当新的e-mail消息到达时所发生的事情。
图一MailManager 
   让我来解释一下这个系统的工作模式。应用的初始化过程是通过构造MailManager实例来实现的。MailManager提供了一个MailMsg事件。当传真和寻呼机对象被构造时,它们将自己注册到MailManager的MailMsg事件中,以便让MailManager知道当新的消息到达时通知传真对象和寻呼机对象。现在,当MailManager接收到新的e-mail消息时,它将激活MailMsg事件,给所有的注册对象一个机会来以任何它们想要的方式处理新消息。
设计提供事件的类型
  首先我们来看一下MailManager的类型定义。下列代码示例了一种推荐的应该被用于暴露事件的设计模式。
MailManager 
class MailManager {

   public class MailMsgEventArgs : EventArgs {

      // 1. 传递到事件接收者的类型定义信息
      public MailMsgEventArgs(
         String from, String to, String subject, String body) {

         this.from    = from; 
         this.to      = to; 
         this.subject = subject; 
         this.body    = body; 
      }

      public readonly String from, to, subject, body; 
   }

   // 2. 委派类型定义回调方法的原型,接收者必须实现
   public delegate void MailMsgEventHandler(
      Object sender, MailMsgEventArgs args);

   // 3. 事件本身
   public event MailMsgEventHandler MailMsg;

   // 4. Protected类型的虚拟方法负责通知事件的注册对象
   protected virtual void OnMailMsg(MailMsgEventArgs e) {

      // 是否有注册的对象对事件感兴趣? 
      if (MailMsg != null) {
         // 通知所有委派链表中的对象
         MailMsg(this, e);
      }
   }

   // 5. 将输入转化为希望事件的方法
   //   当新的e-mail消息到达时调用这个方法
   public void SimulateArrivingMsg(String from, String to,
      String subject, String body) {

      // 构造一个对象来控制要传到通知接收者的信息 
      MailMsgEventArgs e = 
         new MailMsgEventArgs(from, to, subject, body);

// 调用虚拟方法通知对象事件发生。如果派生的类型没有重载这个方法,对象将通知所
//有注册的侦听者。
      OnMailMsg(e);
   }
}
实现这种体系结构所要做的工作要求开发人员定义MailManager类型。开发人员必须做以下五件事情:
第一、定义一个类型,用它控制应该被发送到事件通知接收者的所有附加信息。根据规范,控制事件信息的类型从System.EvenArgs派生,并且类型的名字应该以“EventArgs”结尾。在这个例子中,MailMsgEventArgs 类型具备字段标示谁发送了消息(from),谁接收消息(to),消息主题(subject)以及消息文本(body)。
EventArgs 类型从Object继承:
 [Serializable]
public class EventArgs {
   public static readonly EventArgs Empty = new EventArgs();
   public EventArgs() {  }
}
   可以看出,这个类型很简单,没什么内容。它只作为一个基类并且其它类型可以从这个基类中派生。许多事件是没有附加信息需要传递的。例如,当某个按钮通知其被按下按钮的注册接收者时,只调用回调方法就足够了。当定义没有附加数据传递的事件时,用EventArgs.Empty即可。
   第二,当事件激活时,定义一个委派类型指定将被调用的方法原型。根据规范,委派的名字应该以“EventHandler”结尾。同时原型返回值应该是void类型并且带两个参数(尽管某些事件处理器违反了这个规范,如ResolveEventHandler)。第一个参数是Object,指发送通知的对象,第二个参数是EventArgs派生类型,包含接收者需要的任何附加信息。
   如果定义的某个事件没有要传到事件接收者的附加信息,则不用定义新的委派。可以用System.EventHandler委派并用第二个参数传递EventArgs.Empty。EventHandler的原型如下:
public delegate void EventHandler(Object sender, EventArgs e); 
第三,定义一个事件,在这个例子中,MailMsg为事件的名字。这个事件是MailMsgEventHandler类型的,也就是说所有的事件通知接收者必须提供一个回调方法,其原型要与MailMsgEventHandler委派中的相匹配。
第四,定义一个protected类型的虚拟方法负责通知事件的注册对象。OnMailMsg方法当新e-mail到达时被调用。这个方法接收一个已初始化的MailMsgEventArgs对象,其中包含了附加的事件信息。此方法应该首先检查在事件中是否有对象注册了兴趣,如果有,则事件应被激活。
  使用MailManager作为基类的类型可以随便重载OnMailMsg方法。这就使得派生的类型可以操纵事件的激活。派生类型可以用它认为适合的任何方式处理新的邮件信息。通常,一个派生的类型会调用基类的OnMailMsg方法,以便注册的对象接收通知。但是,派生类型可能决定事件不再发出通知。
  最后,定义一个方法将输入转换为希望的事件。类型必须有允许输入的方法并将输入转换为事件的激活。在这个例子中,SimulateArrivingMsg方法一旦被调用,既表示有新邮件到达MailManager。SimulateArrivingMsg接收关于邮件消息的信息并构造一个新的MailMsgEventArgs对象,将到达的消息信息传到它的构造器。然后,MailManager自己的OnMailMsg方法被调用来正式通知新邮件消息的MailManager对象。通常,这将导致事件的激活,通知所有注册对象。但是,以MailManager为基类的类型可以重载这种行为。
  现在我们来进一步考察一下MailMsg事件定义的含义到底意味着什么。当编译器检查源代码时,它跨过定义事件的代码行,即:
public event MailMsgEventHandler MailMsg; 
//
C# 编译器将这行代码解释成三段构造代码:
//编译器产生的构造代码
// 1. 被初始化为null的私有委派
private MailMsgEventHandler MailMsg = null;

// 2. 公有方法 add_*,允许对象注册事件兴趣

MethodImplAttribute(MethodImplOptions.Synchronized)]
public void add_MailMsg(MailMsgEventHandler handler) {
   MailMsg = (MailMsgEventHandler)
      Delegate.Combine(MailMsg, handler);
}

// 3. 公有方法remove_*,允许对象注销事件兴趣
[MethodImplAttribute(MethodImplOptions.Synchronized)]
public void remove_Click (MailMsgEventHandler handler) {
   MailMsg = (MailMsgEventHandler)
      Delegate.Remove(MailMsg, handler);
}
  第一段构造代码在此类型中定义一个域。这个域就是等待事件通知的委派链表头。它被初始化为空,意思是在这个事件中侦听者没有注册的兴趣。当某个对象在事件中注册兴趣,这个域会引用MailMsgEventHandler委派的一个实例。每个MailMsgEventHandler委派实例都有一个指向另一个MailMsgEventHandler委派或空的指针来标记链表的尾。当某个侦听者在事件中注册兴趣时,这个侦听者只是向链表中添加此委派类型的一个实例。那么,注销的含义既是从链表中删除委派。
  你会注意到,尽管在原来的代码行中,事件总是被定义为public类型数据域,但它们(在这个例子中是MailMsg)总是私有的。原因是要防止类型定义之外的代码不正确地操作这个数据域。例如,只有MailManager知道什么时候一个新的e-mail信息到达,并且,也只有它能够激活事件。如果这个数据域是公共的(public),则任何代码都可以在任何时候激活这个事件,甚至是在邮件消息还没有到达时。
  第二段由C# 编译器产生的构造代码是一个方法,它允许其它对象在实践中注册它们的兴趣。C# 编译器自动在数据域(MailMsg)名字前添加“add_”前缀来命名这个函数。C# 编译器自动产生这个方法内部的代码。这个代码总是调用System.Delegate的静态Combine方法,由它往委派链表中添加一个委派实例,并返回新的链表头。
  第三段也是最后一段由C# 编译器产生的构造代码也是一个方法,它允许某个对象在事件中注销其兴趣。这里,C# 编译器再一次自动通过在数据域(MailMsg)名字前加“remove_”前缀来命名函数。这个方法内部的代码总是调用Delegate的静态Remove方法从委派链表中删除委派实例,并返回新的链表头。
注意,add和remove两个方法都具备MethodImplAttribute属性。更特别的是这些方法都被标记为同步,使它们是线程安全的:多个侦听者可能在同时注册或注销到这个事件,但链表不会被破坏。
  在本文的例子中,add和remove两个方法都是公共类型的。理由是原来的代码行声明的事件是公共的。如果事件被声明为protected类型的,则由C# 编译器产生的add和remove方法也将被声明为protected类型。所以,当你在某个类型中定义事件时,事件的可存取性决定了什么代码能在事件中注册和注销兴趣;但是,只有这个类型本身能在任何时候激活这个事件。
  除了产生上面三段构造代码之外,编译器也产生一个到受管模块元数据的事件定义入口。这个入口包括一些标志,基本的委派类型以及对add和remove存取器方法的引用。这个信息简单地勾勒出了“事件”的抽象概念与其存取器方法之间的联系。编译器和其它工具可以使用这个元数据,并且这些信息当然也能通过使用System.Reflection.EventInfo类来获得。但是,公共语言运行时(CLR)本身并不使用此元数据信息,只是在运行时需要存取器方法。
设计侦听事件的类型
在这一部分中,我们将讨论如何定义一个类型,而它使用另一个类型提供的事件。通过考察Fax类型代码:
class Fax {
   // 将MailManager对象传递到构造器
   public Fax(MailManager mm) {

      // 构造MailMsgEventHandler实例 
      // 委派引用FaxMsg回调方法
      // 用MailManager的MailMsg事件注册回调
      mm.MailMsg += new MailManager.MailMsgEventHandler(FaxMsg);
   }

   // 此为MailManager要调用的方法 
   // 通知Fax 对象新的e-mail消息已到达
   private void FaxMsg(
      Object sender, MailManager.MailMsgEventArgs e) {

      // 当要再次进行返回通讯时,''sender''确定MailManager

      // ''e''表示MailManager给出的附加事件信息 

      // 通常,这段代码会fax此e-mail信息
      // 这一行实现在控制台上显示信息
      Console.WriteLine("Faxing mail message:");
      Console.WriteLine(
         "   To: {0}\n   From: {1}\n   Subject: {2}\n   Body: {3}\n",
         e.from, e.to, e.subject, e.body);
   }

   public void Unregister(MailManager mm) {

      // 构造MailMsgEventHandler实例 
      // 引用FaxMsg 回调方法的委派
      MailManager.MailMsgEventHandler callback = 
         new MailManager.MailMsgEventHandler(FaxMsg);
      // 对MailManager的MailMsg 事件进行自注销
      mm.MailMsg -= callback;
   }
}
  当此e-mail应用初始化时,它首先会构造一个MailManager对象并在某个变量中存储这个变量的引用。然后,应用会构造一个Fax对象,将它作为引用参数传到MailManager。在Fax的构造器中。新的MailManager.MailMsgEventHandler委派对象被构造并且对它的引用被存储在此回调变量中。这个新的委派对象是Fax类型中FaxMsg方法的打包器。你会注意到FaxMsg方法带两个参数并返回void类型,这两个参数与MailManager的MailMsgEventHandler委派所定义的一样。这是必不可少的,否则编译不成功。
构造委派之后,Fax对象用下面的代码行在MailManager的MailMsg事件中注册其兴趣:
mm.MailMsg += callback
  因为C#编译器本身内建了对事件的支持,编译器将上面+=操作符转化为下面的代码行,在事件中添加对象的兴趣:
mm.add_MailMsg(callback);
  如果你用一种不直接支持事件的编程语言,那么仍然要通过显式地调用add存取器方法注册带事件的委派。那样做效果完全一样,只是源代码样子不好看。通过将委派添加到事件的委派链表,由add方法注册带事件的委派。 
  当MailManager激活事件时,Fax对象的FaxMsg方法得到调用。这个方法的引用被传递到MailManager对象。大多数情况下,这个参数被忽略,但Fax对象在响应事件通知的过程中如果要存取MailManager对象的数据和方法时,就可能使用到这个参数。第二参数是MailMsgEventArgs对象的引用,这个对象包含MailManager需要的附加信息,MailManager将利用这些附加信息辅助事件接收。
  FaxMsg方法可以从MailMsgEventArgs对象轻松存取下面这些属于消息的信息:
发送者,消息接收者,主题,消息文本。在实际的Fax对象中,这些信息会被传真到某个地方。在这个例子的代码中,这些信息只是被显示在控制台窗口中。
  虽然不是很常见,但一个对象可以注销其带另一个对象事件的兴趣。在Fax的注销方法Unregister中示范了如何注销一个事件,参见Fax类型的代码。
  这个方法实际上与Fax类型的构造器代码相同。差别只是在这段代码中使用了-=代替了+=。当C#编译器看到用-=操作符注销带事件的委派的代码时,编译器实际上会发出一个事件的Remove方法调用:
mm.remove_MailMsg(callback);
  如果你正在使用的编程语言不直接支持事件,则你可能仍然通过显式地调用Remove存取器方法注销带事件的委派。通过扫描委派链表,看看它是否包含有与传入的回调方法相同的方法,Remove方法从事件中注销委派。如果有匹配,现存的委派被从事件的委派链表中删除。如果没找到匹配,则不出错也不会改变链表。
  此外,C# 要求在代码中用+=和-+操作符添加和删除链表中的委派。如果试图显式地调用add或者remove方法,则C# 编译器会产生“cannot explicitly call operator or accessor”错误,意思是:不能显式调用操作符或存取器。
  MailManager例子程序列出了全部MailManager类型,Fax类型和Pager类型的源代码。你会注意到Pager类型的实现与Fax类型的实现类似。
下载本文示例代码
阅读(1485) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~