How to determine what caused an update to the database - vba

I have a question regrading how to determine where a record update came from...
I have an Access 2010 database with multiple tables and multiple forms. All forms have VBA code behind them. Many of the VBA subs and functions insert, update or delete records. I use the afterupdate, afterinsert and afterdelete events to write details of changes made to an audit table. The audit table stores details such as when the change was made, who by and what was changed (action type, table, record id etc.).
As I mentioned, many different subs / functions make changes to the database. What I want to know is, how can I find out which sub / function module initiated the database change request?
At present I have added a global string variable to each sub / function module that updates the database and called a function within the setfield of a crearecord within the afterupdate / afterdelete / afterinsert event to read the variable and so put it on the audit table record. This is a solution but not a great one. Anybody got any better ideas?
In web development, I could read the HTTP server variables to see how the user got to the current page (referrer and referrer_URL). Is there something hidden away in one of the objects within Access that would give me something similar?

I'm assuming (hoping? praying??) that this all gets done in one single module. In other words, you're passing all this info as a variable to a public Function. If not, you should. And while you're passing the variables, just pass the function name to it along with all the other data.
For instance, you would have a public Function in a Module like this:
Public Function ChangeLog(chgDate As Date, strByWhom As String, etc..., strModName as String)
On Error GoTo Err_Handler
'Put some code here to write all the variables into fields in your ChangeLog table
Exit_Handler:
Exit Function
Err_Handler:
MsgBox "Error " & Err.Number & " - " & Err.Description, vbExclamation, "CalendarFor()"
Resume Exit_Handler
End Function
Then you just pass all the variables into it like (assume a form named MyForm and this is placed in the After Update event):
Call ChangeLog(Now(), CurrUser, etc..., "MyForm - AfterUpdate")
Obviously this is "air code", but I'm sure you get the idea.

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.

MS Access 2019: How do I check for duplicate records before update, then run a specific procedure if duplicates are found?

I am very new to both StackOverflow and to doing any sort of advanced programming in MS Access. I created a database to catalog my trading card collection (Excel just wasn't cutting it since we're talking about over 2000 unique cards). At first it was just a simple table of records, but now it's turned into a full-fledged database that I have search forms for and queries and everything.
What I'm trying to do right now is streamline my process a bit, and there is something very specific that I want to make Access do. I'm almost certain that whatever I want to do will have to be done in VBA, and I'm just not familiar enough with it to do what I want.
What I want it to do is this: Any time a new record is entered, I want it to check the record before it saves the record into the DB (I'm fairly confident that I need to use the "Before Update" event for this) and make sure that the "Sort ID" field (an auto-calculated field I've created) contains no duplicates (I know I'll most likely have to use queries for this since auto-calculated fields can't be indexed). If the program detects a duplicate, I want it to produce a message box saying that I'm trying to enter a duplicate record and ask me if I want to update the "number owned" field of the existing record instead of creating a new one, and then take me to the record in question on an affirmative response.
What I currently have is a validation rule that uses an index (comprised of the fields that generate the Sort ID), which generates a custom error message by using the following VBA code in the "On Error" event:
Private Sub Form_Error(DataErr As Integer, Response As Integer)
Const conErrRequiredData = 3022
If DataErr = conErrRequiredData Then
MsgBox ("Duplicate Sort ID. Please update the 'number owned' field on the existing record instead.")
Response = acDataErrContinue
Else
Response = acDataErrDisplay
End If
End Sub
This code works exactly as it should, but I want more than a pop-up error that I can't do anything with. I have a query entitled "CheckDuplicateSortID" that I created using the Query Wizard, and it checks the "Sort ID" field for duplicates, but that's as far as I've managed to get. The example on This site is about the closest I've managed to find to what I'm looking for, but the code sample given is very difficult for me to understand because there's very little explanation with it; I'm not familiar enough with Access VBA to know which parts are important code and which parts are his specific field names and other variables; I haven't gotten any error messages because I'm stumped on even trying to figure out what needs to be changed from that sample code and what it needs to be changed to.
Edit: Just for the sake of clarification, the solution doesn't have to involve the Sort ID field. I created that so I'd have one field I can point the program to. But if it would be simpler to just use the index that I use for my current validation rules (with the error message generated by the above code), I'm open to that too.
After some considerable finagling, I actually managed to find a solution to this on my own, though it is probably needlessly complicated (I'm definitely open to simpler solutions, if anyone has any).
Rather than using the actual SortID field, I modified the code in the OP to this:
Private Sub Form_Error(DataErr As Integer, Response As Integer)
Dim strMsg As String
Dim iResponse As Integer
'The text to be displayed in the message prompt.
strMsg = "Unable to save record. The values you have entered would generate a duplicate." & Chr(10)
strMsg = strMsg & "Would you like to clear this form and edit the existing record instead?"
'Calls for the yes/no message prompt specifically when the no-duplicate
'validation rule is violated (error 3022).
Const conErrRequiredData = 3022
If DataErr = conErrRequiredData Then
iResponse = MsgBox(strMsg, vbQuestion + vbYesNo, "Invalid Sort ID")
Response = iResponse
If iResponse = vbYes Then
'Calls a custom function that opens the record in question for editing.
UpdateOnError
Else
'Cancels the operation on a negative response and does not clear the form.
Cancel = True
End If
Else
Response = acDataErrDisplay
End If
End Sub
As you can see from the above code, the event calls a custom function, which is coded as follows:
Function UpdateOnError()
On Error GoTo UpdateOnError_Err
Dim UpdateGoToID As Variant
'Selects the Sort ID in question for the purpose of opening the existing record.
UpdateGoToID = Forms![Card List Entry Form]!txtSortID
'Clears the invalid form.
DoCmd.RunCommand acCmdUndo
'Opens the existing record the user attempted to duplicate.
DoCmd.OpenForm "Card List Entry Form", acNormal, "", "[Sort ID]=" & "'" & UpdateGoToID & "'", , acNormal
UpdateOnError_Exit:
Exit Function
UpdateOnError_Err:
MsgBox Error$
Resume UpdateOnError_Exit
End Function
Like I said, this is probably a lot more steps than actually needed, but it does work as I want it to: when the user attempts to enter a duplicate record, an error message pops up asking if they would like to update the existing record. If yes, it takes them to the existing record. If no, it closes the error message without clearing the form or saving the record.

How can I run several macros where the Macro name begins with a specific string of characters?

I've created several custom functions which I would like to Register. Currently, I have a different procedure for which I specify the registration for each function (there's no issue with that piece). However, the only way I know of to registering all these functions is by calling each Macro by name in another procedure like this:
Sub spRegisterFunctions()
Call spRegisterCUSTOMAfunction
Call spRegisterCUSTOMBfunction
Call spRegisterCUSTOMCfunction
Call spRegisterCUSTOMDfunction
End Sub
I'm actually looking for something more dynamic so that every time I create a new function, and it's corresponding "spRegister..." procedure, I don't have to remember to add the "Call" code to the "Sub spRegisterFunction()" procedure for that specific function.
Here's an example of what attempting to do:
Sub spRegisterFunctions()
Dim mc as Macro
For Each mc in VBProject("NameOfProject").Module("NameOfModule")
If Left(mc.Name,10)="spRegister" then
Call mc
End If
Next mc
End Sub
As you can see, I'm attempting to run any macro in a specific module who's name begins with "spRegister". Obviously the code above will not work, partly because some of those objects don't even exist.
Is there any way to do this?

Access Call Macro from Query (Opening a Form) Run-time error '2486': You can't carry out this action at the present time

I have an Access Query that requiers a value to be set in a combo-box within a form in order to work
Criteria: Forms![_SelectCustomer]![CmbSelectCustomer]
So far so good, however, I would like the query to open, read and close this form programatically when it is run using a macro.
I have been following #David-W-Fenton's answer in THIS similar stack overflow question and have come up with the following code:
Public Function rtnSelectCustomer() As Variant
DoCmd.OpenForm "_SelectCustomer", , , , , acDialog
With Forms![_SelectCustomer]
If .Tag <> "Cancel" Then
rtnSelectCustomer = Nz(!CmbSelectCustomer, "*")
Else
rtnSelectCustomer = "*"
End If
End With
Close acForm, "_SelectCustomer"
End Function
I call this function from within the criteria field of the property I want to filter by in the Query:
Like rtnSelectCustomer()
At this point I run into several problems:
The first being, I'm not sure where to place the actual code: I can't seem to create a specific class or module for my query within the "Microsoft Access Class Objects" folder so I have resorted to creating my own module within the Modules folder. (Is this the correct approach?)
The second issue is that when I run the query with the code in the current module I have created I get the following error:
Run-time error '2486':
You can't carry out this action at the present time.
Any advice would be much appreciated
Edit:
I should clarify that after further testing the line that seems to cause the Run-time error is the following:
DoCmd.OpenForm "_SelectCustomer", , , , , acDialog
The function is actually called as replacing the internal code with the following does actually work (although is admittedly useless)
Public Function rtnSelectCustomer() As Variant
rtnSelectCustomer
End Function
Generally, I hate things that are "pre-programmed" by Microsoft, I'd rather do them myself. It seems this is your case as well...
I would do this in 2 steps.
Step1: Show things to the user as if the query was running (without actually running it) and store the values the user picks.
Step2: Use the values to parameterize the query
If your function works well, then simply remember what the user picks and then do:
set qdf = new QueryDef
' set the qdf and add all parameters to it
DoCmd.Execute qdf
for further reference on how QueryDef works I would use this msdn site

Global variable loses its value

On this Access form I am working on I have a global variable that take its value from another form on its Form_Load event. For some reason unknown to me the variable "loses its value" (becomes = "") after some time elapses or some event occurs. I have not been able to notice anything in particular that triggers this behaviour. Are global variables reset after some time of "inactivity" on the form ?
Here is how I set the global variables I am talking about:
Private Sub Form_Load()
'...
Set prev_form = Form_Identification.Form
PasswordSybase = prev_form.Password.Value & vbNullString
UserSybase = prev_form.UserID.Value & vbNullString
'...
End Sub
An alternate solution (Only 2007 and onwards) I've started using is TempVars instead of globals in the odd situation I "needed" something global. It's a collection and it persists for the duration of the application unless you explicitly release it. So in some cases I feel its more useful than globals and in some cases worse.
TempVars.Add myVarName, myVarValue ' To initialize
TempVars.Item(myVarName) = newVarValue ' To reference and assign a new value
TempVars.Remove(myVarName) ' To release
Quick search should show you more lot references, but I've included link to a basic one
http://blogs.office.com/b/microsoft-access/archive/2010/09/27/power-tip-maximize-the-user-of-tempvars-in-access-2007-and-2010.aspx
I do hope that visitors see this post, as it provides an important additional piece.
Even if you declare a variable globally, it appears that - in the event that you set that variable's value in a form module - that value is lost when you UNLOAD the form.
The solution (in my case) was as simple as replacing:
Unload Me
...with...
Me.Hide
The variables (and objects) that I set in that code module then retained their values through the entire lifetime of the application instance.
This may help:
https://accessexperts.com/blog/2011/01/12/multi-session-global-variables/
Juan Soto explains how to use a local table to keep variables and how to call them when needed. It may serve your purpose in 2000 since TempVars isn't an option. You could always delete the variables "on close" of the database so that UID and PWD aren't kept.
You can create a "fake" global variable by
creating a form (e.g. named frmGlobal)
make sure the form is always open but hidden
create a TextBox for each global variable you want (e.g. tVar1)
in your code, reference as e.g. Form_frmGlobal.tVar1
The disadvantage is that an unbound text box may not give you a specific data type you want
The two ways around that are
in your code, explicitly convert the textbox to the data type when referencing the global variable
e.g Clng(Form_frmGlobal.tVar1)
another option is create a one-row table and bind your textboxes on your hidden form to the table, so your data types are enforced
A bonus of this method is you can use for persistent storage between sessions
Caveat: make sure this table is local to a single user only, in the front end database file (don't want to put it in the back end database because of multi-users over-writing each other). This assumes you are using front end + back end separated databases, with distribution of front end to each user's workstation.
I see nothing in that statement that tells me it's a global variable. You set global variables above ALL Subs/Functions and below an Options Compare statement in a module, by stating:
PUBLIC X as string
Any other variable is only good until the sub or function has completed.
Also, Global variables MUST be declared on a PROPER MODULE. You can't declare them on a form's module.