How Events Work in VB (and some side info on .NET events in general)

Mike asked me what event properties are, but before I can really talk about them, you need to understand how events work in VB and .NET. Rather than write up a whole new, long entry, I think I’m just going to reprint an email that I sent to the dotnet@discuss.develop.com alias nearly four years ago. (Wow, has it really been that long?) Then tomorrow or so, I’ll talk about what we’re doing new in VB 2005. So, the following is what I wrote, with some minor clarifications and corrections:

Since the method of doing events is different in .NET than it is in COM2, I wrote up an internal memo about how events work and specifically how they work in VB (since VB has a few extra shortcuts that languages like C# don’t). I though I would forward it along if anyone is interested. This presupposes a little knowledge of .NET and VB, but not much. This is also very VB-specific. Details may be different for other languages such as C#, but keep in mind that events in all .NET languages are built on the same foundation and are totally interoperable.

Step 1: Delegates

The foundation of events in the .NET Framework is delegates, which can be thought of as type-safe function pointers. A delegate is defined by a subroutine or function signature. Using the AddressOf operator, you can capture the address of a subroutine or function that matches that signature in a variable whose type is the delegate type. Once you’ve captured the address of a method in a delegate, you can pass the delegate around as you can any object. You can also use a delegate variable as if it was a subroutine or function to invoke the method pointed at by the delegate. For example:

Delegate Function D1(ByVal intvar As Integer) As Integer

Module M1
Function F1(ByVal intvar As Integer) As Integer
F1 = intvar
End Function

Sub Main()
Dim d As D1 = AddressOf F1
Dim intval As Integer = d(10)

Console.WriteLine(intval) ' Will print out "10"
End Sub
End Module

Delegates have two special properties. First, delegates allow you to take the address not only of shared methods but also of instance methods. In the case of an instance function, both the function address and object instance are stored, allowing for correct invocation of virtual functions. The second special property is that delegates are combinable. When you combine two delegates (which must be of the same delegate type), invoking the resulting delegate will call both delegates in order of combination. This is necessary for events to work, as we’ll see below. Combining delegates that have return types is possible, but the return value of the delegate invocation will be the return value of the last function invoked.

Interoperation Note: VB allows syntactic shortcuts when creating delegates. From the .NET perspective, creating a delegate involves invoking a constructor on the delegate class that takes an object instance (possibly Nothing for shared methods) and the address of a method. So in C#, creating a delegate looks something like “new deltype(objvar, method)”. This syntax is usable in VB, except we require that taking the address of a method be done explicitly and don’t require the object instance (inferring it instead from the reference to the method) – i.e. “new deltype(AddressOf objvar.method)”. In most cases, however, you can just say “AddressOf objvar.method” and VB will infer what delegate type to instantiate based on the target type of the expression. In the case where the target type is ambiguous – “Dim o As Object : o = AddressOf method” – a compile-time error will be given. [Ed note: I believe C# is adding this feature in C# 2005, but I could be wrong about that.]

Step 2: Events (.NET style)

Once we have delegates, events become possible. An event raised by a class is defined by three things: a delegate type, an add listener method and a remove listener method. The delegate type defines the signature of the event handler – in other words, the delegates define the arguments that will be passed to a method that gets called when the event occurs. The add and remove listener methods allow other objects to start and stop listening to the event. The add listener method takes a parameter of the event delegate type and combines it with any other delegates that may be listening to the event. The remove listener method takes a parameter of the event delegate type and removes it from the list of delegates listening to the event. When the object wants to fire the event, it just invokes the delegate. For example:

Class EventRaiser
' The event delegate
Public Delegate Sub ClickEventHandler(ByVal x As Integer, ByVal y As Integer)

' The list of objects listening to the event
Private ClickEventHandlers As ClickEventHandler

' The add and remove methods
Public Sub add_Click(ByVal handler As ClickEventHandler)
System.Delegate.Combine(ClickEventHandlers, handler)
End Sub
Public Sub remove_Click(ByVal handler As ClickEventHandler)
System.Delegate.Remove(ClickEventHandlers, handler)
End Sub

' Some method that fires the event
Public Sub OnClick(ByVal x As Integer, ByVal y As Integer)
ClickEventHandlers(x, y)
End Sub
End Class

Class EventListener
Public e As EventRaiser

Private Sub HandleClick(ByVal x As Integer, ByVal y As Integer)
...
End Sub

Sub New()
e = New EventRaiser
' Hook up to the event
e.add_Click(AddressOf Me.HandleClick)
End Sub
End Class

Step 3: Defining Events (VB.NET style)

The above example, although it shows how events work, does not actually define an event in VB. Events are defined by specific metadata and not just a design pattern. To define an event in VB, there are two syntaxes you can use:

Event Click(ByVal x As Integer, ByVal y As Integer)

This event syntax does the most for you. It implicitly defines all of the code you see in EventRaiser except for the method that raises the event.

Event Click As ClickHandlerDelegate

This event syntax allows you to reuse an existing delegate type as the type of the event. This is useful if you have a number of events that all take the same set of parameters. This still defines the field, the add method and the remove method.

VB does not have a syntax for defining events that allows you to specify the field, the add method or the remove method. Those are always implicitly generated. [Ed note: For those of you who are thinking ahead, this is what we’re changing in VB 2005.]

Step 4: Handling Events (VB style)

Although it is legal to call the add handler method when you want to handle an event raised by an object, as in the example above, VB provides a cleaner syntax to do so. The AddHandler and RemoveHandler statements will take care of calling the add and remove methods with the proper values. For example:

Class EventListener
Public e As EventRaiser

Private Sub HandleClick(ByVal x As Integer, ByVal y As Integer)
...
End Sub

Sub New()
e = New EventRaiser
' Hook up to the event.
AddHandler e.Click, AddressOf Me.HandleClick
End Sub
End Class

In other languages, this is the extent of their support for handling events – the developer has to explicitly hook up to and unhook from events. VB provides another way to hook up to events – declarative event handling – that is in many ways more convenient.

Declaratively handling events is a two step process in VB. First, the object that is going to rais
e the events must be stored in a field with the WithEvents modifier, which indicates the field’s availability for declarative event hookup. Then, methods can state that they handle a particular event raised by the field by specifying a Handles clause and giving the field and event handled. For example:

Class EventListener
Public WithEvents e As EventRaiser

Private Sub HandleClick(ByVal x As Integer, ByVal y As Integer) Handles e.Click
...
End Sub

Sub New()
e = New EventRaiser
End Sub
End Class

It is important to step back for a moment and discuss what happens behind the scenes with the above syntax. When a field is declared as a WithEvents field, the field is changed into a property at compile time. This allows the event hookup to be performed whenever the field is assigned to. The Get part of the property just returns the value of the field. The Set property first unhooks any declarative event handlers from the current instance in the field (if any), assigns to the field, and then re-hooks up any declarative event handlers to the new instance. This is what the above code is transformed to under the covers:

Class EventListener
Private _e As EventRaiser

Public Property e As EventRaiser
Get
Return _e
End Get
Set
If Not _e Is Nothing Then
RemoveHandler _e.Click, AddressOf Me.HandleClick
End If
_e = Value
If Not _e Is Nothing Then
AddHandler _e.Click, AddressOf Me.HandleClick
End If
End Set
End Property

Private Sub HandleClick(ByVal x As Integer, ByVal y As Integer)
...
End Sub

Sub New()
e = New EventRaiser
End Sub
End Class

Note that while declarative event hookup can be very convenient, it does have limitations. First, you can’t declaratively handle events raised by objects not stored in a WithEvents field. Second, you can’t declaratively handle shared events, since they are not tied to an instance that can be assigned to a WithEvents field. And third, you can’t control when the event is hooked up to or unhooked from. For all of those cases, you must use dynamic event handling.

22 thoughts on “How Events Work in VB (and some side info on .NET events in general)

  1. MartinJ

    I enjoyed that last bit where you discuss what the compiler does for you behind the scenes.

    The subtle bug that some people introduce by manually adding/removing their own event handlers is that you must detach before losing an object reference. If you attach an event handler to an expensive object, that object will not be GC’d, even if you remove all "known" references to it. The handler basically creates a hidden link that forces the GC to pass over the dead object.

    This is one of the places that you can introduce a memory leak in the managed world. I don’t know if the 2.0 bits have changed this behavior or not. It would probably be too much work (or hurt performance something fierce) to insert code that unhooks any event handlers when an event source is no longer referenced. Heck, I don’t know if a warning could even be generated for when this condition exists.

    My brain hurts from the weird contortions it’s going through to find a way to absolutely know when it is safe to remove those handlers…

    I gotta get some rest.

    -Martin

    Reply
  2. Karl

    WithEvents is definetly a nice addition, but i dislike how VS.Net declares all controls in asp.net with a "WithEvents". I hope 2005 fixes this.

    Anyways…this issue isn’t really related to you.

    Reply
  3. paulvick

    Martin: Yeah, GC is an issue. What would be nice would be to have a "WeakDelegate" class that was like "WeakReference," which would allow us to hold onto events but let them be GCed out from under us. Something to think about for the future…

    Reply
  4. Beth Massi

    Martin: Yes, I’ve been bitten in the a** more than a few times with this one. WeakDelegate is interesting… but I’d just like a way to specify "RemoveAllHandlers" from an object reference when it goes out of scope. That would be awesome.

    -B

    Reply
  5. Pingback: Panopticon Central

  6. Pingback: Anonymous

  7. Pingback: McFunley.com

  8. Pingback: McFunley.com

  9. Pingback: Panopticon Central

  10. Randy

    Great article. It raised a question. Let’s say that your EventListener VB app wires up to and instantiates EventRaiser and then EventRaiser creates a new thread and raises an event on that thread. Does the event handler provided by EventListener execute in the context of the thread created by EventListener? I’m thinking it depends on whether EventListener was created with an STA attribute on the main thread, which we do often due to legacy COM components that we use. I’d appreciate thoughts on this.

    Reply
  11. paulvick

    Randy: The event handler executes in the context of the thread that raised the event, not the thread that created the delegate. I believe there is a way to marshal calls across threads, but that’s beyond my expertise — I’d search MSDN or check out the forums there to see about help if you need it!

    Reply
  12. Andrew

    The Windows Form Designer overrides the Dispose method. Is this a good place to make my RemoveHandler calls?

    I.e.:

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)

    Dim i As Integer

    For i = 0 to 100

    RemoveHandler MyCheckboxes(i).Click, AddressOf MyCheckboxClicked

    Next

    If disposing Then

    If Not (components Is Nothing) Then

    components.Dispose()

    End If

    End If

    MyBase.Dispose(disposing)

    End Sub

    Reply
  13. Golf cart

    Great article. It raised a question. Let’s say that your EventListener VB app wires up to and instantiates EventRaiser and then EventRaiser creates a new thread and raises an event on that thread. Does the event handler provided by EventListener execute in the context of the thread created by EventListener? I’m thinking it depends on whether EventListener was created with an STA attribute on the main thread, which we do often due to legacy COM components that we use. I’d appreciate thoughts on this.

    Reply
  14. keloyun

    WithEvents is definetly a nice addition, but i dislike how VS.Net declares all controls in asp.net with a "WithEvents". I hope 2005 fixes this.

    Anyways…this issue isn’t really related to you. sahin

    Reply
  15. kral oyun

    WeakDelegate is interesting… but I’d just like a way to specify "RemoveAllHandlers" from an object reference when it goes out of scope. That would be awesome. thanks

    Reply
  16. savas oyunlari

    Great article. It raised a question. Let’s say that your EventListener VB app wires up to and instantiates EventRaiser and then EventRaiser creates a new thread and raises an event on that thread. Does the event handler provided by EventListener execute in the context of the thread created by EventListener? I’m thinking it depends on whether EventListener was created with an STA attribute on the main thread, which we do often due to legacy COM components that we use. I’d appreciate thoughts on this.

    Reply

Leave a Reply to paulvick Cancel reply

Your email address will not be published. Required fields are marked *