This is somewhat related to this question, but I was hoping there may be a more elegant / simple solution than defining a User-Defined Function.
Background
In essence, this is a common question: I need to dynamically modify the RowSource query of a combobox control on an Access form. The twist is that there is a possibility the resulting query may throw an exception when executed. That is because the source table of the query exists in a different database file which is defined dynamically and may not exist, or even if the file does exist, the desired table or column may not.
Ideally, I would like to be able to catch this problem when the assignment is made, so that the combobox will be disabled and the user cannot invoke the invalid query allowing the end-user to know there is no data available for the field.
For example I would like something like this:
Private Sub UpdateComboRows(src as String)
On Error Goto InvalidQueryError
cmbBox.RowSource = "SELECT [colName] FROM [dBase III;DATABASE=K:\" & src & "].[tblName];"
' Something here to invoke RowSource query and throw error
cmbBox.Enabled = True
Exit Sub
InvalidQueryError:
cmbBox.RowSource = ""
cmbBox.Enabled = False
End Sub
Or something using if-then statements.
Question
Are there any 'sleek' approaches to this, or am I stuck with trying to populate a dummy DAO recordset? Is there some way of invoking the Combobox RowSource query besides the Dropdown event?
Why not take a step back and check first that the database exists with Dir, then check that the column exists in a function. Very roughly.
Function IsDBOk() As Boolean
On Error GoTo Err_Trap
''The default for boolean is false, so
''IsDBOK=False at this point
sFile=Dir("K:\" & src)
If sFile<>"" Then
Dim rs As DAO.Recordset
Set rs = CurrentDB.OpenRecordset("SELECT [colName] " _
& "FROM [dBase III;DATABASE=K:\" _
& src & "].[tblName];")
If Not rs.Eof() Then
''The selection is only okay if the recordset
''contains records, however, you could return
''a different error for each problem
''file missing, column missing, empty recordset
IsDBOk = True
End If
End If
Exit Function
Err_Trap:
''A missing column will give error 3061
''IsDBOk is already set to false
End Function
Related
I want to change a date in a specific table to today's date by clicking a button in a related form. So all the button does is changing the date in a certain field in my DB. Is there a simple way to do this with VBA?
*Update
Well I wrote this in my VBA code:
CurrentDb.Execute "UPDATE Machines SET LastMaintenance = Date() WHERE MachineID = MachineID.Value"
With "Machines" being my table, "LastMaintenance" the column containing the date that has to be changed into today's date, "MachineID" the name of the record and "MachineID.Value" the name of the textbox bound to that same record.
When I click the button I get this error:
"Not enough parameters. 1 expected."
When performing an update query, you'll want to be cognizant of the datatype for each field, as you will have to present it differently in your code. Also, you will need to break up your string text when inserting a variable. In your current state, it's looking for a MachineID field with 'MachineID.value' as its contents. Try this:
CurrentDb.Execute "UPDATE Machines SET LastMaintenance = Date() WHERE MachineID = " & MachineID.Value
The most straightforward way is to run a UPDATE query.
CurrentDB.Execute "UPDATE someTable SET someDate = Date() WHERE stuff = 47"
If
a button in a related form
means a form bound to that table displaying the record you wish to update, use the OnClick event of the button:
Private Sub NameOfYourButton_Click()
Me![NameOfYourDateField].Value = Date
' Optionally, save the record at once:
Me.Dirty = False
End Sub
An UPDATE command is dangerous because you are making a change to the database assuming sane inputs. I would recommend using parameterized VBA code to avoid both SQL injection and throw an error in VBA for malformed inputs.
This example uses a static string to load your recordset, then it clearly states that the unverified input is only used in a Find command. Then it only acts if a matching record is found. This is a safer operation, albeit more verbose. It's also debuggable in VBA where the SQL UPDATE command is a kind of black box.
With CurrentDb.OpenRecordset("Machines", dbOpenDynaset)
.FindFirst "[MachineID]=" & CLng(MachineID.Value)
If .NoMatch Then
Debug.Print "ID not found: " & MachineID.Value
Else
.Edit
.Fields.Item("LastMaintenance").Value = Date()
.Update
End If
.Close
End With
I have an AccessDB app where I need to grab the ItemIDs for the current user-applied filter into a new table to use downstream. Using the subform datasheet .recordsetclone property I can see the desired recordset, .recordcount reports the correct number of records. Otherwise, the following does not produce the desired temp table and AccessVBA does not complain.
Dim db As DAO.Database
Dim rstItemIDs As DAO.Recordset
Dim strSQL as String
Set db = CurrentDb
set rstItemIDs = Forms!Mainform![Data subform].Form.RecordsetClone
msgbox rstItemIDs.recordcount 'reports the correct result
strSQL = "SELECT rstItemIDs.ItemID INTO tempTable FROM rstItemIDs;"
db.Execute strSQL
Is it possible to construct a SQL Select query against a dao.recordset?
Thanks for any pointers you can provide.
Access SQL will not accept either a DAO or ADODB Recordset as the data source for a query.
However, I'm puzzled that Access does not complain when you try. With every attempt I made to reproduce your sample code, I got error #3078, "The Microsoft Access database engine cannot find the input table or query 'rstItemIDs'. Make sure it exists and that its name is spelled correctly."
Even DoCmd.SetWarnings False did not suppress that error message.
If you're interested in alternatives, you could persist tempTable (instead of creating a new version each time), then delete its contents and move through rstItemIDs adding each value to the second recordset. Although that is a RBAR (row by agonizing row) method, it may not be too painful with a small recordset.
A set-based approach could be to create a query based on your form's .RecordSource and .Filter properties. For example, with my form's .RecordSource as SELECT * FROM foo and the current form .Filter as id>10, this would give me a SELECT which returns the form's filtered records:
Replace(Me.RecordSource, ";", "") & vbcrlf & "AND " & Me.Filter
I have this qry in access, if I go into its design it has a criteria (which as I understand it is a parameter).
The Report this qry is based off of works great, click on it a little thing pops up asks the required info and off it goes. In code I am trying to do this and get a
Run-time error '424'
Object Required
the offending line:
qdf.Parameters("Insurance Name").Value = inputStr
Lines before it:
Set qfd = CurrentDb.QueryDefs("qryInsGrpRoster")
Dim inputStr As String
inputStr = InputBox("Enter Insurance")
'Supply the parameter value
qdf.Parameters("Insurance Name").Value = inputStr
inputStr definitely equals the value, it fails though.
The criteria line in the qry is:
Like "*" & [Insurance Name] & "*"
Do I need the likes and all that to set that parameter?
in Access 2010 and 2013
This uses DAO and might be of interest
DIM MyQryDef as querydef
Dim a as string
a = ""
a = a & "PARAMETERS Parameter1 INT, Parameter2 INT; "
a = a & "SELECT f1, f2 FROM atable WHERE "
a = a & "f3 = [Parameter1] AND f4 = [Parameter2] "
a = a & ";"
Set MyQryDef = currentdb().CreateQueryDef("MyQueryName", a)
MyQryDef.Parameters("Parameter1").Value = 33
MyQryDef.Parameters("Parameter2").Value = 2
' You could now use MyQryDef with DAO recordsets
' to use it with any of OpenQuery, BrowseTo , OpenForm, OpenQuery, OpenReport, or RunDataMacro
DoCmd.SetParameter "Parameter1", 33
DoCmd.SetParameter "Parameter2", 2
DoCmd.Form YourFormName
' or
DoCmd.SetParameter "Parameter1", 33
DoCmd.SetParameter "Parameter2", 2
DoCmd.OpenQuery MyQryDef.Name
See here:
https://msdn.microsoft.com/en-us/library/office/ff194182(v=office.14).aspx
Harvey
The parameters property of an Access Query is read only.
You have basically two options here that I can think of right off.
The first is to just completely rewrite the SQL of the saved query each time you need to use it. You can see an example of this here: How to change querydef sql programmatically in MS Access
The second option is to manually set the RecordSource of the report each time it opens. Using this method you will not use a saved query at all. You'll need to set/store the entire SQL statement in your code when the report opens, ask for any input from the user and append the input you get to your SQL statement. You could setup a system where the base SQL is stored in a table instead but, for simplicity, that's not necessary to achieve what you're trying to do here.
MS Access does allow you to use parametrized queries in the manner you're attempting here (not the same code you have), but as far as I know, it would require you to use Stored Procedures in MS SQL Server or MySQL and then you'd need to use ADO. One big downside is that Access reports cannot be bound to ADO recordsets so this isn't really an option for what you're trying to do in this particular instance.
Seems like a typo. You're creating the object named 'qfd', and trying to use the object named 'qdf'
Set qfd = ...
and then
qdf.Para...
I like to put Option Explicit in my modules to help me find these types of issues.
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
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.