How to check whether Connection Refresh was successful - vba

In Excel 2016 VBA, I'm refreshing several queries like this:
MyWorkbook.Connections(MyConnectionName).Refresh
After the code is done, and no errors are encountered, I see that the hourglass icons for most of the queries are still spinning for several seconds.
Is it possible to check for success AFTER all the refreshes are completed? I'm concerned that my code isn't going to know if an error happens after the code finishes but before the queries are done refreshing.
BTW I don't want to do a RefreshAll, because some of the queries are dependent on others (uses them as a source). I refresh them in a certain sequence so that dependent queries are refreshed after the queries they are dependent on.
UPDATE:
I see that the Connection objects have a read-only RefreshDate property, which at first glance looked like it could be used to do this check:
MyWorkbook.Connections(MyConnectionName).OLEDBConnection.RefreshDate
HOWEVER, it doesn't seem to be getting set. I get an error trying to check it. If I set a Variant variable to that RefreshDate property, the variable shows as "Empty". The source is a SQL server database.

The QueryTable object exposes two events: BeforeRefresh and AfterRefresh.
You need to change your paradigm from procedural/imperative to event-driven.
Say you have this code in ThisWorkbook (won't work in a standard procedural code module, because WithEvents can only be in a class):
Option Explicit
Private WithEvents table As Excel.QueryTable
Private currentIndex As Long
Private tables As Variant
Private Sub table_AfterRefresh(ByVal Success As Boolean)
Debug.Print table.WorkbookConnection.Name & " refreshed. (success: " & Success & ")"
currentIndex = currentIndex + 1
If Success And currentIndex <= UBound(tables) Then
Set table = tables(currentIndex)
table.Refresh
End If
End Sub
Public Sub Test()
tables = Array(Sheet1.ListObjects(1).QueryTable, Sheet2.ListObjects(1).QueryTable)
currentIndex = 0
Set table = tables(currentIndex)
table.Refresh
End Sub
The tables variable contains an array of QueryTable objects, ordered in the order you wish to refresh them; the currentIndex variable points to the index in that array, for the QueryTable you want to act upon.
So when Test runs, we initialize the tables array with the QueryTable objects we want to refresh, in the order we want to refresh them.
The implicit, event-driven loop begins when table.Refresh is called and the QueryTable fires its AfterRefresh event: then we report success, and update the event-provider table object reference with the next QueryTable in the array (only if the refresh was successful), and call its Refresh method, which will fire AfterRefresh again, until the entire array has been traversed or one of them failed to update.

Just found this solution at Execute code after a data connection is refreshed
The bottom line is: Excel refreshes data connection in the background and thus the rest of the code is executed without interruption.
Solution: set BackgroundQuery property to False
Example:
For Each cnct In ThisWorkbook.Connections
cnct.ODBCConnection.BackgroundQuery = False
Next cnct
Possible problem: don't know which connection it is...
Remedy: case... when...
Dim cnct as WorkbookConnection ' if option explicit
' ODBC and OLE DB
For Each cnct In ThisWorkbook.Connections
Select case cnct.type
case xlconnectiontypeodbc
cnct.ODBCConnection.BackgroundQuery = False
case xlconnectiontypeoledb
cnct.OledbConnection.BackgroundQuery = False
end select
Next cnct
As you can see, code above only deals with ODBC and OLE DB. Depending on what types of data connection you are using, you can expand the select case clause. Unless changed, once run, connection's BackgroundQuery will remain off.

Related

Split Form creates a separate collections for each of its parts

I have a split Form in MS Access where users can select records in the datasheet and add it to a listbox in the Form thereby creating custom selection of records.
The procedure is based on a collection. A selected record gets transformed into a custom Object which gets added to the collection which in turn is used to populate the listbox. I have buttons to Add a Record, Remove a Record or Clear All which work fine.
However I thought, that pressing all these buttons is a bit tedious if you create a Selection of more then a dozen records, so i reckoned it should be simple to bind the actions of the Add and Remove buttons to the doubleclick event of a datasheet field and the listbox respectively.
Unfortunately i was mistaken as this broke the form. While a doubleclick on a field in the datasheet part of the form added the record to the listbox it was now unable to remove an item from the underlying collection giving me Run-time error 5: "Invalid procedure call or argument". Furthermore the clear all Button doesn't properly reset the collection anymore. When trying to Clear and adding a record of the previous selection the code returns my custom error that the record is already part of the selection and the listbox gets populated with the whole previous selection, which should be deleted. This leaves me to believe, that for some Reason the collection gets duplicated or something along these lines. Any hints into the underlying problem is apprecciated. Code as Follows:
Option Compare Database
Dim PersColl As New Collection
Private Sub AddPerson_Click()
AddPersToColl Me!ID
FillListbox
End Sub
Private Sub btnClear_Click()
Set PersColl = Nothing
lBoxSelection.RowSource = vbaNullString
End Sub
Private Sub btnRemovePers_Click()
PersColl.Remove CStr(lBoxSelection.Value)
FillListbox
End Sub
Private Sub FillListbox()
Dim Pers As Person
lBoxSelection.RowSource = vbaNullString
For Each Pers In PersColl
lBoxSelection.AddItem Pers.ID & ";" & Pers.FullName
Next Pers
lBoxSelection.Requery
End Sub
Private Function HasKey(coll As Collection, strKey As String) As Boolean
Dim var As Variant
On Error Resume Next
var = IsObject(coll(strKey))
HasKey = Not IsEmpty(var)
Err.Clear
End Function
Private Sub AddPersToColl(PersonId As Long)
Dim Pers As Person
Set Pers = New Person
Pers.ID = PersonId
If HasKey(PersColl, CStr(PersonId)) = False Then
PersColl.Add Item:=Pers, Key:=CStr(PersonId)
Else: MsgBox "Person Bereits ausgewählt"
End If
End Sub
This works alone, but Simply Adding this breaks it as described above.
Private Sub Nachname_DblClick(Cancel As Integer)
AddPersToColl Me!ID
FillListbox
End Sub
Further testing showed that its not working if i simply remove the Private Sub AddPerson_Click()
Edit1:
Clarification: I suspected that having 2 different events calling the same subs would somehow duplicate the collection in memory, therefore removing one event should work. This is however not the case. Having the subs called by a button_Click event works fine but having the same subs called by a double_click event prompts the behaviour described above. The issue seems therefore not in having the subs bound to more than one event, but rather by having them bound to the Double_Click event.
Edit2: I located the issue but I Haven't found a solution yet. Looks like Split-Forms are not really connected when it comes to the underlying vba code. DoubleClicking on the record in the datasheet view creates a Collection while using the buttons on the form part creates another one. When trying to remove a collection item by clicking a button on the form, it prompts an error because this collection is empty. However clicking the clear all button on the form part doesn't clear the collection associated with the datasheet part.
Putting the collection outside into a separate module might be a workaround but i would appreciate any suggestions which would let me keep the Code in the form module.
The behavior is caused by the split form which creates two separate collections for each one of its parts. Depending from where the event which manipulates the collection gets fired one or the other is affected. I suspect that the split form is in essence not a single form, but rather 2 instances of the same form-class.
A Solution is to Declare a collection in a separate module "Coll":
Option Compare Database
Dim mColl as new Collection
Public Function GetColl() as Collection
Set GetColl= mColl
End Function
And then remove the Declaration of the Collection in the SplitFormclass and Declare the Collection in every Function or Sub by referencing the collection in the separate Module like in the following example:
Option Compare Database
Private Sub AddPersToColl(PersonId As Long)
Dim Pers As Person
Dim PersColl as Collection
Set PersColl = Coll.GetColl
Set Pers = New Person
Pers.ID = PersonId
If HasKey(PersColl, CStr(PersonId)) = False Then
PersColl.Add Item:=Pers, Key:=CStr(PersonId)
Else: MsgBox "Person Bereits ausgewählt"
End If
End Sub
This forces the form to use the same Collection regardless if the event is fired from the form or datasheet part of the split-form. Any Further information is appreciated but the matter is solved for now.
Thanks everyone for their time

How to stop Access from prompting "Do you want to save changes to the layout of query"

How can I stop Access from prompting "Do you want to save changes to the layout of query" when I try to close a form that has a subform in datasheet view?
I have a form with a subform in Datasheet view with the .SourceObject set to a temporary pivot / crosstab query (no actual form object). If the user changes the width of a column and closes the window with the built-in Access close button, the user (and annoyed developer) is always presented with the "Do you want to save changes to the layout of query" prompt.
I am able to avoid this prompt by setting MySubform.SourceObject = "" in the click of my Close button but anyone clicking the [x] button or pressing CTRL+F4 or CTRL+W gets the prompt.
I have put breakpoints in the Form_Close and Form_Unload events but this prompt appears before they fire.
I want to clarify further that this subform object is Unbound and not based on a form object. I am building a dynamic Crosstab SQL statement, creating a QueryDef with the SQL, and then setting MySubform.SourceObject = "query.qry_tmp_Pivot" This is a neat technique for a crosstab/pivot query because the columns can vary and I don't think a form object would support that.
I am able to use the Watch window on the MySubform object and I can see that a MySubform.Form object exists. It has a Controls collection containing a control for all of the columns in my query. I have an optional routine that I can run that will loop through all of the controls and set their .ColumnWidth = -2, which will auto-size the column width based on the widest data of the visible rows in the datasheet. When this routine was running I was noticing that every time I closed the form (not using my Close button) I was getting the save prompt. I have disabled this routine for debugging but a user will still get the prompt if they manually adjust any column width.
I feel like I need to explain this extra detail so you realize this is not an Access 101 issue. You probably know that if you've read this far. Here's another thought I had: Maybe I could trap the Unload event in the subform control before the prompt happens. Because there is no true Form object to put test code in, I created a class object and passed MySubform to it. The class uses WithEvents and creates events like OnClose and OnCurrent on the class module's mForm object. Sample class code is below.
Private WithEvents mForm As Access.Form ' This is the form object of the Subform control
Public Sub InitalizeSubform(Subform As Access.Subform)
Set mForm = Subform.Form
Debug.Print Subform.Name, mForm.Name, mForm.Controls.count
' Create some events to intercept the default events.
mForm.OnClick = "[Event Procedure]"
mForm.OnClose = "[Event Procedure]"
mForm.OnUnload = "[Event Procedure]"
mForm.OnCurrent = "[Event Procedure]"
End Sub
Private Sub mForm_Click()
Debug.Print "Clicking " & mForm.Name
End Sub
Private Sub mForm_Current()
Debug.Print "On Current " & mForm.Name, "Record " & mForm.CurrentRecord & " of " & mForm.RecordsetClone.RecordCount
End Sub
Private Sub mForm_Unload(Cancel As Integer)
Debug.Print "Unloading " & mForm.Name
End Sub
Private Sub mForm_Close()
Debug.Print "Closing " & mForm.Name
End Sub
The VBE Watch window shows my new events on the mForm object but unfortunately they never fire. I know the class works because I used it with a bound subform and all of the events are intercepted by the class. I'm not sure what else to try.
Events on the subform never fire because it's a lightweight form (without a module). See this Q&A and the docs. Lightweight forms don't support event listeners, but do support calling public functions from an event, e.g. mForm.OnClick = "SomePublicFunction()"
Note that the workaround described in this answer also opens up the possibility of displaying a crosstab query in a form without saving it at all.
Alternatively, you could try capturing the event on your main form, and suppress saving there.
I gave answer credit to #ErikA for this question. He directed me to an answer to a somewhat related question here which is what I ended up implementing and it works. His instructions were very straightforward and easier to implement than I first anticipated.
The answer to my question seems to be that it may not be possible to avoid the save prompt when using a lightweight form object. If someone comes along with a solution I'd still like to hear it.
What I learned from this experience:
An unbound subform that has its .SourceObject set to a table or query will not have a code module. This considered to be a lightweight object.
Public functions can be added to event properties on lightweight forms but they don't seem to fire before the save prompt appears as the parent form is closing.
Binding a pivot query or temp table to a real form object that has 254 controls on it is the only way I found that doesn't prompt to save design changes when the parent form closes. Mapping the query/table columns to the 254 datasheet controls is super fast.
A class is a nice way to intercept events if you're not using a lightweight object. A class also lets you open multiple instances of the same object.
I ended up not implementing my solution without using a class because there were no events I needed to capture. At this point I don't need my frmDynDS to be used for other purposes and I'd like to keep the object count down (this is a large app with 1400+ objects).
I put the following code in a function that gets called when the subform is loaded or refreshed.
With subCrosstab
' Set the subform source object to the special Dynamic Datasheet form. This could be set at design time on the Subform control.
.SourceObject = "Form.frmDynDS"
' Run the code to assign the form controls to the Recordset columns.
.Form.LoadTable "qry_tmp_Pivot" ' A query object works as well as a table.
' Optional code
.Form.OnCurrent = "=SomePublicFunction()"
.Form.AllowAdditions = False
.Form.AllowEdits = False
.Form.AllowDeletions = False
.Form.DatasheetFontHeight = 9
End With
I can now resize the columns and never get a prompt to save layout changes no matter how I close the parent form.

Refresh All not triggering QueryTable's BeforeRefresh event - why?

As stated in a title: I've found out that clicking "Refresh All" button on a ribbon doesn't trigger QueryTable's BeforeRefresh event. Why is it so? Is there a way to change this behavior?
The strange thing is that AfterRefresh event of the very same QueryTable is triggered perfectly!
To analyze this behavior I've created two tables in two worksheets:
Simple, unlinked table called Source
Table called Destination linked to Source table. Connection was created using Microsoft Query and Excel Files as Data source.
Then, I've created TestClass as follows:
Option Explicit
Private WithEvents qt As QueryTable
Public Sub Init(pQt As QueryTable)
Set qt = pQt
End Sub
Private Sub qt_BeforeRefresh(Cancel As Boolean)
MsgBox "BeforeRefresh"
End Sub
Private Sub qt_AfterRefresh(ByVal Success As Boolean)
MsgBox "AfterRefresh"
End Sub
Finally, I've created, initialized and stored an instance of TestClass.
Right-clicking Destination table and choosing "Refresh" gives expected results: MsgBox is displayed twice, confirming that both Before- and After- Refresh events were triggered.
However, clicking "Refresh All" on ribbon results in only one MsgBox being displayed: AfterRefresh one.
I've prepared a minimal Excel file that reproduces described behavior. It is available for download here: RefreshAllNotTriggeringBeforeRefresh.xlsm
EDIT 1: In response to Rik Sportel's question on how TestClass instance is being created and initialized.
Here's a relevant part of ThisWorkbook object:
Option Explicit
Private Destination As New TestClass
Private Sub Workbook_Open()
Destination.Init WS_Destination.ListObjects("Destination").QueryTable
End Sub
Where WS_Destination is a value for a (Name) property of a worksheet containing Destination table.
As one can see, Destination is a private field, but it seems that garbage collector doesn't care: I can right-click refresh Destination table as many times as I want and both Before- and After- MsgBoxes always pop up. Whereas, Refresh All never (not even once) triggers BeforeRefresh event, yet always triggers AfterRefresh one.
Option Explicit
Public tc as TestClass
Sub Test()
Set tc = New TestClass
tc.Init Worksheets("SomeSheet").ListObjects(1).QueryTable
End Sub
The BeforeRefresh only triggers when you use the refresh option in the Table tab Excel GUI. When you use this one, both events trigger appropriately.
When you use the refresh option in the Query tab of the Excel GUI, only the second event triggers.
The reason is that the QueryTable does not have this BeforeRefresh event triggered when using the Query refresh option, is because you're not actually refreshing the QueryTable itself, but the underlying Query.
The BeforeRefresh event occurs before any refreshes of the query table. Here
The AfterRefresh event occurs after the query is completed or cancelled. Here
When you do:
Option Explicit
Public tc as TestClass
Sub Test()
Set tc = New TestClass
tc.Init Worksheets("SomeSheet").ListObjects(1).QueryTable
Worksheets("SomeSheet").ListObjects(1).QueryTable.Refresh
End Sub
You'll see that both events trigger appropriately.
Edit: RefreshAll actually refreshes the queries in the workbook, not the querytables. The same answer applies.
In short: It's behaving as expected.

Is there a way in VBA to iterate through specific objects on a form?

I would like to have a subroutine in VBA that conditionally changes the Enabled property of each of 20+ buttons on a form via iteration rather than code them all by hand. These buttons are named similar to tables that they process. For example: A table to process is called "CUTLIST"; its corresponding button is called "but_CUTLIST". There is another table that holds the list of tables to be processed (used for iteration purposes in other subs).
What I have so far...
Private Sub txt_DataSet_GotFocus()
Dim sqlQry as String
Dim butName As String
Dim tableList As Recordset
Dim tempTable As Recordset
Set tableList = CurrentDb.OpenRecordset("TableList") 'names of tables for user to process
tableList.MoveFirst 'this line was corrected by moving out of the loop
Do Until tableList.EOF
sqlQry = 'SQL query that determines need for the button to be enabled/disabled
Set tempTable = CurrentDb.OpenRecordset(sqlQry)
If tempTable.RecordCount > 0 Then
'begin code that eludes me
butName = "but_" & tableList!tName
Me(butName).Enabled False
'end code that eludes me
End If
tableList.MoveNext
Loop
End Sub
If I remember correctly, JavaScript is capable of calling upon objects through a variable by handling them as elements of the document's object "array." Example: this[objID]=objVal Is such a thing possible with VBA or am I just going about this all wrong?
Viewing other questions... is this what's called "reflection"? If so, then this can't be done in VBA. :(
In case more explanation helps to answer the question better... I have a utility that runs SQL queries against a pre-defined set of tables. Each table has its own button, so that the user may process a query against any of the tables as needed. Depending on circumstances happening to data beforehand, any combination of the tables may need to be queried via pressing of said buttons. Constantly referring to the log, to see what was already done, gets cumbersome after processing several data sets. So, I'd like to have the buttons individually disable themselves if they are not needed for the currently focused data set. I have another idea on how to make that happen, but making this code work would be faster and I would learn something.
I'm not an expert on VBA, but I would re-arrange the code to take advantage of the fact that you can iterate through the control collection in the user form
Something like this:
Dim ctrl as Control
For Each ctrl in UserForm1.Controls
If TypeName(ctrl) = "Button" Then
ctrl.Enabled = True
End If
Next
You can pass the button name to some other function (from this loop) to determine whether the button in question should be enabled / disabled.

In Access form with disconnected ADO recordset, filter only works one time

Thanks to a great answer to a previous question (thanks Hansup and Remou), I've made an Access form based on a ADODB recordset that pulls at runtime. This enables it to have a checkbox that is only in memory. The checkbox is fine.
I've now been asked to add a drop-down filter by "Client" to the form. Easy enough. I made the drop-down box, made a little query for the control source, and in the After_Update event, added this code:
Private Sub Combo_PickClient_AfterUpdate()
Me.Filter = "Client='" & Combo_PickClient.Value & "'"
Me.FilterOn = True
Me.Refresh
End Sub
For testing purposes, I chose 2 clients. When I open the form, it shows both client's data (good). When I pick one client, it successfully filters the data (also good). When I pick the second client, it does nothing (not so good)
Why does this filter only work one time? It doesn't throw any error. The screen simply refreshes and that's it.
My best guess is that Access tries to reload the form's recordset from the data provider when you attempt to modify or remove a non-empty .Filter property. Since the disconnected recordset doesn't have a provider, that attempt fails. In my testing, I actually triggered error #31, "Data provider could not be initialized" at one point.
In your first attempt (which succeeded), the .Filter property was empty beforehand. I saw the same behavior and I'm guessing Access can apply a .Filter to an unfiltered recordset without revisiting the data provider.
Sorry about the guesswork. Unfortunately, that's the best I can offer for an explanation.
Anyway, I gave up trying to use the form's .Filter property to accomplish what I think you want. I found it easier to simply reload the disconnected recordset based on a query which includes your .Filter string in its WHERE clause. The code changes were minor, and the run time performance cost is neglible ... the reloaded recordset is displayed instantaneously after changing the combo box selection.
First I moved the code which builds the disconnected recordset from Form_Open to a separate function whose signature is ...
Private Function GetRecordset(Optional ByVal pFilter As String) _
As ADODB.Recordset
The function incorporates a non-empty pFilter argument into the WHERE clause of the SELECT query which feeds the disconnected recordset.
Then I changed Form_Open to one statement ...
Set Me.Recordset = GetRecordset()
So, if your combo box and disconnected recordset are both on the same form, the combo's After Update procedure could be ...
Private Sub Combo_PickClient_AfterUpdate()
Set Me.Recordset = GetRecordset("Client='" & _
Me.Combo_PickClient & "'")
End Sub
In my case, the disconnected recordset is displayed in a subform and the combo box is on its parent form. So I created a wrapper procedure in the subform code module and call that from the combo's After Update:
Call Me.SubFormControlName.Form.ChangeRecordset("Client='" & _
Me.Combo_PickClient & "'")
The wrapper procedure is very simple, but I found it convenient ...
Public Sub ChangeRecordset(Optional ByVal pFilter As String)
Set Me.Recordset = GetRecordset(pFilter)
End Sub