Get A Raise

Discover a Series of Fortunate Event Handlers in Visual Basic

Ken Getz

This article discusses:

  • How events work in Visual Basic 6.0, Visual Basic .NET, and Visual Basic 2005
  • How Visual Basic .NET events are compiled
  • Exceptions and manual invocation of event handlers
  • Writing custom events
This article uses the following technologies:
Visual Basic, .NET Framework

Code download available at:EventHandling.exe(158 KB)

Contents

Events
Doing Things the Visual Basic 6.0 Way
Using ILDASM to Investigate Events
Multiple Event Handlers
Exceptions and Multiple Event Handlers
Investigating the Exception Behavior
Manually Invoking Each Listener
Using the .NET Event Design Pattern
Adding and Removing Handlers Dynamically
Custom Events in Visual Basic 2005
What's Next?

Events are an integral part of your coding arsenal, whether you're using Visual Basic® 6.0, Visual Basic .NET 2002, Visual Basic .NET 2003, or Visual Basic 2005. Forms and controls raise events, and your code handles those events. The first application that you wrote in Visual Basic most likely involved placing a button on a form, handling the click event, and displaying some text in an alert box when you clicked the button at run time. What could be easier?

But how much do you really know about events? What's going on when you add an event handler to a class? In this article, which I've based on courseware I wrote for AppDev, I'll demonstrate various ways to interact with events and event handlers, and I'll show how they can solve common problems. Some of this information may not be new to you—but if you've taken events at face value, there are sure to be some surprises. In either case, download the two sample applications (one for Visual Basic .NET .2002 and 2003 and one for Visual Basic 2005) and follow along. All of the content applies to Visual Basic .NET 2002 and 2003 and Visual Basic 2005, with the exception of the final section covering custom events, which only work in Visual Basic 2005.

I'll assume you have some basic knowledge of delegates and multicast delegates. If you haven't investigated these important Microsoft® .NET Framework features, it's time that you do. For more information, check out Ted Pattison's two-part overview on delegates (Basic Instincts: Implementing Callback Notifications Using Delegates and Basic Instincts: Implementing Callbacks with a Multicast Delegate).

Events

Visual Basic (before Visual Basic .NET) provided simple mechanisms for you to create and handle events, and Visual Basic .NET 2002 and 2003 offered several different ways to do both. Visual Basic 2005 allows you even greater control over event handlers, as you'll see in this article.

Events provide a loosely coupled mechanism that allows classes to register for notifications that may or may not happen sometime in the future. If the "listeners" get notified that the event they're waiting for has occurred, they handle the situation. If not, they just keep listening. A button click event handler registers itself with the class providing the button's functionality; when a user clicks the button, the button's class raises the Click event, all the listeners (there can be multiple Click event handlers for the button) run their code, and execution of the code continues.

My sample application includes a series of classes (FileSearch1 through FileSearch5 for Visual Basic .NET 2002 and 2003, and FileSearch1 through FileSearch6 for Visual Basic 2005) that search for files in a specified location and raise an event when one is found. The FileSearch classes simply want to let any class that happens to be listening know when something interesting happens. In this case, the something interesting occurs each time the FileSearch class finds another file. Any number of listener classes might want to react to this event.

In .NET terms, a class can raise an event at any point during its code execution. Other classes can subscribe to the event, and can be notified by the .NET Framework when the event occurs. The class that raises the event generally has no knowledge of how many (if any) listeners exist, although it can gather this information with some effort, as you'll see later in the article. In addition, multiple listeners can register to be notified, and each can be notified without the knowledge of any other listener.

Doing Things the Visual Basic 6.0 Way

The designers of the .NET Framework, and the Visual Basic .NET language, went to great lengths to ensure that you can treat events in .NET just as you did in Visual Basic 6.0. That is, you can:

  • Declare an event using the Event keyword
  • Raise the event using the RaiseEvent statement
  • Handle the event using a WithEvents variable

The real difference with .NET is the underlying mechanism. As opposed to using some hidden plumbing in Visual Basic 6.0, Visual Basic .NET uses a visible, extensible, common plumbing—delegates—to manage event handling.

Another difference between Visual Basic 6.0 and the versions of Visual Basic .NET is that in .NET, you can use the Handles clause to indicate that a particular procedure should run in response to a particular event. The Handles clause allows any procedure that matches the parameter signature of the event to react to the event. This sounds a lot like a delegate, and under the covers, it is. At compile time, the .NET Framework creates a Delegate class with the name of your event, adding the text "EventHandler" at the end. For example, when you declare an event named FileFound, the .NET Framework creates a delegate type named FileFoundEventHandler for you. Every procedure that handles this event must have a signature that matches that delegate type. You'll see an example of this concept later in the article.

Clicking RaiseEvent on the sample form demonstrates how Visual Basic .NET supports the standard event handling found in Visual Basic 6.0. The sample project includes the FileSearch1 class, which uses the following code to set up the event:

' From FileSearch1.vb Public Class FileSearch1 ' Search for files in specified locations. ' Once found, this class raises the FileFound event ' for each found file. Public Event FileFound(ByVal fi As FileInfo) ... ' Code removed here... End Class

As it finds files, the FileSearch1 class raises the FileFound event:

' From FileSearch1.vb Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) For Each fi As FileInfo In afi RaiseEvent FileFound(fi) Next alFiles.AddRange(afi)

In frmMain.vb, you'll find the following declaration, which allows the code to use the variable fs1 to react to events raised by the FileSearch1 instance:

Private WithEvents fs1 As FileSearch1

Clicking RaiseEvent runs the following code:

' From frmMain.vb fs1 = New FileSearch1( _ Me.txtSearchPath.Text, Me.txtFileSpec.Text, _ Me.chkSearchSubfolders.Checked) Try fs1.Execute() Catch End Try

Finally, frmMain.vb contains an event handler that handles the FileSearch1.FileFound event:

' From frmMain.vb Private Sub EventHandler1( _ ByVal NewFile As System.IO.FileInfo) _ Handles fs1.FileFound AddText("EventHandler1: " & NewFile.FullName) End Sub

Although frmMain.vb only includes a single procedure that handles the FileSearch1.FileFound event, it's quite possible (and likely) that you would have more than one procedure that handles a particular event. That is, when the FileSearch1 class raises its FileFound event, it's possible that multiple procedures might handle it. This should sound a lot like the concept of multicast delegates, because it is. Under the covers, .NET converts events into delegate classes. Investigating the IL using ILDASM.exe will make this clear.

Using ILDASM to Investigate Events

If you use ILDASM (the .NET Framework IL Disassembler tool included with the .NET Framework SDK) to open the sample executable, you'll get information like you see in Figure 1. Even though FileSearch1 didn't explicitly contain a multicast delegate named FileFoundEventHandler, the Visual Basic .NET compiler created one, corresponding to the type defined by the FileFound event that the code declares. The compiler also creates an instance named FileFoundEvent of the delegate type FileFoundEventHandler representing the event listener(s).

Figure 1 Sample Code in ILDASM

Figure 1** Sample Code in ILDASM **

Using this technique, the compiler can enforce strict adherence to the parameter signature of your event declaration without your having to worry about creating your own delegate type and subscribing to that type in your event declaration. As you'll see later, you're perfectly welcome to create your own delegate representing your event and use that delegate as the type for your event.

Multiple Event Handlers

The next class I'll discuss, FileSearch2, is an identical copy of the FileSearch1 class. The only difference in the use of FileSearch2 is that the sample form includes multiple listeners for the FileFound event of the FileSearch2 class. That is, frmMain.vb includes the following declaration:

' From frmMain.vb Private WithEvents fs2 As FileSearch2

The sample form also includes the event handlers shown in Figure 2. These also listen for the FileFound event raised by the FileSearch3 and FileSearch4 classes, which I will cover later.

Figure 2 Multiple Event Handlers

' 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

Clicking Multi-Listener on the main form creates an instance of the FileSearch2 class, calls the instance's execute method, and displays the output shown in Figure 3.

Figure 3 Multiple Listeners Allow Multiple Procedures to Run

Figure 3** Multiple Listeners Allow Multiple Procedures to Run **

But watch out. When you use the multiple Handles clauses reacting to the same event, you have no control over the order in which the event handlers run. The .NET Framework provides two alternatives, discussed later in the article, which allow you to take greater control over multiple listeners.

Exceptions and Multiple Event Handlers

As you've seen, everything is working as expected. If you have multiple handlers for an event, when the event is raised, each of the handlers gets called in turn by the .NET Framework. So far, so good. What happens if one of the event handlers raises an exception? Then things don't go so well.

To demonstrate this, click RaiseEvent Error on the sample form. This example creates a new instance of the FileSearch3 class (there's nothing new in the class itself). The sample form in Figure 4 provides several procedures that handle the FileSearch3.FileFound event, but one throws an exception.

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

Figure 5 One Event Listener Throws an Error

Figure 5** One Event Listener Throws an Error **

What happens when you run this code? You'll get the results shown in Figure 5. If any event listener raises an exception, the whole event-handling chain stops. If you pause to consider what's going on, this behavior makes sense.

Investigating the Exception Behavior

In a moment I'll show you code from the FileSearch3 class. Each time an instance of the class finds a file, the instance raises its FileFound event. This one statement causes the .NET Framework to run each event-handling procedure, one after the other. Take a look at the following code:

' From FileSearch3.vb ' Search for matching file names. Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) For Each fi As FileInfo In afi ' This is equivalent to: ' FileFoundEventHandler.Invoke(fi) RaiseEvent FileFound(fi) Next alFiles.AddRange(afi)

The problem with this technique (as you've seen) is that if an exception occurs in any listener, the exception bubbles back to the event-raising code, and the .NET Framework calls no more of the event listeners, and event handling grinds to a halt.

If you look at the IL generated for the sample code, this situation becomes clearer. Figure 6 shows the disassembly of the FileSearch3.Search method in ILDASM. The RaiseEvent statement you saw in the class compiles down to a FileFoundEventHandler.Invoke method call. Internally, once this method call has been made, your code transfers control to the delegate's implementation, and if an unhandled exception occurs anywhere in the invocation list, the exception bubbles back to the caller (this code), and no more listeners get called.

Figure 6 RaiseEvent Compiled to MSIL

There is an alternative. Rather than simply calling the RaiseEvent statement over and over again, it is possible for you to call each individual listener explicitly. You can take advantage of the members of the Delegate class to solve the problem of unhandled exceptions in multiple listeners.

Manually Invoking Each Listener

Although the RaiseEvent mechanism is convenient (and comfortable if you're used to Visual Basic 6.0), it has its drawbacks, as you've seen. Rather than relying on RaiseEvent to invoke the event's listeners, you can do this work yourself. And you gain a level of flexibility that you relinquish when you use RaiseEvent.

If you want to take complete control over the invocation of the event listeners instead of just hoping things work out the way you want, you'll need to take advantage of the implicit FileFoundEventHandler delegate type. You can retrieve an array containing all of the event's listeners by calling the GetInvocationList method of the event delegate instance itself. Once you have the list, you can call the Invoke method of each listener individually, and trap any exception raised by the event handler. That way, if any listener raises an exception, you can handle it and move on.

The FileSearch4 class contains the code shown in Figure 7 in its Search method. Run the code by clicking GetInvocationList on the sample form. As you'll see, this sample still calls the event listener that raises an error, but in this case, the code doesn't stop looking for files the first time the listener raises the error. Because the FileSearch4.Search method includes code to invoke each listener individually, it can handle exceptions for each call as well.

Figure 7 Catch Errors for Each Listener

' 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

The new code in FileSearch4.Search takes the following actions:

  • Declares an array of System.Delegate types so that the code can track all of the listeners for the event:
Dim ListenerList() As System.Delegate
  • Declares an instance of the delegate type that describes the event in order to loop through the array of listeners (you should remember that all of the listener procedures must be of this particular type, or the code wouldn't have compiled—that's the way delegates work):
Dim Listener As FileFoundEventHandler
  • Retrieves the list of listeners, calling the built-in GetInvocationList method of the FileFoundEvent delegate:
ListenerList = FileFoundEvent.GetInvocationList()
  • Finds the files and loops through the array containing the cor-responding FileInfos , as you've seen previously:
Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) For Each fi As FileInfo In afi ' Code removed here... Next
  • For each file found, FileSearch4.Search loops through the list of event listeners and calls each delegate's Invoke method individually. This allows the code to trap (and in this case, dis-regard) any exceptions raised by each individual listener:
For Each Listener In ListenerList Try Listener.Invoke(fi) Catch ' Something goes wrong? Just move on to ' the next event handler. End Try Next

The sample project declared neither the FileFoundEventHandler type nor the FileFoundEvent variable. The Visual Basic .NET compiler creates these items when it finds the event declaration in your code. Although you can clear up this ambiguity by declaring these objects yourself, you needn't—the Visual Basic .NET compiler will do the work for you.

In Visual Basic .NET 2002 and 2003 you can't modify the manner in which .NET-based applications raise their own built-in events. (You can modify this behavior in Visual Basic 2005, as you'll see later.) Using versions prior to Visual Basic 2005, there is a way to ensure that you don't cause trouble in your event listeners (remember that an unhandled exception in any event handler will cause the .NET Framework to stop calling listeners for the current event). To do so, make sure your own event handlers don't allow exceptions to bubble up. Handle all exceptions in your event procedures so that you won't break the chain of event handlers if you want to have a single event handled by multiple event procedures.

Using the .NET Event Design Pattern

Although there's nothing wrong with raising events as you might have done in Visual Basic 6.0 and in my preceding examples, the .NET Framework has adopted a particular design pattern for events which you should adopt for your applications. In this pattern, all events provide two arguments: an Object, providing a reference to the object that raised the event (normally named sender), and an EventArgs object (or an object that inherits from EventArgs), providing information pertinent to the event (normally named e).

The standard .NET Framework design pattern for events adds three more recommendations. First, if your event needs to pass any information to its listeners, you should create a class that inherits from EventArgs and that contains the additional information. You can use your class's constructor to accept and store the information. In the sample project, the FileFoundEventArgs class looks like Figure 8.

Figure 8 FileFoundEventArgs Class

' 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

Second, provide a procedure that raises the event. Most .NET Framework classes raise an event from an overridable protected procedure, normally named OnEventName (in the case of the FileFound event, the procedure would be named OnFileFound). Code that needs to raise the event calls the OnEventName procedure, which in turn raises the event. Making this a protected method means it's available to the objects of the current type and to any object based on a class that inherits from the current class. Making it overridable means that inheriting classes can change the behavior of the event—an inheriting class could add code that runs before or after calling the base class's OnEventName procedure, or could bypass it altogether. In the sample project, the FileSearch5 class provides the following protected procedure:

' From FileSearch9.vb Protected Overridable Sub OnFileFound(ByVal fi As FileInfo) RaiseEvent FileFound(Me, New FileFoundEventArgs(fi)) End Sub

This procedure passes the keyword Me in the RaiseEvent statement's first parameter. This keyword refers to the object in which code is currently running, which is most certainly the object that's raising the event.

Third, you may find it useful to create your own event delegate type. Although you can get by without defining an explicit event delegate, you gain some flexibility in creating your own. When you create a delegate, you're defining a "type" for procedures. If you have more than one event that requires the same set of parameters, it can be useful to create a delegate that defines the type. That way, should you need to modify the parameters, you can simply modify the delegate, leaving the event declarations alone.

For example, you could declare the FileFound event, without using an event delegate, like so:

Public Event FileFound( _ ByVal sender As Object, ByVal e As FileFoundEventArgs)

If you then wanted to declare another event, using the same parameters, you would need to repeat the whole declaration:

Public Event FileFoundSomeOtherEvent( _ ByVal sender As Object, ByVal e As FileFoundEventArgs)

Alternatively, you could declare a new delegate type that described the parameter signature of your events:

Public Delegate Sub FileFoundEventHandler( _ ByVal sender As Object, ByVal e As FileFoundEventArgs)

Then you could declare events of this type:

Public Event FileFound As FileFoundEventHandler Public Event FileFoundSomeOtherEvent As FileFoundEventHandler

If you don't take this extra step, the Visual Basic .NET compiler will do the work for you, adding the new delegate type to the class's metadata. The FileSearch5.Search method takes advantage of this mechanism, calling the OnFileFound method for each file it finds:

' From FileSearch5.vb Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) For Each fi As FileInfo In afi OnFileFound(fi) Next

Clicking Event Design Pattern on the sample form creates an instance of the FileSearch5 class and calls its Execute method (just like all the previous examples). In this case, the event handler for the FileSearch5.FileFound event is a bit different—rather than accepting simply a FileInfo object, this event handler looks like a standard .NET event handler; it accepts two parameters and uses the FileFound property of its FileFoundEventArgs parameter to display the found file name:

' From frmMain.vb Private Sub fs5_FileFound( _ ByVal sender As Object, ByVal e As FileFoundEventArgs) _ Handles fs5.FileFound AddText(e.FileFound.FullName) End Sub

Although you aren't required to use the standard .NET event-handling design pattern, it's always best to have your own events match those raised by built-in .NET objects. You gain the "familiarity" benefit in your event listeners, and they look like other events. Creating your own event delegate is optional, but if you have multiple events that pass the same parameters, using the event delegate can simplify your code. Plus, it will be easier to modify your code anytime you need to because changes to the event parameters only have to be made in one place.

Adding and Removing Handlers Dynamically

Every event listener that you've seen so far requires you to hook up the event handler at design time. The Handles clause is a convenient and simple way to hook up events raised by objects declared using the WithEvents keyword, but it doesn't provide any flexibility at run time. In addition, when multiple procedures handle the same event, the Handles clause gives you no control over the order in which event handlers execute. (Of course, to get around this you can use the trick you saw earlier, looping through the items returned by calling GetInvocationList. This technique requires additional code in the class that raises the event, not in the code that hooks up event listeners.)

To gain complete flexibility over when and in what order event handlers are called, you can use the AddHandler (and RemoveHandler) statements rather than the Handles clause. The AddHandler and RemoveHandler statements allow you to supply a specific event and the address of a procedure to be called in response to that event. Each call to AddHandler associates the procedure with the event so that the .NET Framework calls the procedure when the event occurs. In addition, AddHandler always adds the event handler to end of the invocation list for the event. This means that you control the order in which events are handled.

Of course, if you think about it for a minute, you realize that when you have multiple procedures, each with a Handles clause for the same event, the Visual Basic compiler creates a multicast delegate instance for the event handler without allowing you to control the order in which they're added. Raising the event calls the Invoke method of that delegate instance, which then calls each of the event listeners in the order in which they were added (and you had no control over that order). When you use the AddHandler and RemoveHandler statements rather than the Handles clause, you're simply taking control over the order in which items are added to the multicast delegate. Each time your application calls the AddHandler statement for the same event, you're adding a new listener for the event to the end of the list. When you raise the event, the .NET runtime calls each of the listeners, in turn.

If you click Add/RemoveHandler on the sample form, a new instance of the FileSearch5 class is created, and multiple event handlers for the instance's FileFound event are hooked up. Then, when the code calls the Execute method of the instance, the listbox on the sample form displays the results:

' From 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()

Next, the code removes EventHandler7 from the invocation list and calls the execute method again:

RemoveHandler fs5.FileFound, AddressOf EventHandler7 AddText(String.Empty) AddText("And then there were two:") fs5.Execute()

Finally, the code removes the remaining event handlers:

RemoveHandler fs5.FileFound, AddressOf EventHandler6 RemoveHandler fs5.FileFound, AddressOf EventHandler5

Remember, the procedures whose address you're supplying when you call the AddHandler and RemoveHandler statements must be of the correct delegate type. Therefore, unless the parameter signature of the procedures whose addresses you supply to AddHandler and RemoveHandler match the event's parameters (that is, unless they're of the correct delegate type), your code won't compile.

Figure 9 Control the Order of Event Handler Invocation

Figure 9** Control the Order of Event Handler Invocation **

Figure 9 shows the output from clicking Add/RemoveHandler. As you can see, the event procedures are called in the order in which you added them to the invocation list.

Custom Events in Visual Basic 2005

Remember the problem that occurred when an event had multiple listeners, and one event listener threw an exception? This problem has a reasonably simple solution, using the invocation list of the multicast delegate instance associated with the event. In Visual Basic .NET 2002 and 2003, however, there were some other event challenges that simply couldn't be worked around without complex or inefficient code. Prior to Visual Basic 2005, the instance of the event delegate type was always created for you by the Visual Basic compiler, and the compiler provided no way for you to modify the behavior of this delegate instance.

Visual Basic 2005 adds the new Custom keyword for event declarations. This keyword allows you to supply code for the AddHandler, RemoveHandler, and RaiseEvent behaviors of the event. It's up to you to create the appropriate delegate type and to create the instance of the type that holds information about the event listeners. Beyond that, however, you have complete control over how you handle the event.

To create a custom event in Visual Studio 2005, place your cursor within a class, and enter a declaration for your event, like this:

Public Custom Event <YourEventName> As <EventDelegateType>

When you end the line of code, the editor will insert the associated AddHandler, RemoveHandler, and RaiseEvent sections. For example, imagine that you want to solve the exception issue by creating a custom event handler. The FileSearch6 class in the Visual Basic 2005 version of the sample project contains a custom FileFound event that does this. The code includes a declaration for the appropriate event delegate, as shown earlier:

Public Delegate Sub FileFoundEventHandler( _ ByVal sender As Object, ByVal e As FileFoundEventArgs)

Typing the event declaration adds an empty custom event, like the one that is shown in Figure 10.

Figure 10 Generated Empty Event

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

It's up to you to provide the storage for the event delegate instance, and to provide the code that stores, removes, and invokes the event listeners. For this example, because the code needs to be able to invoke each listener individually and trap and handle any exceptions, the FileSearch6 class includes a generic List collection in which to store the FileFoundEventHandler instances. Each time any class calls AddHandler for this event, or uses a Handles clause trapping this event, the Visual Basic runtime engine calls the AddHandler portion of the FileFound custom event. The code must add the FileFoundEventHandler passed into the generic List. The RemoveHandler section removes the specified delegate instance from the internal collection. The RaiseEvent section calls the Invoke method of each delegate instance, trapping and dealing with exceptions as they occur. The complete custom event looks like the code shown in Figure 11.

Figure 11 The Custom Event

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

By pushing this code into the event declaration itself, the code that raises the event needn't worry about handling exceptions. That is, rather than using the code you saw previously to raise the event, code consuming the FileSearch6 class in Visual Basic 2005 can simply call the OnFileFound method (which raises the event) or call RaiseEvent directly. The onus of worrying about exceptions is now in the right place—in the event code itself. This technique wasn't available until Visual Basic 2005. Note that Visual Studio 2005 is still in beta. As such, details are subject to change until the final release.

How else might you use custom events? Rocky Lhotka, a Visual Basic MVP, includes another detailed example on his blog (.NET 2.0 solution to serialization of objects that raise events). He demonstrates how you might use this technique to work around problems involving serialization of classes that raise events, in which the listeners aren't serializable. (Amazingly, this comes up quite often, because forms aren't serializable, but are often listeners for user-created events.) Paul Vick, a member of the Visual Basic development team at Microsoft, includes an example on his blog that shows how you might use a custom event to reduce overhead for classes that expose a large number of events in which only a few might be consumed. This is the case with forms, for example—the Form class exposes a ton of events, but most of the time, you only handle one or two of them. Without some tricks, the compiler would generate a delegate instance for each of those events, even though you're not using them. See Paul's blog at Custom events.

What's Next?

All the code you've seen in this article runs synchronously. That is, whether the FileSearch class calls the delegate method itself or raises an event, the search cannot continue until the listener (or listeners) completes its work. If the event-handling procedure stalls, displays an alert, or otherwise neglects to return, the search loop would stall as well. In general, that can't be a good thing. Fortunately, the .NET Framework provides several mechanisms for asynchronous event handling—see my column in the March 2005 issue of MSDN®Magazine (Advanced Basics: Doing Async the Easy Way) for one such technique you can follow to put the finishing touches on your event-handling code, and you'll be that much closer to event-handling Nirvana!

Ken Getz is a senior consultant with MCW Technologies. He is coauthor of ASP.NET Developers Jumpstart (Addison-Wesley, 2002), Access Developer's Handbook, and VBA Developer's Handbook, 2nd Edition (both Sybex, 2001). Reach him at keng@mcwtech.com.