What's making it all work is the WithEvents
declaration. Instance variables declared with the WithEvents
modifier will appear in the editor's left-side dropdown.
To create an event handler procedure for an event provider, select the variable from the left dropdown, then pick an event to handle in the right-side dropdown.
Ultimately the module would look something like this, i.e. with a WithEvents
declaration for each modeless form you want to handle events for:
Private WithEvents UserFormNameStr As UserForm1
Private WithEvents SomeOtherUserForm As UserForm2
Private WithEvents AnotherUserForm As UserForm3
Private Sub Class_Initialize()
Set UserFormNameStr = New UserForm1
Set SomeOtherUserForm = New UserForm2
Set AnotherUserForm = New UserForm3
End Sub
Public Sub ClassSubNameStrSubName() 'weird name, consider methods that begin with a verb
UserFormNameStr.Show vbModeless
End Sub
Public Sub ShowSomeOtherForm()
SomeOtherUserForm.Show vbModeless
End Sub
Public Sub ShowAnotherForm()
AnotherUserForm.Show vbModeless
End Sub
Private Sub UserFormNameStr_Closed() 'select "UserFormNameStr" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (UserFormNameStr)"
CallMeWhenUserFormClosed
End Sub
Private Sub SomeOtherUserForm_Closed() 'select "SomeOtherUserForm" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (SomeOtherUserForm)"
CallMeWhenSomeOtherUserFormClosed
End Sub
Private Sub AnotherUserForm_Closed() 'select "AnotherUserForm" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (AnotherUserForm)"
CallMeWhenAnotherUserFormClosed
End Sub
One Handler, Many Forms?
If all _Closed
handlers need to do exactly the same thing, then we can get interfaces and polymorphism involved and have one class exist in 3 instances that each do their own thing for their respective form - but VBA does not expose Public Event
declarations on a class' default interface, so the paradigm is a little bit different here, and because it doesn't involve Event
and WithEvents
, it's arguably simpler that way, too.
Define an IHandleClosingForm
interface: add a new class module to your project, but give no attention to the implementation - just a very high-level abstraction of the functionality you want (here with Rubberduck annotations):
'@ModuleDescription "An object that handles a form being closed."
'@Interface
'@Exposed
Option Explicit
'@Description "A callback invoked when a form is closed."
Public Sub Closing(ByVal Form As UserForm)
End Sub
In each form module, hold a reference to that interface, and invoke its Closing
method in the form's QueryClose
handler:
Option Explicit
Public CloseHandler As IHandleClosingForm
`...
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
'user clicked the [X] button, form instance is going to be destroyed!
Cancel = True 'prevents a self-destructing form instance.
Me.Hide
End If
'don't assume the caller set the CloseHandler:
If Not CloseHandler Is Nothing Then CloseHandler.Closing(Me)
'...
End Sub
Now implement that interface in the presenter class:
Option Explicit
Implements IHandleClosingForm
'...rest of the module...
Private Sub IHandleClosingForm_Closing(ByVal Form As UserForm)
'NOTE: procedure exits to the still-closing form's QueryClose handler
If TypeOf Form Is UserForm1 Then
CallMeWhenForm1Closes
Else
CallMeWhenAnyOtherFormCloses
End If
End Sub
The final step is to introduce a circular reference between the form and the presenter, by setting the public CloseHandler
property before showing the form:
Set theForm.CloseHandler = Me
theForm.Show vbModeless
This will work, but then there's a memory leak and neither the form nor the presenter instance would terminate (handle Class_Terminate
to find out!), and you will want to strive to avoid that (Excel will/should clean it all up when it shuts down the VBA environment though).
The solution is to untie the knot at the first opportunity, so make sure your forms' QueryClose
handler sets the IHandleClosingForm
reference to Nothing
as soon as it is no longer useful:
'don't assume the caller set the CloseHandler:
If Not CloseHandler Is Nothing Then CloseHandler.Closing(Me)
Set CloseHandler = Nothing 'release the handler reference
The next time the form is shown and the handler is set, it's going to be on another instance of the form.
If you need the state of the form to persist between it being shown and closed, then you must separate the state from the form (and keep the state around but still properly destroy the form object), ...but that's another topic for another day :)