探索VB系列中的事件处理的奥秘


  事件是你的代码兵器库中的主要部分,无论你用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 某个事件侦听器抛出一个错误