VBA ontime cancel scheduling - excel

I have written a macro that runs at 15:30pm every workday when a workbook is first opened.
When the workbook is closed it tries to open itself the next time the macro is scheduled to run. I have tried to turn the scheduler to false and am getting an error. Code below.
Has anyone any ideas why this isn't working?
Private Sub Workbook_Open()
Application.OnTime TimeValue("15:30:00"), "MacroTimeTest"
End Sub
public dtime as date
Sub MacroTimeTest()
dtime = (Format(Application.Evaluate("workday(today(), 1)"), "DD/MM/YY") & " " & TimeValue("15:30:00"))
'other code has been deleted doesn't affect dtime variable
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
'I have tried replacing false with 0 etc but it didn't make a difference
Application.OnTime earliesttime:=dtime, procedure:="MacroTimeTest", schedule:=False
End Sub

I think that you should keep a reference to the time so that you can cancel the action. You can only cancel an action if it hasn't already executed.
In ThisWorkbook enter the following to run the macro at 15:59 until the sheet is closed
Option Explicit
Private Sub Workbook_BeforeClose(Cancel As Boolean)
On Error GoTo CouldNotCancel
Application.OnTime dTime, "MacroTimeTest", , False
Debug.Print "Cancelled task to run at " & dTime
Debug.Print "Workbook close"
Exit Sub
CouldNotCancel:
Debug.Print "No task to cancel"
End Sub
Private Sub Workbook_Open()
Debug.Print "Workbook open"
dTime = TimeValue("15:59:00")
Debug.Print "Next run time " & dTime
Application.OnTime dTime, "MacroTimeTest"
End Sub
Then add your macro to a Module
Option Explicit
Public dTime As Date
Public Sub MacroTimeTest()
'schedule next run
dTime = TimeValue("15:59:00")
'schedule next run
Debug.Print "Scheduling next run at " & dTime
Application.OnTime dTime, "MacroTimeTest"
Debug.Print "Running macro"
End Sub
This way the same value of dTime will be used to cancel the scheduled task as was used to create it.
If no further task has been scheduled i.e. by an error in MacroTimeTest then the Workbook close event will handle the error.
To see the debug output look at the immediate window in the VBA Editor (Ctrl+G)

Related

Workbook.close Cannot run the marco "https://wfp-mysharepoint.com............"

Background infromation:
This workbook is on shared point and I'm trying to run a Marco in Desktop app.
The objective on the marco is to close the workbook automatically after 10 mins. I want to ensure that users do not keep the workbook open without using it.
However, when I run the below code, I get an error message
"Cannot run the marco "https://wfp-mysharepoint.com............" This Macro may not be available in this workbook or marcos maybe disabled.
The code is as follows:
Private Sub Workbook_Open()
Picktime
End Sub
Sub Picktime()
savetime = Hour(Now) & ":" & Minute(Now) + 1 & ":" & Second(Now)
Application.OnTime savetime, "Please_close"
End Sub
Sub Please_close()
ThisWorkbook.Close (True)
End Sub
The procedure Workbook_Open is an event (that runs automatically) on opening of the workbook and therefore has to be in the scope of the workbook ThisWorkbook. Other events that are worksheet related are located in the scope of their worksheet.
All other code should go into a normal module. Especially code that needs to be found by Application.OnTime can only be located in a normal module.
So in ThisWorkbook:
Private Sub Workbook_Open()
Picktime
End Sub
In a normal module:
Public Sub Picktime()
Dim SaveTime As Variant
SaveTime = Now() + TimeValue("00:01:00")
Application.OnTime EarliestTime:=SaveTime, Procedure:="Please_close"
End Sub
Public Sub Please_close()
ThisWorkbook.Close SaveChanges:=True
End Sub
Note that it is easer to add one minute to Now() by using Now() + TimeValue("00:01:00")

How to Fire OnTime Event by a Workbook.Close Statement?

It appears that if the OnTime event is registered by a programmatic MyBook.Close statement, then OnTime never runs.
This code works fine:
Sub TestOnTime()
Application.OnTime Now + TimeValue("00:00:05"), "MySub"
End Sub
Sub MySub()
Debug.Print "hello"
End Sub
Run TestOnTime. MySub will execute, as expected.
And this code runs fine:
ThisWorkbook:
Dim WithEvents oApp As Application
Private Sub Workbook_Open()
Set oApp = Application
End Sub
Private Sub oApp_WorkbookBeforeClose(ByVal Wb As Workbook, Cancel As Boolean)
Application.OnTime Now + TimeValue("00:00:05"), "MySub"
End Sub
Module 1:
Sub MySub()
Debug.Print "hello"
End Sub
Manually close another workbook to fire oApp_WorkbookBeforeClose.
MySub executes, as expected.
But this code fails. The OnTime event never runs.
Book 1
ThisWorkbook:
Dim WithEvents oApp As Application
Private Sub Workbook_Open()
Set oApp = Application
End Sub
Private Sub oApp_WorkbookBeforeClose(ByVal Wb As Workbook, Cancel As Boolean)
Application.OnTime Now + TimeValue("00:00:05"), "MySub"
End Sub
Module 1:
Sub MySub()
Debug.Print "hello"
End Sub
Book 2
Module 1:
Sub Test()
ThisWorkbook.Close
End Sub
Run Test to close Book 2.
Book 1 oApp_WorkbookBeforeClose
executes, as expected.
But the Book 1 MySub event never runs.
Why?
Why doesn't OnTime execute if registered by a Workbook_BeforeClose event? No code is running in the book that's closing. OnTime works no problem with other events (eg programmatically opening a workbook). Somehow, closing a workbook programmatically breaks OnTime. Why?
As Book 2 is being closed, You should include the Application.OnTime procedure in Book 2 and not in Book 1
Also, I think those books should be saved once and not new books.
Sub test()
Application.OnTime Now + TimeValue("00:00:05"), "Book 1.xlsm!MySub"
ThisWorkbook.Close
End Sub
EDIT Jul 6 -
You are closing the workbook and then you are trying to run a macro MySub in the same workbook after 5 seconds. Macro in the same workbook will not run once the book is closed. Application will reopen the file to run the macro. If you want to close Book2 after 5 seconds of closing Thisworkbook then --
in Thisworkbook
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Application.OnTime Now + TimeValue("00:00:05"), "Book2.xlsm!Test"
End Sub
So, after closing Thisworkbook, macro named "Test" in Book2 will run and will close that workbook.

How to detect when a workbook is closing?

The Workbook.BeforeClose event triggers when the workbook is about to close but before the saving message prompt which allows cancelling it.
How can I detect when the workbook is already closing past the point where it can be cancelled without removing nor replacing the saving message with a custom one?
One workaround I have found online is to use the event together with the Workbook.Deactivate event which looks like this:
Code in the workbook:
Private Sub Workbook_BeforeClose(ByRef Cancel As Boolean)
closing_event = True
check_time = VBA.Now + VBA.TimeSerial(Hour:=0, Minute:=0, Second:=1)
Excel.Application.OnTime EarliestTime:=check_time, Procedure:="disable_closing_event"
End Sub
Private Sub Workbook_Deactivate()
If closing_event Then
VBA.MsgBox Prompt:="Closing event."
Excel.Application.OnTime EarliestTime:=check_time, Procedure:="disable_closing_event", Schedule:=False
End If
End Sub
Code in a module:
Public closing_event As Boolean
Public check_time As Date
Public Sub disable_closing_event()
closing_event = False
End Sub
One very specific edge case where it triggers incorrectly is if you click to close the workbook and in less than one second close the saving message (press Esc to do it fast enough) and change to another workbook (Alt + Tab) it triggers the Deactivate event with the closing_event condition variable still set to True because disable_closing_event has still not set it to False (scheduled by Application.OnTime for when one second goes by).
I would like to find a solution that isn't so much of a workaround and that works correctly against that edge case.
Edit:
The accepted answer has the best solution in my opinion out of all the current answers. I have modified it for my needs and preference to the following code in the workbook:
Private WorkbookClosing As Boolean
Private Sub Workbook_BeforeClose(Cancel As Boolean)
WorkbookClosing = True
End Sub
Private Sub Workbook_Deactivate()
If WorkbookClosing And ThisWorkbook.Name = ActiveWindow.Caption Then
Workbook_Closing
Else
WorkbookClosing = False
End If
End Sub
Private Sub Workbook_Closing()
MsgBox "Workbook_Closing event."
End Sub
This is an evolution of my 1st Answer - it detects the edge case problem by comparing the ActiveWindow.Caption against ThisWorkbook.Name so it can detect that issue and deal with it. It's not the most elegant solution but I believe it works.
All Code in the Workbook most of it in DeActivate
Public ByeBye As String
Private Sub Workbook_BeforeClose(Cancel As Boolean)
ByeBye = "B4C"
End Sub
Private Sub Workbook_Deactivate()
If ByeBye = "B4C" Then
If ActiveWindow.Caption = ThisWorkbook.Name Then
If ThisWorkbook.Saved Then
MsgBox "No problem - Closing after Saving"
Else
MsgBox "No problem - Closing without Saving"
End If
Else
If ThisWorkbook.Saved Then
MsgBox "No problem - New Workbook Activation"
Else
MsgBox "Oops Try Again You Cannot Activate '" & ActiveWindow.Caption & "' until '" & ThisWorkbook.Name & "' has completed processing & IT HAS NOW COMPLETED", vbOKOnly, "Hiding"
ThisWorkbook.Activate
End If
End If
Else
MsgBox "No problem - Just Hiding"
End If
ByeBye = "Done"
End Sub
Private Sub Workbook_Open()
ByeBye = "OPENED"
End Sub
In response to comment about saving I tested this for 7 possible combinations as follows
1) Closing without Edits - No Saving Involved ... MsgBox Prompted with ... No problem - Closing after Saving
2) Not closing - Just Switch Workbook - Whether Edited or Not ... MsgBox Prompted with ... No problem - Just Hiding
3) Not closing - Switch Workbook - After Edit & Cancel ... MsgBox Prompted with ... Oops Try Again …
4) Closing and saving ... MsgBox Prompted with ... No problem - Closing after Saving
5) Closing and Saving after a prior Cancel ... MsgBox Prompted with ... No problem - Closing after Saving
6) Closing but Not Saving ... MsgBox Prompted with ... No problem - Closing without Saving
7) Closing but not Saving after a prior Cancel ... MsgBox Prompted with ... No problem - Closing without Saving
I think trying to cancel the close event is the wrong approach for what you are trying to do. A better approach would be to have a function that is only called when the workbook is actually closing.
Thank you for the comments regarding OnTime not being called while the dialog is open as that pointed me in the right direction. What we need to test is the time between the workbook deactivation and the closing of either the workbook itself or the save dialog. Using the Excel.Application.OnTime function to set this close time means this is possible as it can be delayed until the save dialogue has closed.
Once we have this time, a simple comparison to the deactivation time allows us to decide whether to call the exit function or not.
I initially ran into issues with the workbook reopening to run the .OnTime procedure, so an artificial delay needs to be added into the Deactivation function so the workbook hasn't closed until the close time has been set. Using the code from here - Delay Macro to allow events to finish we can accomplish this.
In ThisWorkbook
Option Explicit
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Excel.Application.OnTime EarliestTime:=Now, Procedure:="SetCloseTime"
End Sub
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
If Timer < CloseTime + 0.2 Then Call CloseProcedure
End Sub
Private Sub Workbook_Deactivate()
Delay (0.3)
If Timer < CloseTime + 0.4 Then Call CloseProcedure
End Sub
In a module
Option Explicit
Public CloseTime As Single
Function SetCloseTime()
CloseTime = Timer
End Function
Function Delay(Seconds As Single)
Dim StopTime As Single: StopTime = Timer + Seconds
Do While Timer < StopTime
DoEvents
Loop
End Function
Function CloseProcedure()
MsgBox "Excel is closing"
End Function
The .OnTime seems to run within one second cycles which dictates the length of the delay and the time difference test has a little leeway added with an additional 1/10th of a second (which I found necessary). These timings could potentially need slight tweaking but have so far worked for me with the different scenarios when closing the workbook.
In order to get around your edge case, you need to handle the case where the workbook is deactivated within 1 second of closing it, but only when the save prompt was displayed.
To check if less than 1 second has elapsed, use a high resolution timer to store the time in the Workbook_BeforeClose event, and then compare against it in the Workbook_Deactivate event. Assuming that clsTimer is a suitable high res timer, your code should now be:
Private MyTimer As clsTimer
Private StartTime As Currency
Private Sub Workbook_BeforeClose(ByRef Cancel As Boolean)
closing_event = True
Set MyTimer = New clsTimer
StartTime = MyTimer.MicroTimer
check_time = VBA.Now + VBA.TimeSerial(Hour:=0, Minute:=0, Second:=1)
Excel.Application.OnTime EarliestTime:=check_time, Procedure:="disable_closing_event"
End Sub
Private Sub Workbook_Deactivate()
If closing_event Then
If Not ThisWorkbook.Saved Then
'The Save prompt must have been displayed, and the user clicked No or Cancel or pressed Escape
If MyTimer.MicroTimer - StartTime < 1 Then
'The user must have pressed Escape and Alt-Tabbed
closing_event = False
Else
'Your Windows API calls here
End If
Else
'The workbook was saved before the close event, so the Save prompt was not displayed
'Your Windows API calls here
End If
Excel.Application.OnTime EarliestTime:=check_time, Procedure:="disable_closing_event", Schedule:=False
End If
Set MyTimer = Nothing
End Sub
The class module for clsTimer looks like this:
Private Declare PtrSafe Function getFrequency Lib "kernel32" _
Alias "QueryPerformanceFrequency" (cyFrequency As Currency) As Long
Private Declare PtrSafe Function getTickCount Lib "kernel32" _
Alias "QueryPerformanceCounter" (cyTickCount As Currency) As Long
Public Function MicroTimer() As Currency
' Returns seconds.
Dim cyTicks1 As Currency
Static cyFrequency As Currency
MicroTimer = 0
' Get frequency.
If cyFrequency = 0 Then getFrequency cyFrequency
' Get ticks.
getTickCount cyTicks1
' Seconds
If cyFrequency Then MicroTimer = cyTicks1 / cyFrequency
End Function
This post could be helpful https://www.dummies.com/software/microsoft-office/excel/an-excel-macro-to-save-a-workbook-before-closing/
I found code below from the book Excel 2016 Power Programming with VBA, by Michael Alexander
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Dim msg As String, ans as integer
If Me.Saved = False Then
msg = "Do you want to save?"
ans = MsgBox(msg, vbquestion+vbyesnocancel)
Select Case ans
Case vbYes: Me.Save
Case vbCancel: Cancel = True
End Select
End If
Call mySub
Me.Saved = True
End Sub
I think deactivate is the best way to capture this.
Beforeclose might occur earlier than Save event if the document was not saved. So Excel might prompt to save before closure.
But Deactivate is the final event before closure (after save). So this can be used.
had a similar problem and tried to run some macro before closing but it is dependad whether user wants to save workbook or not.
My solution was the code below, though there is a problem, that window of excel always stays open.
Public ClosedByProgram As Boolean
Private Sub Workbook_BeforeClose(Cancel As Boolean)
If Not ClosedByProgram Then
Cancel = True
Dim Ans As String
Ans = MsgBox("Want to save your changes to '" & ThisWorkbook.Name & "'?", vbYesNoCancel, "Microsoft Excel")
If Ans = vbNo Then
ClosedByProgram = True
ThisWorkbook.Close
ElseIf Ans = vbYes Then
Dim STR As String: STR = "'" & ThisWorkbook.Name & "'!" & "mod16_Versioning.IsSuitableForSaving"
Dim isForSaving As Boolean: isForSaving = Application.Run(STR, SaveAsUI)
If isForSaving Then
Dim STRToRun As String
STRToRun = "'" & ThisWorkbook.Name & "'!" & "mod02_Events.BeforeSave"
Application.Run STRToRun, SaveAsUI
Dim STRVersions As String: STRVersions = "'" & ThisWorkbook.Name & "'!" & "mod16_Versioning.MakeVersion"
Dim blankCheck As Boolean: blankCheck = Application.Run(STRVersions, SaveAsUI)
ClosedByProgram = True
ThisWorkbook.Close
End If
End If
Else
ThisWorkbook.Saved = True
Application.Quit
End If
End Sub
This seems to work
Code in the WorkBook
Public ByeBye As String
Private Sub Workbook_BeforeClose(Cancel As Boolean)
ByeBye = "BB # " & Now()
End Sub
Private Sub Workbook_Deactivate()
If Left(ByeBye, 2) = "BB" Then
ByeBye="Done"
MsgBox "Closing"
Else
ByeBye="Done"
MsgBox "DeActivating BUT NOT Closing"
End If
End Sub
Private Sub Workbook_Open()
ByeBye = "OP # " & Now()
End Sub
Just uses a public variable ByeBye
You must initialise it in WorkBook.Open
You must Set it in WorkBook.BeforeClose
and can test it in WorkBook.DeActivate
In case it is needed for this to work even after a VBA crash - and loss of ByeBye value I'm resetting it in the Workbook_SheetChange and in WorkBook_SheetSelectionChange
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
ByeBye = "SC # " & Now()
End Sub
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
ByeBye = "SSC # " & Now()
End Sub
The above addendum is really only needed if you were going to use the string default of "" for the tested value - but I'm using "BB # " & Now() so this is not really needed

Stop macro from repeating when using Application.OnTime

By using the below code RefreshData I run mg macro every 10 secs.
I'm unable to stop stoprefresh which is assigned to a square shape.
Sub RefreshData()
Application.OnTime Now + TimeValue("00:00:10"), "mg", , True
End Sub
Sub stoprefresh()
On Error Resume Next
Application.OnTime Now + TimeValue("00:00:10"), "mg", , False
End Sub
Sub mg()
ActiveSheet.Cells(ActiveSheet.Cells.Rows.Count, 1).End(xlUp).Offset(1) = "Running"
Call RefreshData
End Sub
Try this code
Option Explicit
Dim iTimerSet As Double
Sub RefreshData()
iTimerSet = Now + TimeValue("00:00:10")
Application.OnTime iTimerSet, "mg", , True
End Sub
Sub stoprefresh()
'On Error Resume Next
Application.OnTime iTimerSet, "mg", , False
End Sub
Sub mg()
ActiveSheet.Cells(ActiveSheet.Cells.Rows.Count, 1).End(xlUp).Offset(1) = "Running"
Call RefreshData
End Sub
Cancelling a scheduled Procedure
It is possible to cancel a procedure that has been scheduled to run but you need to know the
exact date and time it was scheduled for. To cancel a scheduled
procedure you must know the "EarliestTime" it was scheduled for.
Exactly the same syntax except you set the schedule paramater to
False. This tells the application to cancel the schedule.

application.ontime not cancelling or running in background

I'm using the Application.Ontime command to automatically close a spreadsheet after a period of inactivity (10 minutes).
The following code seems to work in general, however, it appears that if you manually close the sheet yourself, the workbook still seems to be active in the background and at the last designated 'endtime' will open itself so that it can close itself.
This is also evident in the VBA code window as after the CloseWB macro runs and the excel workbook appears to be closed, it is still listed in the VBA project explorer window.
Sub RunTime()
Static EndTime
If Not EndTime = "" Then ActiveWorkbook.Application.OnTime EndTime, "CloseWB", , False
EndTime = Now + TimeValue("00:10:00")
ActiveWorkbook.Application.OnTime EndTime, "CloseWB", , True
End Sub
Sub CloseWB()
Application.DisplayAlerts = False
With ThisWorkbook
.Save
.Close
End With
End Sub
I don't want to completely shutdown excel (application.quit) in case users have other workbooks open but need to try and stop the specific workbook running in the background.
Any ideas?
You need to stop the timer. Declare EndTime as a public variable, then turn the timer off in the Workbook_BeforeClose event.
Option Explicit
Public EndTime As Variant
Sub RunTime()
If Not EndTime = "" Then ActiveWorkbook.Application.OnTime EndTime, "CloseWB", , False
EndTime = Now + TimeValue("00:10:00")
ActiveWorkbook.Application.OnTime EndTime, "CloseWB", , True
End Sub
Sub CloseWB()
Application.DisplayAlerts = False
With ThisWorkbook
.Save
.Close
End With
End Sub
In the Workbook object:
Private Sub Workbook_BeforeClose(Cancel As Boolean)
On Error Resume Next
Application.OnTime earliesttime:=EndTime, procedure:="CloseWB", schedule:=False
End Sub

Resources