准备写一个系列文章,深入探讨C#及.Net中的某些特性。
第一篇 事件
事件相信每个人都不陌生,随便一个WinForm程序,就会使用大量的事件,比如:
-
C# code
-
class MainForm : Form { public MainForm() { this .Click += new EventHandler(MainForm_Click); } private void MainForm_Click( object sender, EventArgs e) { } }
就知道产生了单击事件,于是去调用窗体的OnClick方法,该方法会去检测一下是否订阅了Click事件,如果订阅了,就会去调用相应的事件处理程序,这个过程是通过委托实现的。
下面我从语法角度来分析一下事件:
事件是类、结构或接口中的一个成员,它有两种定义形式:
一、
-
C# code
-
event MethodInvoker OneEvent;
二、
-
C# code
-
event MethodInvoker OneEvent { add { } remove { } } 其中MethodInvoker
事件包括两个访问器,其中add访问器会在订阅事件时触发,remove访问器在取消事件订阅时触发。
对于第一种定义形式,系统会自动提供add及remove访问器,同时会提供如下字段:
-
C# code
-
private MethodInvoker OneEvnet;
-
C# code
-
class Demo { public void InvokeEvent() { if (OneEvent != null ) OneEvent(); // 调用事件 if (TwoEvent != null ) { string str = TwoEvent( 217 ); // 调用事件 MessageBox.Show(str); } } public event MethodInvoker OneEvent; public event Func < int , string > TwoEvent; } private void button1_Click( object sender, EventArgs e) { Demo de = new Demo(); de.OneEvent += delegate { MessageBox.Show( " 事件被调用 " ); }; de.TwoEvent += arg => arg.ToString(); de.InvokeEvent(); }
可以看出,这里事件类似于方法和委托,可以传递参数并被调用。事实上,这只是编译器的一种包装,这里其实使用的正是前面提到的同名的委托字段。而如果是在一个类中访问另一个类中的事件,或者如下面将要提到的自己提供访问器的情况,由于不存在同名的委托字段,事件就不能再这样使用了,而只能出现在+=和-=运算符的左侧。
至少有两个理由使得我们需要自己提供访问器:
1. 希望在订阅或取消事件时执行一段代码。
2. 前面提到,如果不提供访问器,每定义一个事件,系统就会生成一个同名的委托字段,如果事件特别多,这就是一项巨大的开销,而如果定义了访问器,则不再提供,此时我们可以用一种统一的方式来处理,从而节省资源,实际上,WinForm就是这样处理的。
-
C# code
-
class Demo { public void InvokeEvent() { if (ehl[oneEvent] != null ) ehl[oneEvent].DynamicInvoke(); // 调用事件 if (ehl[twoEvent] != null ) { string str = ehl[twoEvent].DynamicInvoke( 217 ) as string ; // 调用事件 MessageBox.Show(str); } } EventHandlerList ehl = new EventHandlerList(); static readonly object oneEvent = new object (); static readonly object twoEvent = new object (); public event MethodInvoker OneEvent { // 在add和remove访问器中,类似属性,存在一个value,表示要订阅和取消的委托 add { // 我这里的条件没有什么实际意义,只是想说明可以在访问器中执行代码 // 示例中,起到一个筛选的作用,只有那些以”On”开头,并且定义于其他类中的方法才能被订阅 if (value.Method.Name.StartsWith( " On " ) && value.Target != this ) ehl.AddHandler(oneEvent,value); } remove { // 对不起,禁止你取消静态方法(为什么禁止取消静态方法?没有理由,只用于举例^-^) if ( ! value.Method.IsStatic) ehl.RemoveHandler(oneEvent,value); } } public event Func < int , string > TwoEvent { add { ehl.AddHandler(twoEvent,value); } remove { ehl.RemoveHandler(twoEvent,value); } } public void OnCall() { MessageBox.Show( " Demo.OnCall " ); } } class Pro { public void Call() { MessageBox.Show( " Pro.Call " ); } public void OnCall() { MessageBox.Show( " Pro.OnCall " ); } public static void OnCalls() { MessageBox.Show( " Pro.Static.OnCalls " ); } } private void button1_Click( object sender, EventArgs e) { Pro pr = new Pro(); Demo de = new Demo(); de.OneEvent += pr.Call; // 不以”On”开头,不会订阅 de.OneEvent += pr.OnCall; // 成功订阅 de.OneEvent += Pro.OnCalls; // 成功订阅 de.OneEvent += de.OnCall; // 只有定义在其他类中的方法才会被订阅 de.InvokeEvent(); de.OneEvent -= pr.Call; // 未订阅,谈不上取消 de.OneEvent -= pr.OnCall; // 成功取消 de.OneEvent -= Pro.OnCalls; // 静态方法不会被取消 de.OneEvent -= de.OnCall; // 未订阅,谈不上取消 de.InvokeEvent(); }
再来简单说说WinForm事件:
WinForm的根是组件(Component),可视的组件称为控件(Control)。
Component上定义了一个受保护的属性Events,用于管理事件列表。
Control上定义了一系列静态的私有字段,为事件列表提供索引键,字段名基本上是:
Event事件名
如EventClick,EventEnter等。
举一个应用:
如何获取一个事件订阅的所有方法列表,以及如何在不知道事件处理程序方法名的情况下取消事件,或者更现实一点,如何取消匿名方法(在不声明一个委托引用的前提下,匿名方法显然不可能通过-=运算符来取消)。
-
C# code
-
private void button1_Click( object sender, EventArgs e) { PropertyInfo pi = typeof (Component).GetProperty( " Events " , BindingFlags.NonPublic | BindingFlags.Instance); EventHandlerList ehl = pi.GetValue(button2, null ) as EventHandlerList; FieldInfo fi = typeof (Control).GetField( " EventClick " , BindingFlags.NonPublic | BindingFlags.Static); object key = fi.GetValue( null ); Delegate del = ehl[key]; foreach (Delegate de in del.GetInvocationList()) { Console.WriteLine(de.Method.Name); // 订阅的事件 ehl.RemoveHandler(key, de); // 取消订阅 } }
也许有人会有这样的疑问:GetInvocationList获得的委托数组中,某个委托如果还是包括多个方法链怎么办?微软为大家想得非常周到了——数组中的每个委托都仅表示一种方法。
另外,如果我们需要取消所有事件,不用遍历,直接调用EventHandlerList.Dispose方法即可。
关于WinForm事件,还有一个有趣的现象:
当我们拖曳一个控件到窗体时,双击该控件就会进入某个事件处理程序,例如双击Button控件,会进入Click事件;双击TextBox控件,会进入TextChanged事件。你是否思考过怎么通过编程的方法知道会进入哪个事件呢?
其实这叫做默认事件(类似的,还有一个默认属性的概念),是一个特性:DefaultEventAttribute
-
C# code
-
[DefaultEvent( " Click " )] class Control { }
Click是应用于Control类的默认事件,Button则继承了这个特性,而TextBoxBase这个类将这个特性修改为TextChanged, TextBox又从TextBoxBase继承过来这个特性。
既然知道了原理,要去检索,就很简单了,直接反射就行了。另外,其实系统提供有专门的方法:
-
C# code
-
TypeDescriptor.GetDefaultEvent。
现在来看看TreeView控件的默认事件:
-
C# code
-
Attribute attr = Attribute.GetCustomAttribute( typeof (TreeView), typeof (DefaultEventAttribute)); DefaultEventAttribute de = attr as DefaultEventAttribute; MessageBox.Show(de.Name);
或者
MessageBox.Show(TypeDescriptor.GetDefaultEvent(typeof(TreeView)).Name);
结果正是AfterSelect。