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

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

Related

Access button to open multiple reports depending on user input

I am new to access so this might be an easy task or I am just trying to tackle it wrongly. I have a report that has various columns, sample id, sample time, sample type, dry matter, moisture. I am trying to create a button that has an input box for the user to chose what column to sort the report by. So far I thought of creating various reports that have been sorted by each column, named the reports by the column that sorts them then I am trying to have the open report action have a parameter that opens the report linked to the column entered at the input box. Is this even possible or is there a workaround for this.
PS. I am avoiding creating various buttons since it will fill up the screen.
Okay, this is pretty generic and will require some tweaking but it shows the core of how to do this.
To start with you need a module (so not form/report code). This is where the globals will be assigned values:
Option Compare Database
Option Explicit
Global rptname As String
Global fldname As String
Sub setRptName(name As String)
rptname = "Report Sorted by: " & name
fldname = name
End Sub
You will call that code inside the Click() event of your command button on your form and then open the report after that. This will take a combo box value and pass that value to the module code, creating the two global variables.:
Private Sub cmd_report_Click()
mdl_Globals.setRptName Me.cmb_fields.Value
DoCmd.OpenReport "Report1", acViewPreview
End Sub
I'm unsure if this will work with non-preview views, but you probably want preview anyway.
Lastly in the report code behind, you need the Load() and open() events, you may be able to get it to work with both inside one or other but I know this one works.
So to set the caption:
Private Sub Report_Load()
Me.lbl_header.Caption = rptname
End Sub
And then to sort:
Private Sub report_open(Cancel As Integer)
Me.Report.OrderBy = "[" & fldname & "]"
Me.Report.OrderByOn = True
End Sub
If your entry box on the form does not have entries that exactly match the name(s) of the fields in the table you will get a parameter popup.

Combo Box selecting First Record and not the defined record

first time asking here because i'm stumped!
I have a MS Access Form with 2 combo boxes:
First combo box (cboPub) selects a Publisher which then filters the second combo box (cboTitle), this works fine however the pulled record is the first record and not the one that meets the criteria.
Code below:
Private Sub cboPub_AfterUpdate()
cboTitle = Null
cboTitle.Requery
End Sub
Sub cboTitle_AfterUpdate()
' Find the record that matches the control.
Me.RecordsetClone.FindFirst "[Supplier] = """ & Me![cboTitle] & """"
Me.Bookmark = Me.RecordsetClone.Bookmark
End Sub
I suspect it is because of the line here:
Me.RecordsetClone.FindFirst "[Supplier] = """ & Me![cboTitle] & """"
but i dont know what to change to have it select the correct record.
A common task is to filter the data in a form's details section from unbound combo boxes in the form header section. (don't set the comboboxs' data sources) I assume that is what you are trying to do because me in the afterupdate event refers to the form. If so set the forms filter rather than messing with the forms record source. Your second problem is cboTitle's afterupdate event doesn't fire when cbotitle's value is changed through vba code. So put all the code in cboPub.
Private Sub cboPub_AfterUpdate()
'cboTitle = dlookup("Title", "SomeTable", "SupplierID = " & cboPub.value) 'how I filtered cboTitle
Me.Filter = "SupplierID = " & cboPub.Value
Me.FilterOn = True
End Sub
Private Sub cboTitle_AfterUpdate()
Debug.Print "check the immediate window for how cboTitle's after update event does not fire when cboTitle is updated with VBA"
End Sub
warning: treat this as pseudo code; the exact filter depends on how Supplier, Publisher, and Title are related in your tables and combo boxes.
Explanation: I think the programmers behind access intentionally forced us to set up our forms entirely at design time. No matter what I have tried Access does not display runtime changes to it's record source in any acceptable fashion. It is the access way or the high way. Linking up your record source and form variables at design time then filtering is the only approach I have found that works. I assume the reasons behind why you cannot trigger an afterupdate event through code are similar.

How to make checkbox control element reference itself [duplicate]

I have a bunch of TextBox-Button pairs on a form. When the button is clicked I want to insert the value of the text box into a database. The name TextBoxes and Buttons follow a naming standard, for example Value1Tb - Value1Cmd and Value2Tb - Value2Cmd.
My problem is that since I want to do the same for every button I would like the possibility to write a Sub like:
Private Sub AnyButton_Click(sender As CommandButton)
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
End Sub
But I cannot find a way to point the Click-event of a Button to a different sub than the standard Name_Click().
Anybody know a way around this, that doesn't involve me writing 50 or so different Name_Click() subs?
If you are OK to use Form Controls rather that ActiveX, as it looks as though you may be at the moment, then Chris' solution seems good.
However if you need ActiveX CommandButtons then you are unable (as the VBA compiler will tell you, "Procedure declaration does not match...") to have parameters in the callback for the click event, and you are unable to raise the event from multiple objects, although you do of course know which button raised the event (since the relationship is 1 CommandButton = 1 Sub).
So... I would go with something like:
Private Sub Value1Cmd_Click()
Call TheMethod(Value1Cmd)
End Sub
Private Sub Value2Cmd_Click()
Call TheMethod(Value2Cmd)
End Sub
Private Sub TheRealMethod(sender As CommandButton)
' Do your thing '
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
' Etcetera... '
End Sub
Requires a stub for each button, so some copying and pasting to begin with, but then easy to maintain etcetera as all _Click event callbacks are pointing at the same method...
Edit:
E.g.
Sub AutoWriteTheStubs()
Dim theStubs As String
Dim i As Long
For i = 1 To 10
theStubs = theStubs & "Private Sub Value" & CStr(i) & "Cmd_Click()" & vbCrLf _
& " Call TheMethod(Value" & CStr(i) & "Cmd)" & vbCrLf _
& "End Sub" & vbCrLf & vbCrLf
Next i
Debug.Print theStubs
End Sub
It seems that what you want is to get the name of the clicked button. If you are creating buttons like this:
(where 'i' increments in a loop)
Set btn = Sheet1.Buttons.Add( , , , ,)
With btn
.OnAction = "btnSub"
.Caption = "Upadate"
.Name = "btn" & CStr(i) & "Cmd"
End With
and then defining a generic "private sub btnSub()" for all the buttons, you could at least get the name of the button that was clicked using Application.Caller. Something like:
Private Sub btnSub()
Dim ButtonName As String
ButtonName = Application.Caller
MsgBox ("Hello:" & ButtonName)
End Sub
Hope it helps!
I decided to make this an answer because I am doing something similar and I confirmed that it works.
You can store the OLEobjects in a Collection, of arbitrary size, containing Custom Class Objects that include the OLEobjects and associations and the events that you need. Thus you can completely avoid any code stubs.
Create a Custom Class to bind the Button and TextBox pairs.
Declare the Button object WithEvents.
Include your call-back in the exposed button event handler in the Class Module.
Put a Public routine in a Standard Module to initialise a Collection of these Custom Class objects by spanning the Form Controls. You can also use this to Add the controls programmatically as a 'reBuild' option. The Collection can be inside another Class Module with all of the management routines, but it needs to be Instantiated and loaded in a Standard Module.
Put a public routine in a standard module to receive the call-backs with whatever context you need. This can also be in a Worksheet Module if it makes for better encapsulation. You can use late binding to reference the callback or CallByName.
You need to bear in mind that the Module of the Form will recompile every time you add a control, so you have to be careful where you put your code.
My application has the controls directly on the Worksheet Surface, so I can't put the the Collection Class in, or source any initialisation of the Collection from the Worksheet module. This would amount to self modifying code and it grinds excel to a halt.
I dreamed this idea up through bloody-minded idealism (not necessarily a good thing) but, of course, I was not the first one to think of it as you can see here. #Tim Williams explains it in his answer. You can also google VBA Control Array Events to see plenty of similar examples including an excellent article by #SiddharthRout. In line with the VB6 analogy, he uses an Array instead of a Collection to achieve the same result.
I'll try to post some code later. My application is a bit different so it will take a lot of work to trim it down, but the principle is the same.
The other thing to bear in mind is that VBE really struggles with this type of thing so don't worry if it is loading up you processors. After you re-start with VBE off, all will be fine.
I have this same situation, and I just have a click event for every button that is a wrapper to the function I want to call. This also allows you to pass sheet-specific parameters if you need to.
Example:
Public Sub StoreButton_Click()
' Store values for transaction sheet 3/27/09 ljr
Call StoreTransValues(ActiveSheet)
End Sub
I just published (Open Source) the Event Centralizer for MSForms.
Citation: "The Event Centralizer for MSForms is a VBA programming tool that allows all sorts of custom grouping when writing handlers for the events occurring in UserForms.
With the Event Centralizer for MSForms, it is easy for example to have all TextBoxes react the same way when the Enter event occurs, or all except one, or only those with a particular Tag value.
Thanks to its events logs system, the Event Centralizer for MSForms is a powerful learning and debugging help."
I can't explain here how it works. I tried to do it on the site.
Set the event to =FunctionName(parameter).
A bit late but this may help someone else:
If you have a function called OpenDocumentById(docID as integer), then each control calling the function would have in the event the following:
cmd1's On Click event:
=OpenDocumentById([DocID])
cmd2's On Click event:
=OpenDocumentById([DocID])
etc...

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.

How to check whether Connection Refresh was successful

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.