Out of Stack Space Issue - Can I Repeat a Sub Without Recursing It? - excel

I'm working on a fun project in Excel making a game. It works perfectly except that after about a minute of run time it errors because it's out of stack space. I realized this was because technically I'm using recursion in the stepEvent function, which looks like this:
Private Sub stepEvent()
' All the game code
start = Timer
Do While Timer < start + 0.15
DoEvents
Loop
stepEvent
End Sub
None of the instances of stepEvent ever resolve until either the player loses, or it runs out of stack space and errors. My question is if there's a way to either resolve the recursed subs, but somehow retain control, or if there's an alternate method to repeat that sub without using recursion.

Try looping
Sub asDoWhileLoop()
Do While True
Call stepEvent
Loop
End Sub
Sub stepEvent()
'Do game stuff and wait at end
End Sub
But remember to clean up at the end of your stepEvent(), i.e. set your objects to NOTHING, close recordsets, whatever you are playing with. You can quit using END instead of EXIT SUB. But it's all up to you to ensure it terminates.

Related

Stepping through with F8 the code runs, stops in standard execution with F5

I've written a macro which sums numbers, grouping by year and by month based on our projection model.
It runs from start to end when stepping through with F8.
It stops immediately in standard execution with F5.
The first trouble is
Runtime error 91
in
issmIndex = Range("A1:Z1").Find("ck.IssMon").Column 'issmIndex an integer
Originally I tried Application.WorksheetFunction.Match(...) but had the same problem: runs in debug, but not in execute (Error 1004 instead).
I considered it could have been an Excel version issue (the Match function has a different name in the Italian version). I switched to a more neutral Find, but still no luck.
When you have an error with a line that is a combination of several commands, try breaking it down into the individual steps.
For example, this works:
Sub findDemo()
Const toFind = "blah"
Dim rg As Range, f As Range
Set rg = Range("A2:C5")
Set f = rg.Find(toFind)
If f Is Nothing Then
Stop 'not found
Else
Debug.Print "found in column #" & f.Column
End If
End Sub
Also see the example in the documentation for Range.Find().
Welcome to SO. Sometimes Excel reads code faster than executing, so when reading a command there is a previous one not finished. IT's weird but it happens a lot if your code does a lot of stuff and calculus.
Besides, when debugging, every command line is executed before reading next one, so you cannot detect this just debugging.
So if your code runs perfect when debugged but errors if executed as normal, try to add the command DoEvents right before the problematic line. Something like this:
' your previous code
'
'
'
Doevents
issmIndex = Range("A1:Z1").Find("ck.IssMon").Column 'issmIndex an integer
'
'rest of your code
This commands forces Excel to make sure everything has been executed before reading. It's kind of like a checkpoint, something like make sure you've done everything before going to next line.
DoEvents
function

How does Excel's Workbook_Open() event actually work?

I'm using an Excel file to operate a small business system and have noticed some quirks with the Workbook_Open() event. The file contains dozens of modules (including class modules representing business logic such as invoices and customers) and I have not been able to reproduce the issue with an MCVE but am hoping someone can help me learn something from this.
The setup is straightforward, the Workbook_Open() event in the ThisWorkbook object calls a sub named Init placed in a regular model called Main:
Private Sub Workbook_Open()
Call Main.Init
End Sub
The Init sub in the Main module prints a start-up message to the immediate window and calls some other subs to initiate a couple of global variables to hold some data:
Public Sub Init()
' Called by ThisWorkbook.Workbook_Open().
Debug.Print "Initializing variables..."
Debug.Print "The system contains:"
Call Init_Items
End Sub
Public Sub Init_Items()
' This sub populates a collection of objects of a CInvoice class by looking
' up data in an Invoices table (a VBA ListObject); it produces output
' similar to the following:
Debug.Print "230 invoices."
End Sub
If the above code is placed in a sample file, it will print the following start-up message to the Immediate window when that file is opened:
Initialzing variables...
The system contains:
230 invoices.
This simple setup basically corresponds to the operational business file. However, when the business file is opened the output is printed twice to the Immediate window. To figure out what's going on, I placed a Stop in Workbook_Open(), in the hope that I could step through the code. To my surprise this revealed that the first iteration of the start-up message was written to the Immediate window even before the call to Main.Init.
Private Sub Workbook_Open()
Stop ' <--- The output produced by Main.Init is written to the
' Immediate window twice, both before and after this Stop.
Call Main.Init
End Sub
Moreover, Workbook_Open() also writes the number 3 to the Immediate window. This 3 is typically written after each iteration of the start-up message, but occasionally the first 3 is written before the first iteration of the start-up message and then after the second iteration. Sometimes it's written only once, after the first iteration of the start-up message but I have not noticed any pattern to this behavior.
This isn't really a big issue since the system is working properly but I'm curious to know what's going on.

Consistently receiving user input through a long-running procedure (DoEvents or otherwise)

Cleanly cancelling a long API-Ridden procedure is hellish, and I'm attempting to work out the best way to navigate the hellishness.
I'm using excel 2016 (with manual calculations and no screen updates) - I may take some time to attempt to run the procedure on 2010 to see if any issues resolve in the coming days (I'm aware of the slowdowns).
Over time, my procedure LongProcedure has lost its ability to successfully use its cancel feature (presumably due to increasing complexity). It was initially inconsistent and required significant spam-clicking to cancel, and now it fails altogether
Here's the setup:
First, LongProcedure is in a class module LongClass with a public property checked against for early cancelling, allowing it to clean up.
Public Sub LongProcedure()
' [Set up some things] '
For Each ' [Item In Some Large Collection (Est. 300 Items)] '
' [Some Code (ETA 5 Seconds) Sprinkled with 3-4 DoEvents] '
' [Export workbook (ETA 10 Seconds)] '
If (cancelLongProcedure) Then Exit For
Next
' [Clean up some things] '
GeneratorForm.Reset ' Let the UserForm know we're finished
End Sub
Second, I have a UserForm shown from a macro, which instantiates the procedure class, and runs the procedure. It contains a run button, a status label, and a cancel button.
Private MyLong As LongClass
Public Sub ButtonRunLongProcedure_Click()
Set myLong = New LongClass
myLong.LongProcedure()
End Sub
So the issue overall is twofold.
The ExportAsFixedFormat call opens a "Publishing..." progress bar which freezes excel for around ten seconds - fine. In all of my efforts, I haven't found a single way to process user input while this is happening.
On top of this, the DoEvents calls seemingly no longer do anything to allow the cancel button to be clicked. The process inconsistently freezes excel, tabs into other open programs, and (when not freezing) updates the status label.
I've Tried:
Appending DoEvents to the SetStatusLabel method instead of sprinkling - while the form still often freezes, it otherwise updates the status label consistently (while still not allowing the cancel button)
Using winAPI Sleep in place of, and in addition to DoEvents with delays of 1, 5, 10, 50, and 250ms - The form simply stopped updating at all without doevents, and with both it froze more.
Using a Do While loop to run DoEvents constantly for one second (Froze)
Overriding QueryClose to cancel the form. This one helped significantly. For some reason, the close [x] button can be clicked far more consistently than the userform buttons - Still not as consistently as I'd like. The problem? during publishing, Excel stops responding, and as such, modern windows will end the process if you click the close button twice... without cleanup.
Using Application.OnTime to regularly call DoEvents. Didn't seem to improve the situation overall
Alt-Tabbing. No, really. for some reason, while alt-tabbing occasionally just makes the UserForm freeze harder, sometimes it makes it stop freezing and update.
This is an issue I'm willing to do significant refactor work for, including smashing up the idea of the long procedure into separate methods, performing setup initially, and cleanup on class termination. I'm looking for something that provides consistent results. - I'll accept anything from excel versions to excel settings to refactors to winAPI calls.
Thanks for any insight into this one.
As it turns out simply combining together some of the useful improvements, along with a new one, made all the difference.
QueryClose is up to personal preference. Leave it in to catch more terminations, leave it out to ensure users use the new solution
Stick to sprinkling doEvents in places you feel are logical (not just when the status bar updates - like before and after an Application.Calculate call)
Optimize the long-running process as best you can, avoiding excel calls
And, most significantly
The integrated cancel key feature (CTRL+Break by default) is significantly more responsive than UserForm buttons and the form close button, without the chance of accidentally ending the excel task.
Here's the process to polish that for a finished product
First, set up a debugMode, or the inverse handleErrors, module-level variable to control whether to implement break-to-cancel and error handling. (Error handling will make your code harder to debug, so you'll appreciate the switch)
If your process is handling errors, you'll set Application.EnableCancelKey to xlErrorHandler, and On Error GoTo [ErrorHandlingLabel]. The error handling label should be directly before cleanup, and immediately set EnableCancelKey to xlDisabled to avoid bugs. Your handler should check the stored Err.Number and act accordingly, before continuing on to the cleanup steps.
Ensure that if you defer to any other complex vba in your script (such as using Application.Calculate on a sheet with UDFs), you set On Error GoTo 0 beforehand, and On Error GoTo [ErrorHandlingLabel] after, to avoid catching cellbound errors.
Unfortunately, the drawback is that for the UX to be consistently readable, you'll have to leave the cancel key on xlDisabled until the form is closed.
And in code:
Public Sub LongProcedure()
If handleErrors Then
On Error GoTo ErrorHandler
Application.EnableCancelKey = xlErrorHandler
End If
' [Set up some things] '
For Each ' [Item In Some Large Collection (Est. 300 Items)] '
' [Some Code (ETA 5 Seconds) Sprinkled with 3-4 DoEvents] '
' [Export workbook (ETA 10 Seconds)] '
Next
ErrorHandler:
If handleErrors Then
Application.EnableCancelKey = xlDisabled
If (Err.Number <> 0 And Err.Number <> 18) Then
MsgBox Err.Description, vbOKOnly, "Error " & CStr(Err.Number)
End If
Err.Clear
On Error GoTo 0
End If
' [Clean up some things] '
GeneratorForm.Reset ' Let the UserForm know we're finished
End Sub
and in the UserForm
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If MyLong.handleErrors Then Application.EnableCancelKey = xlInterrupt
End Sub
A small note that this method will likely generate a few bugs you weren't expecting to encounter because the execution jumps directly to the specified label. Your cleanup code will need to have required variables instantiated from the start.
Overall, once these issues are fixed, this setup ensures that the user can click CTRL+Break as many times as they could possibly want without causing crashes or popups.

Excel Userform forcing label update before continuing sub

I have a sub in a spreadsheet to remove individual line items. Because it takes a few seconds to process, I'd like to have a little Ready indicator.
At the moment I've got a small label (see above) called Lab_Indic.
When the user clicks remove, it runs the _Click sub, which opens with this:
With Lab_Indic
.BackColor = &HFF&
.Caption = "WORKING"
End With
...and closes with this:
With Lab_Indic
.BackColor = &H8000&
.Caption = "READY"
End With
But the initial "Working" change only seems to happen if I step through; if I run it normally, it seems to skip past it.
Is there any way to force it to update the label before continuing with the rest? DoEvents sounds like it might do the job, but some sources I've looked at suggest that there might be a downside to that approach.
It seems to work nicely, so I'll post it as an answer, but I'm still very interested in any indication of whether it's a good solution or not...
With Lab_Indic
.BackColor = &HFF& ' &H00008000&
.Caption = "WORKING"
End With
DoEvents
I've just added a single DoEvents to refresh the label, and it seems to work nicely. I don't know if there are any knockon effects.

Selenium Webdriver (VBA): Explicit Wait

I am navigating a web application that will often throw an error if there is an attempt to click an element before it can be interacted with.
When using Selenium WebDriver (java), I can easily work around the problem:
WebDriverWait wait = new WebDriverWait(driver, 15);
wait.until(ExpectedConditions.elementToBeClickable(By.id("element")));
However, I am trying to write the script in VBA utilizing the Selenium type library, and, despite trying numerous different ways, the only success I am having is:
webdriver.wait
which I have been told should be avoided if at all possible. If someone can advise how to translate my java into VBA, or provide any other solution, I would be extremely grateful.
You might try looping until the element has been set correctly with a time out to ensure you can't go into an infinite loop. The danger with the accepted answer is there is no way to escape the loop if not found.
Dim t As Date, ele As Object
t = Timer
Do
DoEvents
On Error Resume Next
Set ele = .FindElementById("element")
On Error GoTo 0
If Timer - t = 10 Then Exit Do '<==To avoid infinite loop
Loop While ele Is Nothing
Note: User #florentbr wrote a js wait clickable function to be executed via Selenium Basic. Example in this SO answer.
The selenium plugin for VBA is unofficial and doesn't support this feature.
You can work around this by using onError to retry the action that is producing an error until it succeeds or times out:
Sub test
OnError GoTo Retry
webDriver.findElementById("element")
Exit Sub
Dim i as integer
:Retry
webDriver.Wait(500)
i = i + 1
if i = 20 then onerror go to 0
Resume
end sub
In vba you can use Implicit wait "driver.Timeouts.ImplicitWait = 10 'Timeunits 'seconds" it will wait maximum limit if the element is found before the set time it will process further.
You can use a variation in the old wait system used with Internet Explorer.
Do While SeDriver.FindElementById("Id").IsDisplayed = False
DoEvents
Loop
Just replace "IsDisplayed" with "IsPresent", as needed. This method introduces an artificial implicit wait, perfect if what you need is to wait for an element generated by an AJAX request.
Often there are elements on-site that can be immediately found by selenium but cannot be interacted with and usually when we debug at this stage and run the code from there, everything works just fine. The solution I used for that was adding wait.
Sub Pause_for_2_seconds()
Application.Wait (Now + TimeValue("00:00:02"))
End Sub
And Where ever you feel that the element require some time just add
Call Pause_for_2_seconds
in your actual Sub
For Example see below
chr.SendKeys (Keys.Tab)
Call Pause_for_2_seconds
chr.SendKeys (Keys.Tab)
chr.SendKeys (Date + 52)
chr.SendKeys (Keys.Tab)
Call Pause_for_2_seconds
chr.SendKeys (Keys.Tab)
chr.SendKeys (Keys.Enter)

Resources