Access VBA - Editing table data isn't actioned immediately - vba

I've been searching high and low to try and figure this out, but haven't been able to find a similar question/thread here or elsewhere.
I have an Access database file (accdb in Access 2013) which holds roughly a dozen tables (with between 50 and 5000 records in each table), and a separate Access database file (also accdb) which acts as a user interface, using forms to allow the user to interact with the data stored in the 'database' file.
I've noticed that some forms are much faster to process (i.e. add, edit, and delete) data than other forms. For example, I have two similar forms (lets call them Form A and Form B) which display a listbox which is populated from the database. Double clicking an item in the listbox on either form opens a separate 'edit' form. When I close Form A's edit screen, the listbox on Form A requeries and displays the new data immediately. When I replicate the same steps on Form B, the listbox requeries, but the new data isn't included in the requery. Both 'edit' forms are using similar code to save data to the database (below):
Dim db As Database
Dim rs As Recordset
Set db = CurrentDb
Set rs = db.OpenRecordset("SELECT * FROM tblCards ORDER BY CardID", dbOpenDynaset)
rs.AddNew
rs!CardID = mintCardID
rs!CardType = Nz(txtType, "")
rs!CardNumber = Nz(txtCardNumber, "")
rs!CardAccount = Nz(txtAccount, "")
rs!Active = True
rs.Update
Set rs = Nothing
Set db = Nothing
If I save data using the 'slow' form, requerying the listbox seems to have no effect. If I close and reopen Form B two or three times, the new data eventually shows up. Form A uses the same code (just referencing a different table) but doesn't suffer from this lag. The lag occurs for all types of data operations (adding new data, editing existing data, and deleting records). If I delete a record from the slow form, I can still double click the deleted record and it will open the edit form, prefilling the form with details of that record.
From the testing I've performed, I suspect that the issue lies with the actual data saving/editing/deleting from the database, and that for some reason, when I save data on the slow form, it takes some time to update the database, so my requery code is pulling data from the database before it has a chance to insert the new data.
Has anyone encountered a similar issue and been able to resolve it?

Like June, I think you make it too hard for yourself. Anyway, if you insist, use the RecordsetClone of the form:
Dim rs As Recordset
Set rs = Me.RecordsetClone
rs.AddNew
rs!CardID.Value = mintCardID
' Avoid empty strings if at all possible.
rs!CardType.Value = Me!txtType.Value
rs!CardNumber.Value = Me!txtCardNumber.Value
rs!CardAccount.Value = Me!txtAccount.Value
rs!Active.Value = True
rs.Update
rs.Close

Related

Refresh forms recordset source with custom SQL query keeping current selection after closing second form

I have 2 forms in MS Access. One is an overview form as an endless form, the other shows details of the single records.
The overview form gets its content by a custom SQL passthrough query on opening:
Dim strSQL As String
strSQL = "SELECT DISTINCT tbl_Parts.strPart, Title, Comment FROM [tbl_Parts] inner JOIN [tbl_PartModules] ON tbl_Parts.strPart = tbl_PartModules.strPart ORDER BY tbl_Parts.strPart DESC"
Dim qdf As DAO.QueryDef, rst As DAO.Recordset
Set qdf = CurrentDb.CreateQueryDef("")
Dim strTmpConnQDF As String
strTmpConnQDF = CStr(Application.TempVars("tempvar_StrCnxn"))
qdf.Connect = strTmpConnQDF
qdf.sql = strSQL
qdf.ReturnsRecords = True
Set rst = qdf.OpenRecordset
Set Forms![frm_PartsOverview].Recordset = rst
Forms![frm_PartsOverview].Requery
'No rst.Close to have the data still in the Form!
Set qdf = Nothing
Within that overview, buttons can be used to change the underlying SQL query to show different records.
Each record has a "show details" button in the endless form which opens the detail form.
Within the detail form, I can change the data of the underlying record. After closing this form, I want to keep all settings (record selection, SQL query data) that were before opening the detail form. But I also want to update the underlying data, showing the changes made in the detail form also on the overview form.
I tried this code with the "hide" button of the detail form, but this does not work:
Forms!frm_PartsDetail.Visible = False
Forms!frm_PartsOverview.Recalc
Forms!frm_PartsOverview.Refresh
Forms!frm_PartsOverview.Requery
I tried all three variants (Recalc, Frequery, Refresh) but none of them did the trick.
I guess its because of the way I assign the SQL query as the recordsource of the form.
But how to update the recordsource, keeping all settings like they were before entering the detail form?
Well, you can use a me.Refresh - that will (should) show updates to the form. (but, the fact of it being basedon a pt query probably will effect this. I suggest changing to a linked view - thus no pt query required and thus access can stay on the given row, and you can use a me.refresh. (with a PT query and you having assigned the forms data source "one time", then me.refresh is unlikely to work.
So, I suggest using a linked view for that form. (and performance will be just as good at the pt query - and your me.Refresh will work (to show any changes made to the table).
the other way? Grab the current row (PK) before you do the me.Requery. Then do a find first on the pk row, and jump back to the row in question.
So, ignoring you have a sub form etc.
Then this code:
Dim PK As Long
PK = Me!ID
Me.Requery
Dim rst As DAO.Recordset
Set rst = Me.Recordset
rst.FindFirst ("ID = " & PK)
However, as noted, if you base that form on a linked view, and not set the datasource as you do, then a me.Refresh will show any changes made, and also stay on that current row.
So, in your case, to re-calc, re-display, and show any updates, and return to current row you were on?
"air code warning"
Dim PK As Long
Dim f As Form
Dim rst As DAO.Recordset
Set f = Forms!frm_PartsDetail
PK = f!ID
f.Requery
Set rst = f.Recordset
rst.FindFirst ("ID = " & PK)

MS Access Form Bind to ADO Recordset

Driving me bonkers...
The problem is one of speed.
I have a working SQL Server linked to a client accessible website, which I am wanting to add an Access front end to enable us office bound staff to better support both client and field staff.
The current form I have constructed is a main form with five sub forms contained within it, giving us all the relevant client information in one view. This works however is taking 24 seconds to load a single clients complete records.
I have looked at the SQL Server and found the absence of indexes, fixed this and got the time down to 24 seconds with consequent loads closer to 18 seconds depending on the client (some have a lot more records). This might be okay, as whilst this is a relative eternity in computing time in real world time its okay...but not great. I would like to see if I can get a better load by changing the way I connect and how the form is bound to the records etc.
In looking at the various ideas and reading a lot I found:
https://learn.microsoft.com/en-us/office/vba/access/concepts/activex-data-objects/bind-a-form-to-an-ado-recordset?source=docs
Which appealed to me as I am more inclined to use ADO, seldom if ever to I use DAO. ADO I understood originally was intended to use with SQL and so on, and it seems like a sensible idea.
Again as I understand it if I can get this to work it will act as a pass through query returning only one record over the net and should consequently speed my form up considerably. However it wont work.
My code is:
Private Sub cssSetForm(lngID As Long)
Dim cnn As New ADODB.Connection
Dim Rs1 As New ADODB.Recordset
Dim strSQL As String
Dim strR As String
cnn = "Provider=MSOLEDBSQL;Server=Server;Database=DatabaseName;UID=UserName; PWD=Password;"
cnn.Open
strSQL = "SELECT Clients.Clientid, Clients.AccountContact, Clients.AccountEmail, Clients.Address, Clients.Name, Clients.OfficePhone, Clients.PostCode, " & _
"Clients.ShentonAcc, Clients.Suburb FROM Clients WHERE (((Clients.Clientid)=" & lngID & "));"
With Rs1
Set .ActiveConnection = cnn
.Source = strSQL
.LockType = adLockPessimistic
.CursorType = adOpenKeyset
.Open
End With
Debug.Print Rs1.RecordCount
Me.Recordset = Rs1
End sub
Now I am getting no errors until Me.Recordset=rs1 which is generating an error 3251 Operation is not supported for this type of object which is very nice for someone that understands why this is not supported when it is no different than I can see to the example I was copying from.
I don't understand why the form I am working on doesn't support recordsets according to the error message? Is there an error in my code? Is the error in my understanding of the destructions from the linked site? Is the error something else?
Thanks for the help
Well, loading up 5 sub forms is a lot of data pulling. converting to ado reocdsets is NOT going to speed this up.
What you want to do here is NOT load up the sub forms until such time the user say clicks on the appropriate tab to load the one given sub form.
As long as the form in question is opened with a were clause, then the one main form will ONLY pull the one main record from sql server. So doing all kinds of fancy reocrdsets etc. will gain you next to nothing. So, always - but always launch your main form to the one record. If that main form is bound to a table of 5,000 rows, or 1 million rows, it will load instant despite the fact that the form is bound directly to the linked table with 1 million rows.
With this one main form, you edit or do whatever, and then close it. You are then of course right back to the search form/prompt you have to ask the user what reocrd to work on. So, how a accouting package works, or even google? You search, display the search resutlts and pick ONE thing to view. This approach should get your main form load down to about 1 second. Again, just use the "where" clause when you open that form:
eg:
dim strInv as string
strInv = InputBox("Enter invoice number to view")
docmd.OpenForm "frmInvoice",,,"InvoiceNum = " & strInv
Of course the above is air code, and you will likely build some search form say like this:
So in above, the user types in a bit of the name. We then fill the form with a simple where clause, or
me.MySubForm.RecordSource = "select * from tourCust where LastName like '" & sTextbox & "*'"
When a user clicks on the glasses icon to edit + view the ONE row, we use this:
docmd.OpenForm "frmDetails",,,"id = " & me!id
Again, all bound forms, and dispite the tables having 500,000+ rows, the loading of the forms is instant - even when the back end is SQL server.
So, adopt a prompt + search results + edit/view design pattern. EVERY single software system has this loop or design pattern. Not only is it user friendly, it also performs well since access DOES NOT pull the whole table, but ONLY the reocrds you tell the form to load by using the where clause as per above.
Now, for the child forms (sub forms).
As noted, don't load them until the user actually clicks on the given tab.
So, in the on-change event of the tab, you can go:
If Me.TabMainpage.Pages(Me.TabMainpage).Name = Me.pgeDocs.Name Then
'' the tab been changed to show past tours
dim strSQL as string
strSQL = "select * from tblPastTours where tour_ID = " & me!ID
me.
' dynamic load the sub form
If Me.frmSubPastTours .SourceObject = "" Then
Me.frmSubPastTours.SourceObject = "Forms.frmPastTours"
End If
' now load sql into form
me.frmSubPastTours.Form.RecordSource = strSQL
The above is mostly air code, but the idea here is:
don't load the sub form until you need to.
OR YOU can have the sub form load, but leave its RecordSource blank and STUFF in the sql you need for display WHEN you click on the tab.
It is possible that you need all sub forms to display. You could thus leave all sub form RecordSource blank, and then it the main form on-load event, simply stuff in the sql to each sub form. This might improve speed a bit, and with your slower connection this would be the least amount of work.
You "fail" to mention how you launch + load the main form, but as noted, it should be opend to the ONE reocrd. The link master/child pulling of data can be bit slow, and I can't say JUST using the above sql stuff into those forms will help a lot. I would try that first as it is the least amount of work. If load time is still too slow, then placing te sub forms behind a tab control and ONLY loading the sub form, and then setting the datasource will be the fastest.
Attempting to build all those recordsets, and then bind them to a form? It not speed things up, and just shoving a sql string into the form (or sub form) recordSource amounts to really the SAME thing and same performance anyway. So, you save tons of work and code, and quite much the above idea not only applies to sql server back ends, but we all VERY often dynamic load sub-forms and don't load them until such time you view the sub form say behind a tab control.

MS Access VBA - take form record out of edit mode to allow recordset operations

I'm editing records on a form. I then want to do a recordset operation on the entire recordset underlying the form. The recordset operation then runs into the error "Error - Could not update; currently locked by another session on this machine"...clearly because a record is being edited.
Regardless of whether this is good practice or not, how might I use vba to get the application to cancel, ignore or complete the form editing operation so that the recordset operation can continue...or it just not possible, because the GUI cannot be "released" from within VBA (kind of like when Excel cells are being edited)?
This can occur quite often. The simple trick is to ensure that the current form record is not "dirty" or is not pending a update.
Thus you can go:
if me.dirty = True then me.Dirty = False
' now call your update routines etc.
Because those other routines may well update the screen/record you are viewing, then you can execute a me.Refresh to ensure that any changes are displayed.
The above will force the record write. In fact if I am in a form, and plan say to launch some popup form, or a form in the next "step" for the user, I also tend to force write the current form out - it is not only safer, but avoids the common "this record been changed by another user". In most cases, the other user is in fact your code.
So simply save the record with the above one line of code - this should eliminate this issue in general. Keep in mind that you can set a form to lock the whole table in the "data" tab of the forms property sheet, but that is unlikely your problem.
Is the form data-bound? It doesn't sound like it.
Me!SomeField = ...
DoCmd.RunCommand acCmdSaveRecord
If your form has control bound to "SomeField", then the form will be updated automatically.
1) Update records using SQL code. For example, you have ID of record that should be updated in the form data set, so you can write something like:
Call CurrentDB.Execute( _
"UPDATE SomeTable SET SomeField = SomeValue WHERE SomeTableID = " & Me!SomeTableID, dbSeeChanges)
2) You can look at the Bookmark property - both Recordset and Form has this property, it describes the record position. So you can write something like this (not the best example, but can help you to get an idea):
Dim Rs as Recordset
Set Rs = Me.RecordsetClone 'make a reference copy of the form recordset
Rs.Bookmark = Me.Bookmark 'locate this recordset to the form current record

Identifying exact location of data entry issues when bulk copying or importing from excel to access

One of the requirements of a project that I have is to allow users to import or copy and paste in bulk a few hundred rows from excel to access. However, there is a reasonable chance due to human error that there will be some data validation issues between the imported data and the table structure/referential integrity rules. I would like to be able to identify exactly the field/s and record/s where these issues are occuring so that I can point them out to the user for correction.
As such the standard error essages like 'you cannot add or change a record because a related record is required in...' or 'data type mismatch in criteria or expression' are not descriptive enough to the exact location of the problem so even if I catch them I can't really give a better descriptor anyway
I am debating importing to a completely free text temporary table, then looping an insert to move one row at a time from the temp table to the properly validated table and using dbfailonerror to catch issues on individual records that need correction (the user needs to correct them I can't do this through code)
My question is whether this is a reasonable approach, is there a better/easier way, or a way to get a more specific error from access rather than using a loop?
Thanks
There are 2 ways to do this. I'm not sure what method you are using to do the import but if is as simple as copying rows from the excel sheet to the table Access will generate a Paste_errors table that will show the rows it couldn't import. The wizard will do the same thing but I think its prone to crashing.
The way I typically do it is actually have the end user use an excel template with a VBA backend that does the uploading. You can check each value conditionally and give a better descriptive alert and/or shuttle any defective rows to a temporary table for you to review.
You can do this the opposite way and do the import through Access VBA but that would be more coding since you would have to create an Excel object in code, open the sheet, etc.
I setup a Quickbooks export of a accounts receivable table by creating a User DSN on the local machine pointing to the Access file, opening an ADO recordset and looping through the rows one column at a time applying logic to each row.
Quickbooks would ask for an existing file to dump the data into so I made that a template file on the network. It sounds like your users may have to enter directly into the spreadsheet so you would have to distribute the template but the results are the same.
Example of Looping through the sheet and validating rows
Create a DSN file to the database and store it on a shared drive, this way you don't have to hardcode the string and if you need to change something, you only have to change the dsn file instead of redistributing templates.
Public Sub Upload
'Declare the main recordset that the records will be uploaded to.
Dim rstUpload as New Adodb.Recordset
'Declare a utility recordset object that you can reuse to check referential tables
Dim rstCheck as New Adodb.recordset
'Declare a utility command object that you can reuse to check referential tables
Dim SQLCommand as New Adodb.Command
'Declare the connection object to the database
Dim dataConn as New Adodb.Connection
'A tracking flag if you find something in a row that won't upload
Dim CannotUpload as Boolean
'Open the connection to the access database
dataConn.Open "\\Server\Share\Mydatabase.dsn" 'Your dsn file'
Set SQLCommand.ActiveConnection = DataConn
rst.Open "yourTable", dataConn, adOpenDynamic, adLockBatchOptimistic
For i = 1 to 100 ' Rows
*You may want to do a pass through the rows so you can get an accurate count, usually just loop through the rows until a column that must have data is blank. If your users are skipping rows that is going to be a problem.
rstUpload.AddNew
'Set the tracking Flag to False indicating you can upload this row, this will be changed if any field cannot be validated
CannotUpload = False
'First Column/Field: 'Non critical field, any value will do
rstUpload("Field1").Value = Range(i,1).Value '
'Second Column/Field has a referential integrity constraints
'Run a query against the table that has the values you are trying to validate.
SQLCommand.CommandText = "Select IDField From YourTable where ID = " & Range(i,2).Value
Set rstCheck = SQLCommand.Execute
'Check if any records were returned, if none than the value you are checking is invalid
If rstCheck.RecordCount > 0 Then 'we matched the value in the cell
rstUpload ("Field2").Value = Range(i,2).Value
else
'Design a flagging method, maybe highlight the cell in question
CannotUpload = True
End if
....continue through your columns in the same fashion, reusing the SQLCommand Object and the Utility recordset when you need to lookup something in another table
'After last column
If CannotUpload = False Then
'Nothing triggered the flag so this row is good to go
rstUpload.Update
Else
'Cannot Upload is true and this row won't be uploaded
rstUpload.Cancel
End If
Next i
dataconn.Close
set dataConn = Nothing
set rstUpload = Nothing
set rstCheck = Nothing
set SQLCommand = Nothing

VBA to search the external data table for a record

I have linked to external data in Visio 2013 and have four different tabs/tables. When a user double clicks a shape the text of that shape will be used to search for a record inthe tables. Note that this data is not linked in the Visio linked data method. Just need to search the table manually not rely on some link between the data record and the shape that is pre-existing.
External data is imported fine. I can capture the text of the double clicked shape. If you can tell me how to run a query against the external data that has been pulled into Visio then HIGHLIGHT the row.
Thank You
Once you have the information regarding the data source (such as server/database name, credentials, etc.), you just need a few lines of VBA in a module like so:
Dim theVariableForDataFieldYouWant as Integer
Dim db as database
Dim rs as recordset
Set db = YourDataConnectionStringHere
Dim sql as string
sql = "Select aFieldYouWant, anotherFieldYouWant from yourTableName WHERE someField = yourCriteria"
set rs = db.openRecordSet(sql)
if rs.eof then
' you recordset is empty, nothing matched
else
theVariableForDataFieldYouWant = rs.fields("theDataField")
end if
rs.close
db.close
set rs = nothing
set db = nothing
If you provide a bit more detail, I can be more precise, but basically that sums it up. Put the code into a function or sub, then call it on your selected event.