How do I chain forms in Access? (pass values between them) - ms-access

I'm using Access 2007 and have a data model like this...
Passenger - Bookings - Destinations
So 1 Passenger can have Many Bookings, each for 1 Destinations.
My problem...
I can create a form to allow the entry of Passenger details,
but I then want to add a NEXT button to take me to a form to enter the details of the Booking (i.e. just a simple drop list of the Destinations).
I've added the NEXT button and it has the events of
RunCommand SaveRecord
OpenForm Destination_form
BUT, I cant work out how to pass accross to the new form the primary key of the passenger that was just entered (PassengerID).
I'd really like to have just one form, and that allow the entry of the Passenger details and the selection of a Destination, that then creates the entries in the 2 Tables (Passenger & Bookings), but I can't get that to work either.
Can anyone help me out please?
Thanks
Jeff Porter

Actually the best suggestion I can give here is to not actually pass parameters. Simple in your form's on-open event, or even better is to use the later on-load event is to simply to pick up a reference in your code to the PREVIOUS calling form. The beauty of this approach is that if overtime you go from one parameter to 10 parameters then you don't have to modify the parameter parsing code, and you don't even have to modify the calling code. In fact there's no code to modify AT ALL if you decide to examine previous values from the calling form.
So, keep in mind using open args is only a one way affair. You can not use it to return values to the calling form. Furthermore all of the open args params will have to be strings. So, you lose any ability of dating typing such as real integers or even date and time formatting which can be quite problematic to parse out. And as you can see the example here the code to parse out strings can get a little bit messy anyway.
The simple solution is that in each form where you want values from the PREVIOUS from, simply declare a module level variable as follows
Dim frmPrevous as form.
Then in your forms on load an event, simply pick up the name of the previous form as follows:
Set frmPrevious = screen.ActiveForm
That is it. We are done!
We only written one line of code here. (ok two if you include the declaration statement). At this point on words ANY place in your form's current code you can reference the events properties and any field or value in the previous form by doing the following
Msgbox "PK id of previous form = " & frmPrevious.ID
And let's say for some reason that you want the previous form to re-load records as in a continues form. Then we can go:
frmPrevious.Requery
Or, force a record save:
frmPrevious.Dirty = false
So, the above becomes almost as natural and handy as using "ME" in your current code. I find this so simple and easy I think this should have been part of access in the first place.
And as mentioned the uses are endless, I can inspect ANY column or value from the calling form. You can even declare variables and functions as public, and then they can be used.
And, note that this works BOTH WAYS. I can stuff and change values in the calling form. So I can update or change the value of any value/column/control from the calling form.
And there is absolutely no parsing required. Furthermore the above code step even works if the same existing form is called by different forms. In all cases the forms calling ID can be picked up without modifying your code.
And even in the case of that I have many different forms launching and calling this particular form, you can still pull out the ID column. And, in the case that columns could be different from different forms, you can simply declare public variables or public functions in the calling form of the SAME name. So, if I wanted to call a form that needs the DateCreate, but each form did NOT have a consistent column name of DateCreate (maybe invoiceDateCreate and inventory Date Create), then you simply declare a public function in the calling forms with a constant name. We then can go:
Msgbox "Date created of calling form record = " & frmPrevious.DateCreated
So, Date created can be a public variable or public function in the previous form that could be any conceivable column from the database.
So don't pass values between forms, simply pass a reference to the calling form, not only is this spectacularly more flexible than the other methods showing here, it's also an object oriented approach in which you're not limited to passing values.
You can ALSO return values in the calling form by simply setting the value of any control you want (frmPrevous.SomeContorlName).
And as mentioned, you not limited to just passing values, but have complete use of code, properties such as dirty and any other thing that exists in the calling form.
I have as a standard coding practice adopted the above for almost every form. I simply declare and set up the form previous reference. This results in a handy previous form reference as useful as "ME" reference when writing code.
With this coding standard I can also cut and paste the code between different forms with different names and as a general rule my code will continue to run without modification.
And as another example all of my forms have a public function called MyDelete which of course deletes the record in the form, therefore if I want to delete the record in the previous calling form for some reason, then it's a simple matter to do the following
frmPrevious.MyDelete
So I can save data in the previous form. I can requery the previous form, I can run code in the previous form, I can return values to the previous form, I can examine values and ALL columns all for the price of just ONE measly line of code that sets a reference to the calling form.

I do this by defining properties in the form with Property Let and Property Get and passing values to those properties after opening the form, like this:
in the destination form:
Dim strCallingForm As String
Dim lngKey As Long
Public Property Get callingform() As String
callingform = strCallingForm
End Property
Public Property Let callingform(NewValue As String)
strCallingForm = NewValue
End Property
Public Property Let PrimaryKey(NewValue As Long)
lngKey = NewValue
End Property
in the calling form:
Sub btnGo_Click()
Const cform As String = "frmDestinationForm"
DoCmd.OpenForm cform
Forms(cform).callingform = Me.Name
Forms(cform).PrimaryKey = Me.PrimaryKey
Me.Visible = False
End Sub
(end)

I would use the openargs method of the form. That way you can pass one piece of data to the new form from any other form. You can also expand of this by sending a delimited string of arguments and then splitting them out. For example I have a form for editing agent activity that is passed the date, the agents name, agents ID and team in the open args
DoCmd.OpenForm "frmEdit_agent_activity", , , , , acDialog, Item & "|" & Me.txtDate & "|" & Item.ListSubItems(1) & "|" & Item.ListSubItems(2)
The form then uses this to pre populate the controls
Private Sub Form_Load()
If IsMissing(Me.OpenArgs) = True Or IsNull(Me.OpenArgs) = True Then
'no args, exit the form
DoCmd.Close acForm, "frmEdit_agent_activity", acSaveNo
Else
'this form has 4 open args
'1 Staff ID
'2 Date
'3 Team_ID
'4 Staff Name
Me.txtStaff_ID = GetDelimitedField(1, Me.OpenArgs, "|")
Me.txtDate = GetDelimitedField(2, Me.OpenArgs, "|")
Me.txtTeam_ID = GetDelimitedField(3, Me.OpenArgs, "|")
Me.txtStaff_name = GetDelimitedField(4, Me.OpenArgs, "|")
End If
End Sub
Ohh and here is the GetDelimitedField function
Function GetDelimitedField(FieldNum As Integer, DelimitedString As String, Delimiter As String) As String
Dim NewPos As Integer
Dim FieldCounter As Integer
Dim FieldData As String
Dim RightLength As Integer
Dim NextDelimiter As Integer
If (DelimitedString = "") Or (Delimiter = "") Or (FieldNum = 0) Then
GetDelimitedField = ""
Exit Function
End If
NewPos = 1
FieldCounter = 1
While (FieldCounter < FieldNum) And (NewPos <> 0)
NewPos = InStr(NewPos, DelimitedString, Delimiter, vbTextCompare)
If NewPos <> 0 Then
FieldCounter = FieldCounter + 1
NewPos = NewPos + 1
End If
Wend
RightLength = Len(DelimitedString) - NewPos + 1
FieldData = Right$(DelimitedString, RightLength)
NextDelimiter = InStr(1, FieldData, Delimiter, vbTextCompare)
If NextDelimiter <> 0 Then
FieldData = Left$(FieldData, NextDelimiter - 1)
End If
GetDelimitedField = FieldData
End Function

Have you considered subforms? There are ideal for one to many relationships. The Link Child Field(s) will be automatically completed from the Link Master Field(s).
If you need an example of subforms in actions, the Northwind database ships with all versions of Access, or you can download which ever is relevant.
2007
http://office.microsoft.com/en-us/templates/TC012289971033.aspx?CategoryID=CT102115771033
2000
http://www.microsoft.com/downloads/details.aspx?familyid=c6661372-8dbe-422b-8676-c632d66c529c&displaylang=en

You can use OpenArgs.
But I would also suggest that you also consider using a tab control.
That allows you to have different sets of controls on the same "screen real estate", using one single recordset, or using sub forms to show child recordsets.
In that case, you could use the "Next" button to just switch to the next page of your tab control.

Related

How to execute a line of code saved as a variable VBA

I have a form called Frm_Dairy and a form called Frm_Bakery
I have a field in each of them that holds a value. The names of the fields in each form are the same but for the beginning of the name of the field which is ether Dairy or Bakery.
Example:
Frm_Dairy.Dairy_Sales
or
Frm_Bakery.Bakery_Sales
I want to execute this code:
M_Sales = Frm_Dairy.Dairy_Sales
This is easy enough but what if I want the program to 1st figure out what form is active?
M_Form = "Dairy" 'in this case
So if M_Form = "Dairy" the code would be:
M_Code = "M_Sales = Frm_" & M_Form & "." & M_Form & "_Sales"
Now how do I execute the M_Code variable to get the data stored into M_Sales
I hope I have asked this question with enough detail and thanks for your help!
To determine if a particular form is open, consider:
If CurrentProject.AllForms("Dairy").IsLoaded Then
...
ElseIf CurrentProject.AllForms("Bakery").IsLoaded Then
...
End If
You describe need to reference fields, probably really need to reference controls.
Several ways to dynamically construct form and control references. One is to use the ActiveForm property, assuming the form you want to address has focus.
Debug.Print Screen.ActiveForm.Controls(Screen.ActiveForm.Name & "_Sales")
Another would be to pass the form name as string argument of procedure.
Debug.Print Forms(strForm).Controls(strForm & "_Sales")
Would be simpler if controls do not have form name prefix.

Access VBA - Turn off aggregation when opening form/sub-form

What I'm trying to do is, whenever a user opens a form (and the sub-form that opens by default), I want to search through all the columns (controls?) on the form, check to see if they are currently set to aggregate (sum, count etc.) with Access' built-in Totals row, and if so, set them to not aggregate.
The reason for this is there are several millions records that are stored, so when someone queries it down to 3-4 and turns on Sum, then closes it, when the next person opens it, it tries to sum millions of numbers and freezes up. The form displays the queried results from a table which is populated via SQL (I think, if that sentence makes sense). Here's what I have so far:
Private Sub Form_Load()
'this form_load is in the UserApxSub sub-form, for reference
Call De_Aggregate
End Sub
Private Sub De_Aggregate()
Dim frm As Form, con As Control
Set frm = Forms!UserAPX!UserApxSub.Form!
For Each con In frm.Controls
If con.ControlType = acTextBox Then
If con.Properties("AggregateType").Value <> -1 Then
'crashes on following line
con.Properties("AggregateType").Value = -1
End If
End If
Next con
End Sub
I have not so much experience in Access VBA (usually work in Excel VBA) so please forgive me if I'm entirely off the mark here. The command con.Properties("AggregateType").Value = -1 doesn't throw an error, but Access just straight-up crashes when reaching that line specifically.
I've tried a number of variations in the syntax with no success, and I've also tried looping through other elements of the file (tabledefs, querydefs, recordsets, etc.) as well to see if I'm trying to change the wrong value, but the controls on this subform are the only things in the entire .mdb file that results when I search for elements with the AggregateType property.
I switched out the line that errors with Debug.Print con.Name & " - " & con.Properties("AggregateType").Value and I can check, have nothing return anything other than -1, turn on aggregation in some column manually, and have it return the correct result (0 for sum for example), so I think I'm looking in the right place, just missing some key factor.
I've been working on this for a couple weeks with no success. Any way to fix what I have or point me toward the right direction would be greatly appreciated!
This is not necessarily the answer but I don't have enough reputation
to give a "comment"...
I tried your scenario and verified can change the property value as you are however I did not iterate through all controls and simply used an onDoubleClick event on a column to simulate.
I would suggest trying to fire your sub with Form_Open or Form_Current to see if the property is getting reset after your code has been called for some reason.
UPDATE:
You are referencing the "Subform" Object of your main Form:
Set frm = Forms!UserAPX!UserApxSub.Form!
Try referencing the actual UserApxSub FORM explicitly.
Something like Set frm = Forms!UserApxSub! (assuming UserApxSub is the name of the form)
then stick in the Form_Open of your main form:
Private Sub Form_Open(Cancel As Integer)
'// the following would set a single control only. You can add your loop through all controls
Me!{your control name}.Properties("AggregateType").Value = -1 '// set control in your main form
Form_UserApxSub!{your control name}.Properties("AggregateType").Value = -1 '// set control in your "sub" form
End Sub

Wildcard in Access VBA

This is kind of a follow up question to this post: Access VBA recordset string comparison not working with wildcard but I don't have the rep to answer/comment on it to ask it in house. What I'm curious about is this line of code specifically:
If ![ACOD] = "*_X" Then '"$ICP_X" works
Debug.Print ![ACOD] 'For testing
'.Delete
End If
I want to know if this can be modified so that on a button click, it looks at all fields in a form with the field name of *_New (with the hope to catch all fields where the name ends in _New) and if they are not Null then confirm that the user wanted to make a the change indicated in the field. I was thinking of something along the lines like this:
If Not isNull(*_New.value) Then
If Msgbox ("Do you want to make these changes?",vbOKCancel, "Confirm Changes") = 1 Then
'### Do something with the record ###
End If
End If
EDIT
As of posting the above information, I did not have the Microsoft VBScript Regular Expressions Reference installed, currently I have version 5.5 (it was the latest version). With that installed (referenced?) and seeing the information from this site MS Access with VBA Regex, I'm wondering if it's better to do something like this:
Dim re As RegExp
Set re = New RegExp
re.IgnoreCase = True
re.Global = True
re.Pattern = "*_New"
If ##Not sure on syntax to match all fields## Then
Msgbox(##Same stuff as above MsgBox##)
End If
EDIT 2
Here's a sample case for my form I'm working on. Each of the fields to the right have names that end in _New. What I want to do is on the button click, to check and see what fields on the right have been filled in and ask the user if they want to confirm the changes to the record.
Not sure what you are trying to achieve but there is a way to access the control collection in a form. Here is a public function where you can loop through all controls and check its name.
Public Function FN_CONFIRM_CHANGES(iSender As Form)
Dim mCtl As control
For Each mCtl In iSender
If VBA.Right(mCtl.name, 4) = "_New" Then
Debug.Print mCtl.name & " is a match and its a " & VBA.TypeName(mCtl)
End If
Next mCtl
End Function
Call this function like
FN_CONFIRM_CHANGES Me 'Where me is referencing the form you are in.
You can modify the above code to return a boolean value to stop further execution if user decided not to save your changes or whatever logic you are trying to implement.

Display only last characters in a textbox

I have an Access 2002 database/application where my clients can enter multiple information about their own clients, including a code which follow some rules.
However, when they view these information after they have been entered, I need to hide every characters in this code except for the 4 last characters. However, the agent needs to be able to edit this code if it needs to be modified.
So basically, I have 3 phases possible:
First time information are filled, empty data. The field must show the characters entered.
At a later date, the code must be hidden in some way to show only the last 4 characters. It can be with * or simply the last 4 characters, but the user must not be able to see what is before these.
The agent edit the code, the code must then be properly modified in the database. The characters must be shown.
I tried to show only the 4 last characters, however my database gets modified... So the code gets cut in the database.
I wrote the following function to obscure sensitive data. Its primary use is to discourage shoulder surfing. I'm not sure if it will meet your particular needs, but it is simple, straightforward and may help others who stumble upon this question.
'Use to hide data in sensitive fields (e.g., BirthDate, PhoneNum, SSN)
'Usage: Ctl OnEnter property: =ObscureInfo(False, Form.ActiveControl)
' Ctl OnExit property: =ObscureInfo(True, Form.ActiveControl)
' Form Open property: =ObscureInfo(True, [BirthDate], [HomePhone], [SSN])
Function ObscureInfo(HideIt As Boolean, ParamArray Ctls() As Variant)
Dim Ctl As Variant
For Each Ctl In Ctls
If HideIt Then
If IsNull(Ctl.Value) Then
Ctl.BackColor = vbWhite
Else
Ctl.BackColor = Ctl.ForeColor
End If
Else
Ctl.BackColor = vbWhite
End If
Next Ctl
End Function
Wow - I'm shocked that this hasn't been answered sufficiently. The best answer is to use an unbound text box in your form instead of a bound one. First you'll need to make your unbound text box populate the actual field. You'll do that in the AfterUpdate event.
Private Sub UnboundTextBox_AfterUpdate()
[MyField] = Me.UnboundTextBox
End Sub
Then you'll need to set an OnCurrent event to populate your unbound text box with the protected view whenever the agents view the record:
Private Sub Form_Current()
Me.UnboundTextBox = String(Len([MyField])-4, "*") & Right([MyField], 4)
End Sub
However, you also want to let your agents edit or view the full code later, if necessary. The best way to do this would be to set the OnEnter event for your unbound text box to pull the whole field value, so the agent can see and edit it - effectively the reverse of your OnUpdate event.
Private Sub UnboundTextBox_Enter()
Me.UnboundTextBox = Nz([Field1]) 'The Nz deals with Null errors
End Sub
I've used this with a field displaying SSN's and it works like a charm.

Access VBA - How would I have a loop in VBA that allows me to loop through control names

I have about 10 text boxes on a form that are actually used for display not entry. They are are named txt_001_Name, txt_002_Title, etc..what kind of loop is used for this.
What kind of VBA should I use to actually loop through the names of the text boxes? So if I was to debug.print it would look like:
txt_001_Title
txt_002_Title
txt_003_Title
This is probably pretty simple to do - all the more reason that I should learn how!
EDIT: Sorry, I should have been more descriptive about this.
Because of the above naming convention, I am looking to iterate through these text boxes so that I can perform something with each. What each of these 10 text boxes actually represent is numeric values, each having a SQL statement behind them in the form's onload event. I also have another set of ten that hold numeric values that are much more static, and finally another ten that use an expression to simply divide each of the first ten, against the relative "second" ten, and the value ends up in the relative 3. So basically it ends up looking like a dashboard table.
'first ten' 'second ten' 'resulting ten'
---------------------------------------------------
txt_001_value txt_001_calc txt_001_result
txt_002_value txt_002_calc txt_002_result
etc.
So I actually want to use this for the 'resulting' text boxes. I want to loop through the first ten and perform this easy calculation:
me.txt_001_result = me.txt_001_value / me.txt_001_calc
All the naming conventions "match up", so I can manually type out the 10 lines of the above for this, but I am sure there is a better way (loop through this), and I should probably learn it.
You can list the names of textbox controls with a simple procedure like this:
Public Sub TextBoxNames(ByRef pfrm As Form)
Dim ctl As Control
For Each ctl In pfrm.Controls
If ctl.ControlType = acTextBox Then
Debug.Print ctl.Name
End If
Next ctl
Set ctl = Nothing
End Sub
You could call it from the form's Load event:
Private Sub Form_Load()
TextBoxNames Me
End Sub
However, I don't understand what you're trying to accomplish. I realize you want to do something with ctl.Name other than Debug.Print, but I don't know what that is.
Rather than computing a result for me.txt_001_result and then assigning that value to the text box, consider setting the control source for txt_001_result to txt_001_value / txt_001_calc and let Access put the proper value into txt_001_result for you.
In response to your comments, I'll suggest this procedure as a starting point for you to build upon:
Public Sub MyTextBoxValues()
Const cintLastTextBoxNum As Integer = 10
Dim i As Integer
Dim strValueControl As String
Dim strCalcControl As String
Dim strResultControl As String
Dim strPrefix As String
For i = 1 To cintLastTextBoxNum
strPrefix = "txt_" & Format(i, "000")
'txt_001_value txt_001_calc txt_001_result '
strValueControl = strPrefix & "_value"
strCalcControl = strPrefix & "_calc"
strResultControl = strPrefix & "_result"
'me.txt_001_result = me.txt_001_value / me.txt_001_calc '
'Debug.Print strResultControl, strValueControl, strCalcControl '
Me.Controls(strResultControl) = Me.Controls(strValueControl) / _
Me.Controls(strCalcControl)
Next i
End Sub
I prefer to use a FOR EACH to iterate through the controls collection of whatever the textboxes are on (either the form itself or a panel control)
dim myBox as Textbox
For each myBox in myForm
myBox.Text = "hello"
Next
Also means you can make custom groups (by putting them all on the same container).
Note that if you have other controls, you might need a typecheck in there (IF TYPEOF(myBox) = "TextBox" THEN ...)
You could also do it like:
dim i as integer
For i = 1 to 10
myForm.Controls("txt_00" & i & "_Title").Text = "hello"
Next i
I definitely prefer the For Each, though.
I can't entirely understand why you need to do what you're doing, but I've had forms like that where I had an unbound form that I wanted to display an arbitrary number of fields, so I can see it. If you're walking the collection of controls only in the form's OnOpen event, that's fine. But if you're doing it in the OnCurrent of a bound form, or multiple times in an unbound form, you might consider a long post of mine on using custom collections to manage groups of controls.