Learn Microsoft Access Advanced Programming Techniques, Tips and Tricks.

WithEvents in Class Module and Data Entry Form

1. Introduction.

So far, we have used unbound text boxes on the form for demo runs and for capturing built-in events within the Class Module object. Since text boxes are the primary controls used on forms for data entry, display, and viewing of information, they were chosen first to demonstrate the most commonly used built-in events: AfterUpdate, OnGotFocus, and OnLostFocus.

However, this does not mean that we have ignored other controls such as command buttons, combo boxes, list boxes, and option buttons. Most of these controls primarily use the Click event to open forms or reports, run macros, call subroutines/functions, or select items from a list or combo box. Their event procedures are generally simpler to handle within the Class Module, and we will include them along with text boxes in our approach.

Once you are comfortable handling text box–based events and understand how they work, adding support for other controls will be a straightforward extension of the same concept.

Creating a Class Module object instance—one instance for each text box on the form—and adding them to an array, each enabled with the required built-in events, may seem confusing at first, but it is not as complicated as it appears.

If you are new to programming with stand-alone Class Modules, it is highly recommended to start there. Understanding the basics will make it much easier to grasp their usage with WithEvents, Event, and RaiseEvent programming.

Refer to the following articles to get a general understanding of Class Module programming.

  1. MS-Access Class Module and VBA
  2. MS-Access VBA Class Object Arrays
  3. MS-Access Base Class and Derived Objects
  4. VBA Base Class and Derived Object-2
  5. Base Class and Derived Object Variants
  6. MS-Access Recordset and Class Module
  7. Access Class Module and Wrapper Classes
  8. Wrapper Class Functionality Transformation

2. Brief Review of what is covered so far.

In the past few weeks, we have begun exploring Class Module and Form Controls Event Programming in Microsoft Access. As part of this journey, we have learned how to create a Class Module object array element for each text box on a form.

The basic steps for preparing a Class Module to handle events for a text box are as follows:

  1. Create a new Class Module and declare a property to hold a Text Box (Access.TextBox) object, using a variable name such as txt.

  2. If the property is declared as Private, then create corresponding Property Get and Property Set procedures to manage access to the txt object.

  3. In the Class Module, create event-handling procedures for the AfterUpdate, GotFocus, and LostFocus events of the text box.

When an instance of the above Class Module is created in memory, it can handle the events (AfterUpdate, GotFocus, and LostFocus) of only one text box on the form.

If there are multiple text boxes—for example, three text boxes—on the form, then we must create three separate instances of the same Class Module, one for each text box.

To keep track of all these instances, they should be stored either in an array or added as items in a Collection object, so that each text box has its own dedicated Class Module instance.

If, instead, the same Class Module instance is shared among multiple text boxes, then the event procedures inside the Class Module would have to be modified. In that scenario, each event procedure must identify which text box triggered the event and then run the appropriate validation checks or other actions for that specific text box within the same subroutine.

Example:
The code shown below is taken from the AfterUpdate() event procedure.

In this example, the FirstName and Designation text boxes are listed in the Select Case ... End Select structure, but they don’t have any executable code under their respective Case blocks. This indicates that:

  • These text boxes exist on the form,

  • but their AfterUpdate event is not currently enabled or used.

Since no logic is implemented for them in the AfterUpdate() event, it is not strictly necessary to include their names in this procedure.

However, keeping them in the list can be useful during testing because it serves as a reminder that these text boxes exist on the form and can be enabled later if needed.

It’s also possible that these text boxes are intended to use a different event, such as OnLostFocus(), and their logic might be implemented in the corresponding LostFocus event procedure instead.

txtName = Txts.Name

Select Case txtName
    Case "LastName" 'TextBox Name
        txtval = Trim(Nz(Txts.Value, ""))

        If Len(txtval) > 15 Then
           msg = "LastName Max 15 chars only."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Left(Nz(txtval, ""), 15)
        End If
    Case "FirstName"
        '
    Case "Designation"
        '
    Case "BirthDate"
         db = CDate(Nz(Txts.Value, 0))
        
         If db > Date Then
           msg = "Future Date: " & db & " Invalid."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Null
           efrm!Age = Null

         ElseIf db > 0 Then
           dbage = Int((Date - db) / 365)
           efrm!Age = dbage
         End If
    Case "Age"
         Dim xage As Integer
         db = CDate(Nz(efrm!BirthDate, 0))
         xage = Nz(Txts.Value, 0)
         
         If (db > 0) And (xage > 0) Then
            dbage = Int((Date - db) / 365)
            If xage <> dbage Then
                msg = "Correct Age as per DB = " & dbage
                MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
                Txts.Value = dbage
            End If
         ElseIf (xage = 0) And (db > 0) Then
            dbage = Int((Date - db) / 365)
            Txts.Value = dbage
         End If
    Case "JoinDate"
       Dim jd As Date
       
       db = CDate(Nz(efrm!BirthDate, 0))
       jd = CDate(Nz(Txts.Value, 0))
       
       If (db > 0) And (jd < db) Then
          msg = "JoinDate < Birth Date Invalid!"
          MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
          Txts.Value = Null
       End If
End Select

Note:

If you need to read or write values from or to any other text box (other than the event-triggered text box), you must include the Property 'Access.Form' in the text box class module (ClsTxtEmployees). This property allows you to reference other text boxes on the same form for reading or updating their values as needed.

This class module can also serve as a template for handling text boxes on other forms. However, you will need to customize the subroutines according to the specific requirements of each form’s text box controls.

For your convenience, links to all the earlier articles in this series are provided below, especially if you are new to working with WithEvents, Event, and RaiseEvent in user-defined or built-in event programming with Microsoft Access.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events

3. What is New in this Post?

Here, we have a sample Employee Data Entry Form bound to a table. If you have gone through the earlier examples that used unbound text boxes on a form, you will notice no functional difference in this demo. The purpose here is simply to demonstrate how event handling in a class module object works when the form is bound to a table and the text boxes use table fields as their control sources.

The sample image of the data entry form is shown below. An error message (manually positioned at the bottom-right corner in the image) appears when an invalid Join Date—earlier than the Birth Date—is entered in the field.


In the first Employee Form demo, the Command Button click event is handled directly in the Form’s own Class Module.

In this demo, the Form’s Class Module works together with a separate Class Module that includes two Private properties:

  • The Property 'Txts' for the TextBox object, and

  • efrm for the Access.Form object.

4.  New Class Module Code.

Insert a new Class Module and change its Name Property value from Class1 to ClsTextEmployees.

Copy and Paste the following VBA Code into the Class Module and save the Code:

Option Compare Database
Option Explicit

Private efrm As Access.Form
Private WithEvents Txts As Access.TextBox

Public Property Get pfrm() As Access.Form
  Set pfrm = efrm
End Property

Public Property Set pfrm(ByRef vNewValue As Access.Form)
  Set efrm = vNewValue
End Property

Public Property Get pTxts() As Access.TextBox
  Set pTxts = Txts
End Property

Public Property Set pTxts(ByRef vNewValue As Access.TextBox)
  Set Txts = vNewValue
End Property

Private Sub Txts_AfterUpdate()
Dim txtName As String, txt As String
Dim msg As String, txtval As Variant
Dim db As Date, dbage As Integer

txtName = Txts.Name

Select Case txtName
    Case "LastName"
        txtval = Trim(Nz(Txts.Value, ""))

        If Len(txtval) > 15 Then
           msg = "LastName Max 15 chars only."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Left(Nz(txtval, ""), 15)
        End If
    Case "FirstName"
        '
    Case "Designation"
        '
    Case "BirthDate"
         db = CDate(Nz(Txts.Value, 0))
        
         If db > Date Then
           msg = "Future Date: " & db & " Invalid."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Null
           efrm!Age = Null

         ElseIf db > 0 Then
           dbage = Int((Date - db) / 365)
           efrm!Age = dbage
         End If
    Case "Age"
         Dim xage As Integer
         db = CDate(Nz(efrm!BirthDate, 0))
         xage = Nz(Txts.Value, 0)
         
         If (db > 0) And (xage > 0) Then
            dbage = Int((Date - db) / 365)
            If xage <> dbage Then
                msg = "Correct Age as per DB = " & dbage
                MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
                Txts.Value = dbage
            End If
         ElseIf (xage = 0) And (db > 0) Then
            dbage = Int((Date - db) / 365)
            Txts.Value = dbage
         End If
    Case "JoinDate"
       Dim jd As Date
       
       db = CDate(Nz(efrm!BirthDate, 0))
       jd = CDate(Nz(Txts.Value, 0))
       
       If (db > 0) And (jd < db) Then
          msg = "JoinDate < Birth Date Invalid!"
          MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
          Txts.Value = Null
       End If
End Select
End Sub

Private Sub Txts_LostFocus()
Dim txtName As String
Dim msg As String, txtval As Variant

txtName = Txts.Name
txtval = Trim(Nz(Txts.Value, ""))

Select Case txtName
    Case "LastName"
        '
    Case "FirstName"
        If Len(txtval) = 0 Then
           msg = "FirstName should not be Blank."
           MsgBox msg, vbInformation, txtName & "_LostFocus()"
           Txts.Value = "XXXXXXXXXX"
        End If
    Case "Designation"
        If Len(txtval) = 0 Then
           msg = "Designation Field is Empty."
           MsgBox msg, vbInformation, txtName & "_LostFocus()"
           Txts.Value = "XXXXXXXXXX"
        End If
    Case "BirthDate"
        '
    Case "Age"
        '
    Case "JoinDate"
        '
End Select

End Sub

5. Class Module Properties and Sub-Routines.

In the global section of the Class Module, a Private Property named efrm is declared to hold the 'Access.Form' object.

Next, an 'Access.TextBox' control is declared as a Private Property named Txts using the WithEvents keyword. The WithEvents keyword enables the Txts object to capture and respond to programmed Events triggered on the Form.

Because these Class Module properties are declared as Private, they are accessible only within the Class Module itself, preventing direct external access. To allow values to be assigned or retrieved from outside, the module must define Public Get and Set Property Procedures.

The first pair of Get/Set Property procedures manages the efrm (Access.Form) object.

The second pair of Get/Set Property procedures manages the Txts (Access.TextBox) object.

Any validation checks on the Set Property Procedure parameter can be performed before attempting to assign the value to the Property efrm or the Property Txts.

In the Class Module, we are handling only two types of built-in Events, AfterUpdate and LostFocus Events from TextBoxes on the Form.

The AfterUpdate() event procedures validate the values entered in the LastName, BirthDate, Age, and JoinDate fields, and display appropriate messages—mainly to confirm that the programmed events are being correctly captured by the Class Module instances.

LostFocus() The event procedure validates the FirstName and Designation field values. If either of these fields is left empty when it loses focus, a default text string “XXXXXXXXXX” is assigned to the field.

All the TextBox control names are listed within the Select Case … End Select structure for clarity, though some of them have no executable code yet. In the LostFocus event procedure, actual code has been written only for the FirstName and Designation fields. The LostFocus event is enabled on the form only for these two fields, while the others are included for clarity and possible future use.

The first TextBox on the form is bound to an AutoNumber field, which generates values automatically. Since no events are intended to be trapped for this control, it is excluded from the Select Case … End Select structure and no events are enabled for it.

6.  Employees Form Module Code.

The VBA Code behind the Employees Form's Class Module is given below:

Option Compare Database
Option Explicit

Dim tc As ClstxtEmployee
Dim C As Collection

Private Sub cmdClose_Click()
'Command Button Click Event is handled
'on the Form Module itself, the Event is
'not programmed in Class Module: ClsTxtEmployee
DoCmd.Close
End Sub

Private Sub Form_Load()
Dim ctl As Control

Set C = New Collection

For Each ctl In Me.Controls
  If TypeName(ctl) = "TextBox" Then
  
     Set tc = New ClstxtEmployee
     
     'Form Object is required to read/write values
     'from other TextbOX, if needed.
    Set tc.pfrm = Me
    'assign TextBox control to the Class Module instance's Property
    Set tc.pTxts = ctl
    
    Select Case ctl.Name
          Case "FirstName", "Designation"
          'enable LostFocus Event for FirstName and Designation
               tc.pTxts.OnLostFocus = "[Event Procedure]"
          Case Else
           'enable AfterUpdate for all other Text Boxes
               tc.pTxts.AfterUpdate = "[Event Procedure]"
    End Select
  End If
  C.Add tc 'add ClstxtEmployee instance as Collection Object Item
Next

End Sub

Private Sub Form_Unload(Cancel As Integer)
'when the form is closed erase Collection Object from memory
Set C = Nothing
End Sub

The Class Module ClsTxtEmployee is declared as a tc Object.

A Collection Object is declared in Object C.

On the Form_Load() Event Procedure, the Collection Object is instantiated.

Within the For Each ... Next loop, the Employees Form Text Box controls are picked and enabled for the required built-in Events.

For each text box on the Form, Employee, a new instance of the Class Module ClsTxtEmployee is created, and the Form Object and Text Control Property Values are passed to the Class Module Object.

7.  Derived Class Module to Replace Form Module Code.

As in the earlier examples, we will now create a derived class object (ClsTxtEmployeeHeader) using the ClsTxtEmployee class as its base. We will then move the existing Form Module code into this new Class Module, leaving only a minimal set of essential lines in the form’s own Class Module.

We will also transfer the Command Button Click Event handling into the Derived Class Module Object.

The Derived Class Module (ClsTxtEmployeeHeader) VBA Code is given below:

Option Compare Database
Option Explicit

Private tc As ClstxtEmployee
Private Col As New Collection

Private fm As Access.Form
Private WithEvents btn As Access.CommandButton


Public Property Get oFrm() As Access.Form
  Set oFrm = fm
End Property

Public Property Set oFrm(ByRef vNewValue As Access.Form)
  Set fm = vNewValue
  Call Class_Init
End Property

Private Sub Class_Init()
Dim ctl As Control

For Each ctl In fm.Controls
  Select Case TypeName(ctl)
  'If TypeName(ctl) = "TextBox" Then
    Case "TextBox"
        'create a new instance of Class Module Object
        Set tc = New ClstxtEmployee
        'assign common property values
        Set tc.pfrm = fm 'pass Form Employyes object to the new instance
        Set tc.pTxts = ctl  'pass text control
            
            'enable required event procedures for Text Boxes
            Select Case ctl.Name
                   'lostfocus event controls
                Case "FirstName", "Designation"
                    tc.pTxts.OnLostFocus = "[Event Procedure]"
                Case Else
                    'after Update Event
                    tc.pTxts.AfterUpdate = "[Event Procedure]"
            End Select
            Col.Add tc 'add to the collection object
    Case "CommandButton"
        Set btn = ctl
        btn.OnClick = "[Event Procedure]"
    End Select
Next

End Sub

Private Sub btn_Click()
   If MsgBox("Close the Form?", vbYesNo, btn.Name & "_Click()") = vbYes Then
    DoCmd.Close acForm, fm.Name
   End If
End Sub

In the newly created derived Class Module, the first two object declarations—one for the TextBox Class Module and the other for the Collection object—previously placed in the Employees Form’s Class Module are now moved into this Class Module.

Additionally, an 'Access.Form' object named fm is declared to hold a reference to the Employees form. This reference is needed to read the value from the BirthDate TextBox, calculate the employee’s age, and update the Age TextBox on the form accordingly.

Next, a Command Button control object is declared within the derived Class Module using the WithEvents keyword to capture the Command Button’s Click event on the form.

Once the form object reference is received as a parameter in the Public Property Set oFrm() procedure and assigned to the fm object, the Class_Init() subroutine is called. This subroutine enables the AfterUpdate and LostFocus event handling, which were previously initialized from the Form_Load() event procedure.

Now, the only code remaining in the form’s Class Module is a few lines in the Form_Load() event that pass the current form object (Me) to the oFrm() property procedure of the derived Class Module ClsTxtEmployeeHeader.

This form reference is then passed on to each instance of the ClsTxtEmployee Class Module through the statement:

Set tc.pfrm = fm.

The Command Button’s Click event is also enabled and captured within the derived Class Module ClsTxtEmployeeHeader itself, through the btn_Click() event procedure.

8.  New Form using Derived Class Module: ClsTxtemployeeHeader

The Image of the second sample form, after transferring all its Form Module Code into the Derived Class Module ClsTxtEmployeeHeader.

The EmployeeHeader Form's Class Module VBA Code is given below.

Option Compare Database
Option Explicit

Dim T As New ClsTxtEmployeeHeader

Private Sub Form_Load()
  Set T.oFrm = Me
End Sub

The Dim statement instantiates the derived object of ClsTxtEmployeeHeader in memory.

The Form_Load() Event Procedure passes the current form object to the class object T.oFrm() Property Procedure as a parameter.

9. Summary

The Derived Class Module holds the entire Form's Class Module Code, except for a few lines on the Form's Code Module.

All the code that would otherwise be written directly in the Form’s Class Module is now safely organized within the Base Class Module ClsTxtEmployees and the Derived Class Module ClsTxtEmployeeHeader.

When you need to create another form with similar functionality—either in this project or in other projects—you can simply reuse and customize these two Class Modules instead of writing new code in each form’s Class Module. This approach keeps your code well-organized, modular, and easier to maintain.

You can download the demo database provided at the end of this page, which includes the sample forms and Class Modules, and experiment with it.

Once you have explored the demo, try creating something similar on your own, using the demo database as a reference point, so you can reinforce and validate what you have learned so far.

Downloads.




Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types
Share:

WithEvents in Class Module for Sub-Form Text Box Events

Introduction.

So far, we have demonstrated the use of WithEvents and built-in event capturing using a single form with TextBox controls, executing subroutines in Class Object arrays or Collection objects. But how can we handle TextBox controls on Subforms of a main form—capturing their built-in events and processing them in class module objects?

For reference, the image of the sample form below displays two sub-forms and multiple TextBoxes across all forms.

We will continue using the familiar AfterUpdate, OnGotFocus, and OnLostFocus events for TextBoxes on both the main form and its subforms. However, let us first focus on one or two aspects that we haven’t addressed so far.


  1. Introduction of the 'Access.Form' property in the Class Module for TextBox controls.

  2. How to reference TextBox controls on a subform to trigger built-in events and capture them in a Class Module.

  3. How to move the insertion point from the last TextBox on the first subform to the first TextBox on the second subform.

  4. How to move the insertion point from the last TextBox on the second subform to the only TextBox on the main form.

  5. How to use references to other controls to read from or write to form controls from within the current event procedure in the Class Module.

Why is Form Property Required in the Class Module?

So far, we have worked with a single TextBox Property in the Class Module.  The Event Procedures also mostly deal with the Text Box Name and its Value alone. While working with the value of the current text box, which triggered the Event, there are situations where we need the value from other text box controls on the Form.

For example, if we update the employee’s date of birth on one text box, we need to find the employee’s retirement date at the age of 56 years and update it in another Text Box.

Option 1: At the after-update event procedure of the date-of-birth text box, calculate the retirement date, and update it directly into the retirement date text box.

Option 2: After updating the date-of-birth Text Box, the cursor jumps to the Retirement Date control.  About the OnGotFocus Event on this text box, read the date-of-birth value from the Birth-Date text box, calculate the retirement date, and insert it into the current text box.

In either case, you have to work with two text boxes to read or write the value in them.  To read/write the value in the other text box, we can reach it only through the Form Object. To do that, we need an 'Access.Form' Object as a Property in the Class Module, besides the Access.TextBox control Object.

The new Class Module (ClsSubForm) Code is given below:

Option Compare Database
Option Explicit

Public WithEvents txt1 As Access.TextBox
Private frm2 As Access.Form

Public Property Get sFrm() As Access.Form
  Set sFrm = frm2
End Property

Public Property Set sFrm(ByRef frmValue As Access.Form)
  Set frm2 = frmValue
End Property

Private Sub txt1_AfterUpdate()
Dim ctl As Control, ctlName As String
Dim bd As Date

ctlName = txt1.Name
Select Case ctlName
    Case "EmpName"
      If Len(Nz(txt1.Value, "")) > 10 Then
         txt1.Value = Left(txt1.Value, 10)
         MsgBox "Maximum 10 characters only allowed.", vbInformation, ctlName & "_AfterUpdate()"
      End If
    Case "BirthDate"
      bd = CDate(Nz(txt1.Value, 0))
      If bd > 0 And bd < Date Then
         MsgBox "BirthDate Valid:" & txt1.Value, vbInformation, ctlName & "_AfterUpdate()"
      End If
    Case "Email"
       MsgBox "Email Address: " & txt1.Value, vbInformation, ctlName & "_AfterUpdate()"
    Case "Mobile"
       MsgBox "Mobile No. " & txt1.Value, vbInformation, ctlName & "_AfterUpdate()"
End Select

End Sub

Private Sub txt1_GotFocus()
'Got Focus Event is set for only
'Retirement Date field
'Testing for the field name is not required
Dim vDate As Variant, rdate As Date
Dim tmpRef As Access.Form

   Set tmpRef = frm2.frmSubForm1.Form
   'vDate = frm2.frmSubForm1.Form!BirthDate
   'vDate = frm2.frmSubForm1.Form.Controls("BirthDate").Value
   vDate = tmpRef!BirthDate
   
If vDate > 0 Then
    vDate = CDate(vDate)
    'calculate retirement Age/Date
    rdate = vDate + Int(56 * 365.25)
   txt1.Value = rdate
End If
MsgBox "Retire Date: " & txt1.Value, vbInformation, txt1.Name & txt1.Name & "_GotFocus()"
End Sub

Private Sub txt1_LostFocus()
Dim txtname As String
txtname = txt1.Name
Select Case txtname
    Case "RetireDate"
         'frm2.Controls("frmSubForm2").SetFocus
          frm2.frmSubForm2.SetFocus
    
    Case "Email"
          MsgBox "Email Address: " & txt1.Value, vbInformation, txtname & "_LostFocus()"
    Case "Mobile"
          MsgBox "Mobile Number: " & txt1.Value, vbInformation, txtname & "_LostFocus()"
          'frm2.Controls("EmpName").SetFocus
            frm2.EmpName.SetFocus
    Case Else
        If CDate(Nz(txt1.Value, 0)) = 0 Then
            MsgBox "Birth Date is Empty!", vbInformation, txtname & "_LostFocus()"
        End If
End Select
End Sub

Class Module Properties and Sub-Routines.

The TextBox property is declared as a WithEvents object named txt1, with public scope. Although this breaks the usual rule of encapsulation by exposing the property publicly, it is acceptable here for learning purposes.

Later, once you are comfortable with the concept, you can declare this property as Private and introduce Get and Set property procedures to access the txt1 object indirectly through these procedures, ensuring proper encapsulation.

The Form object frm2 is assigned to the Main Form (Form_frmMain)—which contains the sub-forms—so that we can read values from, or write values to, text box controls on the sub-forms.

The txt1_AfterUpdate() Event procedure in the ClsSubForm class module doesn’t include anything new, as we have already seen its functionality earlier. The BirthDate, Email, and Mobile text box controls are located on both sub-forms within the main form, and the AfterUpdate Events triggered on these sub-form controls are handled within this same subroutine.

The txt1_GotFocus() event procedure, used for the RetireDate text box, is also handled here. To calculate and insert the employee’s retirement date, we must first read the Date of Birth from the BirthDate text box on the first sub-form. To do this, we use the frm2 main form object property to directly reference that text box control.

Referencing Sub-Form Text Box from Class Module.

Different versions of the BirthDate Text Box references are given below:

vdate = frm2.frmSubForm1.Form!BirthDate
vDate = frm2.frmSubForm1.Form.Controls("BirthDate").Value

Or take the long route:

Dim tmpRef As Access.Form
Set tmpRef = frm2.frmSubForm1.Form
vDate = tmpRef!BirthDate

Now, moving on to the txt1_LostFocus() event procedure.
The key functionality implemented here is the automatic transfer of focus (insertion point) between text boxes across different forms.

  1. Moving the insertion point from the last Text Box on frmSubForm1 to the first Text Box on  frmSubForm2.
    frm2.frmSubForm2.SetFocus
    OR
    frm2.Controls("frmSubForm2").SetFocus
    
  2. Moving the insertion point from the last Text Box on Sub-form2 to the EmpName Text Box on the Main Form.
    frm2.EmpName.SetFocus
    OR
    frm2.Controls("EmpName").SetFocus
    

Common Pitfalls in Setting Focus on Sub-Form Control.

The pitfall that we often encounter in attempting to set the focus on a TextBox inside the sub-form directly is something like the following example:

frm2.frmSubForm2.Form.Email.SetFocus

The above statement is logically correct and will not produce any error messages when executed, but it will not work as intended.

The important point to understand here is that a sub-form resides within a sub-form container control (which typically has the same name as the sub-form itself).

To set focus on a control within a sub-form, you must first set focus on its container control.

Once the sub-form container receives focus, the control within the sub-form that has Tab Index = 0 will automatically become active and receive focus.

In the first example, we set the focus using the reference of the sub-form container control on the Main Form:

frm2.frmSubForm2.SetFocus

Here, frmSubForm2 is the name of the sub-form container control.

If you append '.Form' to this reference (like frm2.frmSubForm2.Form.SetFocus), it becomes a reference to the sub-form’s Form object. The system will simply ignore the SetFocus call, because SetFocus applies only to controls, not to Form objects directly.

As an alternative, you can explicitly refer to it as a control on the Main Form using:

frm2.Controls("frmSubForm2").SetFocus

Setting focus on a control on the Main Form from within the sub-form is more straightforward, for example:

frm2.EmpName.SetFocus

or

frm2.Controls("EmpName").SetFocus

Building the Derived Class Module: ClsSubFormHeader.

With sufficient groundwork laid for modifying the ClsSubForm Class Module, we can now proceed to build the Header Class (Derived Class ClsSubFormHeader). This class will handle adding the TextBox controls from both the main form and its sub-forms into a Collection object.

We will also include the necessary built-in event procedures to confirm that these events are being triggered and handled by the class. These event-triggered messages will serve as visual indicators that the class module is indeed capturing and executing the assigned tasks—especially since all the action now takes place behind the scenes, rather than in the form’s own class module.

Once you’ve verified that everything is working as intended, you can remove these temporary message displays that show values from the TextBox controls.

Separate Class Object Instance for each Form Control

Keep in mind that a separate instance of the ClsSubForm class object is created for each TextBox control across all the forms, and each instance is stored as an individual item in the Collection object.

Before adding a TextBox to the collection, its required built-in event handlers are enabled within its corresponding class object instance.

Although the form’s own code module remains empty, the class object instances stored in the Collection are actively monitoring for their assigned events.

When a particular event occurs on a specific TextBox, the corresponding ClsSubForm class object instance handles it—executing its relevant event procedure (for example, txt1_AfterUpdate()) and then either displaying the result as a message in the application window or updating the value of another TextBox on the form.

The ClsSubFormHeader Class Object Code is given below:

Option Compare Database
Option Explicit

Private frm As Access.Form
Private T As ClsSubForm
Private C As New Collection

Public Property Get mFrm() As Access.Form
  Set mFrm = frm
End Property

Public Property Set mFrm(frmVal As Access.Form)
  Set frm = frmVal
  Call Class_Init
End Property

Private Sub Class_Init()
Dim cnt As Integer
Dim ctl As Control

'Scan for TextBoxes on the Main Form
For Each ctl In frm.Controls
   If TypeName(ctl) = "TextBox" Then
   
       Set T = New ClsSubForm ‘instantiate Class
       Set T.txt1 = ctl
       
       Select Case ctl.Name
           Case "EmpName"
               T.txt1.AfterUpdate = "[Event Procedure]"
       End Select
       C.Add T ‘add to collection object
   End If
Next

'Scan for TextBoxes on the Sub-Form
For Each ctl In frm.frmSubForm1.Form.Controls
   If TypeName(ctl) = "TextBox" Then
   
      Set T = New ClsSubForm ‘instantiate Class
      Set T.txt1 = ctl
      Set T.sFrm = frm
      
       Select Case ctl.Name
           Case "BirthDate"
                  T.txt1.AfterUpdate = "[Event Procedure]"
                  T.txt1.OnLostFocus = "[Event Procedure]"
           Case "RetireDate"
                  T.txt1.OnGotFocus = "[Event Procedure]"
                  T.txt1.OnLostFocus = "[Event Procedure]"
       End Select
       C.Add T 'add to collection Object
   End If
Next

'Scan for TextBoxes on the Sub-Form
For Each ctl In frm.frmSubForm2.Form.Controls
   If TypeName(ctl) = "TextBox" Then
   
      Set T = New ClsSubForm
      Set T.txt1 = ctl
      Set T.sFrm = frm
      
       Select Case ctl.Name
           Case "EMail"
               T.txt1.AfterUpdate = "[Event Procedure]"
           Case "Mobile"
               T.txt1.OnLostFocus = "[Event Procedure]"
       End Select
       C.Add T 'add to collection Object
   End If
Next

End Sub

The change is in the For Each . . . Next statement, where we set the reference to the Sub-Form Controls to pick the Text Box Control from there and add it to the Collection Object, after enabling the required built-in Events.

On the first sub-form, the For ... Next Loop uses the following reference:

For Each ctl In frm.frmSubForm1.Form.Controls
.
.
.
Next

The ctl control carries the reference (address) of the sub-form Text Box while it is added to the Class Module ClsSubForm Txt1 Property and goes to the Collection Object as its Item.

In the same way, the second Sub-form Text Box references are used in the For ... Next Loop.

For Each ctl In frm.frmSubForm2.Form.Controls
.
.
.
Next

Both subforms have their ‘HasModule’ property set to ‘Yes’. However, their form modules do not contain any VBA code.

The Main Form (frmMain) Module Code is given below:

Option Compare Database
Option Explicit

Private T As ClsSubFormHeader

Private Sub Form_Load()
  Set T = New ClsSubFormHeader
  Set T.mFrm = Me
End Sub

The ClsSubFormHeader derived class is declared as the object T. In the Form_Load() event, an instance of the ClsSubFormHeader class is created, and the current form object is passed to its T.mFrm property procedure.

If you look at the ClsSubFormHeader class module code, you’ll see that the form object is passed to the frm2 property of ClsSubForm through the Property Set procedure sFrm, using the statement Set T.sFrm = frm, for each instance of the ClsSubForm class created for every text box added as an item in the Collection object.

Summary.

The Subform control references are added to the Class Module instances just like any other control on the main form. This ensures that when the built-in events are triggered, they remain synchronized with the correct Class Object instance and execute the appropriate event procedures.

You can download a demo database containing all the forms and VBA code to explore and study the implementation in detail.

Feel free to share your observations, suggestions, and comments in the Comments section of this page.

Downloads.



Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types

Share:

Access Form Control Arrays and Event-3

Introduction.

This article builds on last week’s topic, focusing on how to capture TextBox AfterUpdate and LostFocus events and validate their Values through a class module array.

In the previous session, we stopped short of discussing how to move all the VBA code from the Form_Load() event procedure into a separate class module, leaving the form module almost free of event procedures. The VBA code in this new structure will define the TextBox control class module array and handle the required built-in events within their respective array elements. This approach will leave only three or four lines of code in the form module, while shifting all the logic into a derived class module object.

Earlier, we had created derived class objects by using a class module as a base class and extending its functionality. We will apply the same concept here as well.

We are using TextBox controls first—rather than other controls on the form—for these array-based examples because they are the most commonly used controls. A TextBox supports several events, including BeforeUpdate, AfterUpdate, LostFocus, Enter, Exit, KeyDown, KeyUp, and OnKey. Depending on the requirements, we can choose to invoke one or more of these events from within the derived class object.

We can define a set of standard event procedures in the TextBox class module to handle commonly used events such as BeforeUpdate, AfterUpdate, Enter, or Exit. However, only the required event handlers need to be activated for each TextBox control. This can be done during the array element initialization by assigning:

obj.txt.EventName = "[Event Procedure]"

This approach enables the selective activation of event procedures for individual TextBox instances.

Since each Form may require different validation rules or processing logic, the code inside these class event procedures often needs customization. An effective way to manage this is to create a TextBox Class Module Template, incorporating the most frequently used event procedures. For a new form, simply copy this template and modify it to suit the specific requirements of the TextBox controls on that form.

Other control types on a form—such as Command Buttons, Combo Boxes, and List Boxes—generally rely on fewer events, most commonly Click or DblClick. We will address managing these other control types in arrays later.

Eventually, we will also explore whether there are more effective approaches than arrays for managing multiple instances of different types of controls on the same form.

Moving Form's Class Module Code to Derived Class Module

Returning to today’s topic—moving the Form Module code into a separate Class Module—we will create a new Derived Class Module Object based on the existing ClsTxtArray1_2 Class Module as the Base Class. The code currently in the Form_Load() event procedure of the Form Module will be relocated into this new Derived Class.

If you haven’t already downloaded last week’s demo database, please do so using the link provided before proceeding. We will make copies of the relevant Modules and Forms to modify the code, ensuring that both the original and the updated versions of the code and forms are available within the same database. After making these changes, you can immediately run the forms to observe how the new implementation works.


After downloading the database, open it in Microsoft Access. You can then open the Form Module and review its code.

Next, copy the ClsTxtArray1_2 Class Module into a new Class Module named ClsTxtArray1_3, without making any changes to its code. Similarly, make a copy of the existing form and rename it TxtArray1_3Header. Any modifications will be done on these new copies, ensuring that the original Form and Class Module remain intact and unaltered.

We will be using last week’s sample form (shown in the image below) along with its Form Module VBA code, which is also reproduced below for your reference.


Option Compare Database
Option Explicit

Private Ta() As New ClsTxtArray1_2

Private Sub Form_Load()
Dim cnt As Integer
Dim ctl As Control

For Each ctl In Me.Controls
  If TypeName(ctl) = "TextBox" Then
     cnt = cnt + 1
     ReDim Preserve Ta(1 To cnt)
     Set Ta(cnt).Txt = ctl
     
     If ctl.Name = "Text8" Then
       Ta(cnt).Txt.OnLostFocus = "[Event Procedure]"
     Else
       Ta(cnt).Txt.AfterUpdate = "[Event Procedure]"
     End If
     
  End If
Next
End Sub

Make a Copy of the above Form and name it as frmTxtArray1_3Header.

Create a new Class Module with the name ClsTxtArray1_3Copy the VBA Code from the ClsTxtArray1_2 Class Module and paste it into the new Module.

Last week’s Class Module ClsTxtArray1_2  Code is reproduced below for reference.

Option Compare Database
Option Explicit

Private WithEvents Txt As Access.TextBox

Public Property Get mTxt() As Access.TextBox
  Set mTxt = Txt
End Property

Public Property Set mTxt(ByRef txtNewValue As Access.TextBox)
  Set Txt = txtNewValue
End Property

Private Sub Txt_AfterUpdate()
Dim txtName As String, varVal As Variant
Dim msg As String

txtName = Txt.Name
msg = ""
Select Case txtName
    Case "Text0"
        'Valid value range 1 to 5 only
        varVal = Nz(Txt.Value, 0)
        If varVal < 1 Or varVal > 5 Then
           msg = "Valid Value Range 1-5 only: " & varVal
        End If
    Case "Text8"
        'validates in LostFocus Event
    Case "Text10"
        'valid value 10 characters or less
        'Removes extra characters, if entered
        varVal = Nz(Txt.Value, "")
        If Len(varVal) > 10 Then
           msg = "Max 10 Characters Only. " & varVal
           Txt.Value = Left(varVal, 10)
        End If
    Case "Text12"
        'Date must be <= today
        'Future date will be replaced with Today's date
        varVal = DateValue(Txt.Value)
        If varVal > Date Then
          msg = "Future Date Invalid. " & varVal & vbCr & "Corrected to Today's Date."
          Txt.Value = Date
        End If
    Case "Text14"
        'A 10 digit number only valid
        varVal = Trim(Str(Nz(Txt.Value, 0)))
        If Len(varVal) <> 10 Then
          msg = "Invalid Mobile Number: " & varVal
        End If
End Select

If Len(msg) > 0 Then
    MsgBox msg, vbInformation, Txt.Name
End If

End Sub

Private Sub Txt_LostFocus()
Dim tbx As Variant, msg As String

tbx = Nz(Txt.Value, "")

msg = ""
If Len(tbx) = 0 Then
  msg = Txt.Name & " cannot be left Empty."
  Txt.Value = "XXXXXXXXXX"
End If

If Len(msg) > 0 Then
   MsgBox msg, vbInformation, Txt.Name
End If

End Sub

The Derived Class: ClsTxtArray1_3Header

The ClsTxtArray1_3 Class Module will be used as the Base Class for our new Derived Class Module. We will name it ClsTxtArray1_3Header, with extended functionality.

Create a new Class Module with the name ClsTxtArray1_3Header. The Derived Class Module, with its Properties and Property Procedures, is given below:

Option Compare Database
Option Explicit

Private Ta() As New ClsTxtArray1_3
Private frm As Access.Form

Public Property Get mFrm() As Access.Form
  Set mFrm = frm
End Property

Public Property Set mFrm(vFrm As Access.Form)
  Set frm = vFrm
  Call Class_Init
End Property

Private Sub Class_Init()
 'Form Module Code goes here
End Sub

Copy and paste the above code into the new Header Class Module you have created.

Check the first two Property declarations.  First Property ClsTxtArray1_3 Class Object is instantiated as an undefined Array: Ta() – Ta stands for TextBox-Array.

The next property, frm, is introduced to give this Class Module access to the Form from which we plan to transfer the existing VBA code. All actions that were previously handled in the Form Module will now be managed here.

We will create Get and Set Property procedures to handle references to the Form. It will be a Set property (not a Let property) because we are passing a Form object, not a simple value, to it.

Immediately after the Form’s reference is received in the Set Property Procedure, we call the Class_Init() (this is not the same as Class_Initialize(), which runs automatically when a Class Object is instantiated) sub-routine to run the same code moved here from the Form’s Module.

Now, we will transfer the following Code from the Form_Load() Event Procedure into the Class_Init() sub-routine and make changes in the Form Module.

Copy and paste the following lines of code from the Form Module into the Class_init() sub-routine, replacing the Comment line:

Dim cnt As Integer
Dim ctl As Control

For Each ctl In frm.Controls
  If TypeName(ctl) = "TextBox" Then
     cnt = cnt + 1
     ReDim Preserve Ta(1 To cnt)
     Set Ta(cnt).Txt = ctl
     
     Select Case ctl.Name
        Case "Text8"
            'Only LostFocus Event
            Ta(cnt).Txt.OnLostFocus = "[Event Procedure]"
     Case Else
            'All other text Boxes wiil trigger AfterUpdate Event
            'i.e. entering/editing value in textbox
            Ta(cnt).Txt.AfterUpdate = "[Event Procedure]"
     End Select
     
  End If
Next

Form's Class Module Code

Open the Form frmTxtArray1_3Header in the design view. Display the Code Module. Copy and paste the following Code into the Form's Module, overwriting the existing Code:

Option Compare Database
Option Explicit

Private T As New ClsTxtArray1_3Header

Private Sub Form_Load()
  Set T.mFrm = Me
End Sub

We have instantiated the Derived Class ClsTxtArray1_3Header in Object Variable T. With the statement Set T.mFrm = Me, the active form's reference is passed to the T.mFrm() Set Property Procedure.

Immediately after this action, on the form_Load() Event procedure, the Class_Init() sub-routine runs in the ClsTxtArray1_3Header Class, and the txtArray1_3 Class Object array elements are created by invoking Events for each Text Box on the Form.  Hope you are clear with the Code above.

If you are ready to modify the Form Module, compile the database to ensure that everything is in order.

Save and close the Form, open it in Normal View, and try out each TextBox, and ensure that its Event sub-routines are performing as expected.

Replacing Class Object Array with Collection Object Items

The TextBox Class Object Array method works well for handling multiple TextBoxes. However, creating an array requires a counter variable, resizing the array for each new element while preserving the existing elements, and incrementing the counter for the next TextBox on the form, and so on.

When a form contains multiple controls of other types—such as Command Buttons, ComboBoxes, or ListBoxes—we would need to create separate arrays for each control type, each with its own counter and resizing logic in the class module. We will explore this approach in a future example.

A more efficient way to handle such complex scenarios is to use a Collection object instead of arrays. We will demonstrate this approach here, with TextBoxes, so you can get a practical feel for managing multiple controls using collections.

  1. Create a new Derived Class Module with the name ClsTxtArray1_3Coll.
  2. Copy and Paste the following Code into the Class Module:
Option Compare Database
Option Explicit

Private C As New Collection
Private Ta As ClsTxtArray1_3
Private frm As Access.Form

Public Property Get mFrm() As Access.Form
  Set mFrm = frm
End Property

Public Property Set mFrm(vFrm As Access.Form)
  Set frm = vFrm
  Call Class_Init
End Property

Private Sub Class_Init()
'-----------------------------
'Usage of Collection Object, replacing Arrays
'-----------------------------
Dim ctl As Control

For Each ctl In frm.Controls
  If TypeName(ctl) = "TextBox" Then
     
     Set Ta = New ClsTxtArray1_3  'instantiate TextBox Class
     Set Ta.Txt = ctl 'pass control to Public Class Property
     
     Select Case ctl.Name
        Case "Text8"
            'Only LostFocus Event
            Ta.Txt.OnLostFocus = "[Event Procedure]"
     Case Else
            'All other text Boxes wiil trigger AfterUpdate Event
            'i.e. entering/editing value in textbox
            Ta.Txt.AfterUpdate = "[Event Procedure]"
     End Select
     C.Add Ta 'add to Collection Object
  End If
Next

End Sub

A Collection Object Property is declared and instantiated at the beginning. 

The TextBox Class Module is defined, not instantiated, in the Object Variable Ta.

The TextBox Class Ta Object is instantiated within the Control Type Test condition.  A new Ta Object instance is created for each TextBox on the Form.

After enabling the Events, the Ta Class Object is added to the Collection Object as its Item.

This method is repeated by adding a new instance of the TextBox class Object for each TextBox on the Form, with its required Events enabled, as a new Item to the Collection Object.  The Code is cleaner than the Array method.

Make a copy of the Form frmTxtArray1_3Header with the name frmTxtArray1_3Coll

  1. Open it in Design View and display the Form's Code Module.
  2. Copy and paste the Following Code into the Form Module, replacing the existing Code.
Option Compare Database
Option Explicit

Private Ta As New ClsTxtArray1_3Coll

Private Sub Form_Load()
  Set Ta.mFrm = Me
End Sub

The only change here is the name of the derived object, which has been updated to ClstxtArray1_3Coll. After making this change, recompile the database.

Save the Form, Open it in Normal View. Test the TextBoxes as before.

It should work as before.

Downloads

You can download the database, which includes all the modules and forms with the suggested changes applied.



Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types

Share:

Access Form Control Arrays and Event-2

Introduction.

Last week, we learned how to create a Class Object Array, where each Class Object defines a single Access. The TextBox control has its properties to manage multiple TextBox controls on an MS Access Form. The built-in AfterUpdate and LostFocus events raised from the TextBoxes on the Form are captured by their corresponding elements in the Class Object Array. Each element then executes its own AfterUpdate() or LostFocus() subroutine, instead of running the code within the Form’s Class Module as we normally would.

For example, the AfterUpdate event of the first TextBox is captured by the first element of the Class Object Array, and its afterUpdate() subroutine is executed there. In the same way, events from other TextBoxes are also handled by their respective Class Object Array elements.

If you are new to this topic, please refer to the earlier pages using the links below to understand the step-by-step transition of code from one stage to the next.

  1. withevents Button Combo List TextBox Tab
  2. Access Form Control Arrays and Event Capturing

The AfterUpdate and LostFocus event handler code we wrote earlier in the Class Module was generic in nature and applied to all TextBox controls on the Form. These were test subroutines created solely to verify whether the events triggered from each TextBox on the Form were being correctly captured by their respective Class Module array elements.

Data Validation Checks

Now, it’s time to define specific data entry rules for each TextBox on the form and ensure that the user is notified whenever a rule is violated.

To make sure these rules are clear, I’ve placed descriptive labels above each Text Box on the form. These labels indicate how the values entered in the TextBoxes will be validated within the Class Module array through their AfterUpdate and LostFocus events.

An image of the form, showing the TextBoxes along with their corresponding validation rule labels, is provided below.


    Note: This setup is intended purely for demonstration purposes, so the validation rules are not strictly enforced. The second TextBox accepts any type of input—whether text, numbers, or other characters. The Mobile Number field, however, checks only the length of the entered value. Additionally, the Mobile Number TextBox has an Input Mask applied to restrict input to digits only.

  1. The first TextBox accepts only values between 1 and 5. Any value outside this range triggers an error message.

  2. The second TextBox is validated in the LostFocus event to ensure it is not left blank. If it is empty, an error message is displayed, and a sample string is inserted automatically.

  3. The third TextBox accepts text or numbers up to 10 characters long. Any extra characters beyond this limit are removed, and the field is updated accordingly. If left blank, no error is shown.

  4. The fourth TextBox is a date field. Any date later than today is considered invalid.

  5. The last TextBox accepts only a 10-digit number.

The Class Module Changes.

We will write the VBA code for the above simple validation checks in the class module.

The earlier version of the VBA code in the class module  ClstxtArray1 is given below for reference.

Option Compare Database
Option Explicit

Public WithEvents Txt As Access.TextBox

Private Sub Txt_AfterUpdate()
Dim txtName As String, sngval As Single
Dim msg As String

txtName = Txt.Name
sngval = Nz(Txt.Value, 0)

msg = txtName & " _AfterUpdate. :" & sngval
MsgBox msg, vbInformation, Txt.Name

End Sub

Private Sub Txt_LostFocus()
Dim tbx As Variant
tbx = Nz(Txt.Value, "")

If Len(tbx) = 0 Then
  MsgBox Txt.Name & " is Empty.", vbInformation, Txt.Name
End If
End Sub

The Event-based Sub-Routine

Check the Txt_AfterUpdate() event procedure. This single event handler in the class module receives the AfterUpdate event from all the text boxes on the form. The txt.Name property identifies which text box triggered the event, while the txt.Value property provides the value entered in that text box. Using these two properties, you can write specific validation logic for the contents of each text box.

The text box validation sample VBA code of the txt_AfterUpdate() Event Sub-routine is given below.

Private Sub Txt_AfterUpdate()
Dim txtName As String, varVal As Variant
Dim msg As String

txtName = Txt.Name
msg = ""
Select Case txtName
    Case "Text0"
        varVal = Nz(Txt.Value, 0)
        If varVal < 1 Or varVal > 5 Then
           msg = "Valid Value Range 1-5 only: " & varVal
        End If
    Case "Text8"
        ' validation in OnLostFocus Event
    Case "Text10"
        varVal = Nz(Txt.Value, "")
        If Len(varVal) > 10 Then
           msg = "Max 10 Characters Only. " & varVal
           Txt.Value = Left(varVal, 10)
        End If
    Case "Text12"
        varVal = DateValue(Txt.Value)
        If varVal > Date Then
          msg = "Future Date Invalid. " & varVal
        End If
    Case "Text14"
        varVal = Trim(Str(Nz(Txt.Value, 0)))
        If Len(varVal) <> 10 Then
          msg = "Invalid Mobile Number: " & varVal
        End If
End Select

If Len(msg) > 0 Then
    MsgBox msg, vbInformation, Txt.Name
End If

End Sub

The text box name (Txt.Name) received from the AfterUpdate Event is checked in the Select Case. . . End Select structure.  Depending on the text box name and the text box value (Txt.Value), the validation check is performed; if Invalid, an appropriate message is displayed.

On the Form_Load() Event Procedure, we have added the OnLostFocus() Event only for TextBox8 on the form.  When the insertion point leaves this text box, the LostFocus Event fires and captures it in the Private Sub txt_LostFocus()  subroutine of the class module.  If the TextBox is empty, the sample text string “XXXXXXXXXX” is inserted into TextBox8, followed by an error message.

The LostFocus sub-routine is given below:

Private Sub Txt_LostFocus()
Dim tbx As Variant, msg As String

tbx = Nz(Txt.Value, "")

msg = ""
If Len(tbx) = 0 Then
  msg = Txt.Name & " cannot leave it Empty."
  Txt.Value = "XXXXXXXXXX"
End If

If Len(msg) > 0 Then
   MsgBox msg, vbInformation, Txt.Name
End If

End Sub

Here, we are not testing for TextBox8, as we did in the AfterUpdate() event procedure, because we have not added the LostFocus Event for any other TextBox. 

Did you notice that the statement Txt.value = "XXXXXXXXXX" writes the string back to the same TextBox from which the event was captured? But what if we need to access another control on the form to read or write data there?

To achieve this, we must introduce a Form object property in the class module. We will implement this along with the upcoming code changes, as part of our future plan is to move all actions from the form module to the class module.

The full VBA Code of the Class Module: ClsTxtArray1_2 is given below:

Option Compare Database
Option Explicit

Public WithEvents Txt As Access.TextBox

Private Sub Txt_AfterUpdate()
Dim txtName As String, varVal As Variant
Dim msg As String

txtName = Txt.Name
msg = ""
Select Case txtName
    Case "Text0"
        varVal = Nz(Txt.Value, 0)
        If varVal < 1 Or varVal > 5 Then
           msg = "Valid Value Range 1-5 only: " & varVal
        End If
    Case "Text8"
        ' validation in OnLostFocus Event
    Case "Text10"
        varVal = Nz(Txt.Value, "")
        If Len(varVal) > 10 Then
           msg = "Max 10 Characters Only. " & varVal
           Txt.Value = Left(varVal, 10)
        End If
    Case "Text12"
        varVal = DateValue(Txt.Value)
        If varVal > Date Then
          msg = "Future Date Invalid. " & varVal
        End If
    Case "Text14"
        varVal = Trim(Str(Nz(Txt.Value, 0)))
        If Len(varVal) <> 10 Then
          msg = "Invalid Mobile Number: " & varVal
        End If
End Select

If Len(msg) > 0 Then
    MsgBox msg, vbInformation, Txt.Name
End If

End Sub

Private Sub Txt_LostFocus()
Dim tbx As Variant, msg As String

tbx = Nz(Txt.Value, "")

msg = ""
If Len(tbx) = 0 Then
  msg = Txt.Name & " cannot leave it Empty."
  Txt.Value = "XXXXXXXXXX"
End If

If Len(msg) > 0 Then
   MsgBox msg, vbInformation, Txt.Name
End If

End Sub

The Form Module VBA Code

The form module code remains unchanged from last week’s example, except that the class module name has been updated to ClstxtArray1_2.

I maintain the class module code from earlier articles as separate versioned copies, which is why the class module name has changed here.

Additionally, I made a minor change in the form module code for the TextBox8 control — it now raises only the LostFocus event. In the earlier version, the code triggered both the AfterUpdate and LostFocus events.

Option Compare Database
Option Explicit

Private Ta() As New ClsTxtArray1_2

Private Sub Form_Load()
Dim cnt As Integer
Dim ctl As Control

For Each ctl In Me.Controls
  If TypeName(ctl) = "TextBox" Then
     cnt = cnt + 1
     ReDim Preserve Ta(1 To cnt)
     Set Ta(cnt).Txt = ctl
     
     If ctl.Name = "Text8" Then
       Ta(cnt).Txt.OnLostFocus = "[Event Procedure]"
     Else
       Ta(cnt).Txt.AfterUpdate = "[Event Procedure]"
     End If    
  End If
Next
End Sub

Downloads

We have now moved all the event-handling code—normally written in the form’s class module—into a separate class module, keeping all the underlying actions completely hidden from the user.

However, if you look at the current Form_Load() event procedure, you’ll notice that there’s still quite a bit of code left in the form module.

In the coming weeks, we’ll explore some techniques to shift almost all this remaining code into the class module, leaving only three or four lines in the form module.

In the meantime, you can download the demo database from the links below, try it out, and study the code to understand how it works.



Links to WithEvents ...Tutorials.

  1. WithEvents Ms-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types

Share:

Access Form Control Arrays and Event Capturing

Introduction

Forms will have several objects of the same type.  For example, there will be several TextBoxes on the Form.  It is not possible to declare separate TextBox Properties in a Class Module to capture Events from each one of them.

The easiest technique is something like the following steps:

  1. Create a Class Module with a single Text Box object property.

  2. Define separate subroutines in the Class Module—such as AfterUpdate, LostFocus, and others—to handle the respective events triggered by Text Box controls on the Form.

  3. In the Form’s module (or in another dedicated Class Module), create an array of these Class Module objects, assigning one class object instance to each Text Box control on the Form.

  4. When a built-in event is raised from a particular Text Box on the Form, it will be captured by the corresponding class object in the array and executed through its event-handling subroutine.

Sample Demo Project

Let us start with a simple example.

  1. Create a new Class Module and change its Name Property Value to clsTxtArray1.

  2. Copy and paste the following VBA Code into the Class Module and save it:

    Option Compare Database
    Option Explicit
    
    Public WithEvents Txt As Access.TextBox
    
    Private Sub Txt_AfterUpdate()
    Dim txtName As String, sngval As Single
    Dim msg As String
    
    txtName = Txt.Name
    sngval = Nz(Txt.Value, 0)
    
    msg = txtName & " _AfterUpdate. :" & sngval
    MsgBox msg, vbInformation, Txt.Name
    
    End Sub
    

    Declared the Text Box Object Txt with Public scope to avoid the Get and Set Property Procedure for the time being, to keep the Code in the Class Module simple.

    The AfterUpdate() Event Procedure will execute when that Event of the Text Box fires on the Form.

  3. Create a new Form, insert a single Text Box on the Form, and save the Form with the name frmTxtArray1.

    An image of the sample form is given below.


  4. Open the Form in Design View and display the form's Code Module.
  5. Copy and Paste the following VBA Code into the Form Module:

    Option Compare Database
    Option Explicit
    
    Private ta() As New ClsTxtArray1
    
    Private Sub Form_Load()
    Dim cnt As Integer
    Dim ctl As Control
    
    For Each ctl In Me.Controls
      If TypeName(ctl) = "TextBox" Then
         cnt = cnt + 1
         ReDim Preserve ta(1 To cnt)
         Set ta(cnt).Txt = ctl
         ta(cnt).Txt.AfterUpdate = "[Event Procedure]"
      End If
    Next
    End Sub

    The VBA Code Line by Line

    In the global declaration area, ClsTxtArray1 Class is instantiated as an empty Array Object.

    The real action is in the Form_Load() Event Procedure.

    Two Variables, Count (cnt) and Control (ctl), are declared.

    The For Each ... Next Loop is set to scan the Form for all Control Types on the Form, and identify the required control type: TextBox.

    If the Control TypeName is TextBox, then the cnt Variable is incremented by one. The ta Array is re-dimensioned for 1 to cnt of items, preserving the earlier elements of data, if any.

    The statement 'Set ta(cnt).Txt = ctl' assigns the current Class Object Array element’s txt Property with the TextBox Control.

    The next line 'ta(cnt).Txt.AfterUpdate = "[Event Procedure]"' enables the TextBox’s AfterUpdate Event, so that it can be captured in the Private Sub txt_AfterUpdate() sub-routine of ta(cnt) instance of ClsTxtArray1 Array element.

  6. If you are through with the above lines of Code, then save and close the Form.

  7. Sample Data Entry

    Open the Form in normal view.

  8. Enter some numeric Value into the Text Box and press the Tab Key. 

    A message box displays the entered value.  A sample test run image is given below.

    Check the MsgBox image with event-related info in there.

    The TexBox Name is shown in the Title area, from where the AfterUpdate Event is captured.  The message line indicates that it is run from the AfterUpdate subroutine, and the value entered into the TextBox is displayed at the end of the line.

    Adding More Controls on Form

  9. Close the Form and Open it in Design View.

  10. Add a few more TextBoxes, anywhere you like on the Form.

  11. Save and Close the Form.

    A sample Image of the changed Form is given below.

  12. Open the Form in Normal View.

  13. Enter some numeric value in any newly added text box and press the Tab Key.

    When you do this, the MsgBox will pop up, displaying messages like the earlier one. It will contain the TextBox Name and the number entered into the Text Box.

  14. Try out other TextBoxes in this way. Add more TextBoxes, if you like, and try out whether the newly added TextBoxes also respond to the AfterUpdate event.

The After Update Event fires only when you enter a value or edit an existing value and leave the Text Box.

Adding the LostFocus Event

But what if a particular Text Box must not be left blank when the cursor moves away from it (on the LostFocus event) without entering any value?

In such a case, when the insertion point leaves that Text Box, the Form should trigger its LostFocus event. This event must then be captured in the Class Module, and a message should be displayed if the Text Box is empty.

To implement this behavior, we need to make corresponding changes in both the Form Module and the Class Module.

In the Form_Load() Event Procedure, checks whether TextBox8 (on my Form, the second TextBox at the left) is the current control, then adds ta(cnt).Txt.OnLostFocus = "[Event Procedure]"  to trigger the LostFocus() Event on TextBox8. 

Add the following lines to the Form_Load() Event Procedure, replacing Text8 with the Text Box name from your Form.

If ctl.Name = "Text8" Then
   ta(cnt).Txt.OnLostFocus = "[Event Procedure]"
End If

The current control's name is Text8 then ta(cnt).Txt.OnLostFocus Event is also set to invoke this Event.  So Text8 will fire both AfterUpdate and LostFocus Events.   

The changed Form_Load() Event Procedure Code is given below:

Private Sub Form_Load()
Dim cnt As Integer
Dim ctl As Control

For Each ctl In Me.Controls
  If TypeName(ctl) = "TextBox" Then
     cnt = cnt + 1
     ReDim Preserve ta(1 To cnt)
     Set ta(cnt).Txt = ctl
     ta(cnt).Txt.AfterUpdate = "[Event Procedure]"
     
     If ctl.Name = "Text8" Then
       ta(cnt).Txt.OnLostFocus = "[Event Procedure]"
     End If
     
  End If
Next
End Sub

A Subroutine for LostFocus Event is required in the ClstxtArray1 Class Module to capture the Event from the Text8 Text Box.

The Subroutine code to handle the LostFocus Event is given below:

Private Sub Txt_LostFocus()
Dim tbx As Variant
tbx = Nz(Txt.Value, "")

If Len(tbx) = 0 Then
  MsgBox Txt.Name & " is Empty.", vbInformation, Txt.Name
End If
End Sub

If some value is entered into the Text8 TextBox, then the MsgBox will not appear for the LostFocus Event.  If the TextBox8 is empty, then the LostFocus Event will fire.  The full Class Module Code is given below:

Option Compare Database
Option Explicit

Public WithEvents Txt As Access.TextBox

Private Sub Txt_AfterUpdate()
Dim txtName As String, sngval As Single
Dim msg As String

txtName = Txt.Name
sngval = Nz(Txt.Value, 0)

msg = txtName & " _AfterUpdate. :" & sngval
MsgBox msg, vbInformation, Txt.Name

End Sub

Private Sub Txt_LostFocus()
Dim tbx As Variant
tbx = Nz(Txt.Value, "")

If Len(tbx) = 0 Then
  MsgBox Txt.Name & " is Empty.", vbInformation, Txt.Name
End If
End Sub

Moving to the Next Stage

Each TextBox on a Form may accept different types of information, each with its own validation criteria. The data entered in every TextBox must be individually validated to ensure it meets the specified requirements, and appropriate action should be taken if any entry fails to comply.

The above code displays a generalized message for all TextBoxes from the subroutine.  That may not be sufficient for real applications.  We need to write a specific Code for each TextBox when the above Event fires from each TextBox.

We will continue this discussion next week for more details on this topic.

Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types

Share:

PRESENTATION: ACCESS USER GROUPS (EUROPE)

Translate

PageRank

Post Feed


Search

Popular Posts

Blog Archive

Powered by Blogger.

Labels

Forms Functions How Tos MS-Access Security Reports msaccess forms Animations msaccess animation Utilities msaccess controls Access and Internet MS-Access Scurity MS-Access and Internet Class Module External Links Queries Array msaccess reports Accesstips WithEvents msaccess tips Downloads Objects Menus and Toolbars Collection Object MsaccessLinks Process Controls Art Work Property msaccess How Tos Combo Boxes Dictionary Object ListView Control Query VBA msaccessQuery Calculation Event Graph Charts ImageList Control List Boxes TreeView Control Command Buttons Controls Data Emails and Alerts Form Custom Functions Custom Wizards DOS Commands Data Type Key Object Reference ms-access functions msaccess functions msaccess graphs msaccess reporttricks Command Button Report msaccess menus msaccessprocess security advanced Access Security Add Auto-Number Field Type Form Instances ImageList Item Macros Menus Nodes RaiseEvent Recordset Top Values Variables Wrapper Classes msaccess email progressmeter Access2007 Copy Excel Export Expression Fields Join Methods Microsoft Numbering System Records Security Split SubForm Table Tables Time Difference Utility WScript Workgroup database function msaccess wizards tutorial Access Emails and Alerts Access Fields Access How Tos Access Mail Merge Access2003 Accounting Year Action Animation Attachment Binary Numbers Bookmarks Budgeting ChDir Color Palette Common Controls Conditional Formatting Data Filtering Database Records Defining Pages Desktop Shortcuts Diagram Disk Dynamic Lookup Error Handler External Filter Formatting Groups Hexadecimal Numbers Import Labels List Logo Macro Mail Merge Main Form Memo Message Box Monitoring Octal Numbers Operating System Paste Primary-Key Product Rank Reading Remove Rich Text Sequence SetFocus Summary Tab-Page Union Query User Users Water-Mark Word automatically commands hyperlinks iSeries Date iif ms-access msaccess msaccess alerts pdf files reference restore switch text toolbar updating upload vba code