VBA - destroy a modeless UserForm instance properly VBA - destroy a modeless UserForm instance properly vba vba

VBA - destroy a modeless UserForm instance properly


Indeed, I've been focusing quite a lot on modal forms - because that's what's most commonly used. Thanks for the feedback on that article!

The principles are the same for non-modal forms though: simply expand on the Model-View-Presenter pattern roughly outlined in the linked article and here.

The difference is that a non-modal form needs a paradigm shift: you're no longer responding to a preset sequence of events - rather, you need to respond to some asynchronous events that may happen at any given time, or not.

  • When handling a modal form, there's a "before showing" and then an "after hiding" that runs immediately after the form is hidden. You can handle anything that happens "while showing" using events.
  • When handling a non-modal form, there's a "before showing", and then "while showing" and "after showing" both need to be handled through events.

Make your presenter class module responsible for holding the UserForm instance, at module-level and WithEvents:

Option ExplicitPrivate WithEvents myModelessForm As UserForm1

The presenter's Show method will Set the form instance and display it:

Public Sub Show()    'If Not myModelessForm Is Nothing Then    '    myModelessForm.Visible = True 'just to ensure visibility & honor the .Show call    '    Exit Sub    'End If    Set myModelessForm = New UserForm1    '...    myModelessForm.Show vbModelessEnd Sub

You don't want the form instance to be local to the procedure here, so a local variable or a With block can't work: the object will be out of scope before you mean it to. That's why you store the instance in a private field, at module level: now the form lives as long as the presenter instance does.

Now, you need to make the form "talk" to the presenter - the easiest way is to expose events in the UserForm1 code-behind - for example if we want the user to confirm cancellation, we'll add a ByRef parameter to the event, so the handler in the presenter can pass the information back to the event source (i.e. back to the form code):

Option Explicit'...private fields, model, etc...Public Event FormConfirmed()Public Event FormCancelled(ByRef Cancel as Boolean)'returns True if cancellation was cancelled by handlerPrivate Function OnCancel() As Boolean    Dim cancelCancellation As Boolean    RaiseEvent FormCancelled(cancelCancellation)    If Not cancelCancellation Then Me.Hide    OnCancel = cancelCancellationEnd FunctionPrivate Sub CancelButton_Click()    OnCancelEnd SubPrivate Sub OkButton_Click()    Me.Hide    RaiseEvent FormConfirmedEnd SubPrivate Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)    If CloseMode = VbQueryClose.vbFormControlMenu Then        Cancel = Not OnCancel    End IfEnd Sub

Now the presenter can handle that FormCancelled event:

Private Sub myModelessForm_FormCancelled(ByRef Cancel As Boolean)    'setting Cancel to True will leave the form open    Cancel = MsgBox("Cancel this operation?", vbYesNo + vbExclamation) = vbNo    If Not Cancel Then        ' modeless form was cancelled and is now hidden.        ' ...        Set myModelessForm = Nothing    End IfEnd SubPrivate Sub myModelessForm_FormConfirmed()    'form was okayed and is now hidden.    '...    Set myModelessForm = NothingEnd Sub

A non-modal form wouldn't typically have "ok" and "cancel" buttons though. Rather, you'd have a number of functionalities exposed, for example one that brings up some modal dialog UserForm2 that does something else - again, you just expose an event for it, and handle it in the presenter:

Public Event ShowGizmo()Private Sub ShowGizmoButton_Click()    RaiseEvent ShowGizmoEnd Sub

And the presenter goes:

Private Sub myModelessForm_ShowGizmo()    With New GizmoPresenter        .Show    End WithEnd Sub

Note that the modal UserForm2 is a concern of a separate presenter class.


I typically tie the lifetime of a modeless userform instance to the workbook's by putting code along those lines behind ThisWorkbook:

Option ExplicitPrivate m_MyForm As UserForm1Private Sub Workbook_BeforeClose(Cancel As Boolean)    If Not m_MyForm Is Nothing Then        Unload m_MyForm        Set m_MyForm = Nothing    End IfEnd SubFriend Property Get MyForm() As UserForm1    If m_MyForm Is Nothing Then        Set m_MyForm = New UserForm1    End If    Set MyForm = m_MyFormEnd Property

You can then refer to the modeless code throughout your code using e.g.

ThisWorkbook.MyForm.Show vbModeless

etc.


For modeless forms, Use DoEvents coupled with custom userform property.


Sub test()    Dim frm As New UserForm1    frm.Show vbModeless    Do        DoEvents        If frm.Cancelled Then            Unload frm        Exit Do    End If    Loop Until False    MsgBox "You closed the modeless form."    '/ Using With    With New UserForm1        .Show vbModeless        Do            DoEvents            If .Cancelled Then Exit Do        Loop Until False    End With    MsgBox "You closed the modeless form (with)"End Sub

'/ User Form

Private m_bCancelled As BooleanPublic Property Get Cancelled() As Boolean    Cancelled = m_bCancelledEnd PropertyPublic Property Let Cancelled(ByVal bNewValue As Boolean)    m_bCancelled = bNewValueEnd PropertyPrivate Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)    Me.Cancelled = True    Cancel = 1    Me.HideEnd Sub