top of page

Custom events in VBA with WithEvents and RaiseEvent

  • Writer: John
    John
  • 5 days ago
  • 3 min read

Most VBA developers only ever consume events ... Worksheet_Change, Document_Open, Application_Startup, a Command Button's Click. Far fewer realise you can declare and raise your own.


Here's an example of the problem they solve: a long-running Class needs to tell something else how it's getting on, but it shouldn't have to know what that "something" is. Events let the Class shout "I'm 40% done" into the room and carry on, without caring who (if anyone) is listening.


This post builds a small worker Class that raises Progress events, a listener that handles them, and demonstrates how to use ByRef to let a listener cancel the job early.


Key things to note


  • The source Class declares its event with a Public Event statement and fires it with RaiseEvent. Events can't return a value.

  • You cannot declare Events in a standard Module.

  • The listener declares Private WithEvents mTask As Task at module level. WithEvents must be module-level (again, not in a standard Module), and typed to a specific class ... not Object, so no late binding.

  • Handlers are named <variable>_<Event>, e.g. mTask_Progress. The VBE builds them for you: pick the WithEvents variable in the code window's left drop-down, then the event in the right drop-down.

  • Events are synchronous. RaiseEvent blocks until every handler has run, then execution continues inside the Class on the next line. That's what allows Letting a listener cancel work.

  • An event has no return value, so a ByRef argument is how a handler passes something back. The handler writes to the ByRef argument, and the Class reads the change after RaiseEvent returns.

  • Handlers only fire while the WithEvents variable holds the object. Set mTask = Nothing and the Class carries on ... but no one is listening!


The code


Add a Class Module named "Task". This is the worker ... it declares two events and raises them as it goes ...

Then add a Class Module named "Listener". Notice the WithEvents variable declaration, and that the handler names are just that variable name followed by an underscore plus the event name (as noted above, when adding event handlers yourself ... instead of copy / pasting this code! ... you can use the code window's left and right drop-downs) ...

Finally, add a standard Module (doesn't matter what it is named) to wire the two together and start it running ...

Then run DemoEvents ... which results in:



How it works


  • Task.Run just shouts Progress at each step. It has no idea whether anyone is listening, and doesn't care.

  • WithEvents is the keyword that does the work. Because mTask is declared WithEvents, VBA routes each Progress into mTask_Progress. Without WithEvents mTask would be an ordinary variable with no event handlers at all.

  • Nothing fires until oListener.Watch oTask sets the reference. That Set is what subscribes the listener; until then the Task would raise events into thin air.


Letting a listener cancel


The Progress event already has a ByRef bCancel parameter that the listener has been ignoring. Change the mTask_Progress() Sub to the following which sets a value for bCancel when progress gets to 60% ...

... and run DemoEvents again ... which results in:



Because bCancel is passed ByRef, the True the handler writes to it travels back into Run (in the Task Class), which checks it straight after RaiseEvent and exits. That's how a synchronous event feeds a decision back to its source even though events can't return anything.


Why go to this trouble?


The payoff is that Task is now reusable and ignorant (in a good way!) of who is listening. It reports progress and moves on, so the same Class works with a logging listener (as it does here), a UserForm, several listeners at once, or none at all ... and none of that changes a line of Task.


The alternative closely couples them. Without events, Task would have to hold a reference to the listener (a UserForm or whatever) and call it directly, so it can't run anywhere that such a listener doesn't exist (and you can't test it without one). Events invert that ... the listener depends on Task, but Task depends on nothing.


Wrapping up


A RaiseEvent and the handlers that answer it live in different Modules, which can make them fiddly to trace. VBE_Extras' References for code at cursor command will show you all of the references of an Event and allows you to jump between where an event is declared, where it's raised, and where it's handled, which takes the guesswork out of following the wiring.


The listener here just prints to the Immediate window, but it could as easily be a UserForm turning those Progress events into a moving progress bar with a real Cancel button. That's a future post.

Comments


bottom of page