13
votes

Intro:

I am aware that - showing UserForms - it's best practice to

  • handle QueryClose within the userform code (If CloseMode = vbFormControlMenu ...)
  • doing no Unload Me therein, just a timid Me.Hide instruction (after preventing [x]-itting and eventual self-destruction via Cancel = True )
  • setting a related variable/[property] within the [class] code (e.g. .IsCancelled=True)
  • in order to be able to have the UF unloaded by the calling code.

Useful link

An outstanding overview "UserForm1.Show?" can be found at https://rubberduckvba.wordpress.com/2017/10/25/userform1-show/ as well as in numerous examplary SO answers (thx to Mathieu Guindon aka Mat's Mug and RubberDuck).

Further selection (►edit as of 5/1 2019)


1) Working examples for modal UserForms

As far as I understood - and I do try to learn -, the following code should be okay for modal UF's:

Case 1a) .. with a local variable for the UF instance, as often seen:

Public Sub ShowFormA
  Dim ufA As UserForm1
  Set ufA = New UserForm1
' show userform 
  ufA.Show          ' equivalent to: ufA.Show vbModal

' handle data after user okay
  If Not ufA.IsCancelled Then
      '  do something ...
  End If

' >> object reference destroyed expressly (as seen in some examples)
  unload ufA
End Sub

Case 1b) .. without a local variable, but using a With New codeblock:

' ----------------------------------------------------------
' >> no need to destruct object reference expressly,
'    as it will be destroyed whenever exiting the with block
' ----------------------------------------------------------
  With New UserForm1
      .Show         ' equivalent to: ufA.Show vbModal

    ' handle data after user okay
      If Not .IsCancelled Then
      '  do something ...
      End If
  End With

2) Problem

Problems arise using a MODELESS UserForm instance.

Okay, the with block method (cf. 1b) should be sufficient to destroy any object reference after x-iting it:

  With New UserForm1
      .Show vbModeless  ' << show modeless uf
  End With

If I try, however to

  • a) get information about a possible user cancelling as well as
  • b) to Unload a form if baptized using a local variable (e.g. "ufA") after the Show instruction,

all code lines will be executed at once for precisely the reason that the form is MODELESS:

  • code shows the form, the next moment ..
  • code finds no user cancelling, as there was no time for any user action, the next moment ..
  • [code unloads the form if using a local variable for the userform]

3) Question

How can I handle a) correctly reported UserForm cancels by the calling code of a MODELESS form as well as b) a (necessary?) unloading if using a local variable?

3
@Mat's Mug ad) edit: of course there has to be a If Not **Ufa**.IsCancelled in 1a) - one should keep away from copying an apparently similar code some lines below (cf. 1b) :-). Nevertheless I hope having contributed a helpful question at least for learning programmers.T.M.
@Mat's Mug, I appreciate your clearing up some background by your comment, thus confirming me in my resolve to explore the OOP capacities of VBA and to try MVP solutions. This even more as (unexplained) downvoting is pretty unfair if it results from differing philosophies only. BTW, rubber can be a very solid material :-;T.M.
I'd even say SOLID!Mathieu Guindon

3 Answers

13
votes

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 Explicit
Private 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 vbModeless
End 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 handler
Private Function OnCancel() As Boolean
    Dim cancelCancellation As Boolean
    RaiseEvent FormCancelled(cancelCancellation)
    If Not cancelCancellation Then Me.Hide
    OnCancel = cancelCancellation
End Function

Private Sub CancelButton_Click()
    OnCancel
End Sub

Private Sub OkButton_Click()
    Me.Hide
    RaiseEvent FormConfirmed
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CloseMode = VbQueryClose.vbFormControlMenu Then
        Cancel = Not OnCancel
    End If
End 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 If
End Sub

Private Sub myModelessForm_FormConfirmed()
    'form was okayed and is now hidden.
    '...
    Set myModelessForm = Nothing
End 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 ShowGizmo
End Sub

And the presenter goes:

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

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

3
votes

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

Option Explicit

Private m_MyForm As UserForm1

Private Sub Workbook_BeforeClose(Cancel As Boolean)
    If Not m_MyForm Is Nothing Then
        Unload m_MyForm
        Set m_MyForm = Nothing
    End If
End Sub

Friend Property Get MyForm() As UserForm1
    If m_MyForm Is Nothing Then
        Set m_MyForm = New UserForm1
    End If

    Set MyForm = m_MyForm
End Property

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

ThisWorkbook.MyForm.Show vbModeless

etc.

3
votes

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 Boolean

Public Property Get Cancelled() As Boolean
    Cancelled = m_bCancelled
End Property

Public Property Let Cancelled(ByVal bNewValue As Boolean)
    m_bCancelled = bNewValue
End Property
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    Me.Cancelled = True
    Cancel = 1
    Me.Hide
End Sub