A form wrapper to control them all

A form wrapper to control them all

In our previous post we created a control wrapper and performed a very simple task, changing the background color of the text box as it got and lost the focus.  In order to use clsCtlTxt however we had to dimension a variable for each text box we wanted to wrap.  There might be cases where we write a very specific class to control a very small number of text boxes and so doing things this way would be just fine.  However in general we want to have a way to load our control wrapper classes just by telling the form to do so.  Whether we have one text box or thirty, the form somehow figures it out and does it, such that every text box on the form is treated in a consistent way.  The key to doing this is to iterate the controls collection using a form wrapper class.
Create a new database called clsFrmWrapperDemo.  Import the form and clsCtlTxt from the previous database.

ClsFrm

Insert a new class and immediately save it as clsFrm.  Insert the following code in clsFrm:
Option Compare Database
Option Explicit
‘clsFrm
Private WithEvents mFrm As Form ‘Holds the form pointer
Private Const cnEvProc As String = “[Event Procedure]”
Private mcolClsCtls As Collection ‘A collection to hold control wrapper classes
Private Sub Class_Initialize()
‘Initialize the collection
    Set mcolClsCtls = New Collection
End Sub
Private Sub Class_Terminate()
‘Clean up behind ourself
    Set mcolClsCtls = Nothing
    Set mFrm = Nothing
End Sub
Function mInit(lfrm As Form)
    Set mFrm = lfrm ‘Save a pointer to the form
    mFrm.OnClose = cnEvProc ‘Allow riasing the OnClose event in the form
    mFindControls ‘Go look at all of the controls
End Function
‘The event sink for the form’s close event
Private Sub mFrm_Close()
‘Unhook the form pointer in this class
    Set mFrm.fcFrm = Nothing
End Sub
‘Iterate the form’s control collection looking at various control types
Private Function mFindControls()
Dim ctl As Control
    For Each ctl In mFrm.Controls
        Select Case ctl.ControlType
        ‘
        ‘If we find any text boxes, load a text box wrapper class
        Case acTextBox
            ‘Create a text box wrapper variable
            Dim cCtlTxt As clsCtlTxt
            ‘Instantiate it
            Set cCtlTxt = New clsCtlTxt
            ‘Pass in a pointer to the text box to the class
            cCtlTxt.mInit ctl
            ‘Store the class pointer to the collection
            mcolClsCtls.Add cCtlTxt, ctl.Name
        Case acComboBox
        Case acCheckBox
        Case acListBox
        Case Else
        End Select
    Next ctl
End Function
In the form module delete the previous code and paste the following code:
Option Compare Database
Option Explicit
Public fcFrm As clsFrm
Private Sub Form_Open(Cancel As Integer)
    Set fcFrm = New clsFrm
    fcFrm.mInit Me
End Sub
Save everything and open the form.  Notice that the form appears to work exactly the same as it did last time.  As the text boxes get the focus they change color.  Notice however that the form no longer has to have code specifically creating variables for object wrapper classes.  We have created a clsFrm object wrapper which wraps the form itself.  In the form’s module we create a public variable to hold an instance of clsFrm.  In the form’s open event we instantiate the class and then pass in a pointer to the form (me).  Suddenly the form “comes alive” as it opens.  Lots of stuff happens, none of it obvious from looking in the form’s module.
If you trace the code you will notice that in clsFrm, as the class instantiates, a collection mcolClsCtls is initialized.  As the form is passed in to mInit() the pointer is saved just like the control was in its class.  Notice we set up the form’s OnClose to fire and we call a method mFindControls which iterates the form’s controls collection. in mFindControls there is a case statement which knows how to determine the kind of control found.  So far all we have is a text box wrapper class, but every time a text box is found on the form, an instance of clsCtlTxt will be instantiated, initialized, a pointer to the control passed in to the text control wrapper instance and most importantly the pointer will be stored in a collection.
We need to discuss that for a minute.  A class instance exists only as long as there is a pointer to it somewhere.  If we created the instance and set it all up but failed to keep the pointer, it would immediately unload again.  By placing the variable into the collection we keep the pointer around and the class instance sticks around.
Notice also that we are now sinking the form’s close event right here in clsFrm.  When the form tries to close, the close event fires, the event is sunk here in the class and we unhook the instance of clsFrm in the form’s module.
‘The event sink for the form’s close event
Private Sub mFrm_Close()
‘Unhook the form pointer in this class
    Set mFrm.fcFrm = Nothing
End Sub
This line of code:
    Set mFrm.fcFrm = Nothing
reaches back into the code module for mFrm and sets fcFrm to nothing.  Remember that when the last pointer to a class instance is set to nothing the class instance unloads.  As it happens this is the only pointer to clsFrm (for this form) and so when it is set to nothing it starts to unload.  When a class unloads the garbage collector calls the class’ terminate event.  The following code runs.
Private Sub Class_Terminate()
‘Clean up behind ourself
    Set mcolClsCtls = Nothing
    Set mFrm = Nothing
End Sub
The collection pointer is set to nothing causing it to unload all of the the classes that it contains, at this point the clsCtlTxt instances for the text boxes.  So those class instances are handed to the garbage collector which then calls the Terminate event for each of those instances etc.
Once all of the text box wrapper classes are unloaded then we set mFrm to nothing and we are done cleaning up.  fcFrm in the form’s module is unhooked, all of the control wrapper classes are unhooked, everything is cleaned up, and the form closes.
One thing I need to mention is that Access 97 and Access 2000 won’t handle this correctly.  Both will cause Access to crash if you try to use the form’s close event in the form wrapper class in this manner.  You will need to not sink the close event in the form wrapper and instead unhook the fcFrm in the form’s close event in the form module itself.  In Access 2002 and forward this works correctly.
The thing to take away from this post is that we can build a form wrapper similar to any other object wrapper.  The form’s wrapper can sink events, it can access the form’s properties, iterate the form’s Controls collection, set up control wrappers for any controls found on the form and get the whole thing up and running with just a few lines of code in the form’s module.  Since the close event is sunk right in our clsFrm, we can sense that the form is closing and perform all of our own cleanup, reach back into the form’s module to unhook the clsFrm instance which triggers the class to start cleaning itself up, unloading all of the control wrapper classes, unhooking the form pointer etc.  Powerful stuff.
By the way this is what I refer to as a framework.  A framework is more than static code sitting in a library somewhere.  It is a set of classes that when instantiated, sets a chain of events running which can completely initialize an entire ecosystem of objects.  Using clsFrm I can use a few lines of code to cause any form that contains that code to load control wrapper classes for any or all of the controls on that form.  Those control wrappers can perform behaviors in a consistent way, such that every form looks and acts the same.  I do not have to insert tons of code into each form to make the controls on the form behave as I want them.
Suppose that you want every text box that is bound to a date to automatically format the displayed date in a consistent manner and also to use a consistent input mask.  Suppose that you inherit an existing mess where there are dates everywhere and the users have entered them however they wanted, and they are displayed however they were entered.  Using a text box wrapper you can automatically sense that the text box is bound to a date and apply a format string to the format property, and also to apply an input mask to the input mask property.  From that moment on every date in the application looks and acts the same.  I actually do this trick in my framework and I will show you how in a future post.

Leave a Reply

Your email address will not be published. Required fields are marked *