2

I have a for loop in UserForm1 which opens UserForm2 a variable number of times to grab the appropriate amount of data from the user. See below the code running in UserForm1

Private Sub Start_Click()
    Constants
    InitBoard
    While Not ValidTest
        IncParts
    Wend
End Sub

Public Sub InitBoard()
    Dim row As Integer
    Dim col As Integer
    For Mirror = 1 To NumBlocks(2, 1)
        LockType = 1
        UserForm2.Show
    Next Mirror
    For Prism = 1 To NumBlocks(2, 2)
        LockType = 2
        UserForm2.Show
    Next Prism
    For Wormhole = 1 To NumBlocks(2, 3)
        LockType = 3
        UserForm2.Show
    Next Wormhole
    For Blocker = 1 To NumBlocks(2, 4)
        LockType = 4
        UserForm2.Show
    Next Blocker
    For Splitter = 1 To NumBlocks(2, 5)
        LockType = 5
        UserForm2.Show
    Next Splitter
End Sub

Now if I run the code, I can grab data in UserForm2 appropriately. When I close UserForm2 manually with the red 'X' button, everything works as expected. UserForm2 pops up again with showing the data was accepted from the previous run through the loop. After each loop has been run through the appropriate number of times, UserForm2 stops opening and the code continues in UserForm1 to the While loop of the Start_Click() sub. However, if I use Unload Me at the end of a sub inside UserForm2 to close it automatically after it receives the correct input, I get "Run-time error '91': Object variable or With block variable not set". When I press Debug, Line 5 of InitBoard() in the above code is highlighted (UserForm2.Show). Below is a checkbox click function that I was using inside UserForm2. commenting out line 3 fixes the issue, but I must close the form manually.

Private Sub Bstate00_Click()
    BoardState(0, 0) = LockType + 5
    Unload Me
End Sub

I've tried every combination I can think of to Load UserForm2 before showing, making sure not to end the loop before UserForm2 has closed each time, and even adding a delay with no avail. Me.Hide does fix the issue, but does not run the UserForm2_Initialize() sub needed to update the info entered in the previous loop.

Please see below as per the conversation about the 402 error in the comments the minimum code to recreate the error:

In UserForm1:

Private Sub Start_Click()
    For Mirror = 1 To 3
        LockType = 1
        With New UserForm2
            .Show
        End With
    Next Mirror
End Sub

In UserForm2:

Private Sub Bstate00_Click()
    BoardState(0, 0) = LockType + 5
    Me.Hide
End Sub

Private Sub UserForm_Initialize()
    If BoardState(0, 0) = -1 Then
        Me.Controls("BState" & 0 & 0).Value = False
        Me.Controls("BState" & 0 & 0).Enabled = False
    ElseIf BoardState(0, 0) = 0 Then
        Me.Controls("BState" & 0 & 0).Value = False
        Me.Controls("BState" & 0 & 0).Enabled = True
    Else
        Me.Controls("BState" & 0 & 0).Value = True
        Me.Controls("BState" & 0 & 0).Enabled = False
    End If
End Sub

In Module1:

Public BoardState(0 To 5, 0 To 5) As Integer
Public LockType As Integer
  • 2
    This is an inherent problem with using the default instance of a UserForm. I'd start by reading [this answer](https://stackoverflow.com/a/47291028/4088852), then reading [the linked blog post](https://rubberduckvba.wordpress.com/2017/10/25/userform1-show/). – Comintern Feb 11 '19 at 17:26
  • Extra Info: when I put Unload Me in UserForm2 somewhere, the first time I call UserForm2.Show from inside UserForm1, it loads properly and works. As soon as I trigger the code containing Unload Me from inside UserForm2, it closes and immediately shows the error referenced in the question. Or in the case of the delay, the error shows up 5 seconds after I triggered the code that unloads UserForm2. – Andrew Werner Feb 11 '19 at 17:33
  • Please post all of the code in question. The above is not much to go on. – IrwinAllen13 Feb 11 '19 at 18:02
  • 1
    A form is an object, i.e. an instance of a class. UserForm classes have a *default instance*, which is what gets displayed when you do `UserForm1.Show`. The problems you're experiencing are inherent to coding against a stateful default instance, and self-destructing objects (e.g. `Unload Me`). Read the article @Comintern linked (I wrote it), it explains everything you need to fix in a tutorial-like post that covers everything there is to know about *doing this right*. – Mathieu Guindon Feb 11 '19 at 18:15
  • @MathieuGuindon I read through the post as best I could. I've taken a grand total of 0 programming classes and your wording is about 5 light-years above my head. I did however follow the concept of making an instance of an object of class UserForm2, even if I didn't understand how. I copied all of your first section of sample code into UserForm2 and changed the names of things to match actual objects on screen and the second section of sample code into the for loop to call it. Now I get "Run-Time error '402': Must close or hide topmost modal form first". debug points at "Hide" in OnCancel() – Andrew Werner Feb 11 '19 at 18:39
  • Not sure how you got that error, I'm looking at a modal form spawning another modal form right now. It's hard to visualize how your code changed just by reading a comment, but I can assure you there's a clean way to make everything work as you need it, without involving stateful default instances and all the problems that come with them. – Mathieu Guindon Feb 11 '19 at 18:54
  • 1
    Is `UserForm2` "talking" to `UserForm1` in any way? – Mathieu Guindon Feb 11 '19 at 18:55
  • I am doing all information passing through public variables declared in Module1. LockType and BoardState are 2 of the public variables so you see that I set LockType depending on which for loop I'm in, then UserForm2 uses LockType also. Otherwise, no, they don't pass any information. – Andrew Werner Feb 11 '19 at 19:12
  • Mind editing your question with a [mcve] that reproduces the problem? Doesn't have to be the full code, just enough to replicate the 402 error. – Mathieu Guindon Feb 11 '19 at 19:15
  • Here's the abstract: a laser puzzle game with various types of blocks. The user creates the puzzle by setting up the board. The problem I'm working on right now is they can set X number of each type of block that can be moved to solve the puzzle, and Y number of each type of block that are locked in position. UserForm2 is a grid of checkboxes that will turn the color of the blocks already locked into position there. Obviously there are 100 ways to do this, but I'm so close because if I close UF2 manually, it works perfectly. I just want it to close after I click any checkbox automatically. – Andrew Werner Feb 11 '19 at 19:17
  • Try replacing every instance of `UserForm2.Show` with `With New UserForm2 : .Show : End With` - and replace the `Unload Me` instructions in `UserForm2` with `Me.Hide`. – Mathieu Guindon Feb 11 '19 at 19:22
  • 1
    Same 402error, but I'm currently working on the code rewrite so that I still get error with the Minimal, Complete... thing – Andrew Werner Feb 11 '19 at 19:42
  • @MathieuGuindon please see the new code at the bottom of my question. UserForm1 consists of a single button and UserForm2 consists of a single CheckBox – Andrew Werner Feb 11 '19 at 20:03
  • Ok thanks, got a repro. – Mathieu Guindon Feb 11 '19 at 20:09
  • I'll definitely come back here later tonight - got things to do, but there's a very interesting interaction going on. – Mathieu Guindon Feb 11 '19 at 20:38
  • Here's a lead: without the loop that shows another instance of UserForm2, there's no error. Try adding `DoEvents` after `End With` and before `Next Mirror`. Credits go to @Comintern for this one (we were just chatting about this). – Mathieu Guindon Feb 11 '19 at 20:46
  • Either that or explicitly release the reference with `Set UserForm2 = Nothing` (another sign that you need to manage your own references here). – Comintern Feb 11 '19 at 20:49
  • Neither {DoEvents} or {Set UserForm2 = Nothing} or both helped in either the full code or the minimized code above. I still get error 402. Please confirm I am supposed to put Set UF2=Nothing in between End With and Next Mirror. – Andrew Werner Feb 11 '19 at 20:57
  • 2
    FYI part of the problem is an under-documented "feature" of MSForms where closing a form from anything other than the `Click` handler of a `CommandButton` (or menu item, but that's not applicable to VBA) can cause general protection faults and unexpected behavior (again credits go to @Comintern) - is the requirement to close the form upon checking a checkbox a hard requirement? – Mathieu Guindon Feb 12 '19 at 00:42
  • I honestly already rewrote the code so that all of the for loops take place inside UserForm2 and moved the initialization that changes the colors to a sub that runs under each checkbox. So no, not a hard requirement, but I still don't know why I can't do it the way I had it. I just thought there had to be a simpler solution when it already worked manually but not automatically. And if I learn something that helps me next time, or gives me the tools to do a little more advanced coding challenges in the future, then great! I do appreciate your help to that end. – Andrew Werner Feb 12 '19 at 01:02

1 Answers1

2

tldr; Buttons matter. All is not as it seems in UserForm land.


FWIW, I can't reproduce the 402 run-time error in Excel 2013 x64 but I did discover some interesting behavior. Given the following code:

'UserForm1.cls
Private Sub UserForm_Initialize()
    Debug.Print "UserForm1_Initialize"
End Sub

Private Sub UserForm_Click()
    Dim i As Long
    For i = 1 To 3
        With New UserForm2
            .Show vbModal
        End With
    Next
    Debug.Print "Done"
End Sub

'UserForm2.cls
Private Sub UserForm_Initialize()
    Debug.Print "UserForm2_Initialize"
End Sub

Private Sub UserForm_Click()
    Me.Hide
End Sub

Private Sub UserForm_Terminate()
    Debug.Print "UserForm2_Terminate"
End Sub

Executing the above (showing UserForm1 as modal) gives the following output when clicked to dismiss:

UserForm1_Initialize
UserForm2_Initialize
UserForm2_Initialize
UserForm2_Initialize
Done

Note that none of the terminate events fire when the With block exits. Calling Unload Me in the click handler behaved normally. This was completely unexpected, so I dug through the scant documentation for the UserForm class and found this quote from Paul Lomax1:

Microsoft recommends that forms should be unloaded only in the Click event of a CommandButton or menu control. Calling the Unload statement in other event handlers can have undesirable side effects and cause general protection faults (GPFs).

This made me curious what would happen if I moved the Me.Hide statement into a CommandButton handler:

Private Sub UserForm_Initialize()
    Debug.Print "UserForm2_Initialize"
End Sub

Private Sub CommandButton1_Click()
    Me.Hide
End Sub

Private Sub UserForm_Terminate()
    Debug.Print "UserForm2_Terminate"
End Sub

Executing this code gave the following output...

UserForm1_Initialize
UserForm2_Initialize
UserForm2_Terminate
UserForm2_Initialize
UserForm2_Terminate
UserForm2_Initialize
UserForm2_Terminate
Done

...exactly as expected. It seems that there is some undocumented weirdness surrounding VBA's handling of UserForms. But it gets even more bizarre. If I leave the CommandButton on the form and revert back to the original code (hiding the form from the form click handler without the UserForm_Click() handler in the code-behind at all, it still fires the Terminate event. The mere presence of the CommandButton on the form alters the unload behavior. That said, you might try putting a hidden CommandButton on the form and see if that resolves it. If not, you can always explicitly Unload the form from the calling site:

Private Sub UserForm_Click()
    Dim i As Long
    Dim subForm As UserForm2
    For i = 1 To 3
        Set subForm = New UserForm2
        subForm.Show vbModal
        Unload subForm
    Next
    Debug.Print "Done"
End Sub

1 Lomax, Paul. VB & VBA in a Nutshell: The Language. Sebastopol, CA: OReilly, 1999., p.567

Comintern
  • 21,855
  • 5
  • 33
  • 80