VBA Executing CODE from a ComboBox - ms-access

I have a very complex process that involves downloading a number of files from different shares, concatenating those files into working tables, manipulating and calculating related information, and then exporting specific fields (and calculations) as reports into a number of Excel workbooks.
I have this process coded so that I can click one button and the entire process will execute end to end. I have a series of text boxes that function as 'indicators' (red - this part failed, green - this part succeeded). As you can imagine, the code for the entire process is HUGE (32 pages when copied into MSWord) and difficult to weed through when I have a problem.
I got it into my mind that I wanted to put the code into a table so that it was much more modular and easier to deal with. I have setup a combo box with the action that I want to take and a second combo box with the report/file/object that I want to work with (ie Delete - Table 2, Acquire - File 1, Export - Report 4). I have been successful at creating the SQL statement to do simple things like del * from tbl_test and execute that from the combo boxes without any issue.
What I need to know is if there is a way to put what is essentially a code snippet into the table (memo field) and then have that vba code execute when I select the matching combos.
IE the code for 'Acquire - File1' is completely VBA code; it maps a network drive, locates the file, downloads the file, and moves it to a directory.
IE the code for 'Scrub - tblMain_Part1' is a combination of vba and sql code; it checks for the existence of a file (vba), if it finds it, it deletes a portion of the main table (sql) and appends the contents of the file it finds (sql), then it updates the monitor to indicate that it is completed (vba). If the file is not found, it changes the monitor box to red and updates a command button caption (vba)
I am NOT a genius with vba, but I hold my own. The thought process I had was that if I can essentially get the code broken into managable chunks in the table, I could call the code smippets in order if I want to run the entire process, or I could just re-execute portions of the code as needed by selecting the action and report/file/object combination.
Any thoughts/ideas are appreciated.

I think it would be best to split the code into Subs. The table you loop through would have a Sub-Name field and a blnSuccess field. Your code would loop though the table running each sub and then updating blnSuccess based on any errors you receive. This would give you queryable result set when you try to see what happened.

Consider using macros. You shouldn't need a table. Also, consider moving your hard-coded SQL to queries.

I think that you shouldn't use a table, just create a module with different subs for each operation. On your button event, after the combo selections, I would do a case statement.
dim strOperation as string
strOperation = me!selectionOne
Select Case strOperation
Case "delete": deleteTable(me!selectionTwo)
Case "export": export(me!selectionTwo)
case "acquire": acquire(me!selectionTwo)
End Select
Of course, you'd have your acquire, delete, and export methods written in a module and have whatever parameters you need for each operation there.
This is just one idea of many that you could use to approach this.

I was going to edit the original answer but this seems to be off on a different tack....
I think it would be best to split the code into functions that return a string if there is an error. The table you loop through would have a strFunction,strError and strObject fields. Your code would loop though the table running each function based on the case statement while passing the strObject as a string and then updating strError based on any errors you receive. You could query the table after this process to see which records have errors in them.
If the button is called cmdRunAll here is the code for it.
Private Sub cmdRunAll_Click()
On Error GoTo ErrHandler
Dim rst As DAO.Recordset
Set rst = CurrentDb.OpenRecordset("tblCode", dbOpenDynaset, dbSeeChanges)
If Not rst.EOF Then
With rst
.MoveFirst
Do While Not .EOF
.Edit
Select Case !strFunction
Case "fExport"
!strError = fExport(!strObject)
End Select
.Update
.MoveNext
Loop
End With
End If
rst.Close
Set rst = Nothing
MsgBox "Processes complete"
Exit Sub
ErrHandler:
Debug.Print Err.Description & " cmdRunAll_Click " & Me.Name
Resume Next
End Sub
Here is a simple sample function
Public Function fExport(strTable As String) As String
On Error GoTo ErrHandler
Dim strError As String
strError = ""
DoCmd.TransferText acExportDelim, , strTable, "C:\users\IusedMyUserNameHere\" & strTable & ".txt"
fExport = strError
Exit Function
ErrHandler:
strError = Err.Description
Resume Next
End Function

Related

DoCmd.DeleteObject acTable - The record ... specified on this form or report does not exist

Let me start by saying I am not at all familiar with Access or VBA. However, I am the IT guy and at some point someone created an MS Access database that does a thing and now I have to support it.
We have a database that upon opening deletes any old data and re-runs the external query that makes this application work. Occasionally whatever state the program exited out in that table already does not exist. This causes MS Access to hang on the delete line and I have to run the debugger, comment out the DoCmd.DeleteObject line, re run, and then un-comment the line to let the user continue with their day.
I want to add in some sort of conditional statement, but anything I've been able to google in terms of If...Then statements or 'TableExist' type functions always causes an error to be thrown that I haven't defined the function. My best guess is I'm nesting this incorrectly or I'm not calling some sort of external function correctly, but as I said my VBA knowledge is extremely limited.
This code executes on startup:
Public Function Startup() As Integer
DoCmd.Close acForm, "soLookup"
DoCmd.DeleteObject acTable, "sales_order"
DoCmd.RunSavedImportExport "Import-sales_order"
DoCmd.OpenForm "soLookup"
End Function
Its the
DoCmd.DeleteObject acTable, "sales_order"
Line that causes things to fail.
I've attempted to restructure this several times based on several examples I had found, but I'll only bother with one below
Public Function Startup() As Integer
DoCmd.Close acForm, "soLookup"
If TableExists("sales_orders") Then
DoCmd.DeleteObject acTable, "sales_orders"
Else
'Do nothing
End If
DoCmd.RunSavedImportExport "Import-sales_order"
DoCmd.OpenForm "soLookup"
End Function
Nothing I seem to try seems to give me any result other than an error of some sort. All I want to do is add a conditional statement to this 'Startup' bit that checks if the "sales_order" table even if exists, and if it doesn't, then to just move on to the next comment and forget the DoCmd.DeleteObject. How can I achieve this functionality?! Thanks!
The TableExists function is not a standard function in Access. You need to define it yourself.
There are two main ways to define such a function, by trying and error trapping, or by iterating through all tables and checking names.
Way 1 (error trapping):
Public Function TableExists(TableName As String) As Boolean
On Error Resume Next
TableExists = CurrentDb.TableDefs(TableName).Name = TableName
End Function
Way 2 (iterating collection):
Public Function TableExists(TableName As String) As Boolean
Dim td As DAO.TableDef
For Each td In CurrentDb.TableDefs
If td.Name = TableName Then
TableExists = True
Exit Function
End If
Next
End Function
Define either of these functions in a public module, and your last approach should work
I will instead modify the query to import the source data into a new table (sales_orders), which will overwrite existing data when the query runs and so I don't have to delete the table and have additional check for TableExists

How to force an Access query datasheet to refresh its data

I am new on access and what I am trying to do is a select with a criteria so I created a query with the wizard and seted the criteria with a text from a form ([Forms]![Form1]![Transacao]) and created a button to run the query at the first time works great but when I type something else and click the button the datas do not refresh. What I have to do to refresh? I've tryed to add refresh on the event click of the button and did not work.
Thanks in advance for your help.
In Access, a query is usually opened in a default Datasheet view. This default datasheet is contained in a window (or tab) that is only accessible using Macros or DoCmd in VBA.
Once a query window is open, its data will not necessarily update automatically when new records are added to the underlying table(s). The datasheet needs to be "requeried". (Incidentally, the term "refresh" is usually reserved to mean "redrawing" a window on the screen and has nothing to do with the data. This is especially the case in programming and development environments which deal with data and drawing/painting windows and controls on the screen.)
Here is one way to force a query to update its data (when open in its default datasheet view):
DoCmd.OpenQuery "QueryName"
DoCmd.Requery
Calling OpenQuery should also activate the query window if it is already open. If you find that the windows does not activate, you can also call DoCmd.SelectObject acQuery, "QueryName" before DoCmd.Requery.
The DoCmd methods correspond to Macro actions, so if the query is activated by a Macro, just add the Requery action to the macro after the OpenQuery or SelectObject actions. Leave the Control Name parameter of the Requery action blank to force the entire query to updated.
I know this question is a bit stale at this point, but since I couldn't find a suitable answer to this question and the above answer didn't work for me (and still hasn't been accepted), I thought I'd offer my solution for those few poor saps still stuck developing applications in Access. My use case was slightly different (changing the underlying SQL of a query, then opening/refreshing it), but the same principle could be applied. The gist is to first check to see if the query is open and close it if it is. Then open it up again.
To do this, paste this code into a VBA module:
Public Function open_or_refresh_query(query_name As String, Optional sql_str As String = "")
' Refresh or open an existing query
' query_name: Name of the query
' sql_str: optional new SQL string if changing the underlying SQL. If not given,
' the query will open with its existing SQL string
On Error GoTo err_handler
Dim qdf As QueryDef
' Loop through each query in the DB and find the one of interest by name
For Each qdf In CurrentDb.QueryDefs
If qdf.Name = query_name Then
' If the query is open, close it
If SysCmd(acSysCmdGetObjectState, acQuery, query_name) <> 0 Then
DoCmd.Close acQuery, query_name, acSaveNo
End If
Exit For
End If
Next qdf
Set qdf = CurrentDb.QueryDefs(query_name)
' Reset the SQL if new SQL string was given
If Len(sql_str) > 0 Then qdf.sql = sql_str
' Close the QueryDef object to release resources
qdf.Close
' Open the query in default datasheet view
DoCmd.OpenQuery query_name
exit_function:
Exit Function
err_handler:
MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "Error"
Resume exit_function
End Function
At this point you could call this function from any other VBA code in your project. To open/refresh a query from a macro as the OP wanted to do, create a new macro and add a RunCode action with open_or_refresh_query("my_query") in the Function Name field, changing my_query to the name of your query.

Access the database engine could not lock table - Make Table script for fields held on the form

I'm having a bit of trouble with some vba script i'm attempting to run from a button on a form.
Via a linked MDB file, I've written a string of Make Table queries that help certain other queries work. Rather than have the user re-run each individual Make Table query one-by-one, I've put the queries in a VBA script (using DoCmd.RunSQL) and then assigned that script to a button on a form.
The data source i'm using also has a File Info table that contains the File Name and the File Date of the data.
When the user clicks the button, I'd like them to know what data source was used when the string of Make Tables queries was run; that way they know what data the script was run on.
My approach to this was to create a final Make Table query using the File Info table to put the File Name and File Date in it's own table.
I would then add these fields to the form and add the DoCmd.RunSQL for this final Make Table query in to the bottom of the VBA script for the button; once all the Make Tables had run, the File Info and File Date fields would then be displayed/updated on the form telling the user what data file had been used the last time the script was run.
Here's the code for this (for brevity I've summarised all the Make Table scripts that run prior to the FileInfo as some bogus "AllOtherMakeTables" string)
Private Sub Command0_Click()
On Error GoTo Err_Command0_Click
DoCmd.SetWarnings False
Dim AllOtherMakeTables As String
Dim FileInfoStamp As String
AllOtherMakeTables = " SELECT SomeField INTO AnotherTable" _
& " FROM SomeTable" _
FileInfoStamp = " SELECT FileInfo.FileName, FileInfo.FileDate INTO FileInfoStamp" _
& " FROM FileInfo;" _
DoCmd.RunSQL AllOtherMakeTables
DoCmd.RunSQL FileInfoStamp
DoCmd.SetWarnings True
Exit_Command0_Click:
Exit Sub
Err_Command0_Click:
MsgBox Err.Description
Resume Exit_Command0_Click
End Sub
Clicking the button using the script above yields the following error message:
The database engine could not lock table 'FileInfoStamp' because it is
already in use by another person or process
I think what is happening is the fields I added to the form (FileName and FileDate) are locking the FileInfoStamp table when the form is open, so when the script tries to recreate the FileInfoStamp it is unable to do this as the table is locked.
I thought this might be fixed simply by adding a DoCmd.Close at the start of the above script and then adding a DoCmd.OpenForm at the end of the script (essentially closing the form whilst the Make Table commands are run and then re-opening the form at the end).
All this does though is close the form and bring up the same error message. So I guess even though the form is "closed" the connections with the fields on the form still remain active in some way(...?)
Any assistance on how I might get around this would be much appreciated. Thank you.
Do not recreate FileInfoStamp each time. Use these two steps instead:
discard existing rows
append the new data
Dim db As DAO.database
Dim FileInfoStamp As String
FileInfoStamp = "INSERT INTO FileInfoStamp(FileName, FileDate)" & vbCrLf & _
"SELECT fi.FileName, fi.FileDate FROM FileInfo AS fi;"
Debug.Print FileInfoStamp
Set db = CurrentDb
' empty out FileInfoStamp
db.Execute "DELETE FROM FileInfoStamp;", dbFailOnError
' add new data to FileInfoStamp
db.Execute FileInfoStamp, dbFailOnError
Set db = Nothing
Add an error handler to deal with any problems turned up from dbFailOnError.
Instead of DoCmd.RunSQL, use the DAO database .Execute method for your other queries. With that approach, you will not have any motivation to use DoCmd.SetWarnings False. Turning SetWarnings off is unwise because it suppresses information you need to diagnose problems.

Creating and writing data to a table is MS Access

EDIT: Changed as I have a different issue with the same code
2nd Edit: Adding additional code that seems to be casuing the issue
I have created a vba program in access that aggregates data from a number of external sources and write the results into a new table. Ideally when I run the program I want to wipe out all of the data that is currently in the table and replace it with my new data. I am currently currently deleting all of the data in the table... then writing my new data
Here is the code for reference
Function getTestFixtures(FixtureName As String) As Recordset
Dim db As Database
Set db = OpenDatabase(GetDBPath & "TestFixtures.xlsx", False, False, "Excel 12.0;HDR=Yes;")
If db Is Nothing Then
MsgBox "Can't find the file!", vbExclamation, ThisWorkbook.Name
Exit Function
End If
Set getTestFixtures = db.OpenRecordset("Select * from [" & FixtureName & "$]")
End Function
The recordset created above is modified and the output data is placed in a dictionary and passed to this function.
Sub Write_OTDC_Data(POlist As Dictionary)
Dim Rst As Recordset
DoCmd.SetWarnings False
DoCmd.runsql "Delete * from [OTDC Results]"
DoCmd.SetWarnings True
Set Rst = CurrentDb.OpenRecordset("OTDC Results")
With Rst
For Each key In POlist.Keys
.AddNew
For i = 0 To 9
.Fields(i).value = POlist(key)(i)
Next
.Update
Next
.Close
End With
End Sub
My Problem is that I get the following error if I try to change anything after running both of the above procuedures.
Running either in isolation does not generate the error.
I'm unsure whether this question is still unresolved. In case it's not, I have some suggestions for you to try, but not a lot of confidence they will cure the problem.
Try DoCmd.TransferSpreadsheet to import the sheet's data into a scratch table instead of using OpenDatabase with the workbook.
In your MsgBox, I wonder whether ThisWorkbook.Name means anything to an Access application. Aside from that, I would check whether the workbook exists, then open it (or import the sheet from it) only if the file is found.
If Len(Dir(GetDBPath & "TestFixtures.xlsx")) = 0 Then
'not found
Else
'use it
End If
Actually I'm unclear why you don't get an error from OpenDatabase if the workbook file doesn't exist. And that makes me suspicious of DoCmd.SetWarnings False Never, ever turn SetWarnings off. Doing so suppresses important information. And it is completely unnecessary. Set a DAO.Database object variable to CurrentDB(), then use this instead:
dbObjectVariable.Execute "Delete from [OTDC Results]", dbFailOnError
Add an error handler to deal with any problems dbFailOnError exposes.
Finally, this bears repeating because it's so important. NEVER turn SetWarnings off.

Form has my table locked down tight even after docmd.close

Sorry for the wall of text guys but this one requires expliaining, way too much code to post...
I'm importing fixed width files into access in methods that require data entry. I import the file using transferText into two specs (ones global, the other is special circumstance).
I have a function that uses DAO to cycle through all Field objects in TableDefs to build a duplicate table including an AutoIncrement PK so I have the ability to edit these records. I push the data into that table with INSERT INTO.
Works great. Errors are found, user goes into data entry to manually correct them which beats sifting through 400 character lines and reorganizing everything the way its supposed to be. Works great!
The problem: When the data entry changes are made a commit button is pressed which calls a function inside a module outside of the form. It closes the data entry form and pushes the information back to the original table minus the autoincremented PK, and is SUPPOSED to DROP the replicated table with ID's, and generate a new one searching once again for errors...
It pushes back to the original just fine, but it will not DROP the ID table. Always returns to me with a message indicating this table is locked. Ive noticed the table is indefiniatly locked down until all functions/subs exit. At any time stepping through the code I cannot delete it manually, once the execution has finished I am able to remove it.
I am assuming that since I called this through a command in the form, that the lock will not be released until all code finishes and the form terminate can be called and do its thing. Any thoughts? Yes this is very barbaric but it works quite well, I just need to be able to rip this other table off the planet so I can redrop an updated copy...
In the worst case I can make the user close the form out and hit another button in the main form but this is being designed heavily with user compitence in mind. However this now has my full attention and would like to at least find a solution even if it's not the optimal one.
-EDIT-
Two forms are used in this problem
FormA (Role: Load in and search for problems)
Examine button is pressed that:
- Uses TextTransfer based on predefined specs into tempExtract to
import the file
- DAO fires off on the Fields collection in tableDefs for
tempExtract, creates new table tempExtractID
- Performs searches through the file to find errors. Errors are saved to
a table Problem_t. Table contains Problem_ID (Set from the ID field
added to tempExtractID) and Description
- Execution of these tasks is successfully requerying the initial
form to showing a list of problems and number of occurances. A button
gains visibility, with onClick that opens the form DataEntry.
- At this point in the code after DAO execution, I can DROP the table
tempExtractID. DAO is NOT used again and was only used to build a new table.
FormB - Data Entry Form
As soon as I open this form, the table tempExtractID becomes locked and I cannot drop the table. The recordsource to the form querys tempExtractID against the ID's in Problems_t to return only what we need to key.
I cannot drop the table until the form has fully terminated. Button on the Data Entry form is pressed to commit changes, in which there are only 5 lines of code that get to fire off before I get my lock error.
*Xargs refers to the list of Field names pulled earlier through DAO. As DAO loops through Field objects, the physical names are added to an Xargs String which is placed in this table. Basically everything but the AutoNumber is being inserted back
docmd.Close acForm, "frmDataEntry", acSaveNo
call reInitializeExtract
> docmd.RunSQL "DELETE FROM tempExtract"
> docmd.RunSQL "INSERT INTO tempExtract SELECT (" & DLookup("Value", "CONFIG_t", "Item = 'Xargs'" & ") FROM tempExtractID"
docmd.DeleteObject acTable, "tempExtractID"
This is the only code that is run between the time where the form is opened (Where the table first gets locked) and continues to be locked until all subs & functions have completed.
I suggest setting the recordsource of the form to vbNullString and then deleting the table. This should work, unless you also have comboboxes and so forth bound to this table.
Without code it's hard to say, but if you're using DAO, you need to clean up your code objects. That means setting to Nothing your database objects, and closing and setting to Nothing any recordset objects.
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = DBEngine.OpenDatabase("[path to database]")
Set rs = db.OpenRecordset("[SELECT statement]")
rs.Close
Set rs = Nothing
db.Execute("[DML or DDL statement]", dbFailOnError)
db.Close
Set db = Nothing
Set db =CurrentDB
Set rs = db.OpenRecordset("[SELECT statement]")
rs.Close
Set rs = Nothing
Set db = Nothing ' you don't close a db variable initialized with CurrentDB
While VBA is supposed to clean up these objects when they go out of scope, it's not 100% reliable (because VBA uses reference counting to keep track of whether an object can be released, and it doesn't always know when all the references have been cleared).
Objects left open is the most likely source of the locks, so you should make sure you're cleaning up your object variables after you've finished with them.
EDIT after seeing that you're using DoCmd.RunSQL:
Using DoCmd.RunSQL is likely the cause of the problem. It is certainly something that takes away your programmatic management of your connections. If you use DAO instead, you'll have control over the connection, as well as avoiding the real pitfall of DoCmd.RunSQL, which is that it doesn't handle errors. If a DML or DDL statement cannot complete successfully in full, the whole thing should fail. For example, if you're appending 100 records and 10 of them fail for key violations, DoCmd.RunSQL will transparently append the 90 and NOT REPORT THE 10 FAILURES. It's the same with updates and any other DML/DDL statement. DoCmd.RunSQL "helpfully" silently completes as many of the updates as it can, leaving you having no idea that some of it failed to complete.
Granted, in some cases you might want that to happen, e.g., if you're appending records that you know might have PK collisions and don't want to spend the CPU cycles on an outer join that eliminates the duplicates from the set of records you're appending.
But most of the time, that is not the case.
As I said in my comment above, I use a function that is designed to transparently replace DoCmd.RunSQL and uses a DAO Execute statement and error handling. I have posted it a couple of times on SO (here's one), and here's the version I have in production use in my currently most-active development project:
Public Function SQLRun(strSQL As String, Optional db As Database, _
Optional lngRecordsAffected As Long) As Long
On Error GoTo errHandler
Dim bolCleanup As Boolean
If db Is Nothing Then
Set db = CurrentDb
bolCleanup = True
End If
'DBEngine.Workspaces(0).BeginTrans
db.Execute strSQL, dbFailOnError
lngRecordsAffected = db.RecordsAffected
'DBEngine.Workspaces(0).CommitTrans
exitRoutine:
If bolCleanup Then
Set db = Nothing
End If
SQLRun = lngRecordsAffected
'Debug.Print strSQL
Exit Function
errHandler:
MsgBox "There was an error executing your SQL string: " _
& vbCrLf & vbCrLf & Err.Number & ": " & Err.Description, _
vbExclamation, "Error in SQLRun()"
Debug.Print "SQL Error: " & strSQL
'DBEngine.Workspaces(0).Rollback
Resume exitRoutine
End Function
(the transactions are commented out because they were causing problems that I didn't have time to troubleshoot)
You could replace these lines of yours:
DoCmd.RunSQL "DELETE FROM tempExtract"
DoCmd.RunSQL "INSERT INTO tempExtract SELECT (" _
& DLookup("Value", "CONFIG_t", "Item = 'Xargs'" & ") FROM tempExtractID"
...with this:
SQLRun "DELETE FROM tempExtract"
SQLRun "INSERT INTO tempExtract SELECT (" _
& DLookup("Value", "CONFIG_t", "Item = 'Xargs'" & ") FROM tempExtractID"
You could also do this:
Debug.Print SQLRun("DELETE FROM tempExtract") & " records deleted."
Debug.Print SQLRun("INSERT INTO tempExtract SELECT (" _
& DLookup("Value", "CONFIG_t", "Item = 'Xargs'" _
& ") FROM tempExtractID") & " records inserted."
Since the function returns the .RecordsAffected for each Execute, you can print to the Immediate Window, or you could assign the return value to a variable, or pass an existing variable through to it and work with that variable thus:
Dim lngRecordsAffected As Long
...
Call SQLRun("DELETE FROM tempExtract", , lngRecordsAffected)
Debug.Print lngRecordsAffected & " records deleted."
Call SQLRun("INSERT INTO tempExtract SELECT (" _
& DLookup("Value", "CONFIG_t", "Item = 'Xargs'" _
& ") FROM tempExtractID", , lngRecordsAffected)
Debug.Print lngRecordsAffected & " records inserted."
The point is that if there are errors on the Execute statement, the whole thing will fail (and pop up an error message -- you might want to change it so that if there's an error it returns -1 or some such instead of popping an MsgBox).
I use this function most often by passing in a pre-cached database variable, so I don't want to clean it up afterwards. If you're using a different database other than CurrentDB(), you really do want to make sure any database variable pointing to your external db is closed and set to Nothing. Without that, locks are maintained on the top-level database objects, and the LDB file remains open and active.