Xtreme Visual Basic Talk

Xtreme Visual Basic Talk (http://www.xtremevbtalk.com/)
-   Tutors' Corner (http://www.xtremevbtalk.com/tutors-corner/)
-   -   Advanced Events Tutorial -or- Things You Probably Never Knew about Events (http://www.xtremevbtalk.com/tutors-corner/287855-advanced-events-tutorial-probably-events.html)

AtmaWeapon 09-08-2007 08:23 PM

Advanced Events Tutorial -or- Things You Probably Never Knew about Events
This is not intended to be a basic primer on events. I plan on covering some fairly advanced event syntax so I will assume you are marginally familiar with events in an attempt to keep this as brief as possible. I have a problem with verbosity that I am trying to overcome.

All code has been tested in Visual Studio 2005 Professional. My original intent was to post an appendix instructing users how to alter the code so it will compile in .NET 1.x, but after I'd already removed all partial classes it turned out that custom event syntax doesn't exist in .NET 1.X. Sorry guys, just add it to the list of reasons you should upgrade ;)

You'll find I'm quite slim on code examples in the post; I edit the source files constantly and it's too much of a bother to keep the file with this post in sync with the code. If you want to learn the concepts, read the post. If you want to see the code, read the post and download the code. The comments in the code are pretty detailed and may even point out a thing or two that I overlooked in the post.

Anyway, let's discuss events like you've probably never seen them.

Example 1 - How Events are Implemented

Consider a class that defines a single event:

Public Class MyClass
    Public Event SomethingHappened As EventHandler
End Class

Knowing a little bit about events, one will understand that delegates are used under the hood to implement events, but how are they implemented by the compiler?

If you open the class in IL DASM, you'll find that there are quite a few things going on behind the scenes. First, you'll see a private field named after the event. Next, you'll notice an add_SomethingHappened and remove_SomethingHappened method, along with a weird structure that ties the event to these methods. Basically, under the hood events are implemented as if you had written the following code:

Private SomethingHappened As EventHandler

<MethodImpl(MethodImplOptions.Synchronized)> _
Public Sub add_SomethingHappened(ByVal value As EventHandler)
    SomethingHappened = CType([Delegate].Combine(SomethingHappened, value), EventHandler)
End Sub

<MethodImpl(MethodImplOptions.Synchronized)> _
Public Sub remove_SomethingHappened(ByVal value As EventHandler)
    SomethingHappened = CType([Delegate].Remove(SomethingHappened, value), EventHandler)
End Sub

I don't expect you to just take my word for it, so take a look at Example1.zip and see it in action. Example 1 pretty straightforward so I won't waste time discussing it.

Why the Implementation Details are Important

I won't even waste time telling you to not implement your events by writing code as above; you can't use the AddHandler syntax and it's a lot more work than the single line of code. But it is very important to see how the compiler translates that line of code for two reasons that are the key to advanced event design:
  • Every event you define introduces a delegate field to your class. If you have a class like Control with lots of events, you will be wasting lots of memory for each of these fields (even if they hold Nothing, they are taking heap memory). There is a solution, but I will defer discussion of this problem until later.
  • The synchronization method used by the built-in compiler is OK for small classes with few subscribers, but there are several problems with this model for classes with several events or several subscribers. I will discuss this problem and its solution in detail first.

Threading Concerns with the Default Event Implementation

If you take a look at the behavior that MethodImlpOptions.Synchronized will cause, you will see that since add_xxx and remove_xxx are instance methods, the method will lock on the instance. What this means is the methods behave as if they were implemented like so:

Public Sub add_SomethingHappened(ByVal value As EventHandler)
    SyncLock Me
        SomethingHappened = CType([Delegate].Combine(SomethingHappened, value), EventHandler)
    End SyncLock
End Sub

This ensures only one thread can access either method at a time, but what happens if there are two events? Both event add/remove methods will lock on the instance, so if two threads try to subscribe to different events at once, one will have to wait. What happens if you have 70 events like Control? Even worse, what if your class is the middle class of a framework, and perhaps hundreds of classes will need to subscribe and unsubscribe to its events? Odds are you will spend a lot of time blocking threads and performance will be severely degraded.

According to Richter, there's a more subtle problem that has to do with a bug in the framework as well. Normally, these locks will be on an instance, so if there are multiple instances of your class you won't see many ill effects. However, if the type is loaded domain neutral, a bug in the CLR causes the lock to be applied to the type rather than the instance. This means that if two instances of your application have an instance of your type each, both instances will block when one's events are being subscribed or unsubscribed. Now imagine your type is a business object that might have hundreds of instances in a server application. Yikes.

There is a solution! In addition to the standard event syntax, the VB language designers provided a more advanced syntax they call "Event Properties". I wanted to discuss them here, but I believe it will make more sense if I discuss the memory problem next, then illustrate the solution to both. It will be clear why this order must be followed later.

Memory Concerns with the Default Event Implementation

As previously stated, the default event implementation declares a private delegate field for every event you declare. If you implement several events, which is common in commercial controls, this can add up to a lot of wasted memory. There's no getting around the limitation that a delegate field must exist, but consider how many events you tend to subscribe to from a particular control; my guess is there are maybe 3 or 4 events from each control you are actually interested in and the rest are unused. Wouldn't it be nice if you could just not declare a delegate field for an event if no classes had subscribed to the event?

The language designers thought so too, and provided Event Properties and a class or two to help. Let's discuss Event Properties now.

Event Properties: The Solution to (almost) Everything

Event properties are the solution to the problems with the default implementation of events. An event property starts off as simply a more complicated-looking version of the event syntax:

Public Custom Event SomethingHappened As EventArgs
    AddHandler(ByVal handler as EventArgs)

    End AddHandler

    RemoveHandler(ByVal handler as EventArgs)

    End RemoveHandler

    RaiseEvent(ByVal sender As Object, ByVal e as EventArgs)

    End RaiseEvent
End Event

Each block is executed when the appropriate keyword is used for this event. Now that you have full control over the event's implementation, you are able to solve each of the problems that we have described.

Example 2 - Multiple events

The classes in Example 2 solve the problem of the allocation of multiple delegate fields per instance. This can be accomplished via the System.ComponentModel.EventHandlerList class, but there's a catch here. The EventHandlerList class uses a linked list as its underlying structure, which means its performance degrades as more items are added. Since the problem we are trying to solve is "a lot of events", its use here is simply to keep the new concepts introduced by the example to a minimum. An appendix project will use a custom Dictionary class that overcomes this hurdle.

Really most of the explanation is in the comments. You may ask if declaring a key for each event as the code does is the same problem as before, but the answer is since they are static (Shared), only one instance of each key is ever created. This will come up in the next example.

AtmaWeapon 09-08-2007 08:25 PM

5 Attachment(s)
Example 3 - Thread Safety

Example 3 is similar to Example 2, but it has been modified to be more thread safe. We lock on the events collection when we want to work with it. This does mean that we could block if many classes are trying to subscribe at once, but since we are locking on an instance object at least we aren't blocking other, unrelated instances.

It may seem like an alternative would be to lock on the event keys, but there are two problems with this. First, EventHandlerList is not a thread-safe class, so if we don't lock on it the results could get unpredictable in a hurry. Second, since the object keys are static, when we lock on them we inadvertently cause the lock on all instances of the class.

Another subtlety here involves how RaiseEvent is implemented. In the default event implementation it's customary to see something like this:

Protected Sub OnSomethingHappened(ByVal e as EventArgs)
    If Not SomethingHappened Is Nothing Then
        RaiseEvent SomethingHappened(Me, e)
    End If

The problem here is subtle and rare, but must be addressed. Remember, in a preemptive multitasking environment like Windows, any process or thread can be interrupted at any point. If our thread is interrupted after the If statement, and before this thread returns to execution something removes all of the event handlers from SomethingHappened, then a NullReferenceException will be thrown! This can be handled via a simple change:

Dim handler as EventHandler = SomethingHappened
If Not handler is Nothing
    RaiseEvent(SomethingHappened(Me, e))
End If

By keeping a private reference to the event delegate, we insure that even if someone manages to alter the delegate in the EventHandlerList then we will still have the old object.

The RaiseEvent sections of the custom events have been modified to reflect this.

Example 4 - Handling exceptions appropriately

This is kind of nasty and I'm not sure I'd suggest using it in all cases, but since I'm covering everything I can think of it's worth mentioning.

What happens if one of your event handlers throws an exception? In the default implementation, the underlying event delegate is a multicast delegate, and when its Invoke method is called each event subscriber is called one by one. If one of these handlers throws an exception, then none of the subscribers that come after that one will execute. This is pretty bad, and there is a solution but I'm not too pleased with it.

Take a look at Example 4, in particular the large comment I left in ExceptionEventClass. If we get the invocation list of the delegate, then we can execute each subscriber method one by one. The only problem with the example implementation is that we eat the exception; if we rethrow it then execution would halt and we'd cause the problem we're trying to solve. I've tried to find a good solution to this problem and it appears a good solution doesn't really exist. One could definitely create a structure with a delegate member and an exception member, then create one of these structures for each exception that fires, and at the end of invocation do something with this list to indicate to the caller that exceptions happened, but there's no clear way to bubble the exception back to the subscriber itself.

I believe the lesson we take away from this is that if your class is going to subscribe to events, it must take care to handle all exceptions that could be thrown itself. Suppose you were subscribing to events from a singleton class that gives information about a database, and several other classes subscribe to this event; if your class doesn't handle its exceptions, you could cause all of the other subscribers to never see the event! I think it should probably be a good rule of thumb to never throw exceptions from an event handler.

Appendix A - An Event Dictionary Class

The appendix project demonstrates a class that uses an internal instance of Dictionary to store and raise events. This will be more efficient than using EventHandlerList because of the faster lookup. There's not much to say about it because it is not a spectacularly difficult thing to implement.

Appendix B - Running the Examples

Since the forum places some pretty strict guidelines on what files can and cannot be posted, I was forced to separate the examples into several zip files. Some of the examples used a couple of common classes, and I've included them with each example zip file. You should be able to run the example by creating a new Windows Forms project, deleting the auto-generated Form1.vb, adding all of the files from the zip file (both folders), then setting your project to use the right startup object. If you have any problems with this or there are missing files, let me know immediately.

shaul_ahuva 09-10-2007 07:06 AM

Good points, AtmaWeapon! I have to say, I'm guilty of not ensuring my events are thread safe :o

I wanted to point out another issue with events and exceptions - remoting clients. If a remoting server raises an event, there is the distinct possibility (read: it will happen) that the client will disconnect without notice due to power failures, hard reboots or throwing the computer out the window. In this case, the only way to safely handle the problem is to enumerate the invocation list - in the past I've simply swallowed the exception since my server doesn't care if/when clients connect/disconnect, but I realize this won't handle all scenarios.

Ultimately, you're right about "normal" event scenarios - you should *always* wrap your code in try/catch blocks if there's the possibility of an exception being thrown.

All times are GMT -6. The time now is 11:15 AM.

Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Search Engine Optimisation provided by DragonByte SEO v2.0.15 (Lite) - vBulletin Mods & Addons Copyright © 2017 DragonByte Technologies Ltd.
All site content is protected by the Digital Millenium Act of 1998. Copyright©2001-2011 MAS Media Inc. and Extreme Visual Basic Forum. All rights reserved.
You may not copy or reproduce any portion of this site without written consent.