A TextBox Class

The second in a series of articles on Class solutions.

By John Colby

 

I am going to keep this demo simple so that we can focus on how things work. Once you understand the concepts you can expand and make a text box do other neat stuff that you might need. This example will build a class that causes a text box to change color as it is entered and exited.

 

The class built here relies on a framework that I use consistently from one class to the next. This framework includes a number of support functions, included in a separate module called basClassGlobal, and these work with other variables and functions in the class module to provide support for the class in the form of error trapping and certain other functions. This procedure was explained in the Class Foundations article in Vol.1, No. 1 of Many-To-Many. Not all of these elements are reproduced below, although they are all found in the downloadable example. (see http://www.databaseadvisors.com/downloads/TextBoxClass.zip) The Code

 

 

Here I am going to show the module in the form that sets up and tears down the text box classes. In this example I am going to use the Children Collection I mentioned in the previously mentioned article. The Children Collection in the form will hold a pointer to every class that the form instantiates, so that when it’s time to clean up, we can just pass the collection to a cleanup routine instead of having to clean up the instances one by one.

 

For those that have never used collections, they can be visualized as a single dimension array that can hold anything, object or variable, and can be indexed into by the name of the object stored. Collections are very powerful structures that are worth learning about. They are used everywhere in Access, by Access itself, to hold its own objects such as forms, reports, and so on.

The Form Module

The form module code (abbreviated) follows:

 

'THE CHILDREN COLLECTION IS USED TO STORE REFERENCES TO ALL

'CHILD OBJECTS (CLASSES) THAT NEED TO BE INITIALIZED AND

'DESTROYED.  USUALLY THESE WILL BE FOR CONTROLS SUCH AS THE

'TAB CONTROL OR THE RECORD SELECTOR, BUT THEY MAY ALSO BE

'FOR BUSINESS RULES CLASSES, etc.

 

Private mobjChildren As New Collection

Dim clstxtTitle As New dclsCtlTextBox

Dim clstxtNo As New dclsCtlTextBox

Dim clstxtMinutes As New dclsCtlTextBox

Dim clstxtReview As New dclsCtlTextBox

Dim clstxtNote1 As New dclsCtlTextBox

Dim clstxtNote2 As New dclsCtlTextBox

 

The above code is the header section of the form and declares the mobjChildren collection which will hold a pointer to the class instances. It also dimensions a specific set of class pointers, one for each text box in the form. Remember that this is not the most sophisticated way—not the way I would actually do it in practice—but it allows you to see the process without getting bogged down.

 

'Get the pointer to this object's children

Public Property Get Children() As Collection

  Set Children = mobjChildren

End Property

 

This Property Get procedure is nothing more than a way to return a pointer to the Cchildren Ccollection. The classes that we open next will need to register themselves in this collection and will use this function to find the collection from inside the class instance.

 

Private Sub Form_Open(Cancel As Integer)

    clstxtTitle.Init Me, txtTitle

    clstxtNo.Init Me, txtNo

    clstxtMinutes.Init Me, txtMinutes

    clstxtReview.Init Me, txtReview

    clstxtNote1.Init Me, txtNote1

    clstxtNote2.Init Me, txtNote2

End Sub

 

This is the form’s OnOpen event procedure, and here is where we actually initialize the classes dimensioned in the modules header section. In the previous article, we learned about Class Init() functions. Notice that this time we pass into the Init function a pointer to this module (Me), as well as a pointer to the textbox that each class instance will process.

 

We pass a pointer to the form’s module because the class instance is responsible for storing a pointer to itself in the form’s mobjChildren collection for cleanup when the form closes. Without a pointer back to this module, the class instance couldn’t register itself.

 

Private Sub Form_Close()

    TerminateChildren Me.Children

End Sub

 

The TerminateChildren function is in a helper module for classes found in basClassGlobal and simply takes a collection of class pointers, calls the Tterm() method of each class instance, then sets the pointer to nothing. Since the Term() function of the class knows how to clean itself up, and the pointer to the class in the collection is the only pointer in existence, when this process finishes the class instance is completely destroyed–removed from memory.

 

So when the form closes it passes a pointer to its mobjChildren to this cleanup function. Since this collection holds pointers to every class instance that the form created, the cleanup function can destroy all these instances and make sure that the cleanup is complete and reliable.

 

There you have the form stuff. What makes it simple is that we have built upon an established framework where classes know what they are supposed to do to set up, and to tear down—a system that stores pointers to every class ever instantiated—and that includes a function to do the cleanup.

The dclsCtlTextBox Class

Understanding that 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 only cover what makes this class different from the timer class that we learned about in the first article.

 

'POINTER TO THE OBJECT THAT CREATED THIS CLASS INSTANCE

'(THE PARENT OF THE CLASS)

Private mobjParent As Object

'*- Class variables declarations

Private WithEvents mtxt As Access.TextBox

Private mlngBackColor As Long

'*+

 

This is the header section of the class. Notice first that this class has its own mobjChildren collection–the exact same thing that we saw in the header in the form. If this class instantiated classes to help it do its processing, those classes would register themselves in this mobjChildren collection in exactly the same way theat this class instance is about to register itself in its parent’s (the form’s) mobjChildren collection. Each class instance is responsible for cleaning up its children before terminating.

 

The next thing to notice is that we have declared a textbox using the WithEvents keyword. It is this keyword that causes Access to “hook” the event handler stubs created in this class to the actual control whose pointer is stored in this variable.

 

And finally we are declaring a private variable to hold the oldBackColor for the text box we are processing.

 

The Class_Initialize function is actually an event handler for the class as we learned in the first article. Notice again that we initialize the mobjChildren collection in preparation for using it later. In fact this class does not use any other classes, so this structure will not be used by this class instance, but if it did, we would be ready.

 

Private Sub Class_Initialize()

'   assDebugPrint "initialize " & mcstrModuleName, DebugPrint

    IncObjCounter

    Set mobjChildren = New Collection

End Sub

 

Public Sub Init(ByRef robjParent As Object, ByRef rtxt As Access.TextBox)

    Set mobjParent = robjParent

    'LOG MYSELF IN MY PARENT'S COLLECTION

    LogIntoParentCol Me

'    assDebugPrint "init() " & strInstanceName, DebugPrint

    Set mtxt = rtxt

    mlngBackColor = mtxt.BackColor

    mtxt.OnEnter = "[Event Procedure]"

    mtxt.OnExit = "[Event Procedure]"

End Sub

 

The above function is the class Iinit(), which was called from the form’s OnOpen event handler. Looking back the call in the form’s module looked like this:

 

    clstxtTitle.Init Me, txtTitle

 

What we see is that in the form’s OnOpen we passed in a reference to Me (the form’s module).

 

    Set mobjParent = robjParent

 

Takes that pointer to the form’s module and saves it in a private variable.

 

    LogIntoParentCol Me

 

This is the function that performs the magic of telling the parent of this class instance that we exist and where to find us when it’s time to clean up. I am not going to go into how it works, just know that, when all is said and done, a pointer to this class is saved into the form’s mobjChildren collection.

 

    Set mtxt = rtxt

 

Stores a pointer to the text box passed in from the form – txtTitle in the example above. Remember that because we dimensioned mtxt using the WithEvents keyword, we will be able to hook any of the control’s events that we want.

 

    mlngBackColor = mtxt.BackColor

 

Save the text box’s current back color so we can restore it at any time

 

    mtxt.OnEnter = "[Event Procedure]"

    mtxt.OnExit = "[Event Procedure]"

 

This is the secret that once understood seems so obvious but which I never considered before learning about WithEvents.

 

It seems that the event stub for any control, form or report event will actually be called only if the text [Event Procedure] is actually placed into the corresponding event property of the control. In fact if you want to start and stop event handling, you can just create and remove this text in the event property.

 

Under normal conditions I usually open the property box for the control–the text box in this case–and dbl-click the event we want to build an event handler for. Doing this places the [Event Procedure] in the event property. Then when we click on the ellipsis to the right of the property we are transported to the form’s module and the cursor is placed into a code stub that Access builds for us that looks like (for example):

 

Private Sub txtTitle_Enter()

 

End Sub

 

We then proceed to fill in the code we want to run and off we go.

 

Rather than writing into the form’s code stub in this manner, for this project we have built a class to handle the text box. so Therefore, we have to somehow hook the event. This process is called sinking the event. The code stub doesn’t exist in the form’s module and in fact the [Event Procedure] text doesn’t even exist in the control’s property. If we want to sink an event we have to place that [Event Procedure] into the control’s event property ourselves.

 

We saw the class Class_Terminate() function in the first article so there’s really nothing new there.

 

Private Sub Class_Terminate()

'    assDebugPrint "Terminate " & mcstrModuleName, DebugPrint

    DecObjCounter

    TerminateChildren Me.Children

    Set mobjChildren = Nothing

    Term

End Sub

 

We now know what the TerminateChildren function does for us, it cleans up any child classes that this class might use. In fact we don’t use any, but I leave the code in there so that you can get used to seeing it and understand what it does, and also so that if you do decide (in the future) that you need to instantiate some other class from inside this one, those child classes will get cleaned up correctly.

 

'CLEAN UP ALL OF THE CLASS POINTERS

Public Sub Term()

    On Error Resume Next

'    assDebugPrint "Term() " & strInstanceName, DebugPrint

    Set mobjParent = Nothing

    Set mtxt = Nothing

End Sub

 

We clean up the pointer to the form’s module (mobjParent) and the text box (mtxt).

 

And finally, we get down to the only useful thing that this class does for us. The mtxt_Enter() function declaration is exactly what you get if you use the method I described above for creating the event stub in the form’s module. The only difference is that I renamed it to add the m in front of txt_Enter because the dimensioned variable in our class header is called mtxt. If the object is called mtxt, its event stub will be called mtxt_XXXX where XXXX is the event name.

 

'*- Parent/Child links interface

Private Sub mtxt_Enter()

    assDebugPrint mtxt.Name & " Enter() " & strInstanceName, DebugPrint

    mtxt.BackColor = 16776960

End Sub

 

So as you can see, we just reset the BackColor property. We don’t have to save the old BackColor here because we did that in Iinit().

 

Private Sub mtxt_Exit(Cancel As Integer)

    assDebugPrint mtxt.Name & " Exit() " & strInstanceName, DebugPrint

    mtxt.BackColor = mlngBackColor

End Sub

 

And finally, in the Exit event stub we set the BackColor property back to the stored value.

Debug Assistance

Before we close, I want to point out some troubleshooting stuff that allows you to watch what is happening as the classes open, events fire, and the classes close again. First notice the

 

    assDebugPrint mtxt.Name & " Exit() " & strInstanceName, DebugPrint

 

embedded in every function. These lines are there only for troubleshooting and can be removed from production code. Notice the DebugPrint parameter we are passing in to the debugprint function. This is a constant declared at the top of every module. When set true, the string passed to the debugprint function will actually be printed to the debug window. When set false, the string will not be printed.

 

The second thing you should know is that whenever a class instantiates, it causes a counter to count up. When it closes, it decrements the same counter. This counter can be read at any time by going to the debug window and typing in

 

? ObjCntr()

 

Doing so when frmTxtClassTest is open should return a “6”, which is the number of class instances, one for each text box. When the form is closed, it should return a “0”, which is the number of classes still open–none because they were all correctly cleaned up as the form closes. If ObjCntr() returns anything other than a 0 after the form closes, something didn’t clean up properly and we would want to investigate why.

 

Checking this variable as we build more and more complex systems of classes is highly recommended. The sooner we catch our mistake, the easier it is to track down.

 

We also want to note that as the cursor moves from field to field, the background (frmTxtClassTest) changes to blue as the text box gets the focus, and back to white as the text box loses the focus.

Summary

As you can see from the demo form, this class does nothing but cause the background color to change as you move into and out of a control. Because we instantiated an instance of this class for each control (text box) in the form, every text box changes color. Had we not created an instance for one or more controls, that control would not change color.

 

I know that this seems like a lot of work for such a simple result, but in fact we have reviewed some important topics, which we can now use for more complex situations:

 

·         We have seen how to use the cleanup code for the classes we build. The parent object (the form in this case) has an mobjChildren collection which holds a pointer to every instance of every class created by the parent object. The class knows how to register itself in the parent’s mobjChildren collection so that when it’s time to clean up, the parent can “just do it” with a single call to a function that iterates the parent’s mobjChildren collection and destroys the child classes.

·         We have learned about WithEvents and how this keyword changes a simple object declaration to one that allows us to store the event stubs themselves in the class where the events are processed, and to sink the control events in this class.

·         We have learned how to completely set up the event properties for the control in code instead of having to set the event property in the control manually, build the event stub manually etc.

·         And finally we have seen how creating a class allows us to encapsulate a given behavior. We can dimension as many of these class variables as we want, pass a couple of parameters and every object behaves exactly as expected. Setup and teardown are smooth and reliable. Everything just works.

 

Again I would like to thank Shamil Salakhetdinov who designed the class setup and teardown code and the class helper module code.

John Colby©2001
May be distributed as long as the copyright remains.

John Colby Bio