VBScript/VBA quitting an Excel application is affected by standard Excel events - vba

I use Task Scheduler to manage a VBScript which starts a vba sub procedure in Workbook X. This procedure opens two other Workbooks and move data to Workbook X and closes them again. The procedure outcome is an updated Workbook X which I need to save. The VBA procedure works fine and is triggered by the VBScript as expected. However, the part of the VBScript which should save the file doesn't work. The VBScript is based on code I have found on Stackoverflow.com and other sites.
When I open the file X I tried to save with the VBScript, It asks me to decide whether I want to save the file. If I do, I can see the data the above procedure has created has worked.
I can see in the Task Manager that the Excel instance I have created with the VBScript is never closed although I tried to do it in the VBScript. I have also tried the Save method, but with no luck. I have tried different parameters for the methods with no luck. I have search on this site and other sites for how the issue could be solved. However, they suggest Save and SaveAs methods and I tried them with the different suggested parameter configurations without success.
I have now found the root cause but not the solution. It has nothing to do with the script itself. In the open workbook event.
I have code which activate a userform (StartUpForm.Show). In the workbook before close and open events I call procedure RemoveFilter which remove a filter on a set of filtered data. If I mark UserForm.Show and the RemoveFilter in the before close workbook event as notation it works. It is not enough to avoid call StartUpForm.Show. StartUpForm.Show needs to be marked as notation or deleted. How can that be? And is there a workaround?
This doesn't work:
Private Sub Workbook_Open()
Dim batRun As Boolean
RemoveFilter
ResolveStartUp (batRun)
If Not batRun Then
StartUpForm.Show vbModal
End If
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
RemoveFilter
End Sub
This works:
Private Sub Workbook_Open()
Dim batRun As Boolean
RemoveFilter
ResolveStartUp (batRun)
If Not batRun Then
' StartUpForm.Show vbModal
End If
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
' RemoveFilter
End Sub
VBScript:
On Error Resume Next
ImportAccesFilesToHolidaysAndWorkshops
Sub ImportAccesFilesToHolidaysAndWorkshops()
Dim xlApp
Dim xlBook
Set xlApp = CreateObject("Excel.Application")
xlApp.DisplayAlerts = False
Set xlBook = xlApp.Workbooks.Open ("Z:\ResourceManagement\Holiday and workshops input.xlsm", 0, False,,,,,,,,True)
xlApp.Run "ImportResourcesAndProjects"
xlApp.ActiveWorkbook.Close
xlBook.SaveAs "Z:\ResourceManagement\Holiday and workshops input.xlsm",,,,,,xlExclusive, xlLocalSessionChanges
xlBook.Close
Set xlBook = Nothing
xlApp.Quit
Set xlApp = Nothing
WScript.Echo "Finished."
WScript.Quit
End Sub

Try saving to your C: drive. If that works you are probably running into a scheduled task problem. If the task runs whether or not a user is logged in or it is set to use highest privileges, you aren't running under the security context you think you are. In either of those cases job scheduler will use S4U authentication which will prevent you from accessing network resources. I'm guessing that Z: drive is a mapped network drive. Mapped drives belong to the user who mapped the drive so it won't be available unless you are running as the user that mapped the drive and that user is logged in.
If you use "run with highest privileges", you are actually running under the built in Administrator account. That is a separate account and it won't have your drive mappings.
Task Security Context
Some Programs Cannot Access Network Locations When UAC Is Enabled
windows 7 scheduled tasks run with highest privileges

Related

Workbook.Activate method acting strangely

I have an Excel add-in running a procedure that displays an OKOnly MsgBox if a certain criteria is not met, and attempts to close a userform, activate a specific workbook, and terminate code execution. In the UserForm_Terminate() event I have:
Private Sub UserForm_Terminate()
Debug.Print ActiveWorkbook.Name
Application.ScreenUpdating = True
wbk.Activate
sht.Activate
Debug.Print ActiveWorkbook.Name
End
End Sub
If I begin running the procedure with a new blank workbook active, that workbook is still the active workbook when code terminates, but both print statements above indicate that the target wbk is actually active. However if I use a breakpoint and step through wbk.Activate, the target wbk is activated as expected. The workbook and worksheet objects are both available and there is no error. Any ideas why Workbook.Activate is not behaving as expected during execution? I expected turning screenupdating on would solve my issue but no dice.
#barX He's not relying on ActiceWorkbook for code, he's just checking it to see if it's working ....
BTW Welcome to the Excel 2013 SDI bug!
Maybe following the proper way of initializing/using a Userform will help. See Mathieu's RubberDuck article on Userform.Show
On a related note, maybe changing the Parent Window Handle for the form before closing might work as well. See Keeping Userforms On Top Of SDI Windows In Excel 2013 And Up
I'm not sure if the SDI bug is my issue but it did lead me to an answer (thanks #Profex). I was not able to reproduce that bug, and there is also another path in my procedure that ends with the same Userform_Terminate() event and does reactivate the target wbk, though I can't determine what is causing the differing functionality. Nonetheless, one solution to that issue was to hide and then show the active window and that suffices in this case, though probably not ideal in many situations:
Private Sub UserForm_Terminate()
Dim win As Window
wbk.Activate
sht.Activate
Set win = Application.ActiveWindow
win.visible = False
win.visible = True
End
End Sub

How to turn on 'Snap To Grid' on Excel start up with an add-in

I am looking for a way to automatically turn on Snap to Grid every time I start Excel.
I worked out this code and put it in an Add-In (.xlam) that I always load on Excel start-up.
Private Sub Workbook_Open()
Dim cbc As CommandBarControl
Set cbc = Application.CommandBars.FindControl(ID:=549)
If Not cbc.Enabled Then cbc.Execute
End Sub
But when I start Excel, it throws this error at cbc.Execute:
Can anyone tell me what is wrong?
I think you have two problems:
Problem 1
I believe that what you are trying to do with the Enabled property is to check the toggle state of the control. Instead, I think you want the State property instead (which doesn't show in intellisense). Your code should be something like this:
Public Sub ActivateSnapToGrid()
Dim cbc As CommandBarControl
Set cbc = Application.CommandBars.FindControl(ID:=549)
If Not cbc Is Nothing Then
If cbc.Enabled Then
'if snap to grid is off...
If cbc.State = 0 Then
cbc.Execute
'State should now = -1
End If
End If
End If
End Sub
Problem 2
The 'Snap to grid' control is not enabled if there is no workbook present (check this by closing all workbooks). In its current state, your code tries to execute in this case i.e. If Not cbc.Enabled Then cbc.Execute because I think you are trying to check if it is 'on' not if it is enabled.
Because an Excel add-in will load as a 'hidden' workbook, I don't believe it would enable the 'Snap to Grid' command bar control. Therefore, in the Workbook_Open event of the add-in then the control will be disabled and that's why you get the error.
You need an application-level event handler in your add-in. This is very common for Excel add-ins. See here on MSDN for some explanation. Plus also see this article by Chip Pearson which is very useful. It will allow you to write an event for any Workbook_Open event generated after your add-in loads.
So you will end up with this code (per Chip Pearson) in your add-in to call the sub I presented above (in Problem 1 section):
Private WithEvents App As Application
Private Sub Workbook_Open()
Set App = Application
End Sub
Private Sub App_NewWorkbook(ByVal Wb As Workbook)
Debug.Print "New Workbook: " & Wb.Name
Call ActivateSnapToGrid
End Sub
I haven't tested this thoroughly as it can't be known how your wrote your add-in. However, this is a very (if not the) standard pattern for doing this kind of stuff. See MSDN and the Chip Pearson article and you will figure out which code goes in a Class, in a Module etc.

Access VBA: SUB designed to refresh an Excel Spreadsheet giving an error

Relative VBA newbie here. I created a new Module "QCDataRefresh" with a Public Sub "RefreshExcel" that is designed to open an Excel spreadsheet (housed on my network drive) in the background and refresh it. This is done in order to refresh the many data links contained within said spreadsheet.
Here is the code for this Sub:
Public Sub RefreshExcel()
Dim appExcel As Object
Set appExcel = CreateObject("Excel.Application")
appExcel.workbooks.Open ("\\renssfile2\shares\Supply Chain Project Management\QCData.xlsx")
appExcel.activeworkbook.refreshall
Set appExcel = Nothing
End Function
Running this gives a run-time error '1004': "Application Defined or object-defined error. Debugging seems to highlight the line of code "appExcel.activeworkbook.refreshall". Not sure why though?
Additionally, I am also attempting to create a button in Access to run this from my dashboard. To do this, I created a new button, and created a new Event Procedure for this entitled, "Command101_Click".
The following is the code for that Event, which attempts to run my RefreshExcel Sub upon clicking the button:
Private Sub Command101_Click()
QCDataRefresh.RefreshExcel
End Sub
When I run this Event, I would expect it to give me the same error from my refresh Function above. However, instead it opens a window asking me to select a Macro. I'm unsure what my mistake is here, but I'm sure that it must be a simple oversight. Thoughts on this as well?
Thanks all!
I think the problem is that the workbooks.open method returns a workbook.
Try this:
Public Sub RefreshExcel()
Dim appExcel As Object
Dim excelwb As Excel.Workbook
Set appExcel = CreateObject("Excel.Application")
Set excelwb = appExcel.workbooks.Open ("\\renssfile2\shares\Supply Chain Project Management\QCData.xlsx")
excelwb.refreshall
Set excelwb = Nothing
Set appExcel = Nothing
End Sub
Whenever I have unexpected and unusual issue\error for using excel to output to or read from I allways try the following, especially when the code breaks in the middle and doesn't get to the line where you close the spreadsheet and close the Excel object from memory.
So close your spreadsheet and any Excel instances and objects you may have as follow:
Close all Excel files and excel program if you have any open
Go to the Task Manager (by doing Alt+Ctrl+Del) then select Task Manager
Go to processes tab
Sort by process name
Look for any Excel.exe in the list and force close them all one by one
Let us know.

Application.Calculation depending on workbook

I'm managing a workbook with more than 200 000 formulas (some really complicated array formulas) which means that I can't let Excel automatically calculate all the cells every time I click somewhere (it takes around 8 hours to calculate everything).
Instead of that, the calculation is set to manual and I have the following VBA code executed when Calculation.xlsm is opened:
With Application
.CalculateBeforeSave = False
.Calculation = xlCalculationManual
End With
I use custom buttons to calculate only some parts of the 200k cells when needed.
I noticed that Excel does keep track of that setting in each workbooks, which means that if I open my Calculation.xlsm, Excel remembers that the calculation is set to manual. If I open my Values.xlsx, Excel does remember that the calculation is set to automatic. This was before I tried to copy values from Calculation.xlsm to Values.xlsx.
Now, because I'm using VBA in Calculation.xlsm to copy values to Values.xlsx, Excel does apply the Application.Calculation setting to that workbook too, which means that if I open it with a new instance of Excel, the calculation will still be set to manually.
If I add a Application.Calculation = xlCalculationAutomatic before closing the Values.xlsx with VBA in my Calculation.xlsm workbook, it will work, but Excel will also start to compute the 200k cells in my Calculation.xlsm workbook, which I obviously don't want.
So my question is about how to actually set the calculation of Excel based on a specific workbook instead of with the Application object. This is based on the fact that Excel does keep track of that setting depending on which workbook is opened (you can just do the test and create 2 different .xlsx files, one with the calculation enabled and the other with the calculation disabled and Excel will remember these settings).
I know I could use the Worksheets.Range.Calculate method to calculate my Values.xlsx workbook before closing it, but the calculation will still be set to manual if I open it in a new instance of Excel after that.
EDIT 3:20pm: Not sure if I was clear enough, English isn't my native language. In short, I have Calculation.xlsm with VBA and Calculation set to manual. I have Values.xlsx with no VBA and Calculation set to automatic. If I open Values.xlsx with the following VBA code in Calculation.xlsm, Excel will automatically convert my Values.xlsx workbook to manual calculations.
Calculation.xlsm code:
Private Sub Workbook_Open()
With Application
.CalculateBeforeSave = False
.Calculation = xlCalculationManual
End With
End Sub
Sub someFunction()
Set WB = Application.Workbooks.Open("Values.xlsx")
Set WBws = WB.Sheets("mySheet")
DoEvents
wb.Save
WB.Close
End Sub
After the execution of someFunction(), Values.xlsx calculation is set to manual. That's the problem. I would like it to stay on automatic (and I can't add VBA to that file, it must be all done from Calculation.xlsm like above).
EDIT 3:40pm: Could I just have my big workbook with Application.Calculation set to manual, put all the data I need in the clipboard (I only need the values, not the formulas), close it (will the VBA still continue to execute even if I close the workbook from which it is executed?), set Application.Calculation to Auto (since there is no open workbook), then open the destination workbook to paste the values (will Excel still keep the data in the clipboard since the other workbook is closed?), then save and close that workbook, set back the calculation to manual (no workbook opened) and reopen the original workbook from which the code was executed?
One way to do this would be to create a new instance of Excel. While this is probably slower, and might be more difficult to work with in cases where you don't close the book/application within the function, but for simple case like your example, it may be easiest to implement:
Sub someFunction()
Dim newExcel as Excel.Application
Set newExcel = CreateObject("Excel.Application")
Set WB = newExcel.Workbooks.Open("Values.xlsx")
Set WBws = WB.Sheets("mySheet")
DoEvents
wb.Save
WB.Close
newExcel.Quit
Set newExcel = Nothing
End Sub
The Application.Calculation property is relative to that instance of the application, not other instances.
Alternatively, you can use an application-level event handler. I suspect this might be faster but I have not tested it for speed.
Modified slightly from this very similar question (which also asks about conditionally disabling an Application-level property).
If:
I was just worrying about if the code would still be executed if I close the workbook from which it is launched
Then just use the normal Workbook_BeforeClose event handler to restore the desired Application.Calculation property (for the entire application/all other open workbooks).
The rest of the answer:
Create an application-level event handler, create a class module named cEventClass and put this code in it:
Public WithEvents appevent As Application
Dim ret
Private Sub appevent_WorkbookActivate(ByVal wb As Workbook)
Call ToggleCalculation(wb, ret)
End Sub
Use the following in a standard module named mod_Caclulate:
Option Explicit
Public XLEvents As New cEventClass
Sub SetEventHandler()
If XLEvents.appevent Is Nothing Then
Set XLEvents.appevent = Application
End If
End Sub
Sub ToggleCalculation(wb As Workbook, Optional ret)
If wb.Name = ThisWorkbook.Name Then
ret = xlCalculationManual
Else
ret = xlCalculationAutomatic
End If
Application.Calculation = ret
End Sub
Put this in the Workbook_Open event handler of the workbook which you always want to be manual calculation:
Option Explicit
Private Sub Workbook_Open()
'Create the event handler when the workbook opens
Call mod_Caclulate.SetEventHandler
Call mod_Caclulate.ToggleCalculation(Me)
End Sub
This will create the event handler only when the specific workbook is opened, and the handler will toggle the Calculation property whenever you switch views to a different workbook.
Note: If you "end" run-time or do anything while debugging which would cause state loss, you will lose the event handler. This can always be restored by calling the Workbook_Open procedure, so an additional safeguard might be to add this also in the ThisWorkbook code module:
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
' Additional safeguard in case state loss has killed the event handler:
' use some workbook-level events to re-instantiate the event handler
Call Workbook_Open
End Sub

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.