How to close an add-in when there are no more References to it? - vba

I'm writing a simple set of modular add-ins in Excel VBA.
In Addin1.xlam, I have selected "Tools.. References" and added MyMenus.xlam as a reference.
Addin1 also has some code to close itself from a menu. But when Addin1 closes, MyMenus stays open, even though it is no longer needed or referenced by anything.
I may also have Addin2 or Addin3 with a reference to MyMenus.
How can I get MyMenus to automatically close when no other open Project has a Reference to it?
Alternatively, how can I tell Addin1 to "close, and also close anything I had a Reference to"?

I solved this in a different way because the users were constantly saving the sheet and destroying the reference. It's a bit of a hack but what in Excel VBA isn't?
Public Sub CloseYourself()
Application.OnTime Now + TimeValue("00:00:00"), "CloseYourselfNow"
End Sub
Private Sub CloseYourselfNow()
On Error Resume Next
ThisWorkbook.Close False
End Sub
Basically, you call CloseYourself in the Workbook_BeforeClose event, which schedules CloseYourselfNow for 0 seconds from now (this part of Excel is single threaded so it waits for the original workbook to close). By the time CloseYourselfNow runs, the reference will have been removed and the addin can close itself. You need the On Error Resume Next in case other still workbooks have a reference. Finally, you can change the False to some check that is only true on your development machine - like Environ("COMPUTERNAME") - so it saves the addin and you won't lose dev changes.

Is keeping the MyMenus add-in open causing any issue? References like this would normally be left open exactly for the reason you describe, i.e. in case it is referenced elsewhere. I would recommend leaving it all as is but if you do want to proceed then it needs to be done in two steps: First remove the reference from the first add-in (Addin1.xlam), and then close the second add-in (MyMenus.xlam). Something like this should work:
Dim objRef As Reference
Set objRef = ThisWorkbook.VBProject.References("MyMenus")
Call ThisWorkbook.VBProject.References.Remove(objRef)
'Do not raise an exception as may not be able to close if the add-in is referenced elsewhere
On Error Resume Next
Call Workbooks("MyMenus.xlam").Close(False)
On Error Goto 0
Note, I have the Microsoft Visual Basic for Applications Extensibility reference added to the project (alternatively just declare objRef as a Variant).
When you do finally then close AddIn1.xlam, make sure you do so without saving otherwise you'll permanently lose the reference.

Related

How to ask for confirmation before allowing a Word document to close?

I work with 2 different keyboard layouts (qwerty and azerty), and am constantly switching between the two using Alt-Shift. Sometimes when I reach for Ctrl-Z to undo, I get instead Ctrl-W which closes the document. Generally speaking I would in these situations get a Save Prompt, which would alert me to my mistake. However, I am now using OneDrive, with Autosave on the documents I'm working on. As a result, there is never this save prompt. The document is instantly closed and, worse, the undo that I was trying to effect is no longer present when the document is re-opened.
I would like to use VBA attached to the Normal Template, such that, on closing any document created with the VBA template, I will check to see if Autosave is turned on, and if it is, confirm before closing with a messagebox.
Obviously if the user doesn't confirm (e.g. chooses Cancel) then the document should not be closed.
Initially I put my code on Document_Close, but soon realised that here it's too late to Cancel the close. This is no good to me.
Then, based on some online code, I put my code in a class module (EventClassModule). In the ThisDocument module, I initiate the class, and activate the event handler.
My code doesn't ever seem to run, or in anycase, breakpoints don't kick in.
I'm a little unsure of where Normal stops and my normal document begins...
In ThisDocument of Normal Template, I have this VBA
Dim X As New EventClassModule
Sub Register_Event_Handler()
Set X.App = Word.Application
End Sub
Private Sub Document_Open()
Register_Event_Handler
End Sub
And I have a class module called EventClassModule with the following
Public WithEvents App As Word.Application
Private Sub App_DocumentBeforeClose(ByVal Doc As Document, Cancel As Boolean)
Dim res As VbMsgBoxResult
If Me.AutoSaveOn Then
res = MsgBox("OK to Confirm", vbOKCancel, "Closing Document")
If res = vbCancel Then
Cancel = True
End If
End If
End Sub
I would like a prompt to confirm saving, any time a document is closed and the autosave option is turned off.
The Document_Open event handler will only fire if the Normal template is opened via File | Open. To get code to respond when starting Word you need to use an AutoExec routine. Simply rename your routine as below. You can find further info on Auto macros here
Public Sub AutoExec()
Register_Event_Handler
End Sub
Here is an easy way, include a method like this:
Public Sub DocClose()
' Disables Ctrl + W (but not other close methods, e.g. Alt + F4, menus, [X])
' Your code here:
ActiveDocument.Close
End Sub
Basically there are some method names that are used internally by MS-Word, and if you include them in your code they will prevent the default action. If you leave the sub empty it will effectively disable Ctrl + W.
See here for more information: Reserved method names in MS Word VBA
Advantage 1 of this technique
If you use Public WithEvents App As Word.Application and your project gets reset for any reason, and fails to restart... App will be Nothing and none of the events will fire.
Advantage 2 of this technique
On the occasions that you actually want to close a document... with this technique you wont get bugged by a (soon to become) annoying confirmation message - just sayin ;-)

Finding a syntax error in a large VBA/MS Access project

I have a MS Access database that displays an error message about a SQL syntax error on launch. "Syntax Error in query. Incomplete query clause." It also shows another error a few seconds after I hit "OK" on the first one.
Here's the two errors: https://imgur.com/a/PesjIFk
But it doesn't tell me where the syntax error is. There are SQL statements in a bunch of different places all over this project. This is a really large project and it wouldn't be practical to just look through all the code hoping that I notice an error someplace. How can I find out where this error is?
EDIT: Ok, so apparently you have to have a keyboard that has a "Break" key on it in order to even find where the error is. Wow. Fortunately I happen to have one. Here's the code that Access takes me to if I press break when I see the error message. This code is for a subform of another form. It highlights the first line (Private Sub Form_Current()).
Private Sub Form_Current()
If NumEnums > 0 Then
CurrentEnum = val(Nz(bit_value_edit.value)) 'Update CurrentEnum to the currently selected enum
Call UpdateEnumsLabel(Me.Parent![enums label]) 'Update label
End If
End Sub
...and here's UpdateEnumsLabel():
Public Sub UpdateEnumsLabel(ByRef label As Control)
If NumEnums > 0 Then
label.Caption = "Enums: " & CurrentEnum & "/" & NumEnums
Else
label.Caption = "Enums: 0"
End If
End Sub
The definition for CurrentEnum:
Public CurrentEnum, CurrentPort, CurrentFile, CurrentGroup As Long
I'm thinking that this error is unrelated to the code in Form_Current(), but Access is highlighting that line because the error happens when the form is opened. But the form doesn't contain anything that uses a query, so I'm confused as to what query Access has a problem with.
When the error Message pops up, Use Control+Break. It will take you to the line causes the issue.
You should also open a module and form the debug option in the VBA editor select "Compile All Modules"
And since it appears to happening on open/load, you can check both the macros and the main modules to find anything that triggers on AutoExec.
Often ctrl-break will hit the line where you errored out. However, in the case of multiple “events”, and code without error handling, then often the error occurs in the routine that called the code, not the actual code that caused the error.
What I would do launch the application (hold down the shift key to prevent any start up code, or forms running).
Now that you launched the application, but without forms or code running, then check for an autoexecc macro (if there is one, check what code it attempts to run).
If there not an autoexec macro in use, then check under file->options->current database. In this view, you can look/see/determine what the name of the start-up form is.
Once you determined the start-up form (or start up module/code) called from autoexec macro, then you can simply add a blank code module, and place in your code the SAME command that is used to start your application.
So let’s assume no autoexec macro, and you determine that the start-up form is frmMain.
So now, we can launch the application (hold down shift key to prevent any start up code from running). Now, type into a new “test” standard code module the following code:
Sub MyGo
Stop
Docmd.OpenForm "frmMain"
End sub
So you type in above code, hit ctrl-s to save the above code. Now with your cursor anyplace inside of the above code, you simply hit F5 to run.
At this point you hit/stop on the “stop” command in the debugger. At this point, you then can hit f8 to step to the next line of code. If the form in question calls other code etc., you still be able to single step, and “eventually” you hit the line which causes the error.
While the application may be large, a simple look at the start up forms (and huts then the start-up code) means that you ONLY really need to trace and look at the start up code – not the whole application.

Excel 2016 crashes when using Debug.Print Tab() in Workbook_Open() event handler

Just something weird I noticed, tested on 2 computers with similar config (Windows 10 64-bit up-to-date with Excel 2016 32-bit), one was a clean install :
Simply create a new .xlsm workbook, and place the following in the ThisWorkbook.cls class module:
Private Sub Workbook_Open()
Debug.Print Tab(10); "Hello!"
End Sub
Save. Close. Open (if opens in PROTECTED VIEW mode, Enable editing, close and re-open). Excel crashes.
Minimal, Complete, Verifiable example.
Now the big question: why?
I tried to delay the problematic Debug.Print Tab(10); "Hello!" by placing it in a Sub which I call a few sec after the Open event with Application.OnTime Now() + (1/3600/24) * 5, "SubName", still crashing. I call it manually, it works just fine.
MSDN says nothing about it: Tab Function
Edit 19/04/2021
First, I have to say that I am a bit stunned : I asked this question on Apr 19 '18 at 7:42 (2018-04-19 07:42:36Z). There was not at this time, and there still is not, any reference to this bug on Stack Overflow. I have not encountered this bug since. And today, exactly 3 yers later to the day, I stumbled on it again. Fate is quite amazing.
Anyway, it appears this bug occurs as soon as a single Tab character is attempted to be printed in the VBE Immediate window with the Debug.Print and method without the VBE window loaded (meaning not opened once in the current Excel instance). That's why it was easier encountering it during an Workbook_Open event... But if you place the following sub in a workbook, close it, and run it from the Developper tab > Macro button without opening the VBE, Excel will also crash:
Private Sub test_MRE()
Debug.Print Tab
End Sub
So one must be very careful not to send Empty variables to the console (or you will probably look for hours before finding out what's happening, speaking knowingly...), because this will also cause Excel to crash (the , being a Tab and noting being sent before it):
Private Sub test_MRE()
Dim Var1 'Variant/Empty
Debug.Print Var1, "hello"
End Sub
I managed in my project to make the error we can hear before Excel crashes to appear, and it is an Automation Error, if it helps someone to better understand what's really happening:

Directly referencing userform entry in VBA instead of passing value to a variable

folks. I am new to programming, but I am writing some macros to help manage a shared Excel workbook for my job.
I am implementing a few different user roles for people who need to access this workbook. The security is not very critical, just to prevent people from accidentally making (and saving) changes to things they shouldn't be. I am just having a UserForm prompt for the password and, based on what's entered, grant the proper access.
I have it written so that the user's entry into textbox on the UserForm is referenced directly as Me.textboxPasswordEntry.Value for any comparisons. It occurs to me that this may not be best practice, but I can't put my finger on why. Maybe I'm just over thinking? At the same time, it seems silly and wasteful to declare a variable, pass the value to the variable, and then analyze that.
The Sub below is from the UserForm, and I've included it to show you what I mean. This is a very straight-forward scenario, I know, but am I courting trouble if I continue this practice through more complex ones? If so, what kind of problems might I run into?
Thanks.
Private Sub buttonOK_adminPW_Click()
'The subs SetUserType_[level] are in the ChangeUserType module
'AdminPass and DesignPass are module-level variables set on UserForm initialization
'Default user type is User. Read-only access.
'Admins can edit the workbook, but must save via a macro to ensure
' things are reset properly for Users (some sheets hidden, etc.)
'Designers can edit the workbook, but also have full ability to save using
' the regular file menu/ctrl+s/etc.
Application.ScreenUpdating = False
Select Case Me.textboxPasswordEntry.Value
Case AdminPass
'Shows right control buttons and unlocks the wkbk for admin access
SetUserType_admin
Unload Me
Case DesignPass
'Shows all control buttons and unlocks the wkbk for designer access
SetUserType_design
Unload Me
Case Else
MsgBox ("Password incorrect. Please retry.")
With Me.textboxPasswordEntry
.Value = ""
.SetFocus
End With
End Select
Application.ScreenUpdating = True
End Sub
Yeah I've also pondered over "best practise" with userforms over the years... I guess it's just through experience that I use approach below most often:
Use as little code as possible in the userform itself (thinking
is, the form is more "reusable" if it does as little as possible
back to its parent... its reason for existance is just to get input)
Do use code on the "activate" event of the form to clear all the
fields on the form (this makes sense to be in the form because then
you don't need to remember every control on the form to clear at
every point you use it)
Either directly reference objects from
the form in your calling code (i.e. stPassword =
userform1.tbPassword.value) or...
Use "public" variables in the
userform ... i.e. before all code in userform declare "public stPasswordInput as string" then you can reference in your calling code with e.g. stPassword = userform1.stPasswordInput
I'm keen to see what other people suggest though!

Wait until ActiveWorkbook.RefreshAll finishes - VBA

I have a sub that calls on ActiveWorkbook.RefreshAll to bring new data in from an XML source, and then performs multiple modifications to it. The problem is that not enough time is given for the RefreshAll command to finish, so the following subs and functions end up not executing correctly, which result in repeated rows not being correctly erased.
I have tried using Application.Wait and the Sleep function, but they seem to pause the refresh process too. I simply want the rest of the code to wait until the refresh process finishes before executing the rest of the code.
Any ideas on how to implement this? Right now I was only able to fix it by not calling on RefreshAll, which gives me the idea of implementing a second flow to be executed afterwards, but that's not a good workaround.
Please let me know if any of this wasn't clear. Thanks
EDIT
So I tried a few suggestions from the posts below, and this is what I was able to come up with.
Doing a "record macro" and then UNCHECKING the "Enable background refresh" in the table properties did not result in anything. I did a refresh as well afterwards. This was the result of the recorded macro:
With ActiveWorkbook.Connections("XMLTable")
.Name = "XMLTable"
.Description = ""
End With
ActiveWorkbook.Connections("XMLTable").refresh
The class ActiveWorkbook.Connections does NOT have a BackgroundQuery option so that I can set it to False. Any ideas?
Just to be clear. This is an XML file hosted on a website which Excel goes and imports into a table. I then call that data into a pivot and other things. The goal here is to allow the import process from the website to the table to finish BEFORE executing any other commands.
Thanks
EDIT2:
After a little more research, I have found this page: http://www.mrexcel.com/forum/excel-questions/564959-execute-code-after-data-connection-refresh-finished.html
It appears that an XML type of connection does not have a BackgroundQuery boolean. That option is only available for ODBC and OLEDB connections, which are types xlConnectionTypeODBC and xlConnectionTypeOLEDB, respectively. The XML connection I am using is of type xlConnectionTypeXMLMAP which does not have a BackgroundQuery option.
Does anyone have any idea on where to go from here? The only solution I have in mind right now is to make two seperate macro buttons on the excel sheet, one for refreshing and one for data modification, but I'd rather keep that option to the very last.
I had the same issue, however DoEvents didn't help me as my data connections had background-refresh enabled. Instead, using Wayne G. Dunn's answer as a jumping-off point, I created the following solution, which works just fine for me;
Sub Refresh_All_Data_Connections()
For Each objConnection In ThisWorkbook.Connections
'Get current background-refresh value
bBackground = objConnection.OLEDBConnection.BackgroundQuery
'Temporarily disable background-refresh
objConnection.OLEDBConnection.BackgroundQuery = False
'Refresh this connection
objConnection.Refresh
'Set background-refresh value back to original value
objConnection.OLEDBConnection.BackgroundQuery = bBackground
Next
MsgBox "Finished refreshing all data connections"
End Sub
The MsgBox is for testing only and can be removed once you're happy the code waits.
Also, I prefer ThisWorkbook to ActiveWorkbook as I know it will target the workbook where the code resides, just in case focus changes. Nine times out of ten this won't matter, but I like to err on the side of caution.
EDIT: Just saw your edit about using an xlConnectionTypeXMLMAP connection which does not have a BackgroundQuery option, sorry. I'll leave the above for anyone (like me) looking for a way to refresh OLEDBConnection types.
Though #Wayne G. Dunn has given in code. Here is the place when you don't want to code. And uncheck to disable the background refresh.
DISCLAIMER: The code below reportedly casued some crashes! Use with care.
according to THIS answer in Excel 2010 and above CalculateUntilAsyncQueriesDone halts macros until refresh is done
ThisWorkbook.RefreshAll
Application.CalculateUntilAsyncQueriesDone
You must turn off "background refresh" for all queries. If background refresh is on, Excel works ahead while the refresh occurs and you have problems.
Data > Connections > Properties > (uncheck) enable background refresh
Here is a solution found at http://www.mrexcel.com/forum/excel-questions/510011-fails-activeworkbook-refreshall-backgroundquery-%3Dfalse.html:
Either have all the pivotcaches' backgroundquery properties set to False, or loop through all the workbook's pivotcaches:
Code:
For Each pc In ActiveWorkbook.PivotCaches
pc.BackgroundQuery = False
pc.Refresh
Next
this will leave all pivotcaches backgroundquery properties as false. You could retain each one's settings with:
Code:
For Each pc In ActiveWorkbook.PivotCaches
originalBGStatus = pc.BackgroundQuery
pc.BackgroundQuery = False
pc.Refresh
pc.BackgroundQuery = originalBGStatus
Next
This may not be ideal, but try using "Application.OnTime" to pause execution of the remaining code until enough time has elapsed to assure that all refresh processes have finished.
What if the last table in your refresh list were a faux table consisting of only a flag to indicate that the refresh is complete? This table would be deleted at the beginning of the procedure, then, using "Application.OnTime," a Sub would run every 15 seconds or so checking to see if the faux table had been populated. If populated, cease the "Application.OnTime" checker and proceed with the rest of your procedure.
A little wonky, but it should work.
Try executing:
ActiveSheet.Calculate
I use it in a worksheet in which control buttons change values of a dataset. On each click, Excel runs through this command and the graph updates immediately.
This worked for me:
ActiveWorkbook.refreshall
ActiveWorkbook.Save
When you save the workbook it's necessary to complete the refresh.
Here is a trick that has worked for me when some lines of VBA code have trouble executing because preceding lines haven't completed doing their thing. Put the preceding lines in a Sub. The act of calling the Sub to run those lines may help them finish before subsequent lines are executed. I learned of this trick from https://peltiertech.com/ and it has helped me with timing issues using the Windows clipboard.
If you're not married to using Excel Web Query, you might try opening the URL as a separate Workbook instead. Going that route lets you work on the resulting data once the web request completes, just like if you turn off "Enable background refresh."
The nice thing is though, Excel displays a progress bar during the request, instead of just freezing up / showing a load message in the destination cell.
See my answer on this question: How can I post-process the data from an Excel web query when the query is complete?
The tradeoff of that approach is you have to manage processing the data you get back yourself - Excel won't put it in a given destination for you.
We ended up going this route after we tried something pretty similar to what you seem to have been doing.
I was having this same problem, and tried all the above solutions with no success. I finally solved the problem by deleting the entire query and creating a new one.
The new one had the exact same settings as the one that didn't work (literally the same query definition as I simply copied the old one).
I have no idea why this solved the problem, but it did.
I tried a couple of those suggestions above, the best solution for me was to disable backgroundquery for each connection.
With ActiveWorkbook.Connections("Query - DL_3").OLEDBConnection
.BackgroundQuery = False
End With
For Microsoft Query you can go into Connections --> Properties and untick "Enable background refresh".
This will stop anything happening while the refresh is taking place. I needed to refresh data upon entry and then run a userform on the refreshed data, and this method worked perfectly for me.
I have had a similar requirement. After a lot of testing I found a simple but not very elegant solution (not sure if it will work for you?)...
After my macro refresh's the data that Excel is getting, I added into my macro the line "Calculate" (normally used to recalculate the workbook if you have set calculation to manual).
While I don't need to do do this, it appears by adding this in, Excel waits while the data is refreshed before continuing with the rest of my macro.
For me, "BackgroundQuery:=False" did not work alone
But adding a "DoEvents" resolved problem
.QueryTable.Refresh BackgroundQuery:=False
VBA.Interaction.DoEvents
I know, that maybe it sounds stuppid, but perhaps it can be the best and the easiest solution.
You have to create additional Excel file. It can be even empty.
Or you can use any other existing Excel file from your directories.
'Start'
Workbooks.Open("File_where_you_have_to_do_refresh.xlsx")
Workbooks("File_where_you_have_to_do_refresh.xlsx").RefreshAll
Workbooks.Open("Any_file.xlsx)
'Excell is waiting till Refresh on first file will finish'
Workbooks("Any_file.xlsx).Close False
Workbooks("File_where_you_have_to_do_refresh.xlsx").Save
or use this:
Workbooks("File_where_you_have_to_do_refresh.xlsx").Close True
It's working properly on all my files.
What I've done to solve this problem is save the workbook. This forces it to refresh before closing.
The same approach works for copying many formulas before performing the next operation.