1

I've been moving some of my code to utilize UserForms utilizing Modeless so the user can copy data from the worksheet. I finally "generalized" this answer and got it working. But, I want to avoid having to create a class for each UserForm and truth is I don't 100% understand what is going on in the code. Is there a way I can easily migrate all my UserForms to utilize the below functions of "Event Driven Modeless UserForms". Basically, I'd like to pass the UserForm as a variable and have a bunch of subs in the class, mostly general and a specific or perhaps just using If/Then to call the correct exit module after the _Closed event.

Hope that makes sense, let me know if you require further clarification.

Generalized Code:

Module Name doesn't Matter

UserForm Name = UserForm1

Class Name = Class1

Module Code:

Private UserFormNameStr As Class1
Public Sub DoStuff()
    Set UserFormNameStr = New Class1
    UserFormNameStr.ClassSubNameStrSubName
End Sub
Public Sub CallMeWhenUserFormClosed()
 Debug.Print "Module Code Run"
End Sub

Class Code:

Private WithEvents UserFormNameStr As UserForm1
Private Sub Class_Initialize()
    Set UserFormNameStr = New UserForm1
End Sub
Public Sub ClassSubNameStrSubName()
    UserFormNameStr.Show vbModeless
End Sub
Private Sub UserFormNameStr_Closed()
    '_Closed is required syntax
    Debug.Print "Closed Event"
    Call CallMeWhenUserFormClosed
End Sub

UserForm Code:

Public Event Closed()
Private Sub UserForm_Initialize()
    '
End Sub
Sub OkButton_Click()
 Debug.Print "Raising Events from OK Button!"
 RaiseEvent Closed
 Unload Me
End Sub
Private Sub CancelButton_Click()
 Unload Me
 End
End Sub

Update:

Perhaps I'm looking for something on Workbook_Open with a "hook"?

FreeSoftwareServers
  • 2,271
  • 1
  • 33
  • 57
  • 2
    I might be missing something but a userform IS a class. – user14797724 Feb 09 '21 at 05:31
  • Like i said, not 100% sure what im doing, but when I say class i mean right click insert class module vs UserForm – FreeSoftwareServers Feb 09 '21 at 05:52
  • 2
    You need a class module for the `WithEvents` form refs, but a presenter class with 3 `WithEvents` declarations of 3 different form types could very well handle the presentation of all these forms - having them in different classes is just a matter of *separation of concerns*. – Mathieu Guindon Feb 09 '21 at 06:00
  • @MathieuGuindon I'll have to do more reading to fully grasp that comment. An example would really help (an answer?), I'm def more of a "throw spaghetti at the wall and see what sticks" type coder unfortunately. – FreeSoftwareServers Feb 09 '21 at 06:33
  • You say you want to avoid one class per form; I'm saying you never needed one class per form in the first place. The reason you need a class is because you need to hold on to a `WithEvents` declaration of the form (`As MyUserForm`), and such variables are only legal in a class module. But nothing says your `MyPresenter` class cannot also handle the events of `Private WithEvents OtherForm As AnotherForm`, and then have a `ShowSomeForm` and `ShowOtherForm` methods instead of just having a `Show` method. – Mathieu Guindon Feb 09 '21 at 12:46
  • @MathieuGuindon I think more specifically I wasn't looking to have to add code to the class for each UserForm, but I think I'll have to. Could you give an example of a class that you would use w/ two UserForms? That will likely be the best solution and I can accept as an answer. – FreeSoftwareServers Feb 09 '21 at 22:40

1 Answers1

2

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 :)

Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • 1
    I'm interested in your last paragraph, and this is sorta what I was trying to avoid. But, I feel like I'm getting it "straight from the horses mouth" with you saying this is the best way. Your other SO post was arguably the best on the internet regarding getting this setup, tho I struggled adjusting the names at first to be generic and understand what they were referencing. Anyway, thanks for putting in the time! – FreeSoftwareServers Feb 09 '21 at 23:20
  • @FreeSoftwareServers I've edited yet another approach into the answer, hope it helps! – Mathieu Guindon Feb 10 '21 at 01:52
  • Looks good! I think I'll get it working with my existing code utilizing the first approach the attempt to "functionalize" using the second method. – FreeSoftwareServers Feb 10 '21 at 19:14
  • I had trouble getting the first approach to work last night. I think it might be due to the actual name of the "class". Does it matter what it's called (IE: In the pane on the right, it's name). Can you expand further on calling said class from two separate subs? – FreeSoftwareServers Feb 12 '21 at 19:47