Couple gotchas to serve as addenda to the two exceptional answers already posted:
Accessing recordset fields in forms vs. reports
The default item of Form objects in Access is a union of the form's Controls collection and the form recordset's Fields collection. If the name of a control conflicts with the name of a field, I'm not sure which object is actually returned. Since the default property of both a field and a control is their .Value
, it's often a "distinction without a difference." In other words, one normally doesn't care which it is because the values of the field and control are often the same.
Beware of naming conflicts!
This situation is exacerbated by Access's Form and Report designer defaulting to naming bound controls the same as the recordset field to which they are bound. I've personally adopted the convention of renaming controls with their control type prefix (e.g., tbLastName
for the text box bound to the LastName field).
Report recordset fields aren't there!
I said earlier the Form object's default item is a collection of Controls and Fields. However, the Report object's default item is only its collection of Controls. So if one wants to refer to a recordset field using the bang operator, one needs to include that field as the source for a (hidden, if desired) bound control.
Beware conflicts with explicit form/report properties
When one adds controls to a form or report, Access automatically creates properties that refer to these controls. For example, a control named tbLastName
would be available from a form's code module by referring to Me.tbLastName
. However, Access will not create such a property if it conflicts with an existing form or report property. For example, assume one adds a control named Pages. Referring to Me.Pages
in the form's code module will return the form's Pages property, not the control named "Pages".
In this example, one could access the "Pages" control explicitly using Me.Controls("Pages")
or implicitly using the bang operator, Me!Pages
. Be aware, though, that using the bang operator means that Access might instead return a field named "Pages" if one exists in the form's recordset.
What about .Value?
Though not explicitly mentioned in the question, this topic came up in the above comments. The default property for Field objects and most "data-bindable"¹ Control objects is .Value
. Since this is the default property, VBA will implicitly return the .Value
property's value when it does not make sense to return the object itself. Thus, it's common practice to do this...
Dim EmployeeLastName As String
EmployeeLastName = Me.tbLastName
...instead of this...
EmployeeLastName = Me.tbLastName.Value
The above two statements produce identical results because EmployeeLastName
is a string.
Beware the subtle .Value bug when keying dictionaries
There are some cases where this convention can cause subtle bugs. The most notable--and, if memory serves, only--one I've actually run into in practice is when using the value of a Field/Control as a Dictionary key.
Set EmployeePhoneNums = CreateObject("Scripting.Dictionary")
Me.tbLastName.Value = "Jones"
EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-1234"
Me.tbLastName.Value = "Smith"
EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-6789"
One would likely expect that the above code creates two entries in the EmployeePhoneNums
dictionary. Instead, it throws an error on the last line because we are trying to add a duplicate key. That is, the tbLastName
Control object itself is the key, not the value of the control. In this context, the control's value does not even matter.
In fact, I expect that the object's memory address (ObjPtr(Me.tbLastName)
) is likely what's being used behind the scenes to index the dictionary. I did a quick test that seems to bear this out.
'Standard module:
Public testDict As New Scripting.Dictionary
Sub QuickTest()
Dim key As Variant
For Each key In testDict.Keys
Debug.Print ObjPtr(key), testDict.Item(key)
Next key
End Sub
'Form module:
Private Sub Form_Current()
testDict(Me.tbLastName) = Me.tbLastName.Value
Debug.Print ObjPtr(Me.tbLastName); "..."; Me.tbLastName
End Sub
When running the above code, exactly one dictionary item is added each time the form is closed and re-opened. Moving from record to record (and thus causing multiple calls to the Form_Current routine) does not add new dictionary items, because it is the Control object itself indexing the dictionary, and not the Control's value.
My personal recommendations/coding conventions
Over the years, I've adopted the following practices, YMMV:
- Prefix Form/Report control names with control type indicators (e.g.,
tbTextBox
, lblLabel
, etc.)
- Refer to Form/Report controls in code using
Me.
notation (e.g., Me.tbLastName
)
- Avoid creating table/query fields with problematic names in the first place
- Use
Me!
notation when there are conflicts, such as with legacy applications (e.g., Me!Pages
)
- Include hidden report controls to gain access to report Recordset field values
- Explicitly include
.Value
only when the situation warrants the added verbosity (e.g., Dictionary keys)
¹ What's a "data-bindable" control?
Basically, a control with a ControlSource
property, such as a TextBox or ComboBox. A non-bindable control would be something like a Label or CommandButton. The default property of both a TextBox and ComboBox is .Value
; Labels and CommandButtons have no default property.