C# 事件的设计与使用深入理解

相关概念

  定义:事件是用于通知其他对象发生了本对象发生了特定的事情的类型成员。

  说明:事件是.NET类型成员中相对较为难以理解和实践的一个成员,因为事件的定义不是继承自基础的数据类型,而是对委托(delegate)的封装。所以,在了解事件之前,你需要先了解一点委托。

  应用场景:事件的应用场景非常广泛,其中最常见的场景是在各个前端控件中的大量触发事件设计。原因是因为

  意义:事件成员的使用有利于在程序中对面向对象原则的实现。例如类型的单一职责原则,控制反转原则。设想如果前端控件不能抽象出大量丰富的事件,那几乎不能将前端的UI元素与业务逻辑脱钩。程序必然高度耦合。

  设计模式的应用:经典设计模式中的观察者模式就非常依赖于对事件成员的设计而实现。

  本章将通过设计一个电子邮件到达时,触发事件的场景来解析对事件提供者和订阅者类型的设计。案例来源于《CLR Via C#》一书。

  事件提供者类型的设计

  一. 定义类型来容纳所有需要发送给事件订阅者的附加信息

  目标:定义一个类型用于向事件的订阅者传递信息

  方法:继承默认的System.EventArgs类型,实现简单的需要传递信息的字段,属性以及实例构造器成员。示例如下:

  

复制代码 代码如下:

  using System;

  using System.Linq;

  namespace ConsoleTest

  {

  public class NewMailEventArgs : EventArgs

  {

  private readonly string from, to, subject;

  public NewMailEventArgs(string from, string to, string subject)

  {

  this.from = from;

  this.to = to;

  this.subject = subject;

  }

  public string Subject

  {

  get

  {

  return this.subject;

  }

  }

  public string To

  {

  get

  {

  return this.to;

  }

  }

  public string From

  {

  get

  {

  return this.from;

  }

  }

  }

  }

  二. 定义事件成员

  目标:在事件提供者类型中定义一个事件成员,用于事件订阅者对象的注册。

  方法:封装一个自定义委托,来提供事件处理方法的模板;或者实现一个System.EventHandler的泛型类型来达到一样的效果。(EventHandler是一个默认提供的已封装的委托)。两种方法的示例分别如下:

  方法一:

  

复制代码 代码如下:

  public delegate void NewMailHandler(object e, NewMailEventArgs args);

  public class MailManager

  {

  public event NewMailHandler NewMail;

  }

  方法二:

  

复制代码 代码如下:

  public class MailManager

  {

  public event EventHandler<NewMailEventArgs> NewMail;

  }

  为什么这两种方法能够达到同样的效果,查看一下System.EventHandler的定义就能知晓:

  

复制代码 代码如下:

  namespace System

  {

  // 摘要:

  // 表示将处理事件的方法。

  //

  // 参数:

  // sender:

  // 事件源。

  //

  // e:

  // 一个包含事件数据的 System.EventArgs。

  //

  // 类型参数:

  // TEventArgs:

  // 由该事件生成的事件数据的类型。

  [Serializable]

  public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

  }

  三. 定义一个统一触发事件的方法入口来通知事件的订阅对象

  目标:在事件提供者类型中定义一个方法成员,用来统一的引发目标事件。

  说明:为了保证这个方法只能在本类型及派生类型中调用,我们需要将方法修饰为protected, 为了让派生类型可以重写这个方法,我们需要将该方法修饰为virtual

  意义:这个统一入口方法的意义在于,能够统一维护触发事件的方式,并且能够确保事件调用的线程安全性。(避免在不同的线程触发时,事件订阅者的状态不同步)

  示例如下:

  

复制代码 代码如下:

  public class MailManager

  {

  public event EventHandler<NewMailEventArgs> NewMail;

  protected virtual void OnNewMail(NewMailEventArgs e)

  {

  //处于线程安全的考虑,现在将对委托字段的引用复制到一个临时字段中

  EventHandler<NewMailEventArgs> temp = System.Threading.Interlocked.CompareExchange

  (ref NewMail, null, null);

  //如果有事件订阅者对象的存在,则通知他们,事件已触发

  if (temp != null)

  temp(this, e);

  }

  }

  四. 在所有需要触发事件的业务方法中,调用第三步中定义的方法

  目标:在类型中还需要有一个业务方法,来将业务中的场景转化为事件触发。。

  方法:在任意需要的业务方法中,直接调用第三步的方法就可以了,不过需要实现封装一个传递信息的类型。

  示例如下:

  

复制代码 代码如下:

  public class MailManager

  {

  public event EventHandler<NewMailEventArgs> NewMail;

  protected virtual void OnNewMail(NewMailEventArgs e)

  {

  //处于线程安全的考虑,现在将对委托字段的引用复制到一个临时字段中

  EventHandler<NewMailEventArgs> temp = System.Threading.Interlocked.CompareExchange

  (ref NewMail, null, null);

  //如果有事件订阅者对象的存在,则通知他们,事件已触发

  if (temp != null)

  temp(this, e);

  }

  public void SimulateNewMail(string from, string to, string subject)

  {

  //构造一个对象来封装向传给事件订阅者的信息

  NewMailEventArgs e = new NewMailEventArgs(from, to, subject);

  //触发事件引发的入口方法

  OnNewMail(e);

  }

  }

  事件订阅者类型的设计

  一. 定义类型来订阅和侦听事件

  目标:设计一个传真类型Fax类来侦听NewMail事件。

  说明:Fax类型中需要具备对NewMail事件的订阅和取消订阅的方法。示例如下:

  

复制代码 代码如下:

  internal sealed class Fax

  {

  private MailManager mailManager;

  public Fax(MailManager mm)

  {

  this.mailManager = mm;

  }

  public void Register()

  {

  mailManager.NewMail += new EventHandler<NewMailEventArgs>(FaxMsg);

  }

  void FaxMsg(object sender, NewMailEventArgs e)

  {

  Console.WriteLine("Fax mail message");

  Console.WriteLine("From = {0}, To = {1}, Subject = {2}", e.From, e.To, e.Subject);

  }

  public void Unregister()

  {

  mailManager.NewMail -= FaxMsg;

  }

  }