[AccessD] Framework Discussion - Class Interface

John W. Colby jwcolby at colbyconsulting.com
Thu Mar 11 20:51:57 CST 2004


Unfortunately it has come to the point in our discussion where we have to
add a little common interface to all of our classes in order to provide us
with troubleshooting tools and debugging help.  If you remember from
Collections, classes and Garbage Collection I discussed some of the issues
with classes, particularly those classes which reference forms and controls.
Unless we are careful we can create problems unloading the classes, which
can cause memory leaks and even cause Access to fail to close.  Well its
time to address those issues.

Please bear with me here, I really don't want to lose anyone over these
issues and the required code, but I know it can take a little while to get
used to this stuff being in the class headers "clouding the issue".

I am going to discuss a Template class which can then be cut and copied to
start a new class.  All of the necessary interface structures are there; we
just add our own class specific stuff to flesh out our new class.  You can
see all of this code in one place in clsTemplate in the example code
database.
The Comment Block
The first thing we need to do is add a standard header to our classes.  I
use a section for the typical author, date, copyright etc.  This area is
used to extensively document this class, with behaviors explained, why we
have them, as much documentation as you can put in here so that when you
come back next week you can figure out what you were doing.

'.=============================================================
'.Copyright 2004 Colby Consulting.  All rights reserved.
'.Phone        :
'.E-mail       : jcolby at colbyconsulting.com
'.=============================================================
' DO NOT DELETE THE COMMENTS ABOVE.  All other comments in this module
' may be deleted from production code, but lines above must remain.
'--------------------------------------------------------------
'.Description  :
'.
'.Written By   : John W. Colby
'.Date Created : 02/26/2004
' Rev. History :
'
' Comments     :
'.-------------------------------------------------------------
'.
' ADDITIONAL NOTES:
'
'--------------------------------------------------------------
'
' INSTRUCTIONS:
'.-------------------------------------------------------------
'.

Below that I add standard sections for constants and variables, with some of
these already defined.

'THESE CONSTANTS AND VARIABLES ARE USED INTERNALLY TO THE CLASS
'*+ Class constant declaration
Private Const DebugPrint As Boolean = False
Private Const mcstrModuleName As String = "clsFW"
'*- Class constants declaration

DebugPrint will ultimately allow us to turn on/off printing of debug
statements in this specific module.  We do this by calling a debugprint
function instead of just putting debug.print statements directly in code.
We pass in the constant above and the statement prints or doesn't based on
the value of the constant passed in.  Set it true and all debug statements
in the module will print, set it false, they don't print.

We have a constant mcstrModuleName that holds a string that is literally the
class' module name as seen in the database window, modules tab.

Private Const mcstrModuleName As String = "clsFW"

Next come class variables:


'*+ Class variables declarations
'POINTER TO THE OBJECT THAT CREATED THIS CLASS INSTANCE (THE PARENT OF THE
CLASS)
Private mobjParent As Object

'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 Collection

'THE STRING INSTANCE NAME IS BUILT UP FROM THE MODULE NAME AND A RANDOM INT
Public mstrInstanceName As String
Public mstrName As String
'*- Class variables declarations



At this point we also need to discuss some of the process that we use for
"automating" the setup and teardown of classes because the entire standard
header is used for this purpose.  You will add your own variables in here
but the stuff you see so far is all framework interface.

Classes rarely "standalone" in the framework.  There are often classes that
use child classes, which may themselves use child classes.  A good example
is the dclsFrm (the form class) uses dclsCbo (the combo class) which will
use dclsDepObj (the dependent object class).  Obviously the form class may
have dozens of control classes found and instantiated by the control scanner
function, with pointers to these control classes stored in a collection.  A
combo may have one or several objects which "depend on it" to filter its
data, thus the combo class has a clsDepObj which stores pointers to classes
for the controls that depend on the combo.

All of this class using class using class begs for a common interface that
is just always there in every class.  Thus each class is passed a pointer to
its parent object which is then stored in mobjParent.

'*+ Class variables declarations
'POINTER TO THE OBJECT THAT CREATED THIS CLASS INSTANCE (THE PARENT OF THE
CLASS)
Private mobjParent As Object

The parent object is the class that instantiated this class.  dclsFrm is the
parent object to dclsCbo.  dclsCbo is the parent object to clsDepObj and so
forth.  A form passes "Me" to dclsFrm, dclsFrm passes "Me" to dclsCbo,
dclsCbo passes "Me" to clsDepObj etc.  Me in all three cases means the
pointer to the current class.  Me in the form means the form's built-in
class, me in dclsFrm means a pointer to that dclsFrm instance.

By passing a pointer to the parent, any child class can get at any of the
parent's properties and methods that it may need.  In fact the parent object
may have properties and methods added just for the express use of specific
child classes that it knows it must deal with.

Additionally, each class has a children collection which we name
mcolChildren.  mcolChildren holds pointers to child objects (classes).

'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 Collection


The string instance name is built from mcstrModuleName and other info.  I am
moving to a "lineage name" meaning each class uses it's parent's name plus
it's class name.  That makes it very easy to tell how any given class "came
to be".

'THE STRING INSTANCE NAME IS BUILT UP FROM THE MODULE NAME AND A RANDOM INT
Public mstrInstanceName As String

And finally a simple name variable

Public mstrName As String

This variable will usually be the simple name used as the key when the class
is saved in its parent's children class.  In other words, a class is always
saved in its parent's children collection so that the parent can clean up
all of its children when the parent class terminates.  The name used as the
key into this collection is often something simple like a control name.  We
do this so that we can just index into the class using a readily available
piece of information (the control's name) to get the class associated with
that object.
Functional Constants and Variables

I like to organize the header with all the constants grouped together, the
variables, then any custom events declared that this class may raise.  If a
class gets very complicated it may be convenient to group functional pieces
together.  Bottom line, this is about organization and as long as you make
an effort to routinely follow organizational guidelines you will be better
off than if you just let madness reign.

'.-------------------------------------------------------------
'THESE CONSTANTS AND VARIABLES ARE USED BY THE CLASS TO IMPLEMENT CLASS
FUNCTIONALITY
'*+ custom constants declaration
'
'*- Custom constants declaration


'*+ custom variables declarations
'
Private WithEvents mfrm As Form     'A form reference passed in
'*- custom variables declarations


'
'Define any events this class will raise here
'*+ custom events Declarations
'Public Event MyEvent(Status As Integer)
'*- custom events declarations
Class Init/Term
Next I place the class' Initialize sub with all Set statements instantiating
any objects that the class dimensions in its header.  Initialize is an Event
handler, i.e. the class fires this event when the SET statement instantiates
the class and the class starts to load.

'.-------------------------------------------------------------
'THESE FUNCTIONS / SUBS ARE USED INTERNALLY TO THE CLASS
'*+ Private Init/Terminate Interface
Private Sub Class_Initialize()
On Error GoTo Err_Class_Initialize
    assDebugPrint "initialize " & mcstrModuleName, DebugPrint
    Set mcolChildren = New Collection
Exit_Class_Initialize:
Exit Sub
Err_Class_Initialize:
        MsgBox err.Description, , "Error in Sub
clsTemplate.Class_Initialize"
        Resume Exit_Class_Initialize
    Resume 0    '.FOR TROUBLESHOOTING
End Sub


Init() is the first class method usually called, and here we pass in
variables to the class that we need in order to get the ball rolling.

'INITIALIZE THE CLASS
Public Sub Init(ByRef robjParent As Object)
    Set mobjParent = robjParent
    'IF THE PARENT OBJECT HAS A CHILDREN COLLECTION, PUT MYSELF IN IT
    assDebugPrint "init " & mstrInstanceName, DebugPrint
End Sub

The class Terminate event fires when the last pointer to the class is set to
nothing.

Private Sub Class_Terminate()
On Error Resume Next
    assDebugPrint "Terminate " & mcstrModuleName, DebugPrint
    Term
End Sub

Term() is a class method and it is normally called by the cleanup code of
the object that created this class instance in the first place.  For
example, dclsFrm creates a class instance for each control and places a
pointer to that class in its Children collection.  dclsFrm will then iterate
this collection, calling the term method of all the child objects in the
children collection when the form is closing, before removing the pointers
to the objects from the collection.

I also call term from the class' Terminate event.  In all cases a class will
close automatically when the last pointer to the class is set to nothing.
Thus in theory just setting the pointer to the class to nothing should cause
terminate to fire, which calls term.

This seems like "double work" and in fact it is, however I do so because it
is quite possible to set a reference to self for instance, where this class
needs to hold itself open regardless of anything else.  If I have a pointer
to the class in some other class, and I just set that pointer to nothing,
then the class still remains open since the last pointer to it still exists,
in its own header.  However if I call the terminate method, I clean up all
pointers including pointer to self, then when I set my external pointer to
this class instance to nothing, the class will close properly.

This is not a scenario that is used often, but it is used and I just like to
play it safe by always calling the term() of a class instance, then set the
pointer to the class to nothing, which fires the terminate event, which does
call term again.  We can do something like setting a static variable in term
to tell us that the term() method already ran and not to run it again the
second time.

'CLEAN UP ALL OF THE CLASS POINTERS
Public Sub Term()
    On Error Resume Next
Static blnRan As Boolean    'The term may run more than once so
    If blnRan Then Exit Sub 'just exit if it already ran
    blnRan = True
    assDebugPrint "Term() " & mcstrModuleName, DebugPrint
    Set mobjParent = Nothing
    Set mcolChildren = Nothing
End Sub
'*- Public Init/Terminate interface
Standard Properties and Methods

We have discussed the header and the init / term interface.  Next we need to
discuss standard properties and methods.  Obviously we need a way for other
classes (or the developer) to get at the data in the header.  The ModuleName
string, InstanceName, and Name are all properties that just read the
constants or variables holding these data and return them as strings.  In
order to group them in Intellisense I use Name as the first part of the
property.

'get the name of this class / module
Property Get NameModule() As String
    NameModule = mcstrModuleName
End Property

Public Property Get Name() As String
    Name = mstrName
End Property

Public Property Get NameInstance() As String
    NameInstance = mstrInstanceName
End Property

Public Property Let NameInstance(strName As String)
    mstrInstanceName = strName
End Property

And finally we have properties that get the parent object and the children
collection.

'*+ Parent/Child links interface
'get the pointer to this object's parent
Public Property Get Parent() As Object
  Set Parent = mobjParent
End Property

'get the pointer to this object's children
Public Property Get Children() As Collection
  Set Children = mcolChildren
End Property
'*- Parent/Child links interface

The above is all of the standard class interface.  It is important to
understand that all of the above is in every class, which gives us a very
standardized interface to what I will call the framework class interface.
These things have very little to do with the functionality of the class,
what the class actually does in the framework.  All it does is let us set up
and tear down the classes in a standard way.

That may be the most critical piece however since if a class doesn't
terminate it can under certain circumstances hang Access, which can
unfortunately even hang the lesser Windows versions such as Windows 98.  The
three finger salute is not something I want my app forcing on the user!
Organizing Class functionality
Finally, the sections below are just more organizational stuff.  I like to
group all like things together.  Again this is your call as to whether you
want to do this kind of organization.


'.-------------------------------------------------------------------------
'THESE FUNCTIONS SINK EVENTS DECLARED WITHEVENTS IN THIS CLASS
'*+ Form WithEvent interface
'*- Form WithEvent interface
'THESE FUNCTIONS / SUBS ARE USED TO IMPLEMENT CLASS FUNCTIONALITY
'*+PRIVATE Class function / sub declaration
'*-PRIVATE Class function / sub declaration
'*+PUBLIC Class function / sub declaration
'*-PUBLIC Class function / sub declaration

Summary
A framework is going to get complex, with many classes, classes
instantiating other classes, setting up child object chains as they
initialize and tearing down their child object chains as they terminate.  It
is critical that we have a system in place to assist us in getting this
piece right or we will end up with chaos and applications that don't cleanup
correctly.  All of the stuff that I have shown above is an attempt to
standardize the setup and teardown so that we can always count on doing
things the same way, regardless of what class we are instantiating.
Consistency goes a long way towards helping us learn this stuff quickly, and
getting all the pieces to play together peacefully.

John W. Colby
www.ColbyConsulting.com





More information about the AccessD mailing list