How to stop Excel Macro Which Still Runs After Application.Quit Method - vba

I have found similar questions, but for other issues, seems that Excel is called from another application and process is left after Excel application closes.
In my case, I have a macro in my Excel file, and I try to close the application when an error occurs.
I have my error handling set this way:
'Code code code
CleanExit:
Logger.LogData LOG_DEBUG, MODULE_NAME, "Initialize", "Some module initialized!"
Exit Function
ErrorExit:
Logger.LogData LOG_ERROR, MODULE_NAME, "Initialize", "Error found! Description: " & err.description
Main.HandleError err, MODULE_NAME, "Initialize"
GoTo CleanExit
End function
I want my macro to stop running when error occurs in some module and not to stop if it's in another module (hence the GoTo CleanExit).
Error handler is set-up in this way:
Public Function HandleError(ByRef err As ErrObject, ByVal moduleOrgin As String, ByVal methodOrgin As String)
Dim wbk As Workbook
'Do something if module origin meets my parameters and exit function right here if my conditions are met
MsgBox "Some message to the user about the problem"
If GetSetting(SETTING_HIDE_APPLICATION, False) = True Then
For Each wbk In addinWorkbook.Application.Workbooks
wbk.Saved = True
Next wbk
addinWorkbook.Application.Quit
End If
End Function
After this code runs I assume that all further code running stops, as my Excel workbook, which hosts my macro code is closed with the application.
In reality I get a cascade of errors, where the error message is shown 3 times until it closes for good. How can I avoid code any code running after Application.Quit method?
Code workflow when error occurs and what runs after Application.Quit:
Main method to initialize my form
Call to loader method which throws error (Application should quit here)
Main method continues after loader method is finished
Subsequent method is called from main method which also throws an error (because first loader failed)
Lastly my main method throws an error
In total I receive 3 msgboxes with error descriptions.
I must note, that I use the same error handling procedure in all methods, but I would like the code to stop executing, so further code does not trigger any errors.

How can I avoid code any code running after Application.Quit method?
If you want to stop everything, write End after Application.Quit. It stops every piece of VBA and kills all variables you have assigned. This is not considered a good practice (At all!), but it will work exactly as you want.

Related

Run Sub before macro Ends like Finally block

Is there some VBA best practice to use something like 'before end' subroutines?
I am changing Excel's default configs when my macro starts, and before my macro reaches its 'end sub' line I am resetting the configs to its standards.
But what if some error occurs? Am I supposed to define 'On Error' treatment inside all subs to reset the configs to the standard properties?
Just for example, I am changing configs such as:
ScreenUpdating
DisplayStatusBar
Calculation
DisplayAlerts
I'm pretty sure there is no such mechanism that is called unconditionally before exiting a function or a subroutine. You may have though error handlers (but these are executed conditionally; see the comment of ckuhn203 for an example).
However, there is such a mechanism for instances of Class Modules (i.e. for objects). When an object is destroyed (this happens when is not referenced anymore by any variable/storage), its Class_Terminate subroutine is called no-matter-what. If you can wrap your task in such an object that you discard immediately after you create it, you could override overwrite this subroutine to do the cleanup.
If I understand your question correctly, yes, the best way is to define an On Error Goto line, in each method where it's needed, like this:
Public Sub DoSomething()
On Error GoTo Finally ' Try
Application.ScreenUpdating = False
' Do your stuff here
Finally:
Application.ScreenUpdating = True
End Sub
This will ensure the things like ScreenUpdating get done even if there is an error. You can also add a catch block, like this:
Public Sub DoSomething()
On Error GoTo Catch ' Try
Application.ScreenUpdating = False
' Do normal stuff here
GoTo Finally
Catch:
' Do only error stuff here
Finally:
Application.ScreenUpdating = True
End Sub
Generally speaking, GoTo is a hated practice, but for error catching, VBA kind of forces your hand.

Excel VBA - QueryTable AfterRefresh function not being called after Refresh completes

I am developing an Excel (2010+) Application using VBA and have run into an issue where the AfterRefresh event function is not being invoked once the query finishes executing.
I have not been able to find many decent resources or documentation for how to have this event function triggered in a Class Module. I decided to use the Class Module design route instead of putting the event handlers in the worksheet after receiving a response to an earlier question about QueryTables (found here Excel VBA AfterRefresh).
Here is the code for my Class Module called CQtEvents
Option Explicit
Private WithEvents mQryTble As Excel.QueryTable
Private msOldSql As String
' Properties
Public Property Set QryTble(ByVal QryTable As QueryTable): Set mQryTble = QryTable:
End Property
Public Property Get QryTble() As QueryTable: Set QryTble = mQryTble:
End Property
Public Property Let OldSql(ByVal sOldSql As String): msOldSql = sOldSql:
End Property
Public Property Get OldSql() As String: OldSql = msOldSql:
End Property
Private Sub Class_Initialize()
MsgBox "CQtEvents init"
End Sub
' Resets the query sql to the original unmodified sql statement
' This method is invoked when the Refresh thread finishes executing
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
' Problem is here
' This function is never called :( Even if the query successfully runs
Me.QryTble.CommandText = Me.OldSql
End Sub
Here is a quick snapshot of the code the creates an instance of this class, finds a relevant QueryTable, then calls Refresh
Option Explicit
Sub RefreshDataQuery()
'Dependencies: Microsoft Scripting Runtime (Tools->References) for Dictionary (HashTable) object
'From MGLOBALS
cacheSheetName = "Cache"
Set cacheSheet = Worksheets(cacheSheetName)
Dim querySheet As Worksheet
Dim interface As Worksheet
Dim classQtEvents As CQtEvents
Set querySheet = Worksheets("QTable")
Set interface = Worksheets("Interface")
Set classQtEvents = New CQtEvents
Dim qt As QueryTable
Dim qtDict As New Scripting.Dictionary
Set qtDict = UtilFunctions.CollectAllQueryTablesToDict
Set qt = qtDict.Item("Query from fred2")
''' Building SQL Query String '''
Dim sqlQueryString As String
sqlQueryString = qt.CommandText
Set classQtEvents.QryTble = qt
classQtEvents.OldSql = sqlQueryString ' Cache the original query string
QueryBuilder.BuildSQLQueryStringFromInterface interface, sqlQueryString
' Test message
MsgBox sqlQueryString
qt.CommandText = sqlQueryString
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
''' CLEAN UP '''
' Free the dictionary
Set qtDict = Nothing
End Sub
Also here is a screenshot of the Module structure http://imgur.com/8fUcfLV
My first thought on what might be the issue was passing the QueryTable by value. I am not the most experienced VBA developer, but I reasoned this would create a copy and be calling the event on an unrelated table. However, this was not the case and passing by Reference did not fix the problem either.
Also the query is confirmed to run successfully as the data is correctly showing up and being refreshed.
EDIT
I added the BeforeRefresh event function to CQtEvents class Module and confirmed this function is called once Refresh is called
Private Sub mQryTble_BeforeRefresh(Cancel As Boolean)
MsgBox "Start of BeforeRefresh"
End Sub
How might I alter this code get my QueryTable from the QTableModule's RefreshDataQuery() Sub routine to have the AfterRefresh function invoked when the query is successfully ran?
How to catch the AfterRefresh event of QueryTable?
Explanation: in your situation, before event was fired you lost reference of your QueryTable by setting it to nothing when you made cleaning or procedure ended.
General solution: you must be sure that your code is still running and/or you need to keep any references to your QueryTable.
1st solution. When calling QT.Refresh method set the parameter to false in this way:
qt.Refresh false
which will stop further code execution until your qt is refreshed. But I don't consider this solution to be the best one.
2nd solution. Make your classQtEvents variable public and after RefreshDataQuery sub is finished check the status with some other code.
in you CQtEvents class module add the following public variable:
Public Refreshed As Boolean
in your BeforeRefresh event add this:
Refreshed = False
in your AfterRefresh event add this line of code:
Refreshed = True
Make your classQtEvents variable declaration public. Put this before Sub RefreshDataQuery()
Public classQtEvents as CQtEvents
but remove appropriate declaration from within your sub.
Now, even your sub is finished you will be able to check status of refreshment by checking .Refreshed property. You could do it in Immediate or within other Sub. This should work for Immediate:
Debug.Print classQtEvents.Refreshed
3rd solution. (a bit similar to 1st one) Follow steps 1 to 3 from 2nd solution. After you call qt.Refresh method you could add this loop which will stop further code execution until qt is refreshed:
'your code
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
'checking
Do Until classQtEvents.Refreshed
DoEvents
Loop
Final remark. I hope I didn't mixed up qt variable with classQtEvents variable. I didn't tried and tested any solution using your variables but wrote all above with referenced to code I use.
A github repo that demonstrates the minimum code needed to get this working can be found here.
As mentioned, if your event handler isn't in scope, or your QueryTable reference is lost, you won't catch the event. The key factors to ensuring you catch the event are:
Declare a global variable of your event-handling class module's type outside of any subroutines/methods, at the top of a file (I chose the ThisWorkbook file).
Add a Workbook_Open event handler and instantiate that variable there, so that it is available immediately and will remain in scope (since it's global).
At that point, or at any downstream point when you have a QueryTable you're interested in, pass that QueryTable to the global instance to wire up its events.
(It took me a couple tries to figure this out myself, when someone pointed me in this direction as an answer to this question.)

Is it possible the Serialization in .vbs script?

I have combined some of my codes into the below:
Option Explicit
Dim oShell : Set oShell = WScript.CreateObject ("WScript.Shell")
Dim FSO : set FSO = CreateObject("Scripting.FileSystemObject")
Dim StartTime,Elapsed
'msgBox(oShell.CurrentDirectory)
'MsgBox(FSO.GetFile(Wscript.ScriptFullName).ParentFolder )
oShell.CurrentDirectory = FSO.GetFile(Wscript.ScriptFullName).ParentFolder
StartTime = Timer
oShell.run "ParentChildLinkFinal.vbs", 1, True
oShell.run "Parent_Child_Merge_final.vbs", 1, True
oShell.run "CycleTime.vbs", 1, True
oShell.run "Baddata.vbs", 1, True
oShell.run "Matrixrefresh.vbs", 1, True
Elapsed = Timer - StartTime
MsgBox("Total time taken to finish this task:" & Elapsed & "in Seconds")
Now one concern here is- Suppose an error occurs in the any of the CycleTime.vbs file,but the control then going to the next Baddata.vbs file,without killing the Main.vbs file.So how to handle this,If error occurs stop the main.vbs there itself or let the execution to continue.
As Per Daniel
"IF NOT oShell.run( "CycleTime.vbs", 1, True) = 0 then Wscript.Quit would replace the line you are currently using to call "CycleTime.vbs" within Main.vbs
IF oShell.run( "CycleTime.vbs", 1, True) = 99 then Wscript.Quit Would also replace the line you are currently using to call "CycleTime.vbs".
The difference is that you didn't specify how CycleTime.vbs could error out.
If it could fail in such a way that it has a run-time error and simply stops executing, you would want to use the 1st option.
If an error occurs, that you trap with error trapping, you can quit with a code. Like WScript.Quit 99. Then you could check to see if the script ended with that code via the 2nd version that I provided to you.
It really comes down to how you want to handle the failure of your script that you are calling."
WHY SUCH APPROACH
#Daniel True helps the script to suspend its execution until the current .vbs finished. But I am looking for any special value if I can return a special value from each of the .vbs by which main.vbs will get to know that its current .vbs completed successfuly,let's start the next .vbs . But yes iff only success then the next would start. Currently I am seing that suppose an error occurs from any of the .vbs,then as soon as i closed the error window next script started to run. So I am trying atleast thinking iff any success full communication can be sent to the main.vbs. So is it possible?
To expand on what Hiten indicated, you would need to do some checking in your Main.vbs for the return value from your called .vbs files. You do this by utilizing the return value of Run.
For example:
Instead of doing oShell.run "CycleTime.vbs", 1, True do
IF NOT oShell.run( "CycleTime.vbs", 1, True) = 0 then Wscript.Quit
This will exit your current thread of execution if CycleTime.vbs halted due to a run-time error (the call to run the script will return false).
Now if you are able to handle errors gracefully within CycleTime.vbs, you can send a message when you close it by using Wscript.Quit code as explained in the other answer.
So if you want to handle errors gracefully in CycleTime.vbs, you can still indicate an error occurred. Simply check for the return code in your main script with something like this:
IF oShell.run( "CycleTime.vbs", 1, True) = 99 then Wscript.Quit
This way your script will end if CycleTime.vbs ends with code 99. This would happen if within CycleTime.vbs it had code like this:
If err then
'Exit script with code 99 to indicate an error occurred.
Wscript.Quit 99
End if
Use vbscript error handling in main.vbs to stop the code and exit the main.vbs
for more detial click this link:
VBScript -- Using error handling
Sample:
On Error Resume myErrCatch
'Do step 1
'Do step 2
'Do step 3
myErrCatch:
'log error
Resume Next
Create an Exist code from .VBS then exit Main.vbs
DIM returnValue
returnValue = 99
WScript.Quit(returnValue)

How to view VB6 control-level variables in WinDbg?

I have a crash file where I can see that one of my own VB6 user controls is responsible for the crash; i.e. one of its methods is part of the stack trace and I can see the line responsible.
From here, I'd like to inspect the state of its member variables. How do I do this?
Note: I also have the private symbols for my controls. The problem is being able to inspect "Me". The command !object address_of_Me doesn't seem to do the trick and so I'm at a loss.
Thank you.
It's been 10 years since I had to do this in VB6, but I remember a lot of Printer.Print statements in my past life :)
I used to do things like this for debugging (but not for release code)
Sub MySub
On Error Goto ErrorTrap
Dim intX as integer
Dim intY as integer
' do some horrible error here
Exit Sub
ErrorTrap:
Printer.Print "Error"
Printer.Print intX
Printer.Print intY
Printer.Print ...
End Sub
well, codeSMART have one option install global handle on your application first call to SetUnhandledExceptionFilter (win api) is should be installed when load your module main or master form when is closing the program so call to SetUnhandledExceptionFilter.
The code is little long so copy methods names y api calls
Public Sub InstallGlobalHandler()
On Error Resume Next
If Not lnFilterInstalled Then
Call SetUnhandledExceptionFilter(AddressOf GlobalExceptionHandler)
lnFilterInstalled = True
End If
End Sub
Public Sub UninstallGlobalExceptionHandler()
On Error Resume Next
If lnFilterInstalled Then
Call SetUnhandledExceptionFilter(0&)
lnFilterInstalled = False
End If
End Sub
Also here is Record Structure y apis declarations for the module
- CopyMemory
- SetUnhandledExceptionFilter
- RaiseException
' Public enums
-EExceptionType
-EExceptionHandlerReturn
-Private Const EXCEPTION_MAXIMUM_PARAMETERS = 15
' Private record structure
-Private Type CONTEXT
'Structure that describes an exception.
-Private Type EXCEPTION_RECORD
'Structure that contains exception information that can be used by a debugger.
-Private Type EXCEPTION_DEBUG_INFO
-Private Type EXCEPTION_POINTERS
Take a revised that How to route the exe exception back to VB6 app?

vba: passing a variable into error handles

i have a statement:
on error go to label
however i would like to pass into the label the variable which caused the error
is this possible?
You can use Err to get the error No and Description
Sub|Function SomeName()
On Error GoTo Err_SomeName ' Initialize error handling.
' Code to do something here.
Exit_SomeName: ' Label to resume after error.
Exit Sub|Function ' Exit before error handler.
Err_SomeName: ' Label to jump to on error.
MsgBox Err.Number & Err.Description ' Place error handling here.
Resume Exit_SomeName ' Pick up again and quit.
End Sub|Function
First, I think you mean:
on error goto label
And no, you can't pass variables using a goto command. However, you can check the Err.Description for details, and if you are raising your own errors, you can do this:
' Raise a custom error.
Err.Raise Number:=vbObjectError + 1000, _
Source:="TestRaiseCustomError", _
Description:="My custom error description."
So if you are raising your own error, you could set Source to the field that caused the problem.
Refer to the Use the Err Object's Raise Method to Raise Custom Errors section at this link for more info.
I can't think of a clever way to do it. I normally have an error handeling class/function that way I can use "on error goto" to pass the error to the bottom block then call the error handeling function. The advantage of this is it's nice to have a centralised error handler but also you can customise it so in my case I pass the name of the procedure thats crashed. It's not pretty but you could if you really wanted to pass either a collection of variables (dependant on how many you have) or set up something to identify the variable based on the line number (which you'd have to add manauly...)
on error goto err
'Code
err:
ErrorHandeler err, "String with Procedure name"
Declare global variables and use them in the code and in your error code.
Public global_variable1 As Integer
Public global_variable2 As String
Private Sub Btn1234_Click()
....
end sub
Err_abcd:
....
End Sub