Class Foundations
The first in a series of articles on Class solutions.
By John Colby
In this first article on Class modules, we’ll examine a fully functional timer that you can use in your applications, but the objective is to present all the pieces required so that you can build and use more complicated classes. The full code can be found on the DatabaseAdvisor’s web site, in a zipped mdb file, with all code and a demo form using the timer class (see http://www.databaseadvisors.com/downloads/ClassFoundations.zip)
The following is the entire code needed to dimension, use, and destroy a timer class variable. The code is generic—you’ll need to supply names for your own objects.
Function TimeSomething()
Dim clsTimeFrmOpen as new clsTimer ‘DIMENSION THE CLASS VARIABLE
ClsTimeFrmOpen.Init Nothing ‘INITIALIZE THIS INSTANCE OF THE TIMER
ClsTimeFrmOpen.StartTimer ‘START THE TIMER
Docmd.OpenForm “MyForm” ‘DO SOMETHING THAT NEEDS TIMING
Debug.Print ClsTimeFrmOpen.StopTimer ‘PRINT THE TIME TO THE DEBUG WINDOW
Set clsTimeFrmOpen = nothing ‘DESTROY THE TIMER
End
function
Private Const DebugPrint As Boolean = True
Private Const mcstrModuleName As String = "clsTimer"
Private mobjParent As Object
Private mobjChildren As Collection
Public strInstanceName As String
Private Declare Function apiGetTime Lib "winmm.dll" Alias "TimeGetTime" () As Long
Dim lngStartTime As Long
Private Sub Class_Initialize()
assDebugPrint "initialize " & mcstrModuleName, DebugPrint
IncObjCounter
Set mobjChildren = New Collection
End Sub
Public Sub Init(ByRef robjParent As Object)
Set mobjParent = robjParent
LogIntoParentCol Me
assDebugPrint "init() " & strInstanceName, DebugPrint
End Sub
Private Sub Class_Terminate()
assDebugPrint "Terminate " & mcstrModuleName, DebugPrint
DecObjCounter
TerminateChildren Me.Children
Set mobjChildren = Nothing
Term
End Sub
Public Sub Term()
assDebugPrint "Term() " & strInstanceName, DebugPrint
Set mobjParent = Nothing
End Sub
Property Get strModuleName() As String
strModuleName = mcstrModuleName
End Property
Public Property Get Parent() As Object
Set Parent = mobjParent
End Property
Public Property Get Children() As Collection
Set Children = mobjChildren
End Property
Function EndTimer()
EndTimer = apiGetTime() - lngStartTime
End Function
Sub StartTimer()
lngStartTime = apiGetTime()
End Sub
Because I work with classes often, I have created my own framework that I can reuse for every class. Many of the statements above refer to functions that are provided in another module, called basClassGlobal; I import this module into every Access database where I use classes, and it provides these support functions. Most of these functions are mainly for debugging; the purpose of each is explained in the course of the discussion below.
We will discuss each line of the class module as it is used in the course of the example. We can begin, however, by mentioning that I define two constants in the class header; these are purely for debugging purposes, and are primarily used by the statements that refer to the support functions in basClassGlobal:
Private Const DebugPrint As Boolean = True
Private Const mcstrModuleName As String = "clsTimer"
The first of these turns debug printing on or off throughout the module; the second provides the debugging statements with the name of the module.
To create a new instance of the clsTimer class, dimension it with a line similar to
Dim clsTimeFrmOpen as New clsTimer ‘DIMENSION THE CLASS VARIABLE
We are all familiar with dimensioning objects; the New keyword in this code actually causes the class to instantiate, which means to load and initialize. When a class instantiates, its Class_Initialize() event fires. (The Class_Initialize() is similar to a form’s Open event.) The difference between Class_Initialize() and the methods that we will write later is that Class_Initialize is a true event, fired by Access just because the class instantiated. This event has no parameters and is used just to initialize any internal structures needed once the class is loaded and in use. Here is the Class_Initialize() code from our timer class module:
Private Sub Class_Initialize()
assDebugPrint "initialize " & mcstrModuleName, DebugPrint
IncObjCounter
Set mobjChildren = New Collection
End Sub
The three statements in this sub do the following:
· assDebugPrint prints stuff to the debug window; it is one of the functions provided in the extra module, basClassGlobal.
· IncObjCounter counts this instance of the timer being opened. We use this for troubleshooting mostly to see that when we finally clean up, every instance of every class has been accounted for; this function is also found in basClassGlobal.
· The statement Set mobjChildren = New Collection initializes a collection for the storage of child classes that this class may use if needed. One of the keys to successful class usage is ensuring that things get cleaned up after we are done. By using a collection to store all classes that this class instantiates in the course of its operation, we can then simply pass the whole collection to a cleanup routine when it’s time to tear down this class. Since every class has one of these, if the child class uses classes, it also uses a collection, and it will cleanup all of its child classes in exactly the same way.
ClsTimer doesn’t require any child classes, but the real intention of this example is to introduce the structures we will be using in more complex classes (which we’ll present later in subsequent articles). This kind of construction is not necessary to the use of classes in programming, but is one that I find useful for managing classes and have built into my own system.
Once the class has been initialized, the next thing that happens is that the programmer calls an Init() function, as seen in the second line of our programming example:
ClsTimeFrmOpen.Init Nothing ‘INITIALIZE THIS INSTANCE OF THE TIMER
The Class_Initialize function is an event called automatically by Access when the class is initialized; as such it cannot be passed parameters. I have found it useful to create a second function that is called explicitly and that can be passed parameters, and these will be used to initialize variables inside the class. In this case, all we pass in is a reference to the object which is the parent of the class being instantiated (the timer). Because we are using the timer standalone, there is no parent class. If there were, we could pass a pointer to Me as in the following example:
ClsTimeFrmOpen.Init Me ‘INITIALIZE THIS INSTANCE OF THE TIMER
Here is the ‘Init’ sub listing from our class module:
Public Sub Init(ByRef robjParent As Object)
Set mobjParent = robjParent
LogIntoParentCol Me
assDebugPrint "init " & strInstanceName, DebugPrint
End Sub
The code operates as follows:
· The statement Set mobjParent = robjParent stores a pointer to this class’s parent. We often need to be able to find something that the parent is doing, and this pointer allows us to get at anything that the parent is making public.
· LogIntoParentCol Me registers itself in its parent’s mobjChildren collection, so that when the parent closes, it can clean up all of its children. This function is found in basClassGlobal.
·
The statement assDebugPrint "init " & strInstanceName,
DebugPrint prints to the debug window.
The operation of this class is trivial as there are only two methods and a single variable. The StartTimer gets a tick count and stores it in a long integer variable lngStartTime. EndTimer Gets the tick count, subtracts lngStartTime from it, and returns the difference. From our example, here is the code that calls the appropriate functions:
ClsTimeFrmOpen.StartTimer ‘START THE TIMER
Docmd.OpenForm “MyForm” ‘DO SOMETHING THAT NEEDS TIMING
Debug.Print ClsTimeFrmOpen.EndTimer ‘PRINT THE TIME TO THE DEBUG WINDOW
And here are the methods as they appear in the class module:
Sub StartTimer()
lngStartTime = apiGetTime()
End Sub
Function EndTimer()
EndTimer = apiGetTime() - lngStartTime
End Function
Finally, in order to clean up, the programmer sets the timer variable equal to nothing:
Set clsTimeFrmOpen = Nothing ‘DESTROY THE TIMER
Setting the variable = Nothing causes the class’s Class_Terminate() event to fire. Similarly to Class_Initialize, this is an automatic, Access-generated event. The Terminate event will directly perform its cleanup, destroying all the child classes stored in its mobjChildren collection, and calling its Term sub. Just as I create my own Init sub that can be called explicitly by the programmer in addition to the Class_Initialize() event, I also create a separate Term sub because there are times when it should be called before the class instance is destroyed and will be called by the programmer before setting the variable pointer to nothing. Even if that is done, Term() should be written in such a way that it can be called again by Terminate() so that we can have a consistent cleanup in case some unforeseen event causes the variable to be cleared, which would destroy the class instance. In other words, the call to Term() in Terminate() is a failsafe cleanup.
Private Sub Class_Terminate()
assDebugPrint "Terminate " & mcstrModuleName, DebugPrint
DecObjCounter
TerminateChildren Me.Children
Set mobjChildren = Nothing
Term
End Sub
After the debugging statement, the code proceeds as follows:
· DecObjCounter decrements the count of open class instances to account for this class closing (another function found in basClassGlobal).
· TerminateChildren Me.Children passes a reference to our children collection to a function which will clean up any child classes in this collection
· The statement Set mobjChildren = Nothing cleans up the pointer to our child collection.
The Term() sub is as follows:
Public Sub Term()
assDebugPrint "Term() " & mcstrModuleName, DebugPrint
Set mobjParent = Nothing
End Sub
Again a debugging statement precedes the real action of this sub. Set mobjParent = Nothing cleans up the pointer to our parent (the class that instantiated this class).
That in a nutshell is the structure that I use for instantiating, using and tearing down a class. Each piece is required to implement a reliable setup/tear down mechanism, which in turn allows the programmer to concentrate on what the class does, not how to get it to set up and close reliably.
Classes are nothing more than a method of modeling some real world object. They embed all the constants, variables and methods required to make some thing work. They can be instantiated as often as needed to implement as many of the objects as needed. With the proper foundation, we can implement new classes to meet our needs with a minimum of fuss and bother, yet be assured that they will function in a consistent manner and when it is time to cleanup they will do so cleanly and reliably.
All
the code for the clsTimer, basClassGlobal, and a demo of the timer in action
can be found on the web site for DatabaseAdvisors.com. In the next issue I
will demonstrate how to build a class for a custom control.
John
Colby ©2001
May be distributed as long as the copyright remains.
John
Colby Bio