探索VB中系列事件处理程序的奥秘
【IT168 技术文档】
Visual Basic .NET事件如何编译
如何编写自定义事件
事件是你的代码兵器库中的主要部分,无论你用Visual Basic 6.0,Visual Basic .NET 2002,Visual Basic .NET 2003,还是Visual Basic 2005。窗体和控件引发事件,同时你的代码处理这些事件。你用Visual Basic写的最初应用程序大多会是在一个窗体上放置一个按钮,处理这个click事件,并在运行时你点击这个按钮会显示某些文本在提示框中。还有什么比这更容易?
但是你又真正了解事件多少呢?在你向某个类中添加一个事件处理程序是将会发生什么?在本文中,基于我为 AppDev 所写的课件,我将用各种方法来演示事件和事件处理程序交互,并且我将说明它们如何能解决一般问题。也许这些信息中的一些对你来说并不新鲜,但是如果你对事件的了解并不深入,这里肯定有些东西让你惊奇。
事件
Visual Basic(在Visual Basic .NET之前)为你提供了创建和处理事件的简单机制,并且Visual Basic .NET 2002和2003提供了几个不同的方法来做它们。Visual Basic 2005甚至允许你更强地控制事件处理程序,正如你将在本文里所看到的。
事件提供一个松散的联系机制,它允许类为在将来某个时间可能或也许不可能发生的通知注册。如果“侦听器”得到它们正在等待的事件发生的通知,它们就处理这种情况。如果不,它们只是保持监听。一个按钮点击事件处理程序用类提供的按钮的功能(functionality)来注册它自己;在一个用户点击这个按钮时,这个按钮的类引发Click事件,所有侦听器(这里可能是此按钮的多个Click事件处理程序)运行它们的代码,并继续执行代码。
以.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的委托类型。处理这个事件的每个过程必须有一个符合委托类型的签名。
以下代码演示了Visual Basic .NET如何支持以Visual Basic 6.0为基础的事件处理。
Public Class FileSearch1
''在指定位置搜索文件
''一旦找到,这个类就为每个找到的文件引发FileFound 事件
Public Event FileFound(ByVal fi As FileInfo)
... '' 这里的代码去掉了...
End Class
在它找到文件时,FileSearch1类引发FileFound事件:
Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) for Each fi As FileInfo In afi RaiseEvent FileFound(fi) Next alFiles.AddRange(afi) Private WithEvents fs1 As FileSearch1点击RaiseEvent运行以下代码: fs1 = New FileSearch1( _ Me.txtSearchPath.Text, Me.txtFileSpec.Text, _ Me.chkSearchSubfolders.Checked)try fs1.Execute() Catch End Try
最后,包括一个事件处理程序,它处理FileSearch1.FileFound事件:
Private Sub EventHandler1( _ ByVal NewFile As System.IO.FileInfo) _ Handles fs1.FileFound AddText("EventHandler1: " & NewFile.FullName) End Sub
尽管只包括处理FileSearch1.FileFound事件的一个单过程,你将会有不止一个过程处理一个特定事件是相当可能的(并且是很可能的)。这就是说,当FileSearch1类引发它的FileFound事件,多个过程可能处理它是可能的。这听起来应该很像多路广播委托的概念,因为它就是多路广播委托。本质上,.NET将事件转换为委托类。用ILDASM.exe研究一下IL(中间代码)就会拨云见日了。
用ILDASM研究事件
你可以使用ILDASM(包含在.NET Framework SDK中的.NET Framework IL反汇编工具)来打开制作完毕的可执行版本。尽管FileSearch1并没有显式地包含一个命名为FileFoundEventHandler的多路广播委托,Visual Basic .NET编译器已创建了一个(多路广播委托),对应于代码声明的由FileFound事件定义的类型。编译器也创建了一个表示事件侦听器的委托类型FileFoundEventHandler的名为FileFoundEvent的实例。
使用这个技术,编译器可以强迫严格遵循你的事件声明的参数签名,而不用你再去为创建你自己的委托类型和在你的事件声明中预定的类型而操心。正如你后面将看到的一样,你可以完全随意地创建你自己的委托表示你的事件并且可用这个委托作为你的事件的类型。
多事件处理程序
我将讨论的下一个类:FileSearch2,它是FileSearch1类的一个相似的拷贝。使用FileSearch2中的唯一的不同是:示例窗体对于FileSearch2类的 FileFound事件包括多个侦听器。
但是注意,当你使用多个Handles子句对同一事件反应时,你完全不能控制事件处理程序运行的顺序。.NET Framework提供两个选择,这将稍后在文章中讨论,它允许你获得对多侦听器的更强地控制。
异常和多个事件处理程序(Multiple Event Handlers)
如果你有一个事件的多个处理程序,当事件引发时,.NET Framework将依次调用每个处理程序。到目前为止一切顺利。如果其中某个事件处理程序产生一个异常时将会发生什么?事情就没有那么顺利了。
研究异常(Exception)行为
我将很快给你看看从FileSearch3类中选取的代码。每次一个类的实例找到一个文件,实例就引发它的FileFound事件。这一效果导致.NET Framework相继地运行每个事件处理过程。看看下面的代码:
''搜索符合的文件名 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不再调用事件侦听器,而事件处理就慢慢停了下来。
你在类中看到的RaiseEvent语句直到一个FileFoundEventHandler.Invoke方法调用后才会编译。在内部,一旦这个方法已经调用,你的代码将控制权交给委托的执行过程,同时如果一个未处理的异常在调用列表的任何地方发生,这个异常回滚到调用者(这个代码),同时再没有侦听器获得调用。
这里有一个方案。胜于一再地简单调用RaiseEvent语句,它可能为你而显式地调用每个单独的侦听器。你可以利用Delegate类的成员以解决在多侦听器下的未处理异常问题。
手工调用每个侦听器
尽管RaiseEvent机制是方便的(并且也是令人觉得舒服的,如果你使用Visual Basic 6.0的话)它也有它的缺点,正如你所已经看到的。比依赖于RaiseEvent调用事件侦听器更好的是:你可以自己做这个工作。而你获得了一定的灵活性是你放弃使用RaiseEvent时的舒适为代价的。
如果你想要完全控制事件侦听器的调用而不只是希望事情如你所愿的方法解决,你将需要利用隐式的FileFoundEventHandler委托类型。你通过调用事件委托实例本身的GetInvocationList方法重新获得一个包含所有事件的侦听器的数组。一旦你有这个列表,你就可以独立地调用每个侦听器的Invoke方法,并且捕捉由事件处理程序引发的任何异常。如果任何侦听器引发一个异常,你就能处理它并将程序继续下去。
在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的类并且它包含附加信息。你可以使用你的类的构造器来接受并存储信息。
第二,提供一个引发事件的过程。大多.NET Framework类从一个重载的protected过程引发事件,一般命名为OnEventName(在FileFound事件的情况下,过程会被命名为OnFileFound)。需要引发事件的代码调用OnEventName过程,它将接着引发事件。使之成为一个protected方法意味着它可为当前类型的对象所用和基于继承自当前类的任何对象所用。使之重载意味着继承类可以改变事件的行为: 一个继承类可以添加运行于调用基类的OnEventName过程之前或之后的代码,或者可以全部跳过它们。
第三,你可能发现创建你自己的事件委托是很有用的。尽管你无须定义一个显式事件委托就可获得事件委托,但在自己创建时可获得一些灵活性。在你创建一个委托时,你正在为过程定义一个“类型”。如果你有不止一个事件,它们需要同一套参数,创建一个定义这个类型的委托将会有用。如果你需要修改这些参数,你可以只要修改委托,而不用修改事件声明。
举个例子,你可以声明这个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编译器将为你做这些工作,添加新的委托类型到类的元数据中去。尽管你不需要使用标准.NET事件处理设计模式,它总是使得你自己的事件与内部.NET对象引发的事件匹配的最好。你获得在你的事件侦听器中“通晓”的好处,并且它们看起来像其他事件。创建你自己的事件委托是可选的,但是如果你有多个事件传递相同的参数,使用事件委托可以简化你的代码。另外,因为事件参数的更改只能在一个地方进行,所以根据你的需要在任何时候修改你的代码将会更容易。
动态添加和移除处理程序
到目前为止你所看到的每个事件侦听器都需要你在设计时关联到事件处理程序。Handles子句是关联使用WithEvents关键字的对象引发事件的方便而简单的方法,但是它不能在运行时提供任何弹性。另外,当多个过程处理相同的事件,Handles子句不会给你事件处理程序执行顺序的控制权。(当然,为了避免这个问题你可以使用你前面看到的技巧,遍历调用GetInvocationList返回项。这个技术要求引发事件的类的附加代码,不是在关联事件侦听器的代码中。)
为了在何时及以什么顺序调用事件处理程序上获得完全主动,你可以使用AddHandler(和RemoveHandler)语句而不是Handles子句。AddHandler和RemoveHandler语句允许你提供一个特定的事件和准备响应事件被调用过程的地址。每个对AddHandler的调用使过程和事件相关联以使得.NET Framework在事件发生时调用过程。另外,AddHandler总是添加事件处理程序到事件调用列表的末尾。这意味着你控制了事件被处理的顺序。
当然,如果你再多考虑片刻,你会理解当你有多个过程时,相同的事件都有一个Handles子句,Visual Basic编译器为事件处理程序创建一个多路广播委托实例而不允许你控制它们被添加进的顺序。引发的事件调用委托实例的Invoke方法,这时就按每个事件侦听器被添加的顺序(并且你无权控制这个顺序)调用它们。当你使用AddHandler和RemoveHandler语句而不是Handles子句,你只要简单控制各项加入多路广播委托的顺序。每次你的应用程序对相同事件调用AddHandler语句,你就为这个事件添加了一个新的侦听器到列表的最后。当你引发这个事件,.NET运行时按顺序调用每个侦听器。
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版的某个类包含像这样做的一个自定义FileFound事件。代码包括适当的事件委托的一个声明,如下所示:
Public Delegate Sub FileFoundEventHandler(ByVal sender As Object, ByVal e
As FileFoundEventArgs)
键入事件声明添加一个空的自定义事件。
这要由你提供事件委托实例的存储(storage),并提供储存,移出并回调事件侦听器的代码。对于本例,因为代码需要能单独回调每个侦听器并捕捉及处理任何异常,FileSearch6类包括了一个储存了FileFoundEventHandler实例的泛型列表集合(generic List collection)。每次任何类为这个事件调用AddHandler,或用一个Handles子句捕捉这个事件,Visual Basic运行时引擎调用FileFound 自定义事件的AddHandler部分。代码必须添加FileFoundEventHandler以传递到泛型列表。RemoveHandler部分从内部集合里移去指定的委托实例。RaiseEvent部分调用每个委托实例的Invoke方法,捕捉和处理发生的异常。
通过推进代码到事件声明本身,引发事件的代码不再需要担心处理异常的问题了。这就是说,与使用你前面看到的代码来引发事件不同,代码假定在Visual Basic 2005中的FileSearch6类可以简单地调用OnFileFound方法(它来引发事件)或直接调用RaiseEvent。担心异常的责任现在适得其所:它就在事件代码本身之中。这项技术直到Visual Basic 2005才可以使用。
所有你已在本文中所看到的代码都是同步运行的。这就是说,无论FileSearch类调用委托方法本身或引发一个事件,搜索不会继续直到侦听器(或多个侦听器)完成它的的工作。如果事件处理过程暂停,显示一个警示,或返回其他疏漏,则搜索循环也将暂停。
