How to check the CURRENTLY OPEN excel file is open in read only mode? - vba

OPENING: I've been digging around for a long time looking at some of the answers to the questions here. I hope my Google-fu isn't getting rusty but I couldn't find anything helpful. I've tried what I have found with no luck.
PROBLEM: I have a document that is used by more then one user. The short vesion is the document ends up in Read Only mode one way or another and they forget its Read Only (or they just press the Okay button and dont read the warning). You can still operate everything in the worksheet as normal.
Through normal operation of the worksheet and the Macros the Workbook will auto save on completion of the Macro.
Is there any way to check (with the worksheet open) that the version you have is open in Read Only mode? I'd like to introduce code to prevent the use of the main function (disable the workbook macro) while using it in Read Only.
IDEAL SOLUTION: Have a function I could call to check the Read Only Status of the workbook.
Example:
Public Function ReadOnlyMode () as Bool
'Code that I don't know how to write for the funtion
return True/False
End Function

Use the following to test
If ActiveWorkbook.ReadOnly Then 'If True
See .ReadOnly property description.
Workbook.ReadOnly Property (Excel)
Returns True if the object has been opened as read-only. Read-only
Boolean .

Related

Defining variables on Excel VBA

Dears,
I'm starting a new project using Excel VBA and I would like to declare some variables to be loaded during the system's initialization, but during the utilization it may be changed. How can I do it?
Example:
When the system is loaded, the variable RADIO must be equal to "OFF", but during the utilization the user will change this value to "ON", and even to "OFF" again.
This information (RADIO) will be used during the system's utilization in some other pages.
How can I do it?
Thank you so much
Bruno Lelli
Like Tim Williams said, you need a global variable. Best practice is to have it declared in the declaration block (very first lines) of a code mudule.
To have it available within the code module only:
Private booRadio as Boolean
To have it available within the whole VBA project, i.e. all modules, user forms and for the workbook and worksheet events:
Public booRadio as Boolean
When VBA starts, all Boolean variables get initialized to be False (likewise, all Integer or Long get initialized to be 0, String to be "", etc.). This can be used to have a known initial status after system start.
Or - which is better to read in code reviews and like-I-feel more robust - you use an event to initilize the startup status. E.g. like ashleedawg said, you may use the workbook_open event for this. In that case, you need your variable dacelared with the Public statement, if you want to access it outside the ThisWorkbook code.
EDIT:
Copy this code into the ThisWorkbook code module:
Private Sub Workbook_Open()
booRadio = True
End Sub
This will use the event Workbook_Open to initialize the variable upon every opening of the Excel file, because that event gets automatially raised then.

Formulas to be triggered when Excel is opened or saved

I have several custom-made (VBA) formulas in my Excel. Examples are
= isfileExists(c3)
= isReadOnly(c3)
There are multiple calls to these functions (200/column)
These are slowing down the file as they involve heavy VBA. I want the above formulas to be run only when user opens the file or saves the file only!
How can I go on to do this without touching 'enabling/disabling automatic calculations'? (Please note I do know how to write the event functions but I am looking for an idea to put inside them/anywhere)
Sample idea I have is, adding a ' infront the formulas and whenever user opens/saves, that ' (apostrophe) would be removed using macro; hence the formulas would calculate. Any other easier suggestions?
Thanks much!
You can cache the formula results in a Global (or Static inside the function) dictionary object: once you've checked a file once you can use the cached result instead of repeating the file check)
Public Function ExpensiveCheck(fpath As String) As Boolean
Static dict As Object, ans As Boolean
'create the dictionary if not already created
If dict Is Nothing Then
Set dict = CreateObject("scripting.dictionary")
End If
If Not dict.exists(fpath) Then
ans = (Dir(fpath, vbNormal) <> "") 'do your checking here
dict.Add fpath, ans
End If
ExpensiveCheck = dict(fpath)
End Function
The dict will be lost when the session ends, so the formula should always refresh the cache when you open the file.
Downside is you may need some mechanism to clear/refresh the cache if you want to update the output after some change in the files being checked.
If you don't want your UDFs to calculate along with the rest of the worksheet cells, then you don't want UDFs; make them Private, or stick Option Private Module at the top of the standard module where they're declared, to make them unavailable to use as custom worksheet functions.
Then handle Workbook.Open and Workbook.BeforeSave events, and have these two handlers call some Private Sub that's responsible for writing to the cells you currently have these UDFs in.
That way the values will only be computed when the workbook opens and before it's saved.
UDFs that incur disk I/O aren't ideal: keep that processing for macros.

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.

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

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.