动态添加和移除处理程序
到目前为止你所看到的每个事件侦听器都需要你在设计时关联到事件处理程序。Handles子句是关联使用WithEvents关键字的对象引发事件的方便而简单的方法,但是它不能在运行时提供任何弹性。另外,当多个过程处理相同的事件,Handles子句不会给你事件处理程序执行顺序的控制权。(当然,为了避免这个问题你可以使用你前面看到的技巧,遍历调用GetInvocationList返回项。这个技术要求引发事件的类的附加代码,不是在关联事件侦听器的代码中。)
为了在何时及以什么顺序调用事件处理程序上获得完全主动,你可以使用AddHandler(和RemoveHandler)语句而不是Handles子句。AddHandler和RemoveHandler语句允许你提供一个特定的事件和准备响应事件被调用过程的地址。每个对AddHandler的调用使过程和事件相关联以使得.NET Framework在事件发生时调用过程。另外,AddHandler总是添加事件处理程序到事件调用列表的末尾。这意味着你控制了事件被处理的顺序。
当然,如果你再多考虑片刻,你会理解当你有多个过程时,相同的事件都有一个Handles子句,Visual Basic编译器为事件处理程序创建一个多路广播委托实例而不允许你控制它们被添加进的顺序。引发的事件调用委托实例的Invoke方法,这时就按每个事件侦听器被添加的顺序(并且你无权控制这个顺序)调用它们。当你使用AddHandler和RemoveHandler语句而不是Handles子句,你只要简单控制各项加入多路广播委托的顺序。每次你的应用程序对相同事件调用AddHandler语句,你就为这个事件添加了一个新的侦听器到列表的最后。当你引发这个事件,.NET运行时按顺序调用每个侦听器。
如果你点击了示例窗体的Add/RemoveHandler按钮,一个新的FileSearch5类的实例被创建,同时实例的FileFound事件的多个事件处理程序被整合(hooked up)。这时,当代码调用实例的Execute方法,示例窗体的listbox控件显示出结果:
Dim fs5 As New FileSearch5( _
Me.txtSearchPath.Text, Me.txtFileSpec.Text, _
Me.chkSearchSubfolders.Checked)
AddHandler fs5.FileFound, AddressOf EventHandler7
AddHandler fs5.FileFound, AddressOf EventHandler6
AddHandler fs5.FileFound, AddressOf EventHandler5
AddText("Note the order of invocation:")
fs5.Execute()
AddText(String.Empty)
AddText("And then there were two:")
fs5.Execute()
RemoveHandler fs5.FileFound, AddressOf EventHandler5

图 9显示了点击Add/RemoveHandler按钮后的显示。正如你所能看到的,事件过程按你将它们添加到回调列表的顺序被调用。
Visual Basic中的自定义事件
还记得在一个事件有多个侦听器时发生的问题,还有一个事件侦听器抛出一个异常吗?这个问题有一个相当简单的解决方案,用多路广播委托实例的回调列表关联到这个事件。然而,在Visual Basic .NET 2002 和2003,这里有一些其他的事件挑战:(如果)没有复杂或低效率的代码,简单(的方法)将无法体现。Visual Basic 2005以前,事件委托类型的实例总是由Visual Basic编译器为你创建,并且编译器无法提供给你修改这个委托实例的行为。
Visual Basic 2005为事件声明添加了新的Custom关键字。这个关键字允许你为事件的AddHandler, RemoveHandler和RaiseEvent行为提供代码。 这取决于你创建适当的委托类型及创建拥有关于事件侦听器信息的类型的实例。然而,除此之外,你有对你如何处理事件有着完全地控制。
为了在Visual Studio 2005中创建一个自定义事件,在一个类里面放入你的游标,并为你的事件录入一个声明,如下:
ByVal sender As Object, ByVal e As FileFoundEventArgs)
AddHandler(ByVal value As FileFoundEventHandler)
End AddHandler
RemoveHandler(ByVal value As FileFoundEventHandler)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As FileFoundEventArgs)
End RaiseEvent
End Event
Public Custom Event FileFound As FileFoundEventHandler
AddHandler(ByVal value As FileFoundEventHandler)
listeners.Add(value)
End AddHandler
RemoveHandler(ByVal value As FileFoundEventHandler)
If listeners.Contains(value) Then
listeners.Remove(value)
End If
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As FileFoundEventArgs)
For Each listener As FileFoundEventHandler In listeners
Try
listener.Invoke(sender, e)
Catch ex As Exception
’ Something goes wrong? Just move on to the next handler.
End Try
Next
End RaiseEvent
End Event
还有其它可以用到自定义事件吗?Rocky Lhotka,一个Visual Basic MVP,在他的blog上包括另一个详细的例子( .NET 2.0 solution to serialization of objects that raise events )。他论述了 你可能会用到这个技术来解决涉及引发事件类序列化而侦听器没有序列化的问题。(令人惊讶的是,这经常发生,因为窗体没有序列化,但是常常用户创建事件的侦听器是序列化的。)Paul Vick,作为Microsoft的Visual Basic开发团队的成员之一,他的blog上包括一个例子显示你如何可以使用一个 自定义事件来减少暴露给大量事件而只有很少的事件可能会用到的类的系统开销。对于窗体来说就是这个情况,例如—窗体类暴露于大量事件,但是大多数时间里,你只会处理它们中一或两个。没有某些技巧,编译器将为每个事件引发一个委托实例,尽管你不会使用它们。请看Paul的blog在 Custom events。
