Two ways to access controls on other forms (1 Viewer)

SparklySpartan

New member
Local time
Today, 11:51
Joined
Feb 25, 2025
Messages
25
Hi everyone,

I learned that I can use two methods to access controls on other forms from outside their respective modules. I was curious if one or the other is considered best practice so I decided to pick the brains of the community here.

I should preface that with either approach, I always ensure the form I will by trying to reference is loaded and if not design the code to handle it gracefully.

The first way I learned to do this was the "!" approach. For example, here's how I can access the control called "Project_Menu" on the main form:

Code:
Forms!Main_f!Project_Menu

I recently discovered this can also be accomplished by referencing the name of the VBA module for that form

Code:
Form_Main_f.Project_Menu

I believe this is a superior way of doing it, because I have access to autocorrect for all of the controls on the form "Main_f" and autocorrect for the properties of those controls. Whereas with the "!" approach I get no autocorrect at all.

I was wondering which is considered better practice. Is there anything that can be done with the "!" method that can't be done with my new preferred method?

I know this might be a silly question but I'm genuinely curious, so thanks for indulging me on this one.
 
MajP has written a detailed discussion on the use of bang (!) vs. dot(.) in names. Rather than intrude on his eloquence, here's a link.


Your second example could be rewritten as Forms("Main_f").Project_Menu - but that example does not necessarily point you to a control. Pointing to another module points to nothing useful unless you were pointing to a Public or Friend variable. And unless your subs and functions have duplicate names in different modules, you don't need to qualify public entry points anyway.

You would perhaps use something like Forms("Main_f").Control("control-name") to get to a specific control on a form, and you could muddle through from that starting point. In either case, there is the over-arching "gotcha" that the form holding the targeted control has to actually be open at the time, at least to the point of being loaded, because before the form's Form_Load event, there ARE no controls. (They haven't been loaded yet.) If you actually want the value for that control, as source or target, the form has to have a current record - which means it must have previously experienced its Form_Current event.
 
you can also ask Copilot or ChatGPT, they have huge database of answers (and keeps on growing).
here's what ChatGPT has to say:

💠 Dot (.)For properties and methods known at compile-time

  • Used to access built-in properties, methods, or members of an object that VBA recognizes at compile time.
  • IntelliSense (autocomplete) works with dot notation.
  • Syntax:
    vba
    CopyEdit
    Code:
    Me.TextBox1.Value
  • Me is a reference to the current form or report object.
  • TextBox1 is a known control, and Value is a known property.

💠 Bang (!)For controls or fields known only at runtime

  • Used to access controls, fields, or child objects of collections (like Recordset, Controls, Fields, etc.) that may not be known at compile time.
  • It's a shortcut for accessing the default property (often a collection like Controls or Fields).
  • Syntax:
    vba
    CopyEdit
    Code:
    Me!TextBox1
  • Me!TextBox1 is interpreted as Me.Controls("TextBox1")

🔍 Key Differences:​

FeatureDot (.)Bang (!)
Compile-time checkYes (VBA validates it early)No (checked at runtime)
IntelliSense✅ Works❌ Doesn't work
Used forProperties & methodsFields & controls in collections
PerformanceSlightly fasterSlightly slower
Error-proneLess (errors caught early)More (runtime errors if misspelled)

✅ Examples:​

vba
CopyEdit
Code:
' Using dot:
Me.TextBox1.Value = "Hello"

' Using bang:
Me!TextBox1 = "Hello"   ' Also works, but no IntelliSense
vba
CopyEdit
Code:
' Using bang to access a field in a recordset:
rs!FirstName = "John"
' Equivalent to:
rs.Fields("FirstName").Value = "John"

⚠️ Best Practice:​

  • Use . when you're sure the object/member exists and want IntelliSense support.
  • Use ! when referencing dynamic names like form controls or recordset fields.
 
also i read in a book (can't remember the name).
you use dot (.) when you are referencing the "intrinsic" properties of Form/Report.
and use bang (!) on the controls you Added to the form on design view.
btw you cannot, directly, use dot (.) when referencing a field on a recordset object.
 
I know this might be a silly question but I'm genuinely curious, so thanks for indulging me on this one.
Can't test this right now, but I am thinking the second method might open or create a new instance of the form even if it's already open.

I should preface that with either approach, I always ensure the form I will by trying to reference is loaded and if not design the code to handle it gracefully.
If what I was thinking was true, your approach to always make sure the form is loaded becomes essential, because with the second method, you might be getting a different value than what you expected.

Sent from phone...
 
I'm afraid there are many many more ways to do your references, here's a list of some:
Code:
    ' DOTS
    Debug.Print Application.Forms.Item("Main_f").Controls.Item("Project_Menu").Name
    Debug.Print Application.Forms("Main_f").Controls("Project_Menu").Name
    Debug.Print Application.Forms.Item("Main_f").Form.Project_Menu.Name
    Debug.Print Application.Forms.Item("Main_f").Project_Menu.Name
    Debug.Print Application.Forms("Main_f")("Project_Menu").Name
    Debug.Print Forms.Item("Main_f").Controls.Item("Project_Menu").Name
    Debug.Print Forms("Main_f").Controls("Project_Menu").Name
    Debug.Print Forms.Item("Main_f").Form.Project_Menu.Name
    Debug.Print Forms.Item("Main_f").Project_Menu.Name
    Debug.Print Forms("Main_f")("Project_Menu").Name
   
    ' BANGS
    Debug.Print Application.Forms!Main_f.Controls!Project_Menu.Name
    Debug.Print Application.Forms!Main_f!Project_Menu.Name
    Debug.Print Application.Forms!Main_f.Form!Project_Menu.Name
    Debug.Print Forms!Main_f.Controls!Project_Menu.Name
    Debug.Print Forms!Main_f!Project_Menu.Name
    Debug.Print Forms!Main_f.Form!Project_Menu.Name
    Debug.Print Forms!Main_f!Project_Menu.Name
   
    ' CLASS NAME + DOTS
    Debug.Print Form_Main_f.Controls.Item("Project_Menu").Name
    Debug.Print Form_Main_f.Controls("Project_Menu").Name
    Debug.Print Form_Main_f.Form.Project_Menu.Name
    Debug.Print Form_Main_f.Project_Menu.Name
    Debug.Print Form_Main_f("Project_Menu").Name
   
    ' CLASS NAME + BANGS
    Debug.Print Form_Main_f.Controls!Project_Menu.Name
    Debug.Print Form_Main_f!Project_Menu.Name
    Debug.Print Form_Main_f.Form!Project_Menu.Name
    Debug.Print Form_Main_f!Project_Menu.Name
   
    ' CLASS NAME OF SUBFORM
    Debug.Print Form_Project_Menu.Name

You should also consider Parent combinations, which can be found across various members, including the properties of many objects.

Anyway, this topic is often overlooked or only partially covered, primarily because most explanations can't capture the full scope of how these combinations work. So, instead of relying on other's explanations, I recommend a more hands-on approach: using the debugger as you develop. This way, you can directly observe and interact with these relationships in real-time, eliminating the need to memorize the convoluted details.

Some of the details I'm talking about are the following:
- Dot notation is obvious when you look at the Locals and the Watch windows
- Objects have default members, bangs work with them
- Form objects have a Controls collection where you can find the controls of the form
- Form objects load the contents of the Controls collection as members of the form itself
- Referencing forms like Form_YourForm is really handy, but you should not use it if you'll have multiple instances of the same form open simultaneously. In such cases, referencing the form this way will only give you access to one instance, and while all instances will load, identifying the correct one can be tricky. Additionally, this method may inadvertently cause the form to instantiate when you attempt to check one of its properties, potentially opening it unintentionally.


So, what’s the best practice? In my opinion, the most effective approach is to develop with the debugging tools at your side. By adding a simple Stop statement to your code, you can pause execution and then examine the Locals window or open the Watch window. For example, by scoping the Application object across all modules in the Watch window, you'll be able to see exactly what’s loaded, where it's located, and how it’s behaving, saving you countless hours of guesswork. However, please consider that dot and bang notation behave differently across various environments, such as the VBA editor, the Access GUI, the query builder, macros and whatever else I'm missing.
 
btw you cannot, directly, use dot (.) when referencing a field on a recordset object.
Hmm
theRecordset.Fields("FieldName") ✅
------------^dot----------------
theRecordset("FieldName") ✅
theRecordset!FieldName ✅
theRecordset.FieldName ❌

The default member of a Recordset object is the Fields object, so it can be bang'd.
 
I believe this is a superior way of doing it, because I have access to autocorrect for all of the controls on the form "Main_f" and autocorrect for the properties of those controls. Whereas with the "!" approach I get no autocorrect at all.
As @theDBguy points out if you reference a form property through the class it will open a hidden version of the form immediately if the form is not already open. But different from what he guessed it will not open a new instance if a visible (or hidden) instance is already open. This prior method could cause unintended consequences if there is code in the the opening events. So you just need to be aware of this. Also I do this in a way where you can never mistakenly open a hidden instance.

This code shows that you will open a hidden instance. Here I call a property using the class and it open a hidden instance. Then I can use docmd and open a visible instance. You can see that there are two forms created.
Code:
Public Sub Test()
  Dim x As String
  Debug.Print "form count before referencing property " & Forms.Count
  x = Form_Form1.Name
  Debug.Print "form count after referencing property " & Forms.Count & " " & Forms(0).Name
  'You cannot reference this form by Forms("form1"). Although it is added to forms collection its named index is not
  DoCmd.OpenForm ("form1")
  Debug.Print "form Count after opening using docmd " & Forms.Count
  'share the name but not the named index.
  Debug.Print Forms(0).Name & " " & Forms(1).Name
End Sub

In the debug window:
Code:
form count before referencing property 0
form count after referencing property 1 Form1
form Count after opening using docmd 2
Form1 form1
FYI. The hidden instance does not have a named index. You cannot write
debug.print forms("form1").name
but you can write
debug.print forms(0).name

Fortunately if you open a visible instance first it will not create a new hidden instance by referencing a property through the class.
Code:
Public Sub TestOpenThenProperty()
  Dim x As String
  Debug.Print "form count before docmd.openform " & Forms.Count
  DoCmd.OpenForm ("form1")
  Debug.Print "Form count after docmd.openform and before calling property" & Forms.Count & " " & x
  x = Form_Form1.Name
  Debug.Print "Form count after calling property " & Forms.Count
End Sub

in the debug you can see a new instance is not created.
Code:
form count before docmd.openform 0
Form count after docmd.openform and before calling property1
Form count after calling property 1

Normally this is not a problem because it is unlikely you are writing code that calls form properties until you intend to open it. But it is possible to make a mistake. I have.

To get the best of both worlds; get intellisense and make sure you do not inadvertently open an instance prior to intending to open it.

dim frm as Form_Form1 'MAKE SURE not to use NEW keyword
now add code to purposely open the form and you can use "frm." throughout your code.
frm... and get intellisense.

This will possibly shorten your code since "frm" is easier than writing "form_form1." multiple times.

With that said, you can open multiple instance of the same form and make them visible if you understand this. You cannot use the DOCMD to do it.

IMO the OPs question is not about Bang and Dot notation but more about compile time and run time notation. However bang never gives intellisese.

If I did this
Code:
dim frm as Access frm
docmd.openform "form1"
set frm = forms("form1") or set frm = forms!form1
The compiler only knows at compile time that frm is an access form and not a sub class Form1
It cannot give me intellisense for a Form1 only for the super class Form.
Same if I do not have a frm variable but write code like
forms!form1.someProperty
or
forms("form1").someproperty
The compiler has no way to know pre compile that forms!form1 or forms("form1") will return a Form1 object. In both cases !form1 and ("form1") are simply keys to a collection that will return some kind of Form.


if I create a variable of the Form1 class
dim frm as Form_Form1
then it knows at compile time it is a sub class Form1 and can provide intellisense
or if I fully reference it knows at compile time
form_form1.

To reiterate this question is really not about Bang vs dot. Although bang never gives you intellisense
I was wondering which is considered better practice. Is there anything that can be done with the "!" method that can't be done with my new preferred method?
Again it is more about notation that gives a known reference to the Subclass Form1 at design time.

Forms("form1") is dot and does not give design time reference
forms!form1 is bang and does not

Form_Form1.SomeControl.name is dot but gives a reference to the subclass Form1
 
Last edited:
FYI there is never a case where Bang notation is needed and cannot be done in dot notation. There are times when it provides shorthand and there are times when certain Dot notation will not work, but the correct Dot notation will work.

So in @arnelgp post it states
For forms and controls known only at runtime
That is correct, but should be read as "Can be used." Do not assume "Must be used." The correct Dot notation can also be used.

So you may ask what does that mean? Here is an example where something is known only at runtime.
Imagine you assign the recordsource to a form at runtime. Maybe you build a generic form and can assign different recordsources with different fields and maybe bind some controls to these fields. Since you assign the recordsource at runtime the form does not add the fields as properties to the form.

Code:
Private Sub CmdLoad_Click()
  Me.RecordSource = "select ID from table1"
  Me.txtOne.ControlSource = "ID"

 'msgbox me.id  This will not compile because ID does not exist at design time
 
 MsgBox Me!id 'works
 MsgBox Me.Controls("id") 'works

  Debug.Print "Controls('ID'): " & TypeName(Me.Controls("ID"))
  Debug.Print "Me!ID: " & TypeName(Me!ID)
  Debug.Print "Me.RecordSet!ID: " & TypeName(Me.Recordset!ID)
  Debug.Print "Me.controls('TxtOne'): " & TypeName(Me.Controls("txtOne"))
End Sub

In the code above if you type
MsgBox me.id
It will not compile. That is because there is no property "ID" that exists at design time.

Now you may ask why do these work. At design time there is no control with this name.
Me!ID
or
Me.Controls("ID")

These compile because in both methods the !ID or "ID" are simply indexes, and not properties.

But there is a second issue. The bang only works with default properties. The default property of a form is the controls collection and there is no control called "ID". And for sure me.controls("ID") is referencing a control from the controls collection.
Actually there is control in the controls collection. All fields in the recordsource are added to the controls collection as a pseudo control of Type "AccessField" at runtime.
To prove this here is the result of the debug.print
Controls('ID'): AccessField
Me!ID: AccessField
Me.RecordSet!ID: Field3
Me.controls('TxtOne'): TextBox
So "ID" exists in the controls collection as an object of type AccessField, where txtOne is simply an object of type Textbox. Also Me!ID is returning a pseudo control and not a field. Compare that to Me.Recordset!ID

What is IMPORTANT here is you can always reference field values even if there is no bound control on your form, by using the runtime notation.
Me!somefieldnotbound
Me.controls("somefieldnotbound")

I see people littering forms with hidden controls, just so they can reference these values.

Also people think
Me!SomeField is getting a reference from the fields collection. It is not, it is getting its reference from the controls collection. If that control is not bound then it is a pseudo control of type AccessField
Me!SomeField is the same as Me.Controls("SomeField")
Debug.print proves this.
 
Last edited:
Appreciate your responses everyone,

Looks like there are a lot of ways to do this. I definitely don't prefer the bang notation as it turns out to be called, but I see the merit in the case MajP suggested where a form's record source only gets set a runtime and therefore the fields can't be accessed. I also use bang notation with recordsets because it's slightly easier than typing Recordset("field_name")

If the second method I discovered turned out to indeed open a second instance even if there already was one open that would not be good. My testing seemed to be working how I wanted it to, though. My takeaway is that the second method can be used as long as you're ensuring the form is open. In my codebase there's a handy function to tell if a form is loaded or not, and so far I haven't dared trying to access other forms without wrapping it in an if statement to protect from whatever undefined behavior might occur.

I do like the approach of having a variable to access the form if I ever need to access many things on a different form. So far, I've only been doing this once every now and then and have been able to condense it into a single line:

Code:
' Something that looks like:
if isFormLoaded("Main_f") then project_ID = Form_Main_f.Project_Menu
' or:
if isFormLoaded("Search_f") then Form_Search_f.listProjects.Requery

Random aside: I don't know if the single line if statement is a cardinal sin to some of the folks on here, but I respect that. I don't use it when coding in other languages

In the future, the need might arise for a form1 to extensively reference a form2, and in that case it'll definitely make more sense to use a variable for form2 in the code of form1 than to use either of the previous two approaches. So I'm glad I know it can be done that way.

Appreciate the responses as always. Have a blessed week everyone!
 
Random aside: I don't know if the single line if statement is a cardinal sin to some of the folks on here, but I respect that. I don't use it when coding in other languages

I don't presume to speak for others, but for me the single-line IF is just another tool in the toolkit. I never turn it down when it is appropriate. The only real issue is the temptation to have a huge pile of inline IF statements, which after a while would become difficult to read. (It's a matter of "alienation of attention" a.k.a. "highway hypnosis".)
 

Users who are viewing this thread

Back
Top Bottom