豆蔻网络学院
事件是你的代码兵器库中的主要部分,无论你用Visual Basic? 6.0,Visual Basic .NET 2002,Visual Basic .NET 2003,还是Visual Basic 2005。窗体和控件引发事件,同时你的代码处理这些事件。你用Visual Basic写的最初应用程序大多会是在一个窗体上放置一个按钮,处理这个click事件,并在运行时你点击这个按钮会显示某些文本在提示框中。还有什么比这更容易?
但是你又真正了解事件多少呢?在你向某个类中添加一个事件处理程序是将会发生什么?在本文中,基于我为 AppDev 所写的课件,我将用各种方法来演示事件和事件处理程序交互,并且我将说明它们如何能解决一般问题。也许这些信息中的一些对你来说并不新鲜,但是如果你对事件的了解并不深入,这里肯定有些东西让你惊奇。在任一情况下,下载这两个示例应用程序(一个是用Visual Basic .NET .2002和2003,一个是用Visual Basic 2005)并理解之。所有内容适用于Visual Basic .NET2002 和 2003 及 Visual Basic 2005,除了最后的论及自定义事件的部分,它只能在Visual Basic 2005下工作。
我将假定你已有一些关于委托和多路广播委托的基本知识。如果你没有研究过这些重要的Microsoft.NET Framework特性,现在你就该去做了。获得这些问题的更多信息可以看看Ted Pattison的两部分关于委托的概论。
事件
Visual Basic(在Visual Basic .NET之前)为你提供了创建和处理事件的简单机制,并且Visual Basic .NET 2002和2003提供了几个不同的方法来做它们。Visual Basic 2005甚至允许你更强地控制事件处理程序,正如你将在本文里所看到的。
事件提供一个松散的联系机制,它允许类为在将来某个时间可能或也许不可能发生的通知注册。如果“侦听器”得到它们正在等待的事件发生的通知,它们就处理这种情况。如果不,它们只是保持监听。一个按钮点击事件处理程序用类提供的按钮的功能(functionality)来注册它自己;在一个用户点击这个按钮时,这个按钮的类引发Click事件,所有侦听器(这里可能是此按钮的多个Click事件处理程序)运行它们的代码,并继续执行代码。
我的示例程序包括一组类(FileSearch1到FileSearch5用于Visual Basic .NET 2002 和 2003,而FileSearch1到FileSearch6可用于Visual Basic 2005)在一个指定位置搜索文件并在找到时引发一个事件。FileSearch类只想在某个令人感兴趣的事情发生时让某个对应的类监听到。在这种情况下,每当FileSearch类发现另一个文件时某个有趣的事情就会发生。许多侦听器类可能会希望对该事件做出反应。
以.NET的观点来说,一个类可以在代码执行的任何一点引发一个事件。其他类可以订阅这个事件,并且它们可以在事件发生时通过.NET Framework获得通知。这个引发事件的类一般并不会知道有多少(如果有的话)侦听器,尽管它可能做出某些努力以收集这个信息,正如你将在本文后面所看到的。另外,多侦听器可以注册以获得通知,并且每个都可以被通知而对其他任何侦听器一无所知。
以Visual Basic 6.0方法处理
.NET Framework和Visual Basic .NET语言的设计者,已做了非常充分的工作以确保你可以在.NET使用事件如同你在Visual Basic 6.0中所做的一样。这就是你可以:
* 使用Event关键词声明一个事件
* 使用RaiseEvent语句引发一个事件
* 使用一个WithEvents变量处理事件
与.NET的实质上的不同之处是底层的机制。对应于在Visual Basic 6.0中使用某些隐藏 plumbing,Visual Basic .NET使用一个可见的、扩展的、公共plumbing-委托-来管理事件处理。
Visual Basic 6.0 和Visual Basic .NET版本之间的另一个不同是在.NET中:你可以使用Handles子句指示在对一个特殊事件的响应中应该运行的一个特殊过程。Handles子句允许任何与事件的参数签名相符的过程来响应这个事件。听起来很像一个委托,而在表象之下,它就是委托。在编译时间,.NET Framework用你的事件名称创建一个委托类,只是在结尾添加“EventHandler”字样。举个例子,在你声明一个命名为FileFound的事件时,.NET Framework创建为你创建一个命名为FileFoundEventHandler的委托类型。处理这个事件的每个过程必须有一个符合委托类型的签名。
点击在示例窗体上的RaiseEvent按钮演示了Visual Basic .NET如何支持以Visual Basic 6.0为基础的事件处理。示例项目包括了FileSearch1类,它使用以下代码建立事件:
’’ 以下代码来自FileSearch1.vb
Public Class FileSearch1
’’在指定位置搜索文件
’’一旦找到,这个类就为每个找到的文件引发FileFound 事件
Public Event FileFound(ByVal fi As FileInfo) ... ’’ 这里的代码去掉了...
End Class
在它找到文件时,FileSearch1类引发FileFound事件:
’’以下代码来自FileSearch1.vb
Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec)
For Each fi As FileInfo In afi
RaiseEvent FileFound(fi)
Next
alFiles.AddRange(afi)
在frmMain.vb,你将找到以下声明,它允许代码使用变量fs1来对由FileSearch1实例引发的事件做出反应:
Private WithEvents fs1 As FileSearch1
点击RaiseEvent运行以下代码:
’’以下代码来自FileSearch1.vb
fs1 = New FileSearch1( _
Me.txtSearchPath.Text, Me.txtFileSpec.Text, _
Me.chkSearchSubfolders.Checked)Try
fs1.Execute()
Catch
End Try
最后,frmMain.vb包括了一个事件处理程序,它处理FileSearch1.FileFound事件:
’’以下代码来自FileSearch1.vbPrivate Sub EventHandler1( _
ByVal NewFile As System.IO.FileInfo) _
Handles fs1.FileFound AddText("EventHandler1: " & NewFile.FullName)
End Sub
尽管frmMain.vb只包括处理FileSearch1.FileFound事件的一个单过程,你将会有不止一个过程处理一个特定事件是相当可能的(并且是很可能的)。这就是说,当FileSearch1类引发它的FileFound事件,多个过程可能处理它是可能的。这听起来应该很像多路广播委托的概念,因为它就是多路广播委托。本质上,.NET将事件转换为委托类。用ILDASM.exe研究一下IL(中间代码)就会拨云见日了。
用ILDASM研究事件
如果你使用ILDASM(包含在.NET Framework SDK中的.NET Framework IL反汇编工具)来打开这个示例的可执行版本,你将获得如你在图 1中所看到的信息。尽管FileSearch1并没有显式地包含一个命名为FileFoundEventHandler的多路广播委托,Visual Basic .NET编译器已创建了一个(多路广播委托),对应于代码声明的由FileFound事件定义的类型。编译器也创建了一个表示事件侦听器的委托类型FileFoundEventHandler的名为FileFoundEvent的实例。
图 1 在ILDASM中的示例代码
使用这个技术,编译器可以强迫严格遵循你的事件声明的参数签名,而不用你再去为创建你自己的委托类型和在你的事件声明中预定的类型而操心。正如你后面将看到的一样,你可以完全随意地创建你自己的委托表示你的事件并且可用这个委托作为你的事件的类型。
多事件处理程序
我将讨论的下一个类:FileSearch2,它是FileSearch1类的一个相似的拷贝。使用FileSearch2中的唯一的不同是:示例窗体对于FileSearch2类的 FileFound事件包括多个侦听器。这就是,frmMain.vb包括以下的声明:
’’以下代码来自FileSearch1.vb
Private WithEvents fs2 As FileSearch2
这个示例窗体也包括了在图 2 中所示的事件处理程序。这些事件处理程序也监听由FileSearch3和FileSearch4类引发的FileFound事件,这个我下面还有讲到。
’ From frmMain.vb
Private Sub EventHandler2( _
ByVal NewFile As System.IO.FileInfo) _
Handles fs2.FileFound, fs3.FileFound, fs4.FileFound
AddText("EventHandler2: " & NewFile.FullName)
End Sub
Private Sub EventHandler3( _
ByVal NewFile As System.IO.FileInfo) _
Handles fs2.FileFound, fs3.FileFound, fs4.FileFound
AddText("EventHandler3: " & NewFile.FullName)
End Sub
点击在主窗体上的Multi-Listener按钮会创建一个FileSearch2类的实例,调用这个实例的执行(execute)方法,就会显示如图 3中所示的输出。
图 3 多侦听器(Multiple Listeners)允许多过程(Multiple Procedures)运行
但是注意,当你使用多个Handles子句对同一事件反应时,你完全不能控制事件处理程序运行的顺序。.NET Framework提供两个选择,这将稍后在文章中讨论,它允许你获得对多侦听器的更强地控制。 异常和多个事件处理程序(Multiple Event Handlers)
正如你已经看到的,一切都让人称心如意。如果你有一个事件的多个处理程序,当事件引发时,.NET Framework将依次调用每个处理程序。到目前为止一切顺利。如果其中某个事件处理程序产生一个异常时将会发生什么?事情就没有那么顺利了。
为证实这个问题,点击在示例窗体上的RaiseEvent Error按钮。这个例子创建了FileSearch3类的一个新的实例(在这个类本身没有什么新东西)。在图 4 中的示例窗体中提供了若干过程,它处理了FileSearch3.FileFound事件,但是有个过程抛出了一个异常。
图 5 某个事件侦听器抛出一个错误
Figure 4 One Event Handler Raises an Error
’ From frmMain.vb
Private Sub EventHandler2( _
ByVal NewFile As System.IO.FileInfo) _
Handles fs2.FileFound, fs3.FileFound, fs4.FileFound
AddText("EventHandler2: " & NewFile.FullName)
End Sub
Private Sub EventHandler3( _
ByVal NewFile As System.IO.FileInfo) _
Handles fs2.FileFound, fs3.FileFound, fs4.FileFound
AddText("EventHandler3: " & NewFile.FullName)
End Sub
Private Sub EventHandler4( _
ByVal NewFile As System.IO.FileInfo) _
Handles fs3.FileFound, fs4.FileFound
AddText("EventHandler4: Throwing exception!")
Throw New ArgumentException
End Sub
在你运行这个代码时会发生什么?你将获得图 5所示的结果。若有任何一个事件侦听器引发一个异常,整个“事件处理链(event-handling chain)”就停下来。如果你停下来考虑这是怎么回事,你会知道这个行为是有意义的。
研究异常(Exception)行为
我将很快给你看看从FileSearch3类中选取的代码。每次一个类的实例找到一个文件,实例就引发它的FileFound事件。这一效果导致.NET Framework相继地运行每个事件处理过程。看看下面的代码:
’’以下代码来自FileSearch3.vb
’’搜索符合的文件名
Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec)For Each fi As FileInfo In afi
’’这相当于:
’’ FileFoundEventHandler.Invoke(fi)
RaiseEvent FileFound(fi)
Next
alFiles.AddRange(afi)
这个技术的问题(正如你已经看到的)是:如果一个异常出现在任何一个侦听器中,异常回滚(bubbles back)到事件引发(event-raising)代码,.NET Framework不再调用事件侦听器,而事件处理就慢慢停了下来。
如果查看一下IL为示例代码所生成的,情况就变得清楚了。图 6 显示了在ILDASM中FileSearch3.Search方法的反汇编。你在类中看到的RaiseEvent语句直到一个FileFoundEventHandler.Invoke方法调用后才会编译。在内部,一旦这个方法已经调用,你的代码将控制权交给委托的执行过程,同时如果一个未处理的异常在调用列表的任何地方发生,这个异常回滚到调用者(这个代码),同时再没有侦听器获得调用。
这里有一个方案。胜于一再地简单调用RaiseEvent语句,它可能为你而显式地调用每个单独的侦听器。你可以利用Delegate类的成员以解决在多侦听器下的未处理异常问题。
手工调用每个侦听器
尽管RaiseEvent机制是方便的(并且也是令人觉得舒服的,如果你使用Visual Basic 6.0的话)它也有它的缺点,正如你所已经看到的。比依赖于RaiseEvent调用事件侦听器更好的是:你可以自己做这个工作。而你获得了一定的灵活性是你放弃使用RaiseEvent时的舒适为代价的。
如果你想要完全控制事件侦听器的调用而不只是希望事情如你所愿的方法解决,你将需要利用隐式的FileFoundEventHandler委托类型。你通过调用事件委托实例本身的GetInvocationList方法重新获得一个包含所有事件的侦听器的数组。一旦你有这个列表,你就可以独立地调用每个侦听器的Invoke方法,并且捕捉由事件处理程序引发的任何异常。如果任何侦听器引发一个异常,你就能处理它并将程序继续下去。
FileSearch4类包含其Search方法中的代码如图 7 显示。通过点击示例窗体上的GetInvocationList按钮运行这个代码。正如你将看到的,示例仍然调用引发一个错误的事件侦听器,但是既然这样,代码就不会在第一次侦听器引发错误时停止搜索文件。因为FileSearch4.Search方法包括独立地调用每个侦听器的代码,它也可以为每个调用处理异常。
’ From FileSearch4.vb
Dim ListenerList() As System.Delegate
Dim Listener As FileFoundEventHandler
ListenerList = FileFoundEvent.GetInvocationList()
’ Search for matching file names.
Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec)
For Each fi As FileInfo In afi
For Each Listener In ListenerList
Try
Listener.Invoke(fi)
Catch
’ Something goes wrong? Just move on to
’ the next event handler.
End Try
Next
alFiles.AddRange(afi)
Next
FileSearch4.Search中的新代码采取了以下动作:
* 声明一个System.Delegate类型的数组以使得代码可以追踪到事件的所有侦听器:
Dim ListenerList() As System.Delegate
* 声明一个描述事件的委托类型的实例,以遍历侦听器数组(你应该记得:所有侦听器过程必须有其特定的类型,否则这个代码将不能被编译,这就是委托的工作方法):
Dim Listener As FileFoundEventHandler
* 重新获得侦听器列表,调用FileFoundEvent委托的内部GetInvocationList方法:
ListenerList = FileFoundEvent.GetInvocationList()
* 找到文件并遍历包含cor-responding FileInfos的数组,正如你前面已经看到的一样:
Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec)
For Each fi As FileInfo In afi
’’ Code removed here...
Next
* 每找到一个文件,FileSearch4.Search就遍历事件侦听器的列表并独立地调用每个委托的回调(Invoke)方法。这允许代码捕捉(而且既然这样,就忽略)任何由每个独立地的侦听器引发的异常。
For Each Listener In ListenerList
Try
Listener.Invoke(fi)
Catch
’’ 出了什么错?只是前进到下一个事件处理程序
End Try
Next
这个示例项目既没有声明FileFoundEventHandler类型也没有声明FileFoundEvent变量。Visual Basic .NET编译器在你的代码中发现事件声明时它会创建这些条目。尽管你可以通过自己声明这些对象以去处含糊不清的感觉,但你并不需要这样做,因为Visual Basic .NET编译器将会为你做好这个工作。
在Visual Basic .NET 2002 and 2003里你不能修改这个基于.NET的应用程序引发它们自己的内部事件的方式。(你可以在Visual Basic 2005中修改这个行为,正如你后面将要看到的)使用先于Visual Basic 2005的版本时,这里有个方法可以确保你不会在你的事件侦听器中引发问题(记住:在任何事件处理程序中一个未处理的异常将导致.NET Framework停止为当前事件调用侦听器)。为了这样做,确保你自己的事件处理不允许回滚。如果你希望通过多个事件过程获得一个单独的事件处理的话,在你的事件过程中处理所有异常以使得你不会打破事件处理程序的链条。
使用.NET事件设计模式
虽然如你在Visual Basic 6.0和我前面的例子中所可能做的一样引发事件并没有什么错误,.NET Framework已经为事件采用了一种特别的设计模式,一种你应该在你的应用程序中采用的设计模式。在这个模式里,所有事件提供两个参数:一个对象,提供一个对引发事件(一般命名为sender)对象的引用,和一个EventArgs对象(或者一个继承于EventArgs的对象),提供相关信息给事件(一般命名为e)。
标准.NET Framework事件的设计模式添加了三个建议。首先,如果你的事件需要传递任何信息到它的侦听器,你应该创建一个继承于EventArgs的类并且它包含附加信息。你可以使用你的类的构造器来接受并存储信息。在示例项目中,FileFoundEventArgs类如图 8 所示。
第二,提供一个引发事件的过程。大多.NET Framework类从一个重载的protected过程引发事件,一般命名为OnEventName(在FileFound事件的情况下,过程会被命名为OnFileFound)。需要引发事件的代码调用OnEventName过程,它将接着引发事件。使之成为一个protected方法意味着它可为当前类型的对象所用和基于继承自当前类的任何对象所用。使之重载意味着继承类可以改变事件的行为: 一个继承类可以添加运行于调用基类的OnEventName过程之前或之后的代码,或者可以全部跳过它们。在这个示例项目中,FileSearch5类提供以下protected过程:
’ From FileFoundEventsArgs.vb
Public Class FileFoundEventArgs
Inherits EventArgs
Private mfi As FileInfo
Public ReadOnly Property FileFound() As FileInfo
Get
Return mfi
End Get
End Property
Public Sub New(ByVal fi As FileInfo)
’ Store the FileInfo object for later use.
mfi = fi
End Sub
End Class
’’ 来自 FileSearch9.vb
Protected Overridable Sub OnFileFound(ByVal fi As FileInfo)
RaiseEvent FileFound(Me, New FileFoundEventArgs(fi))
End Sub
这个过程用RaiseEvent语句的第一个参数传递关键词Me。这个关键词引用在当前运行的代码中的对象,它当然就是那个引发事件的对象。
第三,你可能发现创建你自己的事件委托是很有用的。尽管你无须定义一个显式事件委托就可获得事件委托,但在自己创建时可获得一些灵活性。在你创建一个委托时,你正在为过程定义一个“类型”。如果你有不止一个事件,它们需要同一套参数,创建一个定义这个类型的委托将会有用。如果你需要修改这些参数,你可以只要修改委托,而不用修改事件声明。
举个例子,你可以声明这个FileFound事件,不用事件委托,如下:
Public Event FileFound( _
ByVal sender As Object, ByVal e As FileFoundEventArgs)
如果你这时想要声明其他事件,使用相同参数,你将必须重复整个声明:
Public Event FileFoundSomeOtherEvent( _
ByVal sender As Object, ByVal e As FileFoundEventArgs)
作为选择,你也可以声明一个新的委托类型,它描述了你的事件的参数签名:
Public Delegate Sub FileFoundEventHandler( _
ByVal sender As Object, ByVal e As FileFoundEventArgs)
这时你应该声明这个类型的声明事件:
Public Event FileFound As FileFoundEventHandler
Public Event FileFoundSomeOtherEvent As FileFoundEventHandler
如果你没有采取这个额外的步骤,Visual Basic .NET编译器将为你做这些工作,添加新的委托类型到类的元数据中去。FileSearch5.Search方法利用了这一机制,在每个文件找到时调用OnFileFound方法:
’’ 来自 FileSearch5.vb
Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec)
For Each fi As FileInfo In afi
OnFileFound(fi)
Next
点击示例窗体上的Event Design Pattern按钮创建一个FileSearch5类的实例并调用它的Execute方法(正如所有前面的例子一样)。在此情况下,FileSearch5.FileFound事件的事件处理程序是有点不同:不是只接受一个FileInfo对象,这个事件处理程序看起来像一个标准的.NET事件处理程序;它接受两个参数并使用了FileFoundEventArgs参数的 FileFound属性来显示找到的文件名称:
’’ From frmMain.vb
Private Sub fs5_FileFound( _
ByVal sender As Object, ByVal e As FileFoundEventArgs) _
Handles fs5.FileFound
AddText(e.FileFound.FullName)
End Sub
尽管你不需要使用标准.NET事件处理设计模式,它总是使得你自己的事件与内部.NET对象引发的事件匹配的最好。你获得在你的事件侦听器中“通晓”的好处,并且它们看起来像其他事件。创建你自己的事件委托是可选的,但是如果你有多个事件传递相同的参数,使用事件委托可以简化你的代码。另外,因为事件参数的更改只能在一个地方进行,所以根据你的需要在任何时候修改你的代码将会更容易。
动态添加和移除处理程序
到目前为止你所看到的每个事件侦听器都需要你在设计时关联到事件处理程序。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控件显示出结果:
’’ 来自 frmMain.vb
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()
然后,代码将EventHandler7从回调列表中移去并再次调用execute方法:
RemoveHandler fs5.FileFound, AddressOf EventHandler7
AddText(String.Empty)
AddText("And then there were two:")
fs5.Execute()
最后,代码移除剩余的事件处理程序:
RemoveHandler fs5.FileFound, AddressOf EventHandler6
RemoveHandler fs5.FileFound, AddressOf EventHandler5
记住,在你调用AddHandler和RemoveHandler语句时你提供地址的过程必须有正确的委托类型。因此,除非你提供给AddHandler 和 RemoveHandler地址的过程参数签名与事件的参数相符(就是说,除非它们有正确的委托的类型),否则你的代码将不能被编译。
图9 控制事件处理程序回调的顺序
图 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中创建一个自定义事件,在一个类里面放入你的游标,并为你的事件录入一个声明,如下:
Public Custom Event <YourEventName> As <EventDelegateType>
当你完成这行代码时,编辑器将插入关联的AddHandler, RemoveHandler和RaiseEvent部分。比如,设想一下,你希望通过创建一个自定义事件处理程序来解决异常问题。示例项目的Visual Basic 2005版的FileSearch6类包含像这样做的一个自定义FileFound事件。代码包括适当的事件委托的一个声明,如下所示:
Public Delegate Sub FileFoundEventHandler( _
ByVal sender As Object, ByVal e As FileFoundEventArgs)
键入事件声明添加一个空的自定义事件,如图 10 所示的。
Public Custom Event FileFound As FileFoundEventHandler
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
这要由你提供事件委托实例的存储(storage),并提供储存,移出并回调事件侦听器的代码。对于本例,因为代码需要能单独回调每个侦听器并捕捉及处理任何异常,FileSearch6类包括了一个储存了FileFoundEventHandler实例的泛型列表集合(generic List collection)。每次任何类为这个事件调用AddHandler,或用一个Handles子句捕捉这个事件,Visual Basic运行时引擎调用FileFound 自定义事件的AddHandler部分。代码必须添加FileFoundEventHandler以传递到泛型列表。RemoveHandler部分从内部集合里移去指定的委托实例。RaiseEvent部分调用每个委托实例的Invoke方法,捕捉和处理发生的异常。完全的自定义事件看起来如图 11 所示的代码。
Private listeners As New List(Of FileFoundEventHandler)
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
通过推进代码到事件声明本身,引发事件的代码不再需要担心处理异常的问题了。这就是说,与使用你前面看到的代码来引发事件不同,代码假定在Visual Basic 2005中的FileSearch6类可以简单地调用OnFileFound方法(它来引发事件)或直接调用RaiseEvent。担心异常的责任现在适得其所:它就在事件代码本身之中。这项技术直到Visual Basic 2005才可以使用。注意Visual Studio 2005现仍在beta版(测试中)。正因为如此,到最终版本发布前其细节会有变化。
还有其它可以用到自定义事件吗?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。