Calling a macro from another workbook not always being executed - vba

In Excel Workbook A, I have a macro which
fills inputs for a series of other workbooks
calls the AUTORUN
collects results in a summary tab
The logic is quite simple but I found that Step 2 is not always being executed, which is quite strange... I have confirmed that inputs have been properly entered and I can go each failed Excel workbook and run the simulation by clicking the button linked to AUTORUN. Does anyone have any suggestions on this (I am using EXCEL 2013)? Thanks in advance! Below is how macro from a different workbook is called.
Workbooks(NewFileName).Activate
Application.Run ("'" & NewFileName & "'!AUTORUN")

I run some tests
File 1 (the caller):
The method AAATEST() is in a Module
Public Sub AAATEST()
Dim file As String: file = "test.xlsm"
call Workbooks.Open("C:\temp\" & file)
Workbooks(file).Activate
Call Application.Run("'" & file & "'!AUTORUN")
End Sub
File 2 (with autorun):
The method AUTORUN() is in a Module
Public Sub AUTORUN()
MsgBox "autorun is my life"
End Sub
Result:
I always receive the message when running the AAATEST() method. I suspect a not raised error in the AUTORUN.
To check if the Autorun is run or not, add Stop as first instruction in your Method and execute your code step by step (with F8)
Public Sub AUTORUN()
Stop
...
MsgBox "autorun is my life"
End Sub

Related

Passing VBA execution to another workbook

I though there would be many answers to this question, but didn't find any obvious one.
Is it possible to pass VBA execution to another workbook ?
What I'd like to do is : pass execution from ThisWorbook (WbA) to another workbook (WbB, ie make WbB the new ThisWorkook), then close Wb A.
Edit
What I'm actually trying to do...
I'm looking for a way to update a workbook I'm distributing to a few people and which contains 20 sheets + 10 code modules + 10 userforms.
My current solution
Every time the workbook (WbA) is open, the code will first check if a newer version is available on a network directory. If so, the macro propose to the user to automatically download/save the new workbook (WbB) and export user data from WbA.
But, after that, I'm forced to tell the user it can now closed WbA and open WbB and import the data back in WbB.
I'd rather automate the close WbA / open WbB / import data back too.
You can do this by using Application.OnTime:
Code in WbA.xlsm:
Sub Macro1()
Application.OnTime Now(), "WbB.xlsm!Macro2"
ThisWorkbook.Close
End Sub
Code in WbB.xlsm:
Sub Macro2()
MsgBox ThisWorkbook.Name & " says hello"
End Sub
Because Macro2 is being called by the Application via a scheduled task, it no longer needs WbA to be open.
Note: WbB.xlsm does not even need to be open when Macro1 is run, providing you fully qualify the workbook name:
Code in WbA.xlsm:
Sub Macro1()
Application.OnTime Now(), "'C:\Temp\SO_Test\WbB.xlsm'!Macro2"
ThisWorkbook.Close
End Sub
Code in WbB.xlsm:
Sub Macro2()
MsgBox ThisWorkbook.Name & " says hello"
End Sub
Assuming WbA.xlsm was the only workbook open in Excel when Macro1 was run, you might actually see an empty Excel for a very short time before WbB.xlsm is opened and execution continues.
You just have to leave workbook A open while the code runs, the last line of code you need to execute should be
WorkbookA.Close

VBA Global variables no longer declared after deleting worksheet

I have some public worksheet variables that are first initialized when the workbook is open. I have a button that does this essentially:
Dim Response As Variant
Response = MsgBox("Are you sure you want to delete this worksheet?", vbYesNo + vbExclamation, "Confirm Action")
If Response = vbNo Then
GoTo exit sub
End If
'Save workbook prior to deletion as a precaution
ThisWorkbook.Save
ActiveSheet.Delete
For some reason after this runs, those worksheet variables are no longer declared and I have to reinitialize them every time. I tried adding my InitVariables macro call after the .Delete and it still doesn't work.
Any reason why this might be happening?
The reason is actually really simple - a Worksheet is a class in VBA, and its code module gets compiled along with the rest of your project even if it's empty. When you delete a worksheet and let code execution stop, the next time you run some code the VBE has to recompile the project because you removed a code module. That causes your custom class extensions to lose their state.
Note that this does not happen unless the code stops running and is recompiled. This works just fine:
Sheet1.foo = 42 'foo is a public variable in Sheet1
Sheet2.Delete
Debug.Print Sheet1.foo 'Prints 42
I just tested it using Comintern foo. It's interesting that the standard module foo losses it value but the public foo variable in a worksheet module does not loses it's value.

Shared Excel Document

I've got a Excel documents with macros with input and update forms for information, the document is used as a monthly database where you can run reports from as well.
The issue is that when using the document sometimes the input and update options are used at the same time causing information loss. Both the input and output save at the end of the macro to minimise the losses, but I was wondering if there is anyway of checking at runtime if there is a macro being use by another user and if so delay the next macro run until the other user is finished?
There is one way I can think of. Logically it should work. However I have not tested it.
Create a temp sheet and hide it
When anyone runs a macro, check if cell A1 of that sheet is empty or not
If it is empty then run the macro
Before running the macro, write to that cell and once the macro is run, clear the contents of the other cell
Sandwich your macro code as mentioned below
Code
Sub Sample()
Dim ws As Worksheet
ThisWorkbook.Save
Doevents
Set ws = ThisWorkbook.Sheets("HiddenSheetName")
If Len(Trim(ws.Range("A1").Value)) = 0 Then
ws.Range("A1").Value = "Macro Starts"
ThisWorkbook.Save
Doevents
'
'~~> Rest of your code goes here
'
ws.Range("A1").ClearContents
ThisWorkbook.Save
Doevents
Else
MsgBox "Please try after some time. There is a macro running... Blah Blah"
End If
End Sub
CAUTION: Once the code runs, you cannot undo the changes since the code save the file programatically. The changes are permanent. In a file which is not shared, you can undo by closing the file without saving and re-opening it.

Why doesn't VBE.ActiveCodePane.CodeModule work when the VBE (code window) isn't open?

Create a form that runs the following code.
MsgBox (VBE.ActiveCodePane.CodeModule)
And this message appears.
Now save, close, and reopen the database, and see this message:
Run-time error '91': Object variable or With block variable not set
If you open the Visual Basic Editor, it runs again. Even if you close the VBE, it still runs.
But when you close the whole application and reopen it, leaving the VBE closed, you get the error.
Why? What's going on here?
You reference the active pane object. The object isn't set until a pane gets activated. So before you open the VBE, the object is not set yet. Once you close the VBE, the object remains, so you can still reference it.
To get a handle to the ActiveCodepane object, without opening the VBE, is by activating a VBComponent, like this:
VBE.ActiveVBProject.VBComponents("Module1").Activate
You can activate any VBComponent like this.
Well when the VBE is closed upon the first opening of the application, there is no ActiveCodePane, you can check this in a conditional upon loading your form:
If (Application.VBE.ActiveCodePane Is Nothing) Then MsgBox "ActiveCodePane is Nothing"
The VBE exists and properties and methods can be used, but there is no ActiveCodePane which is why you're receiving the null reference exception. Just opening the VBE will still produce your error if you closed all CodePanes before saving and closing previously (unless a module exists for some reason). You must the explicitly open a CodePane, to set the 'ActiveCodePane' property.
This makes sense. What is it that you're trying to access via the ActiveCodePane property? Perhaps I can help find a way around?
Edit
Presumably, as you develop this Form and associated Modules, you'll know what they're called, and would be able to use a different method than the ActiveCodePane, such as that which #Bas Verlaat mentioned. Alternatively, you can loop through each code pane in the active VBProject and try and match on a name or something:
Option Compare Database
Option Explicit
Private vbProj As VBIDE.VBProject
Private vbComp As VBIDE.VBComponent
Private vbMod As VBIDE.CodeModule
Private Sub Command0_Click()
Set vbProj = Application.VBE.ActiveVBProject
For Each vbComp In vbProj.VBComponents
MsgBox vbComp.CodeModule
Next
End Sub
As Bas Verlaat said, before you open the VBE, the object is not set yet.
Obviously the VBE won't be open when a user is using the program. Referencing the Active Code Pane should only be done when developing and debugging, and never in production.
For example the following custom made error message is great for debugging, but will fail if deployed to production.
ErrorHandler:
MsgBox "Error " & Err.number & ": " & Err.Description & " in " & _
VBE.ActiveCodePane.CodeModule, vbOKOnly, "Error"
Approach
Since sometimes it can't be avoided to use VBE functionality, the general solution for me is this ...
make sure to initialize/activate the relevant workbooks VB project (components)
make sure to initialize/activate other workbooks VB projects (components) after they are opened (if their VBE properties/code may be relevant later in the process)
VbeInit usage
To do this one could call (see code below)
VbeInit in the beginning of some event (e.g. the Workbook_Open() event) and
VbeInit someOpenedWorkbook whenever a relevant workbook was just opened (e.g. using Workbooks.Open(...))
Example Case
It works for the simpler above case.
A more complex case, where this is very obvious and necessary, is when you rely on the CodeName of sheets in workbooks. E.g.
if the user is allowed to reName some sheets, but they should still be uniquely identifiable by the VB application, via their CodeName and
such sheets (e.g. serving as template sheets) are copied (sheet.Copy ...) to other workbooks.
Then the sheets CodeName will only be copied, if the sheets VBComponent has been activated (e.g. via myVBComponent.Activate or opening the source-sheet-containing workbook in the VBE).
The VbeInit procedure
Procedure VbeInit(Optional wb As Workbook)
With Application.VBE
Dim pj As VBProject: For Each pj In .VBProjects
'ignore unsaved (=> fully initialized) workbooks
If Not wb Is Nothing Then If wb.FullName <> VBProjFilename(pj) Then GoTo continue
Dim c As VBComponent: For Each c In pj.VBComponents
c.Activate
Next c
continue:
Next pj
End With
End Procedure
Function VBProjFilename(pj As VBProject) As String
On Error Resume Next 'leave result empty if workbook (code) not saved yet
VBProjFilename = pj.Filename
End Function

Cancel External Query in Excel VBA

I have created an Excel Spreadsheet which helps with data analysis from an Oracle database.
The user enters then clicks the "Refresh Query" button which generates a query for Oracle to execute. The query takes a minute or so to complete. Although the VBA code does not hang on ".Refresh", all Excel windows remain frozen until the query completes.
Sub refreshQuery_click()
Dim queryStr as String
' Validate parameters and generate query
' ** Code not included **
'
' Refresh Query
With ActiveWorkbook.Connections("Connection").OLEDBConnection
.CommandText = queryStr
.Refresh
End With
End Sub
Is there a way for the user to manually cancel the query (calling .CancelRefresh) while the Excel user-interface is frozen?
EDIT I don't know if the following is worth noting or regular behavior. While the query is executing, all open Excel windows (including the VBA Editor) become "Not Responding" in Task Manager. Neither pressing Esc nor Ctrl+Break will cancel the script. Also, calling DoEvents (either before or after .Refresh) does not change this behavior.
Here's a method that I know will work. However, there are some complications.
Here's how it's done:
Put the spreadsheet with the data in a separate workbook. This worksheet should execute the refresh query when it's opened and then close once the data is updated.
Create a batch file to call the "Data" Excel file.
Within a different workbook, create a procedure (macro) for the user to call. This procedure will call the batch file, which subsequently calls the Excel file. Since you are calling a batch file and not Excel directly, the Excel procedure will continue because the command shell is released so quickly and opens the other Excel file in a different thread. This allows you to continue working within the main Excel file.
Here are some complications:
I included a method to alert the user that the data has been udpated. There are timing issues where it's possible to try to check if the data has been update when the workbook is not accessible, which forces the user to try to update values. I included a method called my time which pauses the execution of the code so it only checks every so many seconds.
The updated worksheet will pop up in a new window, so the user will need to click on their original worksheet and keep working. You could learn to hide this if you're comfortable with Windows scripting (I haven't learned that yet).
Here are some files and code. Be sure to read the comments in the code for why some things are there.
FILE: C:\DataUpdate.xls
We'll make a workbook called "DataUpdate.xls" and put it in our C:\ folder. In cell A1 of Sheet1, we'll add our QueryTable which grabs external data.
Option Explicit
Sub UpdateTable()
Dim ws As Worksheet
Dim qt As QueryTable
Set ws = Worksheets("Sheet1")
Set qt = ws.Range("A1").QueryTable
qt.Refresh BackgroundQuery:=False
End Sub
Sub OnWorkbookOpen()
Dim wb As Workbook
Set wb = ActiveWorkbook
'I put this If statement in so I can change the file's
'name and then edit the file without code
'running. You may find a better way to do this.
If ActiveWorkbook.Name = "DataUpdate.xls" Then
UpdateTable
'I update a cell in a different sheet once the update is completed.
'I'll check this cell from the "user workbook" to see when the data's been updated.
Sheets("Sheet2").Range("A1").Value = "Update Table Completed " & Now()
wb.Save
Application.Quit
End If
End Sub
In the ThisWorkbook object in Excel, there's a procedure called Workbook_Open(). It should look like the following so it executes the update code when it is opened.
Private Sub Workbook_Open()
OnWorkbookOpen
End Sub
NOTE: I found a bug when this file closed if 1) you accessed the file from the command line or shell and 2) you have the Office Live Add-in installed. If you have the Office Live Add-in installed, it will throw an exception on exit.
FILE: C:\RunExcel.bat
Next, we're going to create a batch file that will open the Excel file we just made. The reason that call the Excel file from within the batch file and not directly from the other Excel file using Shell is because Shell will not continue until the other application closes (at least when using Excel.exe "c:\File.xls"). The batch file, however, runs its code and then immediately closes, thus allowing the original code that called it to continue. This is what will let your uses continue working in Excel.
All this file needs is:
cd "C:\Program Files\Microsoft Office\Office10\"
Excel.exe "C:\DataUpdate.xls"
If you're handy with Windows Scripting, you do fancy things like open the window in a hidden mode or pass a parameter of the file name or Excel location. I kept it simple with a batch file.
FILE: C:\UserWorkbook.xls
This is the file that the user will open to "do their work in." They'll call the code to update the other workbook from within this workbook and they'll still be able to work in this workbook while this one is updating.
You need a cell in this workbook where you'll check the "Update Table Completed" cell from the DataUpdate workbook. I chose cell G1 in Sheet1 for my example.
Add the following code to a VBA module in this workbook:
Option Explicit
Sub UpdateOtherWorkbook()
Dim strFilePath As String
Dim intOpenMode As Integer
Dim strCallPath As String
Dim strCellValue As String
Dim strCellFormula As String
Dim ws As Worksheet
Dim rng As Range
Set ws = Worksheets("Sheet1")
Set rng = ws.Range("G1")
strCellFormula = "='C:\[DataUpdate.xls]Sheet2'!A1"
'This makes sure the formula has the most recent "Updated" value
'from the data file.
rng.Formula = strCellFormula
strFilePath = "C:\RunExcel.bat"
intOpenMode = vbHide
'This will call the batch file that calls the Excel file.
'Since the batch file executes it's code and then closes,
'the Excel file will be able to keep running.
Shell strFilePath, intOpenMode
'This method, defined below, will alert the user with a
'message box once the update is complete. We know that
'the update is complete because the "Updated" value will
'have changed in the Data workbook.
AlertWhenChanged
End Sub
'
Sub AlertWhenChanged()
Dim strCellValue As String
Dim strUpdatedCellValue As String
Dim strCellFormula As String
Dim ws As Worksheet
Dim rng As Range
Set ws = Worksheets("Sheet1")
Set rng = ws.Range("G1")
strCellFormula = "='C:\[DataUpdate.xls]Sheet2'!A1"
strCellValue = rng.Value
strUpdatedCellValue = strCellValue
'This will check every 4 seconds to see if the Update value of the
'Data workbook has been changed. MyWait is included to make sure
'we don't try to access the Data file while it is inaccessible.
'During this entire process, the user is still able to work.
Do While strCellValue = strUpdatedCellValue
MyWait 2
rng.Formula = strCellFormula
MyWait 2
strUpdatedCellValue = rng.Value
DoEvents
Loop
MsgBox "Data Has Been Updated!"
End Sub
'
Sub MyWait(lngSeconds As Long)
Dim dtmNewTime As Date
dtmNewTime = DateAdd("s", lngSeconds, Now)
Do While Now < dtmNewTime
DoEvents
Loop
End Sub
As you can see, I constantly updated the formula in the "Listening Cell" to see when the other cell was updated. Once the data workbook has been updated, I'm not sure how you'd force an update in code without rewriting all the cells. Closing the workbook and reopening it should refresh the values, but I'm not sure of the best way to do it in code.
This whole process works because you're using a batch file to call Excel into a different thread from the original file. This allows you to work in the original file and still be alerted when the other file has been updated.
Good luck!
EDIT: Rather than include a more complete answer in this same answer, I've created a separate answer dedicated entirely to that solution. Check it out below (or above if it gets voted up)
Your users can break the VBA function by pressing Ctrl+Break on the keyboard. However, I've found that this can cause your functions to randomly break until each time any function is run. It goes away when the computer is restarted.
If you open this file in a new instance of Excel (meaning, go to Start > Programs and open Excel from there), I think that the only workbook that will be frozen will be the one executing the code. Other intances of Excel shouldn't be affected.
Lastly, you might research the DoEvents functions, which yields execution back to the Operating System so that it can process other events. I'm not sure if it would work in your case, but you could look into it. That way you can do other things while the process is being completed (It's kind of dangerous because the user can then change the state of your application while the process is working).
I believe I know a way that actually will work, but it's complicated and I don't have the code in front of me. It involves creating a separate instance of the Excel application in code and attaching a handler to the execution of that instance. You include the DoEvents part of the code in a loop that releases once the application closes. The other instantiated Excel application has the sole purpose of opening a file to execute a script and then close itself. I've done something like this before so I know that it works. I'll see if I can find the code tomorrow and add it.
Well, you could consider the old-fashion way -- split the query into smaller batches and use Do Events in between batches.
You could try XLLoop. This lets you write excel functions (UDfs) on an external server. It includes server implementations in many languages (eg. Java, Ruby, Python, PHP).
You could then connect to your oracle database (and potentially add a caching layer) and serve up the data to your spreadsheet that way.
The XLL also has a feature to popup a "busy" GUI that lets the user cancel the function call (which disconnects from the server).
BTW, I work on the project so let me know if you have any questions.