A Form Class Demo

By John Colby

 

This demo is going to be just a tad more complex than the previous two, simply because the form is the center of the framework. First, we have to introduce a couple of concepts relating to WithEvents. Then we are going to build the control scanner; and we are going to integrate the text box class we used in the last article to demonstrate how the control scanner automatically finds and sets up controls for us.

Form Module:

Because we use a class module for the form’s event processing, the form’s module shrinks considerably. It is, in fact, possible to go totally lightweight, and have no form class module at all; but for learning purposes, having a module makes things easier to understand.

 

Public fclsFrm As New dclsFrm

 

Private Sub Form_Open(Cancel As Integer)

    fclsFrm.Init Nothing, Me

End Sub

 

Well, that’s it, folks. We declare a variable that will hold a pointer to this form’s class, and in the form’s Open event we initialize the class.

DclsFrm - initialization:

Since we are building on what we learned in the previous article, I am not going to cover the pieces of the class or how we lay out the class. This section will cover only what makes this class unique to dealing with the form’s events.

 

'*+ custom variables declarations

Private mobjParent As Object

Private mobjSelfRef As Object

Private WithEvents mfrm As Form

Private WithEvents mcmdClose As CommandButton

 

The only new thing here is that we have dimensioned mfrm and mcmdClose to use WithEvents.

 

Public Sub Init(ByRef robjParent As Object, _

                 ByRef rfrm As Form)

    Set mobjParent = robjParent 'Save a reference to the parent object

    Set mfrm = rfrm             'save a reference to the parent form

    Set mfrm.fclsFrm = Me       'Reach into the form and set a reference to this                                     'class

    Set mobjChildren = New Collection   'init the children collection

   

    'HOOK EVERY FORM EVENT.  IN PRACTICE, YOU MAY NOT WANT TO HOOK THINGS LIKE

    'THE MOUSE MOVE, KEY UP, AND SO FORTH, UNLESS YOU HAVE AN ACTUAL USE FOR THEM

    With mfrm

        VlObjEvpPrpSet mfrm, "OnClose"

        VlObjEvpPrpSet mfrm, "OnActivate"

        VlObjEvpPrpSet mfrm, "AfterDelConfirm"

        VlObjEvpPrpSet mfrm, "AfterInsert"

        VlObjEvpPrpSet mfrm, "AfterUpdate"

        VlObjEvpPrpSet mfrm, "OnApplyFilter"

        VlObjEvpPrpSet mfrm, "BeforeDelConfirm"

        VlObjEvpPrpSet mfrm, "BeforeInsert"

        VlObjEvpPrpSet mfrm, "BeforeUpdate"

        VlObjEvpPrpSet mfrm, "OnClick"

        VlObjEvpPrpSet mfrm, "OnClose"

        VlObjEvpPrpSet mfrm, "OnCurrent"

        VlObjEvpPrpSet mfrm, "OnDblClick"

        VlObjEvpPrpSet mfrm, "OnDeactivate"

        VlObjEvpPrpSet mfrm, "OnDelete"

        VlObjEvpPrpSet mfrm, "OnError"

        VlObjEvpPrpSet mfrm, "OnFilter"

        VlObjEvpPrpSet mfrm, "OnGotFocus"

        VlObjEvpPrpSet mfrm, "OnKeyDown"

        VlObjEvpPrpSet mfrm, "OnKeyPress"

        VlObjEvpPrpSet mfrm, "OnKeyUp"

        VlObjEvpPrpSet mfrm, "OnLoad"

        VlObjEvpPrpSet mfrm, "OnLostFocus"

        VlObjEvpPrpSet mfrm, "OnMouseDown"

        VlObjEvpPrpSet mfrm, "OnMouseMove"

        VlObjEvpPrpSet mfrm, "OnMouseUp"

        VlObjEvpPrpSet mfrm, "OnOpen"

        VlObjEvpPrpSet mfrm, "OnResize"

        VlObjEvpPrpSet mfrm, "OnTimer"

        VlObjEvpPrpSet mfrm, "OnUnload"

    End With

   

    'THIS FUNCTION WILL SCAN FOR KNOWN CONTROLS AND INSTANTIATE CLASSES FOR THEM.

    FindControls

 

    'WE ARE GOING TO TIME HOW LONG THE FORM IS OPEN, JUST TO DEMO ANOTHER CLASS

    'USED.

    mobjChildren.Add New clsTimer, "FormTimer"

    mobjChildren("FormTimer").Init Me

    mobjChildren("FormTimer").StartTimer

 

assDebugPrint "Init " & mcstrModuleName & " " & mfrm.Name & ", ObjCounter = " _ & ObjCounter, DebugPrint

End Sub

 

There are four points of interest here:

 

·         Set mfrm.fclsFrm = Me       'Reach into the form and set a reference to this class

 

This line sets the pointer in the form’s class to point to this class instance. As I mentioned earlier, we can build the form totally without a class module (lightweight), but in practice we often have something specific to a particular form that needs its code in the form’s class module, so I usually just leave things like you see them here. By building a pointer to dclsFrm, any code running in the form’s class module can get at this class’s code if needed.

 

·         VlObjEvpPrpSet mfrm, "OnClose"

 

These lines hook the form’s event properties, allowing us to sink the events in this class.

 

·         FindControls

 

This function will scan the form looking for controls, trying to figure out if the framework understands how to handle the controls, and loading classes or directly hooking events for the controls so that they can be used without effort by the developer.

 

·         'WE ARE GOING TO TIME HOW LONG THE FORM IS OPEN, JUST TO DEMO 'ANOTHER CLASS USED.
mobjChildren.Add New clsTimer, "FormTimer"
mobjChildren("FormTimer").Init Me
mobjChildren("FormTimer").StartTimer

 

I just threw this in to demonstrate that a form can instantiate other classes at will, for whatever reason they may be needed. Here we are instantiating a timer to time how long the form is open. We will use the EndTimer method in the form class’ Term() to demonstrate that the timer class was functioning the whole time the form was open, and to demonstrate how easy these things are to use. Setup and teardown are handled with no extra effort on the part of the developer.

DclsFrm – Event Handlers:

Having hooked the event properties in the init() function, we build a stub for every event here in the class.

 

'*+ Form Event interface

Private Sub mFrm_Activate()

    'DoCmd.Maximize

    assDebugPrintEvent mfrm, "Frm_Activate", DebugPrint

End Sub

Private Sub mFrm_AfterDelConfirm(Status As Integer)

    assDebugPrintEvent mfrm, "Frm_AfterDelConfirm", DebugPrint

End Sub

Private Sub mFrm_Close()

    assDebugPrintEvent mfrm, "Frm_Close", DebugPrint

    Me.Term

End Sub

 

There are a few details to know about form event processing using WithEvents. First, the Open event won’t fire here in the class, even though I have included it here just for the sake of completeness. The Open event is used in the class module to initialize this class, and it is come and gone by the time this class finishes initializing.

 

Second, it is possible to have an event (form or control) processed both in the form’s class module and in the class that normally processes the event. If you build an event stub in the form’s class module, any code in that module will run before the code in the class with the WithEvents declaration. I am still working on a method of running code specific to the form that must be run after the class processing. Essentially, the class event will have to call a function in the form to finish processing the event, and I haven’t yet figured out a way to do that.

 

Notice that it is the close event in this class module that calls the Term() function for the form class. Calling Term() before starting the process of closing the form will cause a GPF, so don’t do that.

 

And the final thing to know about WithEvent processing is that, while Microsoft claims that it is possible to hook events in different classes for the same control or form object, trying to do so creates a severe risk of generating GPFs. This bug has apparently been fixed in XP, but it definitely exists in A97 and A2K. This feature would solve some problems, but since it isn’t available to many of us yet, why discuss it?

DclsFrm – FindControls:

The FindControls function is a key player in making the framework automatic. The idea is simply that we have packages of functionality, usually wrapped up in classes. The classes will be tied to some given control or type of control, and if found by this function, the package of functionality will automatically be initialized and be made available to the form/user.

 

Private Sub FindControls()

On Error GoTo Err_FindControls

Dim ctl As Control

    For Each ctl In mfrm.Controls

        With ctl

            Select Case .ControlType

            Case acSubform

            Case acLabel

                GoTo NextCtl

            Case acTextBox  'Find all text boxes and load class to

    'change backcolor

                mobjChildren.Add New dclsCtlTextBox, ctl.Name

                mobjChildren(ctl.Name).Init mobjSelfRef, ctl

            Case acSubform

            Case acTabCtl

            Case acCommandButton, acToggleButton

                If ctl.Name = "cmdClose" Then   'Find the close button

                    Set mcmdClose = ctl         'store a reference to

'it

                    VlObjEvpPrpSet mcmdClose, "OnClick" 'And hook its

  'click event

                Else

                End If

            Case acComboBox

                Select Case ctl.Name

                Case "dcboRecSel"

                    mobjChildren.Add New dclsCtlRecSel, ctl.Name

mobjChildren(ctl.Name).Init mobjSelfRef, mfrm, ctl

                Case Else

                End Select

            Case Else

            End Select

        End With

NextCtl:

    Next ctl

 

Exit_FindControls:

    Set ctl = Nothing

Exit Sub

 

Err_FindControls:

   Select Case Err

'   Case 0, 5     'insert Errors you wish to ignore here

'      Resume Next

   Case 92

      Resume Next 'CONTROL NOT FOUND ON THIS FORM

'   Case 438, 2455, 457

'      Resume NextCtl

'   Case 2465      'insert Errors you wish to ignore here

'      Resume NextCtl

   Case Else   'All other errors will trap

      Beep

      MsgBox Err.Description, , "Error in function Forms.FindControls"

   Resume Exit_FindControls

   End Select

   Resume 0 'FOR TROUBLESHOOTING

End Sub

 

FindControls is just a big loop that scans the form looking for recognizable controls. Recognition may occur because we see a control named something specific, or may simply be because we find a specific type of control. I have inserted three different things that the scanner will find and set up for us in this demo. I have built two different forms, identical except for the fact that one has a record selector and the other doesn’t.

 

I will explain the workings of the record selector class in the next article. I needed it here in order to demonstrate that if you use a system like this, it really is as simple as dropping a control on the form, named using a convention so that the scanner can find it.Its functionality is just hooked in, and it works.

 

Every control has a property that allows us to determine its type, and Access has defined constants that match the various controls. So, the first thing we do is look for specific types of controls. One small part of this code finds the text box type of controls and sets up a class for them.

 

            Case acTextBox  'Find all text boxes and load class to

    'change backcolor

                mobjChildren.Add New dclsCtlTextBox, ctl.Name

                mobjChildren(ctl.Name).Init mobjSelfRef, ctl

 

You will recognize this class as the one we built in the second article, which changes the background color of the text box as the focus moves in and out of the control. This is an example of where we simply set up a class for every control of that type. Notice that this code dimensions a new class, adds it to this class’s mobjChildren collection, then initializes the class by using the class variable stored in the mobjChildren collection. This method avoids having to dimension a variable for each class type inside this function.  Dimension it directly in the mobjChildren collection and use it from there.

 

            Case acCommandButton, acToggleButton

                If ctl.Name = "cmdClose" Then   'Find the close button

                    Set mcmdClose = ctl         'store a reference to

'it

                    VlObjEvpPrpSet mcmdClose, "OnClick" 'And hook its

  'click event

                Else

                End If

 

This code finds a specific control, again using a naming convention, and hooks the event for the command button that closes the form. Notice that this control doesn’t have its own class, and mobjChildren only holds class instances; so we can’t use the same trick for this control. I therefore dimension a variable in this class’s header, and if the control is found, I store a pointer using that variable.

 

            Case acComboBox

                Select Case ctl.Name

                Case "dcboRecSel"

                    mobjChildren.Add New dclsCtlRecSel, ctl.Name

                    mobjChildren(ctl.Name).Init mobjSelfRef, mfrm, ctl

                Case Else

                End Select

 

This case is also very common. We are looking at a specific kind of control – the combo box – and within that type of control we look for controls named in a certain manner or named something in particular.

The power of Collections:

I mentioned that classes are a very powerful storage medium that allow us to store any type of object. The collection looks like an array that can be indexed into using the name of the object stored. That is exactly what we are doing here. When we add the object to the collection, we do so using the name of the object:

 

    mobjChildren.Add New dclsCtlRecSel, ctl.Name

 

Notice that we dimension the class variable directly into the mobjChildren collection, and use it in place. We do not need a local variable to hold a pointer to the control.

 

The ctl.name part of the call is called the key when we store the object in the collection, and allows us to directly look up the object later if we know the name of the object stored:

 

Private Sub mFrm_Current()

    assDebugPrintEvent mfrm, "Frm_Current", DebugPrint

    On Error Resume Next

    mobjChildren("dcboRecSel").FrmSyncRecSel

End Sub

 

    mobjChildren("dcboRecSel").FrmSyncRecSel

 

This is where the naming convention comes in. I know that I use “dcboRecSel” as the name of my recordselector on every form in every project. I therefore look in mobjChildren to see if a class for it exists, and if so I call the method .FrmSyncRecSel.

 

Notice again that we dimension the class variable directly into the mobjChildren collection, and use it in place. And to further drive home this point, the form’s Current event has to call a function for this control, and again we do so right in place in the mobjChildren collection as follows:

 

    mobjChildren("dcboRecSel").FrmSyncRecSel

 

This calls a function FrmSyncRecSel that belongs to the class dcboRecSel stored in mobjChildren, and does so from OnCurrent of the form’s class.

Behaviors to notice:

Go to the Debug window and notice the sequence of events as the form loads. Type in ?ObjCounter and Enter. Notice the number of class objects instantiated.

 

Close the first form. Go back to the Debug window and notice the sequence of events as the form closes and the next form opens. The debug constant and the debugprint functions provide a nice troubleshooting tool to watch what happens as a form opens, as a control’s events fire, etc. Close this form. Go back to the Debug window and type in ?ObjCounter and Enter. Notice that the number of objects is back to zero. This tells us that all the objects cleaned up correctly.

Summary:

In this article we have seen how to build a form class to encapsulate the events for the form object using WithEvents. We reviewed the process of hooking the event property, which we learned in the previous article. We place the text "[Event Procedure]" in whatever event property we want to be able to respond to, and then build a matching event stub in this class to handle the synced event.

 

We have also started thinking about the framework that allows our forms to respond in a predictable way—automatically—with minimal effort on the part of the developer.  One thing we want to avoid is having every form respond a little differently to the user. By building systems that do things for us, such as the record selector class, and having the framework just know about and install the system if it finds the associated controls, we can make our forms consistent across the application, and do so with a minimum of effort on our (the developer’s) part.

 

We have seen how a collection can hold pointers to objects indexed using the object’s name. We can dimension a variable directly into the collection, avoiding the need to dimension a separate variable and later store that variable in the collection. We can later look up that object in the collection using the object’s name. And, finally, we can just use the object directly in the collection, referencing any property or method of the object directly from inside the collection instead of dragging a pointer back out and using that pointer. Collections are powerful objects which, if you have never used them before, deserve a closer look.

 

Finally we have watched the form and control events fire as the forms load and close. We have seen how easy it is to set up and use a timer in the form, and how the cleanup just happens for us with no additional effort on our part.


continue - Troubleshooting Form Class