Events and Handlers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example marked "built and run" is a complete console program that starts with Option Strict On, was built with warnings treated as errors (except where the text says otherwise) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The root namespace of the scratch projects was App. The Windows Forms and WPF examples can only run on Windows and are labelled "built, not run": they were compiled against the .NET 10 net10.0-windows targeting pack and nothing more is claimed.

An event is a notification a type publishes so that other code can react without the publisher knowing who that code is. The publisher declares an event, raises it when something happens, and any number of subscribers handle it. Visual Basic has had first-class events since its first .NET version, and it offers two complete, interchangeable ways to subscribe: the declarative WithEvents + Handles pair (resolved by the compiler when the class is compiled) and the imperative AddHandler/RemoveHandler pair (resolved at run time). Both end in the same place: a multicast delegate stored behind the event. The running example is the shelf library catalogue, where Loan.Return() raises Returned and a form, a log or a statistics counter reacts. Delegates themselves are covered in Delegates and Lambda Expressions, and the .NET event model that underlies this page is covered from the C# side in Delegates, Lambdas and Events.

The model in one picture

An event is a restricted view of a delegate field. Outside the declaring class the only things you can do with an event are subscribe (AddHandler, or Handles on a WithEvents variable) and unsubscribe (RemoveHandler). Only the declaring class can raise it, with RaiseEvent. The following sequence shows what happens when the shelf’s Loan.Return() is called from a Windows Forms form that declared its Loan WithEvents:

sequenceDiagram participant Form as ReturnForm (UI thread) participant Loan as Loan participant Field as ReturnedEvent (multicast delegate) participant Log as Other subscriber Form->>Loan: Return() Loan->>Loan: State = LoanState.Returned Loan->>Field: RaiseEvent Returned(Me, New LoanEventArgs(Me)) Note over Field: takes a snapshot of the invocation list Field->>Form: Loan_Returned(sender, e) [Handles _loan.Returned] Form->>Form: update label, disable button Field->>Log: lambda added with AddHandler Log->>Log: write a log line Field-->>Loan: all handlers finished (or the first exception propagates) Loan-->>Form: Return() completes

Three facts in the diagram drive the rest of the page. Handlers run synchronously, on the thread that called RaiseEvent, before Return() completes. They run in subscription order. And an exception thrown by one handler propagates through RaiseEvent to the publisher’s caller, skipping the handlers that had not run yet.

Declaring an event

[<attributes>] [accessmodifier] [Shared] [Shadows] Event Name(parameterlist) [Implements interfacename.eventname]
[<attributes>] [accessmodifier] [Shared] [Shadows] Event Name As DelegateType [Implements interfacename.eventname]
[<attributes>] [accessmodifier] [Shared] [Shadows] Custom Event Name As DelegateType [Implements ...]

There are two ways to say what the handler looks like, and a third way to take control of the plumbing.

  • Signature form: Event Renewed(sender As Object, newDueOn As Date). The compiler declares a nested delegate type named RenewedEventHandler inside the class. Fewer keystrokes, but the delegate type belongs to your class alone and is not the generic EventHandler(Of T) that the rest of .NET uses, so a handler written for another publisher cannot be reused as is.

  • Delegate-type form: Event Returned As EventHandler(Of LoanEventArgs). The event has the type of an existing delegate. This is the form to use for public events: it follows the .NET design guidelines, and consumers (including C#) see a standard type.

  • Custom form: Custom Event, covered in Custom events.

An event is always a Sub-shaped member. The delegate behind it must not return a value (the compiler reports BC31084), the parameters cannot be Optional or ParamArray (BC33010, BC33009), but they may be ByRef. An event may be declared in a class, structure, module or interface; in a Module it is implicitly Shared. Events can carry the Shared, Shadows and access modifiers, but Overridable is rejected (BC30243): an event is not virtual, so use the OnReturned pattern in the .NET pattern section.

The compiler adds a hidden private field named NameEvent (for example ReturnedEvent) holding the multicast delegate. The field is visible only inside the declaring class (a derived class gets BC30389), which is why code in Loan can write If ReturnedEvent Is Nothing, as a later example does. It is a compiler detail, not a feature to build on: the Custom Event form has no such field (BC30451 if you try).

Built and run: both declaration forms, RaiseEvent, and what the compiler generated
Option Strict On

Imports System.Reflection

Public Enum LoanState
    Active = 1
    Returned = 2
    Overdue = 3
End Enum

Public MustInherit Class CatalogItem
    Public ReadOnly Property Title As String

    Protected Sub New(title As String)
        Me.Title = title
    End Sub
End Class

Public Class Magazine
    Inherits CatalogItem

    Public Sub New(title As String)
        MyBase.New(title)
    End Sub
End Class

Public Class LoanEventArgs
    Inherits EventArgs

    Public ReadOnly Property Loan As Loan

    Public Sub New(loan As Loan)
        Me.Loan = loan
    End Sub
End Class

Public Class Loan
    ' Delegate-type form: any delegate type with a Sub signature.
    Public Event Returned As EventHandler(Of LoanEventArgs)
    ' Signature form: the compiler declares a nested delegate type RenewedEventHandler.
    Public Event Renewed(sender As Object, newDueOn As Date)

    Public ReadOnly Property Item As CatalogItem
    Public ReadOnly Property DueOn As Date
    Public Property State As LoanState = LoanState.Active

    Public Sub New(item As CatalogItem, dueOn As Date)
        Me.Item = item
        Me.DueOn = dueOn
    End Sub

    Public Sub Renew(days As Integer)
        _DueOn = _DueOn.AddDays(days)
        RaiseEvent Renewed(Me, _DueOn)
    End Sub

    Public Sub [Return]()
        If State = LoanState.Returned Then
            Throw New InvalidOperationException("The loan was already returned.")
        End If
        State = LoanState.Returned
        RaiseEvent Returned(Me, New LoanEventArgs(Me))
    End Sub
End Class

Module Program
    Private Sub OnReturned(sender As Object, e As LoanEventArgs)
        Console.WriteLine("named handler: '" & e.Loan.Item.Title & "' is back, state " & e.Loan.State.ToString())
    End Sub

    Sub Main()
        Dim loan As New Loan(New Magazine("Byte"), #2026-11-02#)

        ' No subscriber yet: RaiseEvent does nothing and does not throw.
        loan.Renew(7)
        Console.WriteLine("due after the first renewal: " & loan.DueOn.ToString("yyyy-MM-dd"))

        AddHandler loan.Returned, AddressOf OnReturned
        AddHandler loan.Returned, Sub(sender, e) Console.WriteLine("lambda handler: sender is the loan = " & ReferenceEquals(sender, loan))
        AddHandler loan.Renewed, Sub(sender, newDueOn) Console.WriteLine("renewed until " & newDueOn.ToString("yyyy-MM-dd"))

        loan.Renew(14)
        loan.Return()

        Try
            loan.Return()
        Catch ex As InvalidOperationException
            Console.WriteLine("second Return: " & ex.Message)
        End Try

        ' What the compiler generated.
        Dim returned = GetType(Loan).GetEvent("Returned")
        Dim renewed = GetType(Loan).GetEvent("Renewed")
        Console.WriteLine("Returned delegate type: " & returned.EventHandlerType.Name)
        Console.WriteLine("Renewed  delegate type: " & renewed.EventHandlerType.Name)
        Console.WriteLine("nested types: " & String.Join(", ", GetType(Loan).GetNestedTypes().Select(Function(t) t.Name)))
        Dim field = GetType(Loan).GetField("ReturnedEvent", BindingFlags.Instance Or BindingFlags.NonPublic)
        Console.WriteLine("backing field: " & field.Name & " As " & field.FieldType.Name)
    End Sub
End Module
due after the first renewal: 2026-11-09
renewed until 2026-11-23
named handler: 'Byte' is back, state Returned
lambda handler: sender is the loan = True
second Return: The loan was already returned.
Returned delegate type: EventHandler`1
Renewed  delegate type: RenewedEventHandler
nested types: RenewedEventHandler
backing field: ReturnedEvent As EventHandler`1

Source: Event Statement

What the program shows:

  • RaiseEvent with nobody subscribed does nothing: there is no Nothing check to write. This is the first difference from C#, where Returned?.Invoke(this, e) is needed.

  • The delegate-type event is exposed with the generic EventHandler(Of T) type, the signature-form event with the nested delegate RenewedEventHandler.

  • sender is whatever the publisher passes in RaiseEvent. Passing Me is the convention; for a Shared event pass Nothing.

  • [Return] is in brackets because Return is a reserved word; the brackets make it an identifier (see Program Structure and Code Conventions).

  • The shelf Loan throws InvalidOperationException on a second Return(), so the event fires once per loan.

Raising an event

RaiseEvent Name(arguments) calls every subscribed handler with the arguments, in the order in which they subscribed. The argument list must match the event’s parameters under the usual conversion rules, and RaiseEvent can only be written inside the class (or structure, or module) that declares the event. A derived class cannot raise an inherited event with RaiseEvent (BC30029), and nobody can call an event like a method (BC32022): see Errors and what they mean.

RaiseEvent behaves as if the compiler read the delegate field once into a local variable and invoked that copy. That snapshot is why, unlike a hand-written C# if (Returned != null) Returned(...), a RaiseEvent cannot fail because another thread unsubscribed the last handler between the check and the call. It also means that subscribing or unsubscribing while the event is being raised never changes who is called in the current round:

Built and run: the snapshot taken by RaiseEvent
Option Strict On

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        RaiseEvent Returned(Me, EventArgs.Empty)
    End Sub
End Class

Module Program
    Private ReadOnly ActiveLoan As New Loan()
    Private Calls As Integer

    Private Sub Once(sender As Object, e As EventArgs)
        Calls += 1
        Console.WriteLine("Once: call " & Calls & ", unsubscribing myself")
        RemoveHandler ActiveLoan.Returned, AddressOf Once      ' safe: RaiseEvent works on a snapshot
    End Sub

    Private Sub Late(sender As Object, e As EventArgs)
        Console.WriteLine("Late: I was added while the event was being raised")
    End Sub

    Private Sub Adder(sender As Object, e As EventArgs)
        Console.WriteLine("Adder: subscribing Late")
        AddHandler ActiveLoan.Returned, AddressOf Late
    End Sub

    Sub Main()
        AddHandler ActiveLoan.Returned, AddressOf Once
        AddHandler ActiveLoan.Returned, AddressOf Adder

        Console.WriteLine("first raise")
        ActiveLoan.Return()
        Console.WriteLine("second raise")
        ActiveLoan.Return()
    End Sub
End Module
first raise
Once: call 1, unsubscribing myself
Adder: subscribing Late
second raise
Adder: subscribing Late
Late: I was added while the event was being raised

In the first round Once removes itself and Adder subscribes Late, but Late does not run until the second round. The second round also shows the pitfall of subscribing from inside a handler: Adder adds Late again, so the third round would call Late twice. Subscribe once, outside the handlers.

When a handler throws

RaiseEvent is a plain multicast delegate invocation. There is no isolation between handlers:

Built and run: an exception in the second handler
Option Strict On

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        Console.WriteLine("Return: before RaiseEvent")
        RaiseEvent Returned(Me, EventArgs.Empty)
        Console.WriteLine("Return: after RaiseEvent")
    End Sub

    Public Function SubscriberCount() As Integer
        Return If(ReturnedEvent Is Nothing, 0, ReturnedEvent.GetInvocationList().Length)
    End Function
End Class

Module Program
    Sub Main()
        Dim loan As New Loan()
        Console.WriteLine("subscribers: " & loan.SubscriberCount())

        AddHandler loan.Returned, Sub(s, e) Console.WriteLine("first handler")
        AddHandler loan.Returned, Sub(s, e) Throw New InvalidOperationException("second handler failed")
        AddHandler loan.Returned, Sub(s, e) Console.WriteLine("third handler (never runs)")
        Console.WriteLine("subscribers: " & loan.SubscriberCount())

        Try
            loan.Return()
        Catch ex As InvalidOperationException
            Console.WriteLine("caller of Return caught: " & ex.Message)
        End Try
    End Sub
End Module
subscribers: 0
subscribers: 3
Return: before RaiseEvent
first handler
caller of Return caught: second handler failed

The second handler throws, the third never runs, and the "Return: after RaiseEvent" line is never printed: the exception propagates out of RaiseEvent and out of the publisher’s own method. In a real application this has three consequences.

  • A buggy subscriber can break the publisher. After State = LoanState.Returned has already been assigned, the caller of Return() sees an exception for a loan that is in fact returned. Raise events as the last thing a method does, after the object is in a consistent state, as the shelf Loan does.

  • Subscription order silently becomes a dependency. Do not rely on it.

  • If you need isolation, iterate the invocation list yourself, which requires a Custom Event (the RaiseEvent accessor in Custom events) or ReturnedEvent.GetInvocationList() inside the declaring class, as the example does to count subscribers.

Handling events with WithEvents and Handles

The declarative style is the heart of classic Visual Basic and still the default in the Windows Forms designer. You declare a field (or a module-level variable) with the WithEvents modifier, and mark each handler method with a Handles clause that names the event:

Private WithEvents Name As SomeClass

Private Sub MethodName(parameters) Handles Name.EventName [, Other.EventName ...]

The rules, all enforced by the compiler:

  • WithEvents applies to a field of a class or module (also a Shared field), a type parameter with a class constraint, and an interface-typed variable. It is rejected on arrays (BC30476), on ReadOnly fields (BC30234), on variables of structure and other value types (BC30413), on members of a structure (BC30435), and it cannot be used for a local variable.

  • Handles goes on a Sub or Function declaration and names an event of a WithEvents variable of the same type or a base type (BC30506 otherwise), or an event of Me, MyClass or MyBase, which handles the type’s own or inherited events. Handles cannot name a type: a Shared event is subscribed with AddHandler (BC30506 again). A Handles method may itself be Shared.

  • The method signature must be compatible with the event’s delegate (BC31029), under relaxed delegate conversion: it may take base types of the event’s parameters, may drop all parameters, and if it is a Function its result is discarded (see Relaxed delegate conversion).

  • One method can handle several events, and an event can be handled by several methods. A method name is irrelevant; the Control_Event naming (ReturnButton_Click, the one the designer generates) is just a convention.

  • The compiler turns the WithEvents field into a property. Its setter calls RemoveHandler on the old object and AddHandler on the new one for every Handles clause. That makes WithEvents the one subscription style that cannot leak by forgetting to unsubscribe from the previous object: reassigning the variable (even to Nothing) detaches the old publisher.

Built and run: WithEvents, Handles, MyBase and an inherited WithEvents variable
Option Strict On

Public Class Loan
    Public Event Returned As EventHandler
    Public Event Renewed(sender As Object, newDueOn As Date)

    Public ReadOnly Property Title As String

    Public Sub New(title As String)
        Me.Title = title
    End Sub

    Public Sub [Return]()
        RaiseEvent Returned(Me, EventArgs.Empty)
    End Sub

    Public Sub Renew()
        RaiseEvent Renewed(Me, #2026-12-01#)
    End Sub
End Class

Public Class ReturnDesk
    ' WithEvents: the compiler generates a property whose setter unhooks the old object and hooks the new one.
    Protected WithEvents Current As Loan

    Public Event DeskClosed As EventHandler

    Public Sub Watch(loan As Loan)
        Current = loan
    End Sub

    Public Sub CloseDesk()
        RaiseEvent DeskClosed(Me, EventArgs.Empty)
    End Sub

    Private Sub OnReturned(sender As Object, e As EventArgs) Handles Current.Returned
        Console.WriteLine("desk: '" & DirectCast(sender, Loan).Title & "' returned")
    End Sub

    ' One method for two events; relaxed delegate conversion lets it drop the parameters.
    Private Sub OnAnyChange() Handles Current.Returned, Current.Renewed
        Console.WriteLine("desk: something changed")
    End Sub
End Class

Public Class AuditedDesk
    Inherits ReturnDesk

    Private Sub OnInheritedReturned(sender As Object, e As EventArgs) Handles Current.Returned
        Console.WriteLine("audit: logged a return")
    End Sub

    Private Sub OnClosed(sender As Object, e As EventArgs) Handles MyBase.DeskClosed
        Console.WriteLine("audit: desk closed")
    End Sub
End Class

Module Program
    Sub Main()
        Dim desk As New AuditedDesk()
        Dim first As New Loan("Dune")
        Dim second As New Loan("Emma")

        desk.Watch(first)
        first.Renew()
        first.Return()

        Console.WriteLine("-- switching to the second loan")
        desk.Watch(second)
        first.Return()      ' no longer observed
        second.Return()

        Console.WriteLine("-- unwatching")
        desk.Watch(Nothing)
        second.Return()     ' nobody listens
        desk.CloseDesk()
    End Sub
End Module
desk: something changed
desk: 'Dune' returned
desk: something changed
audit: logged a return
-- switching to the second loan
desk: 'Emma' returned
desk: something changed
audit: logged a return
-- unwatching
audit: desk closed

Source: Handles Clause

Reading the output:

  • desk.Watch(first) assigns the WithEvents variable and hooks up both handlers declared in ReturnDesk and the one declared in AuditedDesk (it can, because Current is Protected; a Private WithEvents variable is invisible to derived classes).

  • OnAnyChange() Handles Current.Returned, Current.Renewed takes no parameters at all and is still accepted under Option Strict On because Handles applies relaxed delegate conversion. The same drop-everything trick is not accepted for AddressOf under Option Strict On (see Relaxed delegate conversion).

  • After desk.Watch(second) the first loan’s Returned is silent: the setter removed the handlers.

  • Handles MyBase.DeskClosed handles an event declared by the base class on the same object. Handles Me.DeskClosed or Handles MyClass.DeskClosed work for the type’s own events.

  • Inheritance and Handles interact in non-obvious ways when a handler is overridden or a WithEvents variable is shadowed; the Learn page on troubleshooting inherited event handlers lists the cases.

Because the connections are made by the compiler, Handles is easy to read and hard to get wrong. It also has limits: the publisher must be a field (not a local variable or a collection element), and the subscription is fixed in the source. A subscription that depends on run-time data, such as "the loans on this screen", needs AddHandler.

Handling events with AddHandler and RemoveHandler

AddHandler    event_expression, delegate_expression
RemoveHandler event_expression, delegate_expression

The first argument is an event of an object (loan.Returned) or of a type (Desk.DeskOpened for a Shared event); the second is a delegate: AddressOf Method, a lambda, or a delegate variable. The statement can appear anywhere a statement can appear, including inside loops, constructors and other handlers.

Built and run: order, duplicates, removing and the kept lambda
Option Strict On

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        RaiseEvent Returned(Me, EventArgs.Empty)
    End Sub
End Class

Module Program
    Private Sub Alpha(sender As Object, e As EventArgs)
        Console.WriteLine("Alpha")
    End Sub

    Private Sub Beta(sender As Object, e As EventArgs)
        Console.WriteLine("Beta")
    End Sub

    Sub Main()
        Dim loan As New Loan()

        ' Handlers run in the order they were added; the same method added twice runs twice.
        AddHandler loan.Returned, AddressOf Alpha
        AddHandler loan.Returned, AddressOf Beta
        AddHandler loan.Returned, AddressOf Alpha
        Console.WriteLine("-- three subscriptions")
        loan.Return()

        ' RemoveHandler with AddressOf removes the LAST matching subscription.
        RemoveHandler loan.Returned, AddressOf Alpha
        Console.WriteLine("-- after one RemoveHandler of Alpha")
        loan.Return()

        ' To unsubscribe a lambda, keep it in a variable.
        Dim keep As EventHandler = Sub(sender, e) Console.WriteLine("kept lambda")
        AddHandler loan.Returned, keep
        RemoveHandler loan.Returned, keep
        Console.WriteLine("-- after adding and removing the kept lambda")
        loan.Return()

        ' Removing something that was never added is silently ignored.
        RemoveHandler loan.Returned, AddressOf Beta
        RemoveHandler loan.Returned, AddressOf Beta
        RemoveHandler loan.Returned, AddressOf Beta
        Console.WriteLine("-- after removing Beta more often than it was added")
        loan.Return()
    End Sub
End Module
-- three subscriptions
Alpha
Beta
Alpha
-- after one RemoveHandler of Alpha
Alpha
Beta
-- after adding and removing the kept lambda
Alpha
Beta
-- after removing Beta more often than it was added
Alpha

The rules this program demonstrates:

  • Subscribing the same method twice means it runs twice. There is no de-duplication.

  • RemoveHandler with AddressOf removes the last matching subscription, compared by target object and method. Removing a handler that is not subscribed is silently ignored, no matter how often you repeat it, which hides unsubscribe bugs.

  • The only reliable way to remove a lambda handler is to store it in a variable (Dim keep As EventHandler = Sub(...)) and use the variable in both statements.

The lambda that cannot be unsubscribed

Writing the same lambda text twice produces two different delegate objects. Visual Basic knows this and reports BC42326 as a warning. The next program is built without treating warnings as errors, because the warning is the point:

Built and run (warnings not treated as errors): BC42326
Option Strict On

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        RaiseEvent Returned(Me, EventArgs.Empty)
    End Sub
End Class

Module Program
    Sub Main()
        Dim loan As New Loan()

        AddHandler loan.Returned, Sub(sender, e) Console.WriteLine("lambda handler ran")
        ' BC42326: this lambda is a different delegate, so nothing is removed.
        RemoveHandler loan.Returned, Sub(sender, e) Console.WriteLine("lambda handler ran")

        loan.Return()
    End Sub
End Module
Program.vb(17,38): warning BC42326: Lambda expression will not be removed from this event handler. Assign the lambda expression to a variable and use the variable to add and remove the event.
lambda handler ran

The handler is still subscribed and still runs. In a project that treats warnings as errors (the setting the shelf uses) this is a build break, which is the better outcome. The same applies to a lambda that captures Me or a local: it is a new delegate every time the line runs.

Relaxed delegate conversion

A handler need not repeat the event’s parameter list. Under relaxed delegate conversion the compiler accepts a method or lambda whose parameters are base types of the delegate’s (contravariance), whose body ignores the result of a Function, or whose parameter list is empty. Option Strict On keeps the first two and has a surprise for the third:

Built and run: relaxed conversions, an OnReturned override, cancellation and a Shared event
Option Strict On

Imports System.ComponentModel

Public Class LoanEventArgs
    Inherits EventArgs

    Public ReadOnly Property Title As String

    Public Sub New(title As String)
        Me.Title = title
    End Sub
End Class

Public Class Desk
    Public Event Returned As EventHandler(Of LoanEventArgs)
    Public Event Closing As CancelEventHandler
    Public Shared Event DeskOpened As EventHandler

    ' The .NET pattern: a Protected Overridable OnXxx method raises the event, so derived classes can
    ' react first and can raise it too (a derived class cannot use RaiseEvent on a base-class event).
    Protected Overridable Sub OnReturned(e As LoanEventArgs)
        RaiseEvent Returned(Me, e)
    End Sub

    Public Sub TakeBack(title As String)
        OnReturned(New LoanEventArgs(title))
    End Sub

    Public Function TryClose() As Boolean
        Dim e As New CancelEventArgs()
        RaiseEvent Closing(Me, e)
        Return Not e.Cancel
    End Function

    Public Shared Sub Open()
        RaiseEvent DeskOpened(Nothing, EventArgs.Empty)
    End Sub
End Class

Public Class LoudDesk
    Inherits Desk

    Protected Overrides Sub OnReturned(e As LoanEventArgs)
        Console.WriteLine("LoudDesk: about to announce '" & e.Title & "'")
        MyBase.OnReturned(e)
    End Sub
End Class

Module Program
    Private Sub BaseTyped(sender As Object, e As EventArgs)
        Console.WriteLine("  contravariant: handler takes EventArgs, event passes LoanEventArgs")
    End Sub

    Private Function ReturnsSomething(sender As Object, e As LoanEventArgs) As Integer
        Console.WriteLine("  relaxed: a Function whose result is discarded")
        Return 42
    End Function

    Sub Main()
        Dim frontDesk As New LoudDesk()   ' not "desk": VB is case-insensitive, so a local named desk would hide the type Desk
        AddHandler frontDesk.Returned, Sub() Console.WriteLine("  relaxed lambda: no parameters at all")
        AddHandler frontDesk.Returned, AddressOf BaseTyped
        AddHandler frontDesk.Returned, AddressOf ReturnsSomething
        AddHandler frontDesk.Returned, Sub(sender, e) Console.WriteLine("  lambda: " & e.Title)
        frontDesk.TakeBack("Dune")

        ' A cancellable event: any handler can veto.
        AddHandler frontDesk.Closing, Sub(sender, e) e.Cancel = True
        Console.WriteLine("closed? " & frontDesk.TryClose())

        ' A Shared event is subscribed through the type name.
        AddHandler Desk.DeskOpened, Sub(sender, e) Console.WriteLine("a desk opened; sender is Nothing: " & (sender Is Nothing))
        Desk.Open()
    End Sub
End Module
LoudDesk: about to announce 'Dune'
  relaxed lambda: no parameters at all
  contravariant: handler takes EventArgs, event passes LoanEventArgs
  relaxed: a Function whose result is discarded
  lambda: Dune
closed? False
a desk opened; sender is Nothing: True

The program shows that a lambda Sub() may omit all parameters, a method taking e As EventArgs accepts the more derived LoanEventArgs, and a Function handler is accepted with its return value discarded. It also contains a lesson in case-insensitivity: the local is called frontDesk because a local called desk would silently hide the type Desk in the same scope (Visual Basic is case-insensitive), and Desk.DeskOpened would then be compiled as a call on the local variable. The compiler reports BC42025 for that mistake.

What Option Strict On rejects is a method group with fewer parameters:

Built (compile error expected): AddressOf cannot drop the parameters
Option Strict On

Public Class Loan
    Public Event Returned As EventHandler
End Class

Module Program
    Private Sub Parameterless()
    End Sub

    Sub Main()
        Dim loan As New Loan()
        AddHandler loan.Returned, AddressOf Parameterless          ' error under Option Strict On
        AddHandler loan.Returned, Sub() Console.WriteLine("ok")    ' a lambda may drop all parameters
    End Sub
End Module
Program.vb(13,45): error BC36663: Option Strict On does not allow narrowing in implicit type conversions between method 'Private Sub Parameterless()' and delegate 'Delegate Sub EventHandler(sender As Object, e As EventArgs)'.

The error message says "narrowing", which is misleading: it is the rule that a method group (AddressOf) with a shorter parameter list is only convertible when Option Strict is off. Use a lambda, or give the method the event’s parameters (sender As Object, e As EventArgs), or declare it with Handles where the dropped-parameter form is accepted.

The .NET pattern: sender, e and OnXxx

The convention that every .NET framework follows, and that your public events should follow as well:

Element Convention

Delegate type

EventHandler when there is no data, EventHandler(Of TEventArgs) otherwise. On .NET 10 TEventArgs has no constraint (Event Returned As EventHandler(Of String) compiles, as the interface example below shows), but deriving from EventArgs remains the convention, and the one Windows Forms and WPF use.

First parameter

sender As Object: the object that raises the event. Nothing for Shared events.

Second parameter

e As SomethingEventArgs, or EventArgs.Empty when there is nothing to send. Data goes in read-only properties. For cancellable events use CancelEventArgs (and CancelEventHandler) and read e.Cancel after RaiseEvent.

Raising method

Protected Overridable Sub OnReturned(e As LoanEventArgs) raises the event. Derived classes cannot use RaiseEvent for an inherited event, so this is the only way for them to raise it or to run code before and after.

Name

A verb: Returned (after) and Returning (before), Closing and Closed.

The program of the previous section already contains all of this, and its output was shown there. LoudDesk overrides OnReturned, runs its own code, and calls MyBase.OnReturned(e), which is the only place where RaiseEvent Returned is written. Closing is cancellable: the handler sets e.Cancel = True and TryClose() returns False. DeskOpened is a Shared event: subscribers use AddHandler Desk.DeskOpened, ... through the type name, and the publisher passes Nothing as the sender. A Shared event is a global subscription point and every subscriber to it lives as long as the process unless it unsubscribes: see Event leaks and unsubscribing.

Events on interfaces and INotifyPropertyChanged

An interface can declare an event, and an implementer uses Implements on its own event declaration. The implementing event can have a different name:

Built and run: Event ... Implements with a renamed event and INotifyPropertyChanged
Option Strict On

Imports System.ComponentModel

Public Interface IReturnSource
    Event Returned As EventHandler
End Interface

Public Class Loan
    Implements IReturnSource

    ' The name of the implementing event need not match the interface's.
    Public Event Done As EventHandler Implements IReturnSource.Returned

    Public Sub [Return]()
        RaiseEvent Done(Me, EventArgs.Empty)
    End Sub
End Class

Public Class Book
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private _title As String = ""

    Public Property Title As String
        Get
            Return _title
        End Get
        Set(value As String)
            If _title = value Then Return       ' no change, no event
            _title = value
            OnPropertyChanged(NameOf(Title))
        End Set
    End Property

    Protected Overridable Sub OnPropertyChanged(propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

Module Program
    Sub Main()
        ' Through the interface, the event is called Returned ...
        Dim source As IReturnSource = New Loan()
        AddHandler source.Returned, Sub(s, e) Console.WriteLine("interface subscriber: " & s.GetType().Name & " returned")
        ' ... through the class it is called Done, and both reach the same subscribers.
        Dim loan = DirectCast(source, Loan)
        AddHandler loan.Done, Sub(s, e) Console.WriteLine("class subscriber: Done fired")
        loan.Return()

        Dim book As New Book()
        AddHandler book.PropertyChanged, Sub(s, e) Console.WriteLine("changed: " & e.PropertyName & " = " & DirectCast(s, Book).Title)
        book.Title = "Dune"
        book.Title = "Dune"     ' unchanged: silent
        book.Title = "Emma"
    End Sub
End Module
interface subscriber: Loan returned
class subscriber: Done fired
changed: Title = Dune
changed: Title = Emma

INotifyPropertyChanged is the event interface behind data binding in Windows Forms and WPF: the property setter raises PropertyChanged with the property name (NameOf(Title) keeps the string refactor-safe), only when the value actually changed. The matching Protected Overridable Sub OnPropertyChanged is the pattern from the table. Interfaces are covered in Interfaces; properties in Properties and Default Properties.

A WithEvents variable may be typed as an interface, in which case Handles can name any event the interface declares:

Built and run: WithEvents over an interface and EventHandler(Of String)
Option Strict On

Public Interface IReturnSource
    Event Returned As EventHandler(Of String)
    Sub Fire()
End Interface

Public Class Source
    Implements IReturnSource

    Public Event Returned As EventHandler(Of String) Implements IReturnSource.Returned

    Public Sub Fire() Implements IReturnSource.Fire
        RaiseEvent Returned(Me, "Dune")
    End Sub
End Class

Public Class Watcher
    Private WithEvents _source As IReturnSource

    Public Sub New(source As IReturnSource)
        _source = source
    End Sub

    Private Sub OnReturned(sender As Object, title As String) Handles _source.Returned
        Console.WriteLine("returned: " & title)
    End Sub
End Class

Module Program
    Sub Main()
        Dim s As New Source()
        Dim w As New Watcher(s)
        s.Fire()
        GC.KeepAlive(w)
    End Sub
End Module
returned: Dune

Source: WithEvents

Custom events

A Custom Event replaces the compiler’s delegate field with three accessors that you write. It must give a delegate type (Custom Event Added As EventHandler(Of ItemEventArgs); BC31122 if you try the signature form), and it must define all of AddHandler, RemoveHandler and RaiseEvent (BC31132 for a missing one):

Public Custom Event Name As DelegateType
    AddHandler(value As DelegateType)
    End AddHandler

    RemoveHandler(value As DelegateType)
    End RemoveHandler

    RaiseEvent(parameters matching the delegate)
    End RaiseEvent
End Event

The accessors run when somebody writes AddHandler obj.Name, ... (or a Handles hooks it), RemoveHandler obj.Name, ..., and when the class writes RaiseEvent Name(...). Typical reasons for writing them:

  • Memory: a class with dozens of events, such as a control, would pay one delegate field for each. Storing handlers in an EventHandlerList keyed by a Shared object means only subscribed events cost anything. This is how the Windows Forms Component class does it.

  • Thread safety of your own storage: if the accessors keep the handlers in your own collection, they must lock around it, as the example does.

  • Isolation or ordering: iterate the handlers yourself and decide what to do with a failure.

  • Forwarding: the custom event exposes another object’s event as its own (AddHandler(value) calls AddHandler _inner.Changed, value).

Built and run: two custom events, a list-backed one with exception isolation and an EventHandlerList-backed one
Option Strict On

Imports System.ComponentModel

Public Class ItemEventArgs
    Inherits EventArgs

    Public ReadOnly Property Title As String

    Public Sub New(title As String)
        Me.Title = title
    End Sub
End Class

Public Class Catalog
    Private Shared ReadOnly RemovedKey As New Object()

    Private ReadOnly _addedHandlers As New List(Of EventHandler(Of ItemEventArgs))
    Private ReadOnly _events As New EventHandlerList()

    ' Custom event 1: the accessors store the handlers in a list we control.
    Public Custom Event Added As EventHandler(Of ItemEventArgs)
        AddHandler(value As EventHandler(Of ItemEventArgs))
            SyncLock _addedHandlers
                _addedHandlers.Add(value)
            End SyncLock
            Console.WriteLine("  Added: subscriber count is now " & _addedHandlers.Count)
        End AddHandler

        RemoveHandler(value As EventHandler(Of ItemEventArgs))
            SyncLock _addedHandlers
                _addedHandlers.Remove(value)
            End SyncLock
            Console.WriteLine("  Added: subscriber count is now " & _addedHandlers.Count)
        End RemoveHandler

        ' RaiseEvent accessor: one failing handler does not stop the others.
        RaiseEvent(sender As Object, e As ItemEventArgs)
            Dim snapshot As EventHandler(Of ItemEventArgs)()
            SyncLock _addedHandlers
                snapshot = _addedHandlers.ToArray()
            End SyncLock
            For Each handler In snapshot
                Try
                    handler(sender, e)
                Catch ex As Exception
                    Console.WriteLine("  handler failed: " & ex.Message)
                End Try
            Next
        End RaiseEvent
    End Event

    ' Custom event 2: handlers live in an EventHandlerList, one slot per key.
    ' A class with many rarely used events pays only for the events somebody subscribed to.
    Public Custom Event Removed As EventHandler(Of ItemEventArgs)
        AddHandler(value As EventHandler(Of ItemEventArgs))
            _events.AddHandler(RemovedKey, value)
        End AddHandler

        RemoveHandler(value As EventHandler(Of ItemEventArgs))
            _events.RemoveHandler(RemovedKey, value)
        End RemoveHandler

        RaiseEvent(sender As Object, e As ItemEventArgs)
            Dim handler = TryCast(_events(RemovedKey), EventHandler(Of ItemEventArgs))
            If handler IsNot Nothing Then
                handler(sender, e)
            End If
        End RaiseEvent
    End Event

    Public Sub Add(title As String)
        RaiseEvent Added(Me, New ItemEventArgs(title))
    End Sub

    Public Sub Remove(title As String)
        RaiseEvent Removed(Me, New ItemEventArgs(title))
    End Sub
End Class

Module Program
    Private Sub Good(sender As Object, e As ItemEventArgs)
        Console.WriteLine("  good handler saw '" & e.Title & "'")
    End Sub

    Private Sub Bad(sender As Object, e As ItemEventArgs)
        Throw New InvalidOperationException("bad handler for '" & e.Title & "'")
    End Sub

    Sub Main()
        Dim catalog As New Catalog()

        Console.WriteLine("subscribing")
        AddHandler catalog.Added, AddressOf Bad
        AddHandler catalog.Added, AddressOf Good
        AddHandler catalog.Removed, AddressOf Good

        Console.WriteLine("raising Added")
        catalog.Add("Dune")
        Console.WriteLine("raising Removed")
        catalog.Remove("Dune")

        Console.WriteLine("unsubscribing")
        RemoveHandler catalog.Added, AddressOf Bad
        catalog.Add("Emma")
    End Sub
End Module
subscribing
  Added: subscriber count is now 1
  Added: subscriber count is now 2
raising Added
  handler failed: bad handler for 'Dune'
  good handler saw 'Dune'
raising Removed
  good handler saw 'Dune'
unsubscribing
  Added: subscriber count is now 1
  good handler saw 'Emma'

Notes on the example:

  • The RaiseEvent accessor receives exactly the delegate’s parameters. It is called when the class writes RaiseEvent Added(...).

  • The raise accessor copies the list inside the lock and invokes outside it, so a handler that subscribes or unsubscribes does not deadlock or throw InvalidOperationException (the collection changed). Invoking handlers while holding a lock is a classic deadlock source.

  • There is no AddedEvent field for a Custom Event: BC30451 if you try to use it.

  • EventHandlerList is a plain dictionary from key to delegate and is not thread-safe by itself.

Event leaks and unsubscribing

A subscription is a strong reference from the publisher to the subscriber: the delegate in the publisher’s invocation list holds the handler’s target object (printer in AddressOf printer.OnReturned, Me for a lambda that uses an instance member). If the publisher lives longer than the subscriber, the subscriber cannot be collected until it unsubscribes. The typical victims are forms and view models subscribing to a long-lived singleton (a Shared event, an application-wide service or a domain object), and per-screen objects subscribing in a constructor that is run many times.

Built and run: a subscriber that stays alive because the publisher refers to it
Option Strict On

Imports System.Runtime.CompilerServices

' A long-lived publisher: it lives as long as the application.
Public Class ReturnBus
    Public Event Returned As EventHandler
    Public Sub Publish()
        RaiseEvent Returned(Me, EventArgs.Empty)
    End Sub
End Class

' A short-lived subscriber: it should die when the screen that created it closes.
Public Class ReturnPrinter
    Public Sub OnReturned(sender As Object, e As EventArgs)
        Console.WriteLine("printing a return slip")
    End Sub
End Class

Module Program
    ' NoInlining keeps the strong references out of Main's frame, so the collector can really free them.
    <MethodImpl(MethodImplOptions.NoInlining)>
    Private Function CreateSubscriber(bus As ReturnBus, unsubscribe As Boolean) As WeakReference
        Dim printer As New ReturnPrinter()
        AddHandler bus.Returned, AddressOf printer.OnReturned
        If unsubscribe Then
            RemoveHandler bus.Returned, AddressOf printer.OnReturned
        End If
        Return New WeakReference(printer)
    End Function

    Private Function IsCollected(reference As WeakReference) As Boolean
        GC.Collect()
        GC.WaitForPendingFinalizers()
        GC.Collect()
        Return Not reference.IsAlive
    End Function

    Sub Main()
        Dim bus As New ReturnBus()

        Dim leaked = CreateSubscriber(bus, unsubscribe:=False)
        Console.WriteLine("subscriber that never unsubscribed collected: " & IsCollected(leaked))
        bus.Publish()   ' the "dead" printer still prints: the bus kept it alive

        Dim clean = CreateSubscriber(bus, unsubscribe:=True)
        Console.WriteLine("subscriber that unsubscribed collected:      " & IsCollected(clean))

        GC.KeepAlive(bus)
    End Sub
End Module
subscriber that never unsubscribed collected: False
printing a return slip
subscriber that unsubscribed collected:      True

The first printer was unreachable from the application, yet the collector could not free it, and it still printed a slip when the next event was published: a leak with a visible symptom. The second printer unsubscribed and was collected. The <MethodImpl(MethodImplOptions.NoInlining)> method is there so that no local variable of Main keeps the printer alive in a debug build; without it the test result would depend on the JIT.

Rules that follow:

  • Pair every AddHandler with a RemoveHandler in a symmetrical place: Load/Closed, constructor/Dispose, Start/Stop. If the subscriber implements IDisposable, unsubscribe in Dispose (see Object Lifetime and Disposal).

  • Prefer WithEvents + Handles where the publisher is a field: reassigning or clearing the variable unsubscribes for you. A form that closes still has to set long-lived WithEvents fields to Nothing if the publisher outlives it, because the publisher’s list holds the form, not the other way round.

  • A form does not need to unsubscribe from its own controls: they die together.

  • Never subscribe with a lambda you will need to remove (BC42326), and do not subscribe from inside a handler.

  • Static (Shared) events and events of singletons are the highest-risk case: always unsubscribe.

  • A finalizer or GC.Collect() is not a remedy: the object is still reachable.

  • A weak event (the publisher refers to the subscriber through a WeakReference) breaks the chain. WPF ships the pattern as WeakEventManager and the IWeakEventListener interface; the base class library has no equivalent for non-WPF code, so write one with a Custom Event that stores WeakReference objects, or use the WPF types in a WPF app.

  • To find a leak, take a heap snapshot with a memory profiler or dotnet-gcdump and look at the retention path of the leaked object; it ends in a delegate in a ReturnedEvent field or in an EventHandlerList (see Debugging and Diagnostics).

Async Sub handlers

Event handlers are the only legitimate use of Async Sub in Visual Basic. An event’s delegate returns nothing, and so does an Async Sub, so it is the form that fits; everywhere else use Async Function ... As Task. The compiler will let you write AddHandler loan.Returned, AddressOf OnReturnedAsync for an Async Sub, and Handles works too.

Built and run: the publisher does not wait for an Async Sub
Option Strict On

Imports System.Threading.Tasks

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        Console.WriteLine("Return: raising")
        RaiseEvent Returned(Me, EventArgs.Empty)
        Console.WriteLine("Return: RaiseEvent has returned")
    End Sub
End Class

Module Program
    Private Async Sub OnReturnedAsync(sender As Object, e As EventArgs)
        Console.WriteLine("handler: started on thread " & If(Environment.CurrentManagedThreadId = 1, "main", "pool"))
        Await Task.Delay(200)
        Console.WriteLine("handler: finished after the await")
    End Sub

    Private Async Sub OnReturnedSafe(sender As Object, e As EventArgs)
        Try
            Await Task.Delay(50)
            Throw New InvalidOperationException("the e-mail server is down")
        Catch ex As InvalidOperationException
            ' An Async Sub has no Task for the caller to observe: handle the failure here.
            Console.WriteLine("handler: handled '" & ex.Message & "' itself")
        End Try
    End Sub

    Sub Main()
        Dim loan As New Loan()
        AddHandler loan.Returned, AddressOf OnReturnedAsync
        AddHandler loan.Returned, AddressOf OnReturnedSafe

        loan.Return()
        Console.WriteLine("Main: Return() is done, but the handlers are still running")
        Threading.Thread.Sleep(500)
        Console.WriteLine("Main: exiting")
    End Sub
End Module
Return: raising
handler: started on thread main
Return: RaiseEvent has returned
Main: Return() is done, but the handlers are still running
handler: handled 'the e-mail server is down' itself
handler: finished after the await
Main: exiting

Source: Async

RaiseEvent returned as soon as the handler reached its first Await, which is before the handler finished. The consequences:

  • The publisher cannot wait for the handler to complete, and cannot get its result.

  • An exception thrown after the first Await has nowhere to go but the SynchronizationContext that was current when the handler started: in a console program that is the thread pool, and an unhandled exception there ends the process. In a Windows Forms or WPF program it is posted back to the UI thread and reaches that framework’s unhandled-exception handling.

  • Therefore: wrap the body of an Async Sub handler in Try ... Catch, as OnReturnedSafe does.

Built and run: what happens when the exception is not caught
Option Strict On

Imports System.Threading.Tasks

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        RaiseEvent Returned(Me, EventArgs.Empty)
        Console.WriteLine("Return: returned before the handler finished")
    End Sub
End Class

Module Program
    Private Async Sub OnReturnedAsync(sender As Object, e As EventArgs)
        Await Task.Delay(50)
        Throw New InvalidOperationException("failure after the await")
    End Sub

    Sub Main()
        AddHandler AppDomain.CurrentDomain.UnhandledException,
            Sub(s, args) Console.WriteLine("unhandled exception event: " & DirectCast(args.ExceptionObject, Exception).Message &
                                           " (terminating: " & args.IsTerminating & ")")

        Dim loan As New Loan()
        AddHandler loan.Returned, AddressOf OnReturnedAsync

        Try
            loan.Return()
            Console.WriteLine("Main: Return() completed normally")
            Threading.Thread.Sleep(1000)
            Console.WriteLine("Main: this line is never reached")
        Catch ex As InvalidOperationException
            Console.WriteLine("caught in Main: " & ex.Message)
        End Try
    End Sub
End Module
Return: returned before the handler finished
Main: Return() completed normally
unhandled exception event: failure after the await (terminating: True)
Unhandled exception. System.InvalidOperationException: failure after the await

Only the first four output lines are shown; the rest is the stack trace of the exception. The process then terminates (exit code 134 on Linux, with the UnhandledException event reporting IsTerminating = True). The Try ... Catch in Main never saw the exception; it could not, because Return() had finished long before. See Async and Await for the full rules.

If the publisher needs to wait for asynchronous subscribers, a plain event is the wrong tool (a delegate type that returns a Task is illegal for an Event, BC31084). Expose a method that returns a Task, or define your own interface with a Function OnReturnedAsync(...) As Task that subscribers implement and that the publisher awaits in a loop.

BeginInvoke does not exist on .NET

Older material (including the Learn topic about declaring custom events to avoid blocking) calls each handler with handler.BeginInvoke(...) to run them without blocking the publisher. Asynchronous delegate calls are not supported on .NET (Core) and later:

Built and run: BeginInvoke on a delegate and the replacement
Option Strict On

Imports System.Threading
Imports System.Threading.Tasks

Public Class Loan
    Public Event Returned As EventHandler

    Public Sub [Return]()
        RaiseEvent Returned(Me, EventArgs.Empty)
    End Sub

    ' The Framework-era way to "call handlers asynchronously": BeginInvoke on every delegate in the list.
    Public Sub ReturnWithBeginInvoke()
        Dim handlers = ReturnedEvent
        If handlers Is Nothing Then Return
        For Each handler In handlers.GetInvocationList()
            Dim h = DirectCast(handler, EventHandler)
            h.BeginInvoke(Me, EventArgs.Empty, Nothing, Nothing)
        Next
    End Sub

    ' The modern way: run each handler on the thread pool and wait for all of them.
    Public Async Function ReturnConcurrentlyAsync() As Task
        Dim handlers = ReturnedEvent
        If handlers Is Nothing Then Return
        Dim tasks = handlers.GetInvocationList().Select(
            Function(handler) Task.Run(Sub() DirectCast(handler, EventHandler)(Me, EventArgs.Empty)))
        Await Task.WhenAll(tasks)
    End Function
End Class

Module Program
    Sub Main()
        Dim loan As New Loan()
        AddHandler loan.Returned, Sub(s, e) Console.WriteLine("handler ran on a pool thread: " & Thread.CurrentThread.IsThreadPoolThread)

        Try
            loan.ReturnWithBeginInvoke()
        Catch ex As PlatformNotSupportedException
            Console.WriteLine("BeginInvoke: " & ex.GetType().Name & " - " & ex.Message)
        End Try

        loan.ReturnConcurrentlyAsync().GetAwaiter().GetResult()
    End Sub
End Module
BeginInvoke: PlatformNotSupportedException - Operation is not supported on this platform.
handler ran on a pool thread: True

Source: Task.WhenAll

Use Task.Run (as above) or, in UI applications, the Dispatcher or Control.BeginInvoke of the control type, which is a different API with the same name.

Threads and user-interface events

An event is raised on the thread that calls RaiseEvent. A handler that touches a Windows Forms control or a WPF element must run on the thread that owns it. If the domain object raises its event from a worker thread, the handler has to hop:

  • Windows Forms: test InvokeRequired, then BeginInvoke (post) or Invoke (send) a delegate, or capture the SynchronizationContext. Code that uses Async/Await on the UI thread usually does not need this.

  • WPF: Dispatcher.Invoke or Dispatcher.BeginInvoke, or Await in an Async Sub handler running on the UI thread.

  • A deadlock appears when a handler on the UI thread blocks on the worker (Task.Wait) while the worker waits for the UI thread (Invoke): prefer BeginInvoke.

The pitfall in a modern Windows Forms app is the old workaround Control.CheckForIllegalCrossThreadCalls = False; do not use it.

Built, not run (Windows Forms): WithEvents, Handles, MyBase.Load, a timer, cross-thread marshalling
Option Strict On

Imports Shelf.Core

Public Class ReturnForm
    Inherits Form

    ' Control fields declared WithEvents: this is what the Windows Forms designer generates for you.
    Private WithEvents ReturnButton As New Button With {.Text = "Return the loan", .Dock = DockStyle.Top}
    Private WithEvents Clock As New Timer With {.Interval = 1000}
    Private ReadOnly _status As New Label With {.Dock = DockStyle.Fill, .Text = "Waiting"}

    ' A domain object can be WithEvents too.
    Private WithEvents _loan As New Loan(New Magazine("Byte", 1975, 1), "Ada", Date.Today.AddDays(14))

    Public Sub New()
        Text = "Shelf - return a loan"
        Controls.Add(_status)
        Controls.Add(ReturnButton)
    End Sub

    ' Handles MyBase.Load (or Me.Load) is how a form reacts to its own events.
    Private Sub ReturnForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Clock.Start()
    End Sub

    Private Sub ReturnButton_Click(sender As Object, e As EventArgs) Handles ReturnButton.Click
        ' The domain event may be raised on another thread; see Loan_Returned.
        Task.Run(Sub() _loan.Return())
    End Sub

    Private Sub Loan_Returned(sender As Object, e As LoanEventArgs) Handles _loan.Returned
        If InvokeRequired Then
            ' Controls may only be touched on the UI thread: marshal the call and leave.
            BeginInvoke(Sub() Loan_Returned(sender, e))
            Return
        End If
        _status.Text = e.Loan.Item.Title & " was returned"
        ReturnButton.Enabled = False
        Clock.Stop()
    End Sub

    Private Sub Clock_Tick(sender As Object, e As EventArgs) Handles Clock.Tick
        Text = "Shelf - " & Date.Now.ToString("HH:mm:ss")
    End Sub

    ' One handler for several events (or several controls) is a comma-separated list.
    Private Sub AnyInteraction(sender As Object, e As EventArgs) Handles ReturnButton.MouseEnter, ReturnButton.GotFocus
        _status.Text = "Ready to return"
    End Sub

    Private Sub ReturnForm_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
        If e.CloseReason = CloseReason.UserClosing AndAlso _loan.State = LoanState.Active Then
            e.Cancel = MessageBox.Show("The loan is still active. Close anyway?", Text, MessageBoxButtons.YesNo) = DialogResult.No
        End If
    End Sub
End Class

Friend Module Startup
    <STAThread>
    Public Sub Main()
        Application.EnableVisualStyles()
        Application.Run(New ReturnForm())
    End Sub
End Module

This is the shelf Shelf.Desktop front: Loan comes from Shelf.Core, whose Loan.Return raises Returned exactly as in the first example. The project file sets MyType to WindowsFormsWithCustomSubMain, adds System.Windows.Forms to the imports and targets net10.0-windows; that is what makes Form, Button, Label and Timer available without Imports lines. What the form shows:

  • The designer writes the same pattern: each control is a Friend WithEvents field in Form1.Designer.vb, and the click handler in Form1.vb carries Handles Button1.Click. A C# designer instead writes button1.Click += button1_Click; in InitializeComponent; renaming a control in VB only changes the Handles text.

  • Handles MyBase.Load is the Visual Basic idiom for the form’s own events. A derived form can override OnLoad instead; that is what C# programmers do, and it avoids ordering questions.

  • _loan is a WithEvents field of a domain object, so Loan_Returned is called whenever it raises Returned, and the BeginInvoke makes it safe because the click handler raised the event from a Task.Run worker.

  • Handles ReturnButton.MouseEnter, ReturnButton.GotFocus shows one method for two events.

  • The Application Framework adds its own events (Startup, Shutdown, UnhandledException, StartupNextInstance) in ApplicationEvents.vb; see Windows Forms Applications.

Built, not run (WPF): Click in markup, Handles on a named element, routed events and the dispatcher
<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Shelf" Height="200" Width="360">
    <StackPanel Margin="12">
        <!-- Click="..." wires the event in markup: no Handles clause is needed (and using both runs the handler twice) -->
        <Button x:Name="RenewButton" Content="Renew" Click="RenewButton_Click" />
        <!-- A named element becomes a WithEvents field, so Handles works too -->
        <Button x:Name="ReturnButton" Content="Return" />
        <TextBlock x:Name="StatusText" Text="Waiting" Margin="0,12,0,0" />
    </StackPanel>
</Window>
Option Strict On

Imports System.Windows
Imports System.Windows.Input
Imports Shelf.Core

Class MainWindow
    Private WithEvents _loan As New Loan(New Magazine("Byte", 1975, 1), "Ada", Date.Today.AddDays(14))

    Private Sub RenewButton_Click(sender As Object, e As RoutedEventArgs)
        StatusText.Text = "Renewal requested"
    End Sub

    Private Sub ReturnButton_Click(sender As Object, e As RoutedEventArgs) Handles ReturnButton.Click
        _loan.Return()
    End Sub

    Private Sub Loan_Returned(sender As Object, e As LoanEventArgs) Handles _loan.Returned
        ' Dispatcher.Invoke is the WPF counterpart of Control.Invoke.
        Dispatcher.Invoke(Sub() StatusText.Text = e.Loan.Item.Title & " was returned")
    End Sub

    ' A routed event bubbles: mark it handled to stop it travelling further up the tree.
    Private Sub Window_PreviewKeyDown(sender As Object, e As KeyEventArgs) Handles Me.PreviewKeyDown
        If e.Key = Key.Escape Then
            e.Handled = True
            StatusText.Text = "Escape swallowed"
        End If
    End Sub

    Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
        ' AddHandler works with routed events as well.
        AddHandler RenewButton.MouseRightButtonUp, Sub(s, args) StatusText.Text = "Right click on Renew"
    End Sub
End Class

In WPF the events of the elements are routed events: they travel up (bubble) or down (tunnel) the element tree, and a handler that sets e.Handled = True stops further handlers on the route. The tunnelling twin of an event has the Preview prefix (PreviewKeyDown). Handler wiring has three spellings that you can mix but must not duplicate: the Click="RenewButton_Click" attribute (wired by the code the XAML compiler generates), a Handles clause (the elements with x:Name become WithEvents fields), and AddHandler in code. Wiring the same method with both the attribute and Handles registers it twice. Code-behind needs Imports System.Windows for RoutedEventArgs, which is not a default import. More in WPF Applications.

Errors and what they mean

Declaration errors hide binding errors, so the compiler may report only one group at a time. This program is expected to fail; its three messages are the usual "I tried to treat an event as a delegate" mistakes:

Built (compile errors expected): raising an inherited event and calling an event like a method
Option Strict On

Public Class Publisher
    Public Event Changed As EventHandler
End Class

Public Class Subscriber
    Inherits Publisher

    Public Sub Fire()
        RaiseEvent Changed(Me, EventArgs.Empty)       ' a derived class cannot raise it
    End Sub
End Class

Module Program
    Sub Main()
        Dim p As New Publisher()
        p.Changed(p, EventArgs.Empty)                 ' an event is not a delegate field
        p.Changed = Nothing                           ' and cannot be assigned
    End Sub
End Module
Program.vb(11,20): error BC30029: Derived classes cannot raise base class events.
Program.vb(18,9): error BC32022: 'Public Event Changed As EventHandler' is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event.
Program.vb(19,9): error BC32022: 'Public Event Changed As EventHandler' is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event.

Compiler messages seen while writing this page (each reproduced from a real build with the .NET 10 SDK):

Error Message (shortened) Typical cause and fix

BC30029

Derived classes cannot raise base class events.

RaiseEvent of an inherited event. Add a Protected Overridable Sub OnXxx to the base class and call that.

BC32022

'Public Event Changed As EventHandler' is an event, and cannot be called directly.

Calling or assigning an event outside the declaring class. Use RaiseEvent in the class, AddHandler outside.

BC30506

Handles clause requires a WithEvents variable defined in the containing type or one of its base types.

Handles on a local variable, a Shared event through the type name, or a variable that is not WithEvents.

BC30590

Event 'Nope' cannot be found.

Misspelled event name in Handles.

BC31029

Method 'H' cannot handle event 'E' because they do not have a compatible signature.

Handler parameters are not convertible from the event’s (for example e As String for an EventArgs event).

BC31084

Events cannot be declared with a delegate type that has a return type.

Event Computed As Func(Of Integer): use a Sub delegate, or a method returning the value.

BC31122

'Custom' modifier is not valid on events declared without explicit delegate types.

Custom Event E(sender As Object): give a delegate type.

BC31132

'RaiseEvent' definition missing for event.

A Custom Event must define all three accessors.

BC30476

'WithEvents' variables cannot be typed as arrays.

Use an AddHandler loop over the elements.

BC30234

'ReadOnly' is not valid on a WithEvents declaration.

WithEvents fields are properties internally; drop ReadOnly.

BC30413 and BC30435

'WithEvents' variables can only be typed as classes, interfaces or type parameters with class constraints. / Members in a Structure cannot be declared 'WithEvents'.

Use a class (or a wrapper class) as the publisher.

BC33009 and BC33010

'Event' parameters cannot be declared 'ParamArray' / 'Optional'.

Pass an array or a separate argument class.

BC36663

Option Strict On does not allow narrowing in implicit type conversions between method …​ and delegate …​

AddressOf Method with fewer parameters than the event’s; use a lambda or restate the parameters.

BC42326

Lambda expression will not be removed from this event handler.

RemoveHandler with a lambda written inline. Store the lambda in a variable.

BC42025

Access of shared member …​ through an instance; qualifying expression will not be evaluated.

A local variable has the case-insensitive name of the type that declares a Shared event.

An instance event declared in a Structure compiles but is almost never useful: the event field is copied with the structure, so a subscriber subscribes to one copy only.

Visual Basic versus C#

Topic Visual Basic C#

Declare

Event Returned As EventHandler(Of LoanEventArgs) or Event Renewed(sender As Object, d As Date)

public event EventHandler<LoanEventArgs> Returned;

Raise

RaiseEvent Returned(Me, e); no Nothing check

Returned?.Invoke(this, e); the null check is the programmer’s

Subscribe

AddHandler loan.Returned, AddressOf OnReturned, or Handles on a WithEvents field

loan.Returned += OnReturned;

Unsubscribe

RemoveHandler loan.Returned, AddressOf OnReturned; WithEvents unhooks automatically on reassignment

loan.Returned -= OnReturned;

Declarative wiring

WithEvents + Handles, which the designer uses

none; the designer writes += in InitializeComponent

Accessors

Custom Event with AddHandler, RemoveHandler and RaiseEvent

event with add and remove only

Raise from a derived class

BC30029; use OnXxx

not possible either; use OnXxx

Invoke the delegate

not allowed (BC32022); ReturnedEvent only inside the class

a field-like event is a delegate field inside the class

Handler with fewer parameters

relaxed delegate conversion for lambdas and Handles; not for AddressOf under Option Strict On

the parameter list must match (a lambda still lists its parameters, for example (_, _) => ...)

Unsubscribing a lambda

warning BC42326

no compiler warning; silently does nothing

Async handlers

Async Sub

async void

Events in an interface or Module

both; Implements on the event; Module events are implicitly Shared

interfaces yes; no modules

Older material you may meet

  • Visual Basic 6 and VBA have Event, RaiseEvent, WithEvents and the Private Sub Command1_Click() naming, but handlers have no sender or e, and the handler’s name is its wiring: rename the control and the handler becomes an ordinary method. In VB .NET the name is irrelevant and Handles is the wiring, which is why a pasted VB6 handler compiles but does not fire until you add the Handles clause. VB6 control arrays (Command1(Index)) no longer exist: one Handles list, with sender identifying the control.

  • Tutorials from the Framework era show events inside Delegate Sub declarations and BackgroundWorker events (DoWork, RunWorkerCompleted) for background work; both still compile, but Async/Await replaces them in new code.

  • Learn’s custom-event examples that "avoid blocking" use BeginInvoke, which fails on .NET with PlatformNotSupportedException (shown above).

  • Control.CheckForIllegalCrossThreadCalls = False and Application.DoEvents() as ways to keep a UI responsive are workarounds that hide the thread-affinity rule instead of observing it.

  • Books that teach Handles through the designer are still correct about Handles, sender and EventArgs; what has changed is the platform around it (SDK-style projects with the Application Framework configured in .vbproj properties, Async handlers instead of background-worker events, no BeginInvoke on delegates).

Pitfalls checklist

Pitfall What to do

The publisher outlives the subscriber

Unsubscribe in Dispose or in the symmetrical lifecycle method; prefer WithEvents when the publisher is a field; never rely on the collector.

A handler throws and the publisher’s caller sees it

Raise last in the method; catch inside handlers; use a Custom Event to isolate.

Subscribing the same method twice

It runs twice; guard with a Boolean or subscribe in one place.

Lambda used to unsubscribe

Store it in a variable (BC42326).

Cross-thread update of a control

InvokeRequired + BeginInvoke in Windows Forms, Dispatcher in WPF.

Async Sub handler that throws after an Await

Try ... Catch inside it; otherwise the exception ends the process.

Local variable named like the type with a Shared event

Rename the local (case-insensitive hiding, BC42025).

Click="..." in XAML and Handles on the same element

Pick one; using both registers the method twice.

Waiting for handlers

Events are fire-and-forget by design; return a Task from a method when completion matters.

Raising before the state is consistent

Assign state first, RaiseEvent last.