在C#程序设计中所谓事件,就是由某个对象发出的消息。这个消息标志着某个特定的行为发生了,或者某个特定的条件成立了。比如用户点击了鼠标,这一单击就会引起Windows给按钮消息处理程序发送一个消息,这就是OnClick事件。在深入讲解事件之前,必须先弄明白几个概念。
发布者(publisher)与订阅者(subscriber),一个消息的发送者 (Sender)称之为发布者,而该消息的接收者 (Receiver),则称之为订阅者,订阅者需要在发布者那里注册自己,发布者在某个个条件满足的时候,通知订阅者做适当的动作或者操作。发布者与订阅者的这种模式要求二者之间的耦合性很小,而且消息要一定由发布者发出。
针对发布者与订阅者模式,分析一下事件发生的整个过程,发布者的主要任务就是在条件合适的时候发布消息给订阅者,触发订阅者的某些动作,其实就是调用订阅者的一些方法。订阅者在事件发生前,需要将自己注册到发布者那里,以便发布者发布消息时能够通知到自己。在这个过程中需要订阅者注册自己到发布者,以及要满足发布者某个条件的一个环境。这个环境就是客户端(Client)。所以三个参与者的职责分别是:
1) 发布者:只管发布信息给订阅者,而不管订阅者是什么。
2) 订阅者:需要注册自己到发布者,然后实现自己受到信息后的动作
3) 客户端:触发发布者发送信息,提供订阅者注册到发布者的环境。
8.2.1 C#事件的模拟
在c#中,委托是事件的实现基础,前面已经介绍过委托。现在使用委托来模拟一下事件。在模拟例子EventSimulate中,分别用类Publisher表示发布者,用类Receiver,用具有main函数的类Program模拟客户端。类Receiver中两个方法用来表示当接到发布者的信息时的动作,这两个方法分别是OnClick_1与OnClick_2。它们都有一个string类型的参数,用来表示发布消息的名字。类Publisher有一个方法SendMessage,这个方法用来表示触发发布信息的条件,即在main()函数里调用这个方法就表示事件发生了,可以发送信息。为了让订阅者能够注册到发布者,以及发布者能够调用订阅者的方法,需要有一个委托,这个委托叫ClickHandler。分析到这里,具体还是从看程序的源代码。
类Publisher的代码
namespace EventSimulate
{
class Publisher
{
private string name;
public ClickHandler Click;
/// <summary>
/// 本方法需要在外部调用,用来触发事件
/// </summary>
public void SendMessage()
{
Click(name);
}
/// <summary>
/// 构造函数,用来初始化名字
/// </summary>
public Publisher()
{
name = "Publisher";
}
}
}
类Receiver的代码
namespace EventSimulate
{
class Receiver
{
public void OnClick_1(string name)
{
Console.WriteLine("我是由{0}触发以后,执行的第一个动作。",name);
}
public void OnClick_2(string name)
{
Console.WriteLine("我是由{0}触发以后,执行的第二个动作。",name);
}
}
}
客户端的源代码
namespace EventSimulate
{
public delegate void ClickHandler(string name);
class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Receiver receiver = new Receiver();
#region 开始订阅者注册到发布者的过程
publisher.Click = receiver.OnClick_1;
publisher.Click += receiver.OnClick_2;
#endregion 结束订阅者到发布者的注册
publisher.SendMessage();
Console.ReadKey();
}
}
}
运行程序的结果
程序好像达到了我们的要求,好像用委托就能够实现事件了。在客户端把代码改成如下形式:
namespace EventSimulate
{
public delegate void ClickHandler(string name);
class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Receiver receiver = new Receiver();
#region 开始订阅者注册到发布者的过程
publisher.Click = receiver.OnClick_1;
publisher.Click += receiver.OnClick_2;
#endregion 结束订阅者到发布者的注册
//publisher.SendMessage();
publisher.Click("Publisher");
Console.ReadKey();
}
}
}
注意代码中把publisher.SendMessage();一行换成了publisher.Click("Publisher");,也就是说现在信息的发布者不再是publisher了,变成了客户端。分析一下原因,原来主要原因是因为委托Click在类Publisher中是公开的,为了不让信息的发布在类外进行,那么需要把委托Click变成类Publisher的私有变量。但是,委托Click变成类Publisher的私有变量后,订阅者在客户端没办法注册自己到发布者了。你可能立即想到了,类的字段可以通过字段封装使之称为外在的属性,从而在类外的操作。但是委托毕竟不同于一般的类型,如果像一般类型那样封装,仍然达不到禁止在类外发布信息的目的。这个时候该事件出场了。
C#中的事件使用关键字event来声明,它封装了委托类型的变量,使得委托类型的变量在发布者类的内部,不管声明它是public还是protected,该变量总是发布者类的private的变量。在类的外部,订阅者注册使用符号“+=”,订阅者注销使用符号“-=”。发布者类的外部对时间的访问权限来源于在声明事件时使用的访问符。所以说,事件是特殊类型的多路广播委托,仅可从声明它们的类或结构(发布者类)中调用。如果其他类或结构订阅了该事件,则当发布者类引发该事件时,会调用其事件处理程序方法。所以改写类Publisher的代码,声明Click是一个事件。
namespace EventSimulate
{
class Publisher
{
private string name;
public event ClickHandler Click;
/// <summary>
/// 本方法需要在外部调用,用来触发事件
/// </summary>
public void SendMessage()
{
Click(name);
}
/// <summary>
/// 构造函数,用来初始化名字
/// </summary>
public Publisher()
{
name = "Publisher";
}
}
}
相对应,客户端中的代码也必须修改成
namespace EventSimulate
{
public delegate void ClickHandler(string name);
class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Receiver receiver = new Receiver();
#region 开始订阅者注册到发布者的过程
publisher.Click+= receiver.OnClick_1;
publisher.Click += receiver.OnClick_2;
#endregion 结束订阅者到发布者的注册
publisher.SendMessage();
//publisher.Click("Publisher");
Console.ReadKey();
}
}
}
注意,代码中publisher.Click+= receiver.OnClick_1;这条语句原来前面使用的是publisher.Click= receiver.OnClick_1,这是委托与事件的不同之处。publisher.Click("Publisher");语句将会引发一个如图8-6的错误。
图 8-6
把生成的代码反编译一下,观察一下事件Click的真是面目。如图8-7所示
图8-7
事件Click的确被编译为私有字段,而且还有两个方法add_Click与remove_Click方法。具体看一下方法add_Click。
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_Click(ClickHandler value)
{
this.Click = (ClickHandler) Delegate.Combine(this.Click, value);
}
原来是使用了Delegate.Combine方法将需要注册的订阅者类的方法加入到了该委托的委托列表中了。而remove_Click方法是使用了Delegate.Remove方法将需要注销的订阅者类的方法从委托列表删除掉了。
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_Click(ClickHandler value)
{
this.Click = (ClickHandler) Delegate.Remove(this.Click, value);
}
8.3 EventHandler委托
在C#中,对于事件的处理是通过委托进行的,事件就是对发布者类中委托字段的封装,在客户端可以对发布者类提供的方法“+=”或者“-=”来注册订阅者或者注销订阅者。同时通过发布者类中公开的引发事件的方法引发事件,由发布者类发出调用订阅者方法的信息。上例中的委托ClickHandler是自己定义的。其实在系统内部已经提供了一个预先定义好的委托EventHandler。EventHandler的标准签名定义一个没有返回值的方法,其定义的格式是
[SerializableAttribute]
[ComVisibleAttribute(true)]
public delegate void EventHandler (Object sender,EventArgs e)
该委托中定义了两个函数参数,Object sender表示事件源,其实就是事件的发布者,而第二个参数EventArgs e是一个EventArgs类的实例。EventArgs类是包含事件数据的类的基类。其继承层次结构如图8-8所示.
图8-8
此类不包含事件数据,在事件引发时不向事件处理程序传递状态信息的事件会使用此类。如果事件处理程序需要状态信息,则应用程序必须从此类派生一个类来保存数据。
下面来演示向事件处理程序传递状态信息的一个实现。例子名称是EventHandlerSample。
因为要向事件处理程序传递信息,所以必须从EventArgs类中派生出一个新类,用来记录事件发生时的相关信息,在该示例中这个派生类为InforEventArgs。InforEventArgs类中保存了事件发生时的点的坐标,所以InforEventArgs类有两个字段x和y,分别记录事件发生时点的横坐标与纵坐标。在类内对x与y字段进行了封装,并添加了构造函数,使得InforEventArgs类的对象生成的时候初始化。
因为委托EventHandler不能向订阅者传递信息,所以在本例中模仿EventHandler定义了新的委托PointEventHandler,该委托的定义与EventHandler基本相同,形式是:public void delegate PointEventHandler(Object sender, InforEventArgs e);。区别在于第二个函数参数e能够携带相关信息。
信息发布者为Publisher类,Publisher类中有对PointEventHandler委托封装的Click事件,引发Click事件的SendMessage方法。
订阅者为Receiver类,在本类中有响应事件的一个方法OnClick,因为要使用PointEventHandler,所以OnClick方法有两个函数参数,分别是Object sender与InforEventArgs e,在OnClick方法中将InforEventArgs类的实例e携带的信息打印出来。
客户端依然使用具有main函数的Program类。信息的发布者在发布信息时,需要同时将事件发生时点的坐标传递给事件处理者。
类InforEventArgs的代码
namespace EventHandlerSample
{
class InforEventArgs:EventArgs
{
private double x;//表示横坐标
private double y;//表示纵坐标
/// <summary>
/// 对私有字段x的封装
/// </summary>
public double X
{
get { return x; }
set { x = value; }
}
/// <summary>
/// 对私有字段y的封装
/// </summary>
public double Y
{
get { return y; }
set { y = value; }
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="x">点的横坐标</param>
/// <param name="y">点的纵坐标</param>
public InforEventArgs(double x,double y)
{
this.x = x;
this.y = y;
}
}
}
类Publisher的代码
namespace EventHandlerSample
{
class Publisher
{
public event PointEventHanlder Click;
public void SendMessage(double x,double y)
{
InforEventArgs infor = new InforEventArgs(x, y);
Click(this, infor);
}
}
}
类Receiver的代码
namespace EventHandlerSample
{
class Receiver
{
public void OnClick(Object sender, InforEventArgs e)
{
Console.WriteLine("事件发生时的点的坐标是({0},{1})", e.X, e.Y);
}
}
}
客户端的代码
namespace EventHandlerSample
{
public delegate void PointEventHanlder(Object sender, InforEventArgs e);
class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Receiver receiver = new Receiver();
#region 开始将订阅者的方法注册到发布者
publisher.Click +=new PointEventHanlder(receiver.OnClick);
#endregion 结束注册
//事件的引发
publisher.SendMessage(50.0, 100.0);
Console.ReadKey();
}
}
}
程序的运行结果
讲到目前为止,你可能已将发现,尽管委托没有强制规定返回值的类型,但是大多数委托的返回值都是void,你可能会有疑问,为什么?
其实因为委托变量可以供多个订阅者注册,如果定义了返回值,那么多个订阅者的方法都会向发布者返回数据,结果就是后面一个返回的方法值会将前面的返回值覆盖掉了,因此,实际上只能获得最后一个方法调用的返回值。除此以外,发布者和订阅者之间要求是松耦合的,发布者根本不关心谁订阅了它的事件、为什么要订阅,更别说订阅者的返回值了,所以返回订阅者的方法返回值大多数情况下根本没有必要。下面通过示例NoVoidDelegate来测试一下上面的说法。
示例NoVoidDelegate使用五个类:发布者Publisher类,订阅者Receiver_1, Receiver_2, Receiver_3类,客户端Program类,与前面示例不同的地方是,本例中使用了一个委托StringDelegate,它返回一个字符串类型的数据。
每个订阅者类中包含一个方法OnClick,它们都被注册到Publisher类的Click事件,在每个OnClick方法中,返回一个字符串,而且使用静态方法Thread.Sleep让每个方法都延迟2秒执行。
客户端调用Publisher类的SendMessageClick方法来触发Click事件。在事件Click调用结束,打印出一句话,告诉用户SendMessageClick方法执行完毕。
该示例完整的代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace NoVoidDelegate
{
//定义返回字符串的委托
public delegate string StingDelegate();
class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Receiver_1 receiver1 = new Receiver_1();
Receiver_2 receiver2 = new Receiver_2();
Receiver_3 receiver3 = new Receiver_3();
#region 开始注册订阅者到发布者
publisher.Click += new StingDelegate(receiver1.OnClick);
publisher.Click += new StingDelegate(receiver2.OnClick);
publisher.Click += new StingDelegate(receiver3.OnClick);
#endregion
publisher.SendMessage();
Console.ReadKey();
}
}
class Publisher
{
//定义事件Click
public event StingDelegate Click;
//引发事件Click的方法,并将事件的处理结果打印出来
public void SendMessage()
{
Console.WriteLine("事件执行完毕的返回结果是:{0}",Click());
Console.WriteLine("事件引发完毕");
}
}
/// <summary>
/// 订阅者1
/// </summary>
class Receiver_1
{
public string OnClick()
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("订阅者1的处理方法被延迟两秒!");
return ("订阅者1的处理方法");
}
}
/// <summary>
/// 订阅者2
/// </summary>
class Receiver_2
{
public string OnClick()
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("订阅者2的处理方法被延迟两秒!");
return ("订阅者2的处理方法");
}
}
/// <summary>
/// 订阅者3
/// </summary>
class Receiver_3
{
public string OnClick()
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("订阅者3的处理方法被延迟两秒!");
return ("订阅者3的处理方法");
}
}
}
程序的运行结果
程序的运行结果表明:只有处于委托列表的最后一个方法的返回值被打印了出来。这个就是大部分委托返回值是void类型的原因。
通过示例NoVoidDelegate的运行,可以看到程序不是立即就执行完毕,而是有一定的延迟,过一段时间才打印出一条信息,最后才打印出发布者执行完毕的信息“事件引发完毕”信息。这样的执行过程给我们传达了另一个信息,发布者发布消息以后,订阅者得到消息,开始执行事件处理程序。在事件处理程序执行期间,发布者将会等待。一直到所有订阅者的事件处理程序执行完毕,发布者才能继续执行。
8.4 事件的异步调用
示例NoVoidDelegate表明,发布者与订阅者目前的关系很紧密。但是在很多情况下,尤其是远程调用的时候(比如说在Remoting中),发布者和订阅者应该是完全的松耦合,发布者不关心谁订阅了它、不关心订阅者的方法有什么返回值、不关心订阅者会不会抛出异常,当然也不关心订阅者需要多长时间才能完成订阅的方法,它只要在事件发生的那一瞬间告知订阅者事件已经发生并将相关参数传给订阅者就可以了。然后它就应该继续执行它后面的动作,在例NoVoidDelegate中就是打印“事件引发完毕”。而订阅者不管如何执行自己的事件处理程序都不应该影响到发布者,但在上面的例子中,发布者却不得不等待订阅者的方法执行完毕才能继续运行。
现在看一下如何能够解决这个问题?回顾一下之前提到的内容,委托的定义会生成继承自MulticastDelegate的类,新类中包含Invoke()、BeginInvoke()和EndInvoke()方法。当直接调用委托时,实际上是调用了Invoke()方法,它会中断调用它的客户端,然后在客户端线程上执行所有订阅者的方法(客户端无法继续执行后面代码),最后将控制权返回客户端。注意到BeginInvoke()、EndInvoke()方法,在.Net中,异步执行的方法通常都会配对出现,并且以Begin和End作为方法的开头。它们用于方法的异步执行,即是在调用BeginInvoke()之后,客户端从线程池中抓取一个闲置线程,然后交由这个线程去执行订阅者的方法,而客户端线程则可以继续执行下面的代码。
BeginInvoke()接受“动态”的参数个数和类型,为什么说“动态”的呢?因为它的参数是在编译时根据委托的定义动态生成的,所有参数中前面参数的个数和类型与委托定义中接受的参数个数和类型相同,最后两个参数分别是AsyncCallback和Object类型,现在,我们仅需要对这两个参数传入null就可以了。
另外需要注意在委托类型上调用BeginInvoke()时,此委托对象只能包含一个目标方法,所以对于多个订阅者注册的情况,必须使用GetInvocationList()获得所有委托对象,然后遍历它们,分别在其上调用BeginInvoke()方法。如果直接在委托上调用BeginInvoke(),会抛出异常,提示“委托只能包含一个目标方法”。
以NoVoidDelegate例子为原型,对其进行修改,改造成示例EventBeginInvoke,该示例使用系统提供的预设的EventHandler作为委托,在SendMessage方法中使用了GetInvocationList()得到事件Click的委托列表,然后对委托列表中的每个对象采用BeginInvoke()方法调用。
程序完整的源代码
namespace EventBeginInvoke
{
class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Receiver_1 receiver1 = new Receiver_1();
Receiver_2 receiver2 = new Receiver_2();
Receiver_3 receiver3 = new Receiver_3();
#region 开始注册订阅者到发布者
publisher.Click += new EventHandler(receiver1.OnClick);
publisher.Click += new EventHandler(receiver2.OnClick);
publisher.Click += new EventHandler(receiver3.OnClick);
#endregion
Console.WriteLine("main中调用SendMessage函数");
publisher.SendMessage();
Console.WriteLine("返回到客户端");
Console.ReadKey();
}
}
//Publisher类的定义
class Publisher
{
//定义事件Click
public event EventHandler Click;
//发送信息,引发Click事件
public void SendMessage()
{
Console.WriteLine("引发Click事件开始......");
//得到事件Click的委托列表
Delegate[] delArray = Click.GetInvocationList();
//开始遍历数组delArray中的每个元素,分别进行异步调用
foreach (Delegate item in delArray)
{
EventHandler task=(EventHandler)item;
task.BeginInvoke(this, EventArgs.Empty, null, null);
}
Console.WriteLine("事件Click调用过程结束");
}
}
/// <summary>
/// 订阅者1
/// </summary>
class Receiver_1
{
public void OnClick(Object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("订阅者1的处理方法被延迟两秒!");
}
}
/// <summary>
/// 订阅者2
/// </summary>
class Receiver_2
{
public void OnClick(Object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("订阅者2的处理方法被延迟两秒!");
}
}
/// <summary>
/// 订阅者3
/// </summary>
class Receiver_3
{
public void OnClick(Object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("订阅者3的处理方法被延迟两秒!");
}
}
}
程序的运行结果。
在程序的运行中,你会看到,返回到客户端以后,即输出“返回客户端”这句话以后,后面三句话隔上一段时间才逐条输出。说明在本程序中达到了事件异步调用的目的。