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 it’s own objects
such as forms, reports, and so on.
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 module’s 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.
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 it’s 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 it’s parent’s (the form’s) mobjChildren
collection. Each class instance is responsible for cleaning up it’s
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, it’s
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.
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.
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