Implementing Decoupled Component Communication with WithEvents in VB.NET
Learn how to use the WithEvents keyword in VB.NET to implement the Observer pattern, reducing boilerplate while managing memory and data boundaries.
04 Feb 2026, 19:00 UTC

The Problem: Boilerplate in Event Subscriptions
In complex Visual Basic .NET applications, components often need to react to state changes in other objects. The standard approach involves creating delegates and manually attaching listeners using AddHandler. While flexible, this creates significant boilerplate code and increases the risk of forgetting to detach listeners, leading to memory leaks.
The WithEvents keyword provides a declarative way to implement the Observer pattern. It allows a class to declare a variable that can trigger specific methods when events occur, shifting the wiring from runtime logic to compile-time declarations.
Architectural Requirements
To use WithEvents effectively, the architecture must satisfy three conditions:
- Fixed Producer-Consumer Relationship: The consumer class must know the type of the event producer at design time.
- Consistent Event Signatures: The producer must raise events that match the signature expected by the consumer's handling method.
- Lifecycle Alignment: The producer and consumer should generally share a similar scope to avoid dangling references.
The Smallest Suitable Design
The most efficient implementation uses a WithEvents declaration combined with the Handles clause. This removes the need for manual AddHandler calls in the constructor.
' The Event Producer
Public Class DataService
Public Event DataUpdated(ByVal newValue As String)
Public Sub UpdateValue(val As String)
' Logic to update data
RaiseEvent DataUpdated(val)
End Sub
End Class
' The Event Consumer
Public Class DashboardView
' Declare the producer with the WithEvents modifier
Private WithEvents _service As New DataService()
' The Handles clause links this method to the specific event
Private Sub OnDataUpdated(newValue As String) Handles _service.DataUpdated
Console.WriteLine($"UI updated with: {newValue}")
End Sub
End Class
Operational Mechanics
When the compiler encounters WithEvents and Handles, it generates the necessary delegate plumbing in the background. At runtime, the RaiseEvent call in the DataService checks if any delegates are attached. If no one is listening, it is a no-op, preventing null reference exceptions.
Trust and Data Boundaries
The WithEvents pattern maintains a strict boundary between the producer and consumer. The DataService (producer) has no knowledge of the DashboardView (consumer). It only knows that an event is being raised to whoever is listening.
Data is passed via the event arguments. To maintain security and stability, only immutable data or specific Data Transfer Objects (DTOs) should be passed through events to prevent the consumer from accidentally modifying the producer's internal state.
Failure Modes and Memory Management
The primary risk with WithEvents is the Strong Reference Leak. Because the event producer holds a reference to the consumer's handler method, the Garbage Collector (GC) cannot reclaim the consumer object as long as the producer is alive.
Comparison: Handles vs. AddHandler
| Feature | Handles (WithEvents) | AddHandler |
|---|---|---|
| Binding Time | Compile-time | Runtime |
| Flexibility | Static (Fixed object) | Dynamic (Any object of type) |
| Boilerplate | Minimal | Moderate |
| Detachment | Automatic on object disposal | Manual via RemoveHandler |
Operational Checks and Verification
To verify the implementation is working without leaking memory, perform the following checks:
- Trigger Verification: Run the application and call the producer's trigger method. Verify the
Handlesmethod executes. - Lifecycle Check: Use a memory profiler (such as dotMemory or Visual Studio Diagnostic Tools). Create and destroy several instances of the consumer. If the memory usage climbs linearly without dropping, the producer is likely holding a reference to the consumer via the event delegate.
- IL Inspection: Use a tool like ILSpy to verify that the
Handlesclause has been converted into aAddHandlercall within the class constructor.
Conditions for Redesign
You should move away from WithEvents and toward a more explicit AddHandler or a Message Bus architecture if:
- The producer is a long-lived singleton and the consumers are short-lived transient objects.
- You need to attach handlers to objects created dynamically at runtime (e.g., a list of controls generated from a database).
- The application requires a many-to-many communication pattern where producers and consumers are completely decoupled via an interface.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.