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:
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 namedRenewedEventHandlerinside the class. Fewer keystrokes, but the delegate type belongs to your class alone and is not the genericEventHandler(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).
RaiseEvent, and what the compiler generatedOption 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:
-
RaiseEventwith nobody subscribed does nothing: there is noNothingcheck to write. This is the first difference from C#, whereReturned?.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 delegateRenewedEventHandler. -
senderis whatever the publisher passes inRaiseEvent. PassingMeis the convention; for aSharedevent passNothing. -
[Return]is in brackets becauseReturnis a reserved word; the brackets make it an identifier (see Program Structure and Code Conventions). -
The shelf
LoanthrowsInvalidOperationExceptionon a secondReturn(), 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:
RaiseEventOption 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
Source: RaiseEvent Statement
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:
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
Source: Delegate.GetInvocationList
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.Returnedhas already been assigned, the caller ofReturn()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 shelfLoandoes. -
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(theRaiseEventaccessor in Custom events) orReturnedEvent.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:
-
WithEventsapplies to a field of a class or module (also aSharedfield), a type parameter with a class constraint, and an interface-typed variable. It is rejected on arrays (BC30476), onReadOnlyfields (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. -
Handlesgoes on aSuborFunctiondeclaration and names an event of aWithEventsvariable of the same type or a base type (BC30506 otherwise), or an event ofMe,MyClassorMyBase, which handles the type’s own or inherited events.Handlescannot name a type: aSharedevent is subscribed withAddHandler(BC30506 again). AHandlesmethod may itself beShared. -
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
Functionits 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_Eventnaming (ReturnButton_Click, the one the designer generates) is just a convention. -
The compiler turns the
WithEventsfield into a property. Its setter callsRemoveHandleron the old object andAddHandleron the new one for everyHandlesclause. That makesWithEventsthe one subscription style that cannot leak by forgetting to unsubscribe from the previous object: reassigning the variable (even toNothing) detaches the old publisher.
WithEvents, Handles, MyBase and an inherited WithEvents variableOption 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 theWithEventsvariable and hooks up both handlers declared inReturnDeskand the one declared inAuditedDesk(it can, becauseCurrentisProtected; aPrivate WithEventsvariable is invisible to derived classes). -
OnAnyChange() Handles Current.Returned, Current.Renewedtakes no parameters at all and is still accepted underOption Strict OnbecauseHandlesapplies relaxed delegate conversion. The same drop-everything trick is not accepted forAddressOfunderOption Strict On(see Relaxed delegate conversion). -
After
desk.Watch(second)the first loan’sReturnedis silent: the setter removed the handlers. -
Handles MyBase.DeskClosedhandles an event declared by the base class on the same object.Handles Me.DeskClosedorHandles MyClass.DeskClosedwork for the type’s own events. -
Inheritance and
Handlesinteract in non-obvious ways when a handler is overridden or aWithEventsvariable 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.
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
Source: AddHandler Statement
The rules this program demonstrates:
-
Subscribing the same method twice means it runs twice. There is no de-duplication.
-
RemoveHandlerwithAddressOfremoves 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:
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
Source: RemoveHandler Statement
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:
OnReturned override, cancellation and a Shared eventOption 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
Source: Relaxed Delegate Conversion
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:
AddressOf cannot drop the parametersOption 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)'.
Source: AddressOf Operator
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 |
|
First parameter |
|
Second parameter |
|
Raising method |
|
Name |
A verb: |
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:
Event ... Implements with a renamed event and INotifyPropertyChangedOption 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
Source: INotifyPropertyChanged Interface
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:
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
EventHandlerListkeyed by aSharedobject means only subscribed events cost anything. This is how the Windows FormsComponentclass 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)callsAddHandler _inner.Changed, value).
EventHandlerList-backed oneOption 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
RaiseEventaccessor receives exactly the delegate’s parameters. It is called when the class writesRaiseEvent 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
AddedEventfield for aCustom Event: BC30451 if you try to use it. -
EventHandlerListis 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.
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
Source: WeakReference Class
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
AddHandlerwith aRemoveHandlerin a symmetrical place:Load/Closed, constructor/Dispose,Start/Stop. If the subscriber implementsIDisposable, unsubscribe inDispose(see Object Lifetime and Disposal). -
Prefer
WithEvents+Handleswhere the publisher is a field: reassigning or clearing the variable unsubscribes for you. A form that closes still has to set long-livedWithEventsfields toNothingif 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 asWeakEventManagerand theIWeakEventListenerinterface; the base class library has no equivalent for non-WPF code, so write one with aCustom Eventthat storesWeakReferenceobjects, or use the WPF types in a WPF app. -
To find a leak, take a heap snapshot with a memory profiler or
dotnet-gcdumpand look at the retention path of the leaked object; it ends in a delegate in aReturnedEventfield or in anEventHandlerList(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.
Async SubOption 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
Awaithas nowhere to go but theSynchronizationContextthat 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 Subhandler inTry ... Catch, asOnReturnedSafedoes.
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
Source: AppDomain.UnhandledException
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:
BeginInvoke on a delegate and the replacementOption 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, thenBeginInvoke(post) orInvoke(send) a delegate, or capture theSynchronizationContext. Code that usesAsync/Awaiton the UI thread usually does not need this. -
WPF:
Dispatcher.InvokeorDispatcher.BeginInvoke, orAwaitin anAsync Subhandler 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): preferBeginInvoke.
The pitfall in a modern Windows Forms app is the old workaround Control.CheckForIllegalCrossThreadCalls = False; do not use it.
WithEvents, Handles, MyBase.Load, a timer, cross-thread marshallingOption 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 WithEventsfield inForm1.Designer.vb, and the click handler inForm1.vbcarriesHandles Button1.Click. A C# designer instead writesbutton1.Click += button1_Click;inInitializeComponent; renaming a control in VB only changes theHandlestext. -
Handles MyBase.Loadis the Visual Basic idiom for the form’s own events. A derived form can overrideOnLoadinstead; that is what C# programmers do, and it avoids ordering questions. -
_loanis aWithEventsfield of a domain object, soLoan_Returnedis called whenever it raisesReturned, and theBeginInvokemakes it safe because the click handler raised the event from aTask.Runworker. -
Handles ReturnButton.MouseEnter, ReturnButton.GotFocusshows one method for two events. -
The Application Framework adds its own events (
Startup,Shutdown,UnhandledException,StartupNextInstance) inApplicationEvents.vb; see Windows Forms Applications.
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
Source: Routed events overview
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:
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.
Source: RaiseEvent Statement
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. |
|
BC32022 |
|
Calling or assigning an event outside the declaring class. Use |
BC30506 |
Handles clause requires a WithEvents variable defined in the containing type or one of its base types. |
|
BC30590 |
Event 'Nope' cannot be found. |
Misspelled event name in |
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 |
BC31084 |
Events cannot be declared with a delegate type that has a return type. |
|
BC31122 |
'Custom' modifier is not valid on events declared without explicit delegate types. |
|
BC31132 |
'RaiseEvent' definition missing for event. |
A |
BC30476 |
'WithEvents' variables cannot be typed as arrays. |
Use an |
BC30234 |
'ReadOnly' is not valid on a WithEvents declaration. |
|
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 … |
|
BC42326 |
Lambda expression will not be removed from this event handler. |
|
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 |
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 |
|
|
Raise |
|
|
Subscribe |
|
|
Unsubscribe |
|
|
Declarative wiring |
|
none; the designer writes |
Accessors |
|
|
Raise from a derived class |
BC30029; use |
not possible either; use |
Invoke the delegate |
not allowed (BC32022); |
a field-like event is a delegate field inside the class |
Handler with fewer parameters |
relaxed delegate conversion for lambdas and |
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 |
|
|
Events in an interface or |
both; |
interfaces yes; no modules |
Older material you may meet
-
Visual Basic 6 and VBA have
Event,RaiseEvent,WithEventsand thePrivate Sub Command1_Click()naming, but handlers have nosenderore, 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 andHandlesis the wiring, which is why a pasted VB6 handler compiles but does not fire until you add theHandlesclause. VB6 control arrays (Command1(Index)) no longer exist: oneHandleslist, withsenderidentifying the control. -
Tutorials from the Framework era show events inside
Delegate Subdeclarations andBackgroundWorkerevents (DoWork,RunWorkerCompleted) for background work; both still compile, butAsync/Awaitreplaces them in new code. -
Learn’s custom-event examples that "avoid blocking" use
BeginInvoke, which fails on .NET withPlatformNotSupportedException(shown above). -
Control.CheckForIllegalCrossThreadCalls = FalseandApplication.DoEvents()as ways to keep a UI responsive are workarounds that hide the thread-affinity rule instead of observing it. -
Books that teach
Handlesthrough the designer are still correct aboutHandles,senderandEventArgs; what has changed is the platform around it (SDK-style projects with the Application Framework configured in.vbprojproperties,Asynchandlers instead of background-worker events, noBeginInvokeon delegates).
Pitfalls checklist
| Pitfall | What to do |
|---|---|
The publisher outlives the subscriber |
Unsubscribe in |
A handler throws and the publisher’s caller sees it |
Raise last in the method; catch inside handlers; use a |
Subscribing the same method twice |
It runs twice; guard with a |
Lambda used to unsubscribe |
Store it in a variable (BC42326). |
Cross-thread update of a control |
|
|
|
Local variable named like the type with a |
Rename the local (case-insensitive hiding, BC42025). |
|
Pick one; using both registers the method twice. |
Waiting for handlers |
Events are fire-and-forget by design; return a |
Raising before the state is consistent |
Assign state first, |
References
-
Event Statement, RaiseEvent Statement, AddHandler Statement, RemoveHandler Statement and Handles Clause
-
WithEvents and Troubleshooting Inherited Event Handlers in Visual Basic
-
How to: Declare Custom Events To Conserve Memory and How to: Declare Custom Events To Avoid Blocking (uses
BeginInvoke; see above) -
Relaxed Delegate Conversion, AddressOf Operator and Lambda Expressions
-
Handling and Raising Events, How to: Raise and Consume Events and Event Design
-
EventHandler(Of TEventArgs), EventArgs, CancelEventArgs, EventHandlerList and INotifyPropertyChanged
-
Events overview (Windows Forms), How to: Make thread-safe calls to Windows Forms controls, Routed events overview (WPF) and Dispatcher.Invoke
-
Events (C#) for the C# side