事件,我相信开始学C#的朋友都会用过,在C#中很常见,比如点击一个按钮,上传一张图片等等,在WinForm或WebForm中都在使用着事件。今天,趁着有少少事件,我决定来重温一下之前被自己略过的东西 - 事件。
好记得在之前,在用一个方法的时候,如果参数里面有个Handler,就好害怕,其实事件还是用委托来做中介的,在事件上两次转到定义就去到委托了,将委托复制出来,去掉delegate就是方法签名了,写上自己要实现的代码给事件赋值就OK了。
一、什么是事件
事件涉及两类角色:事件的发布者和事件的订阅者。触发事件的对象称为事件发布者,捕获时间并对其作出响应的对象叫做事件订阅者。
二、事件和委托的关系
在事件触发以后,事件发布者需要发布消息,通知事件订阅者进行事件处理,但事件发布者并不知道要通知哪些事件订阅者,这就需要在发布者和订阅者之间存在一个中介,这个中介就是委托。我们知道,委托都有一个调用列表,那么,只需要事件发布者有这样一个委托,各个事件订阅者将自己的事件处理程序都加入到该委托的调用列表中,那么事件触发时,发布者只需要调用委托即可触发订阅者的事件处理程序。
三、如何声明事件
声明事件的语法和定义一个类的成员非常相似,也非常简单。事实上,事件就是类成员的一种,只是事件定义中包含一种特殊的关键字:event。
事件的声明有两种方式:
1、采用自定义委托类型。
2、采用EventHandler预定义委托类型。
这两种方式基本相同,只不过第二种是.Net Framework中普遍采用的一种形式,因此建议尽量采用第二种方式。
// 关键字 委托类型 时间名 public event EventHandler PrintComplete;
EventHandler是在BCL中预定义的委托类型,它位于System命名空间,用以处理不包含事件数据的事件。事件如果需要包含事件数据,可以通过派生EventArgs实现。
先来看看EventHandler委托的签名:
public delegate void EventHandler(Object sender,EventArgs e);
1、委托的返回类型为void;
2、第一个参数--sender参数,它负责保存触发事件的对象的引用,因为参数的类型是Object类型,因此它可以保存任何类型的实例;
3、第二个参数--e参数,它负责保存事件数据,这里是在BCL中定义的默认的EventArgs类,它位于System命名空间中,他不能保存任何数据。
四、订阅事件
事件订阅者角色需要订阅事件发布者发布的事件,这样才能在事件发布时接受到消息并作出响应,事件事实上是委托类型,因此事件处理方法必须和委托签名相匹配。如果事件使用预定义的委托类型:EventHandler,那么匹配它的事件处理方法如下:
public void SomeEventHandler(object sender, EventArgs e) { //.. }
有了事件处理方法,就可以订阅事件了,只需要使用加法赋值运算符(+=)即可。
五、触发事件
//检查事件是否为空 if (PrintComplete != null) { //像调用方法一样触发事件,参数 PrintComplete(this,new EventArgs(); }
完整的一个事件例子:
namespace ConsoleApplication1 { public class Program { static void Main(string[] args) { Console.WriteLine("该做的东西做完,然后触发事件!"); EventSample es = new EventSample(); es.ShowComplete += es.MyEventHandler; es.OnShowComplete(); Console.ReadKey(); } } public class EventSample { //定义一个事件 public event EventHandler ShowComplete; //触发事件 public void OnShowComplete() { //判断是否绑定了事件处理方法,null表示没有事件处理方法 if (ShowComplete != null) { //像调用方法一样触发事件 ShowComplete(this, new EventArgs()); } } //事件处理方法 public void MyEventHandler(object sender, EventArgs e) { Console.WriteLine("谁触发了我?" + sender.ToString()); } } }
六、使用和扩展EventArgs类
前面提到过,默认的预定义委托EventHandler的第二个参数,本身不能包含事件数据。但是在很多.net提供的方法,都能用e调用出某些信息这是因为这不是默认的EventArgs类了。因此,在事件引发时不能向事件处理程序传递状态信息,如果要想传递状态信息,则需要从此类派生出一个类来保存信息。
以下为扩展EventArgs类的示例:
public class PrintEventArgs : EventArgs { public string PrintState { get; set; } public PrintEventArgs(string state) { PrintState = state; } }
而在调用时,只需将EventArgs换成PrintEventArgs
public void SomeEventHandler(object sender, PrintEventArgs e) { Console.WriteLine("打印已完成!"); }
但是要注意此时,绑定事件编译器会报错:
enentSample.PrintComplete += ShowMessage; //此行代码编译器报错
为什么呢?因此事件委托的第二个参数是EventArgs类型与扩展的PrintEventArgs不同,因此不能在绑定旧的方法;所以就要使用自定义委托了。
七、自定义委托
既然EventHandler委托不能使用了,那么就只有考虑使用自定义委托来声明事件了。首先声明一个自定义委托:
public delegate void PrintEventDelegate(object sender,PrintEventArgs e);
接下来,将事件声明中使用的预定义委托EventDelegate换成我们自定义的委托:
public event PrintEventDelegate PrintComplete;
因为我们扩展的PrintEventArgs类没有不带参数的构造函数,因此需要修改事件触发部分的代码,传递一个参数进去,该参数的值就是要发送给事件处理方法的状态信息,这里以一个简单的字符串代替:
if(PrintComplete != null) { PrintComplete(this,new PrintEventArgs("测试消息")); }
八、事件访问器
事件是特殊的多路广播委托,事件默认有一个私有的委托类型变量,用以保存对订阅事件的事件处理方法的引用,此委托类型的变量仅能从声明该事件的类中委托。事件订阅者通过提供对事件处理方法的引用来订阅事件,这些方法通过默认的时间访问器添加到委托的调用列表中。这里的事件访问器类似于属性访问器,不同之处在于,时间访问器被命名为add和remove,而不是属性的get和set。在大多数情况下都不需要提供自定义的事件访问器。如果没有提供,则编译器会自动添加事件访问器。如果需要添加自定义事件访问器,以支持某些自定义行为,可以使用如下语法:
public event MyEventHandler PrintComplete { add { //.. } remove { //.. } }
在声明了事件访问器以后,编译器将不会提供私有的委托对象,此时对于订阅者事件处理方法引用的管理需要我们自己去实现。
public event MyEventHandler PrintComplete { add { myEventHandler += value; } remove { myEventHandler -= value; } }
下面给出一个扩展EventArgs与自定义委托,传递数据到方法的示例:
namespace ConsoleApplication1 { public class Program { static void Main(string[] args) { Console.WriteLine("该做的东西做完,然后触发事件!"); EventSample es = new EventSample(); es.ShowComplete += es.MyEventHandler; es.OnShowComplete(); Console.ReadKey(); } } public class EventSample { //此事件已不能如此使用 //public event EventHandler ShowComplete; //自定义委托 public delegate void ShowEventDelegate(object sender,ShowEventArgs e); //将事件中的委托换成自己的自定义委托 public event ShowEventDelegate ShowComplete; public void OnShowComplete() { //判断是否绑定了事件处理方法,null表示没有事件处理方法 if (ShowComplete != null) { //这次要传递参数数据了 ShowComplete(this, new ShowEventArgs("传给你的数据,接着吧!")); } } //事件处理方法,注意第二个参数 public void MyEventHandler(object sender, ShowEventArgs e) { Console.WriteLine("谁触发了我 " + sender.ToString()); Console.WriteLine("传过来什么数据: " + e.ShowResult); } } //自定义EventArgs public class ShowEventArgs : EventArgs { public string ShowResult { get; set; } public ShowEventArgs(string result) { ShowResult = result; } } }
输出结果如下: