[AccessD] Framework Discussion - Troubleshooting unload problems

John W. Colby jwcolby at colbyconsulting.com
Sun Mar 28 13:38:37 CST 2004


Framework – Troubleshooting unload problems
As I have mentioned, potentially the biggest problem with programming
objects is getting them all unloaded reliably.  In the best case this is
easy and everything just works.  Unfortunately we all know that eventually
we will run into the worst case.

In our case, the worst case is a class that holds a pointer to a form.  The
class fails to unload, the form never truly closes, Access appears to close
but doesn’t, and a HUGE memory leak just occurred.  Windows may refuse to
close, particular the Win98 variants.  Nasty stuff.

How can this happen?  As we know the framework uses a form class.  The form
class dimensions a form object WithEvents, a form object is passed in and
saved in a variable in the class header.  Each control class is passed a
form object and stores the pointer in the control class’ header.  We just
set up the scenario.

Now suppose some smart developer wants to sink events from dclsFrm.  Yes,
dclsFrm raises events to inform the native form class that things have
occurred.  So the developer dimensions a dclsFrm variable and grabs a
pointer to the class from the form’s variable.  The new variable can be
declared WithEvents, and sink the dclsFrm events which can be pretty useful.
However, the user now tries to close the form.  The form appears to close,
however there is a pointer to the dclsFrm instance for that form that is not
set to nothing, and voila, trouble!

These kinds of problems are really not uncommon in class programming.  Any
object that broadcasts events can have pointers to it in multiple locations,
all sinking those events.  A class (or indeed any object) doesn’t truly
close (unload from memory) until the last variable holding a pointer to the
object is set to nothing.  Not clearing out that “last instance pointer”
will absolutely prevent the object from unloading and may cause the
nightmare scenario described in the second paragraph.

The answer is not to avoid object programming, but to be aware of the
potential problems and to build a tool to assist us in at least knowing that
objects are still referenced when we believe they should be shut down.

Pointers to objects (forms, controls and classes) can be stored in variables
of type variant and object, in variables of the specific type of the object
being stored, and in collections (which are if I’m not mistaken just
variables in a special stack like structure.).

It would be convenient if we could pass a variable that we wanted a pointer
to the class stored in
 to the class itself.  In other words, tell the class
to “store a pointer to yourself in this variable”.  This would allow the
class itself to then place a pointer to each such variable in a collection
inside the class.  Now the class could “clean up EVERY pointer to itself”
just by iterating that collection and setting each variable = nothing, then
removing the variable from its collection.  Unfortunately this just isn’t an
option that can be enforced and without the enforcement it becomes more
trouble than it is worth.

The point of this discussion is to familiarize the reader with why the
problem exists and how it really does become a problem, and that there is no
easy solution.

If there is no easy solution, then at least we need to know when objects are
failing to unload.  If the developer is aware, and uses some basic
troubleshooting techniques on a regular basis as the application is being
developed, we can stay ahead of the problem.  Building a system that loads
the framework with 30 or 40 class instances, plus a dozen forms with several
hundred class instances, and discovering that there are 70 class instances
not unloading can cause a mild panic.

However if we have some method of knowing every single instance loaded, we
can load / unload objects and watch the object count left in memory, print
out object name lists etc and discover where things are not unloaded.
Knowing that, we can then go find the problem and solve it.

Thus we might build a test for the SysVar class (for example) where we load
it all by itself, without the framework.  Make absolutely sure that all
class instances loaded by the SysVar system unload correctly before we allow
the Framework class to automatically load / unload the SysVar class.  Even
then, we would want to load the framework, then unload it and examine the
class instance stack to see what is left loaded.  Don’t go on until you can
load / unload the framework and leave nothing in memory.

Of course in order to do this we need a tool.  In my previous versions of
the framework I used a module with a set of private variables, a handful of
collections and code to increment / decrement counts and store class
instance names.  While this worked, it had issues.  In order to address
these issues I am designing a class (of course) with all of the structures
and code required to allow us to track classes loading and unloading.  This
class will hold an actual pointer to every class instance.  I do this for a
variety of reasons, first of which is that the variable holding the original
pointer to the class may be difficult to find unless we can get at the class
instance itself.  Just knowing the name of the class instance is often
unhelpful.

The first thing we need to do is allow every class instance to register
itself as it loads and unregister itself as it unloads.  It turns out to be
easy to do this, just call a “register” function in Init() and an
“unregister” function in Term().  We could have the class call a function
passing a pointer to itself (the class instance) and have that function save
the pointer into a “stack”.  This is actually quite useful since we can now
get at the class instance and run methods, query properties etc.

However if you think about it for a minute, the Terminate() event of a class
does not fire until the class unloads, thus if we try to save an actual
pointer to the class we end up in a catch 22 where just setting the original
pointer to the class to nothing doesn’t cause the class to unload since we
have our saved pointer.  Calling the class’ term and having the class unload
its saved pointer works just fine, but then if the original pointer is never
set to nothing, the class never unloads.  Further if there are other
pointers to the class out there the class never unloads.  However we think
it unloaded because the pointer isn’t in the instance stack any more.

For this reason we can’t simply use a class instance pointer stack by
itself.  In fact, if we do use an instance stack, then we MUST call the
class’ term() before setting the class variable to nothing or the class won’
t unload for the reason discussed above.  In fact, while I like the instance
stack (collection of pointers to each instance) it is often not strictly
necessary.  If you don’t want it we will provide a method of not using that
piece using a SysVar to turn on/off the instance stack.
So even if we are going to use an instance pointer stack, we also need
another tool to determine whether we really truly unloaded the class
instance.  Since we know that the class Terminate() event will not run
unless it is specifically called inside the class  (don’t do that!) or the
class actually unloads we can build another stack of the class instance
names and have the Terminate() event remove it’s instance name from this
stack.  Thus we have the very useful tool of a stack of actual pointers to
every class instance (that we haven’t called the term() method of), and we
have a backup tool of a stack of class instance names.  The stack of names
will only be cleared out by the class instances unloading, thus it is the
“failsafe” that tells us when we have indeed unloaded everything.
If you followed this whole discussion, go to the head of the class.
What we’re really saying is that we want to build a collection of pointers
to class instances, but also a collection of just the names of the
instances.  The collection of the names will be the final judge of what got
unloaded since the only way to remove a name from that collection is the
Terminate of a given instance removing it.  If we expose the count of these
two collections, and the count differs, we have problems.  We tried to
unload something, but failed to really set the last pointer to it to
nothing.  The pointer to it was removed from the pointer collection but not
from the name collection.
Is all this really necessary?  Well, yes.  We can get along without the
pointer collection, it is really a nicety.  The collection of instance names
however is a must – it tells the truth about what doesn’t unload and we
really want to know that!
Implementation
To implement the system for tracking, I built clsInstanceStack that will be
instantiated one time, before the framework class.  The class has all of the
default properties and methods of any other class (uses clsTemplate).  This
class must be initialized and ready to go before anything else so that all
other classes can use it to register themselves.

Framework Initialization code:

In the header of basFWInit we dimension a new variable mclsInstanceStack.

Option Compare Database
Option Explicit

Private mclsInstanceStack As clsInstanceStack   'A single instance of the
troubleshooting class.
Private mclsFramework As clsFramework   'The framework foundation class
Private mblnFWInitialized As Boolean    'A boolean to tell us that we have
already initialized

In FWInit we instantiate and initialize the new class first.

'
'The init function for the framework
'
Public Function FWInit()
    If mblnFWInitialized = False Then '

        mblnFWInitialized = True
        Randomize
        Set mclsInstanceStack = New clsInstanceStack
        mclsInstanceStack.Init Nothing
        Set mclsFramework = New clsFramework
        mclsFramework.Init Nothing
    End If
End Function

In FWTerm we terminate the new class last so that all other classes have
unloaded before this one will.

'
'The term function for the framework
'
Public Function FWTerm()
    mclsFramework.Term
    Set mclsFramework = Nothing
    mclsInstanceStack.Term
    Set mclsInstanceStack = Nothing
    mblnFWInitialized = False
End Function

We also need a random number generator that we can use to generate random
numbers of a fixed length.

'
'Returns a long integer random number between lngUpperBound and
lngLowerBound
'
Function Random(lngUpperBound As Long, lngLowerBound As Long) As Long
    Random = Int((lngUpperBound - lngLowerBound + 1) * Rnd + lngLowerBound)
End Function

And we need a function to call that will empty collections of objects,
calling the object’s Term event.  There will be cases where we have
collections of object pointers that don’t “belong” to the class holding the
pointer, thus we need to be able to call the object’s term or not as the
situation requires.

'
'Empties out a collection containing class instances
'
Public Function ColEmpty(col As Collection, Optional blnCallTerm As Boolean
= True)
On Error GoTo Err_ColEmpty
    On Error Resume Next
    While col.Count > 0
        'If we are supposed to call term() do so
        If blnCallTerm Then
            col(1).Term
        End If
On Error GoTo Err_ColEmpty
        col.Remove 1
    Wend
exit_ColEmpty:
Exit Function
Err_ColEmpty:
    Select Case Err
    Case 91 'Collection empty
        Resume exit_ColEmpty
    Case Else
        MsgBox Err.Description, , "Error in Function clsSysVars.colEmpty"
        Resume exit_ColEmpty
    End Select
    Resume 0    '.FOR TROUBLESHOOTING
End Function


And of course we need a function to return a pointer to the troubleshooting
class.

Public Function cIS() As clsInstanceStack
    Set cIS = mclsInstanceStack
End Function

This is all code found in basFWInit.
clsInstanceStack:
The instance stack class header has two collections for holding the name and
a pointer to the class.

'A name for the class module
Private Const mcstrModuleName As String = "clsInstanceStack"

'A name for the class instance
Private mstrInstanceName As String

'Collections for the name and the class pointers
Private mcolName As Collection  'Holds name string for each class loaded
Private mcolPtr As Collection   'Holds a pointer to every class instantiated

'We want to be able to turn off this logging
Dim blnEnblPtrStack As Boolean
Dim blnEnblNameStack As Boolean

Initialize sets these two collections.

Private Sub Class_Initialize()
    Set mcolName = New Collection
    Set mcolPtr = New Collection
End Sub

Init() registers this class in its own collections.

Public Sub Init(ByRef robjParent As Object)

    blnEnblPtrStack = True
    blnEnblNameStack = True

    'Register this class in the troubleshooting collections
    cIS.Register Me
End Sub

Terminate() removes the name from the name collection.  If this happens,
then we really have properly removed the instance from memory.

Private Sub Class_Terminate()
    'Remove this class' name from the troubleshooting class
    cIS.NameDel mstrInstanceName
End Sub

Term() removes the pointer to the class from the pointer collection.  This
method is called from the “owner” of the class when it is trying to shut
down the class instance.

Public Sub Term()
Static blnRan As Boolean    'The term may run more than once so
    'remove this class' pointer from the troubleshooting pointer class
    cIS.PtrDel Me
End Sub

A property allows getting and setting the InstanceName variable in the class
’ header.

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

The code below is really the guts of this troubleshooting class.  First we
need to be able to get counts of each collection.

'
'Gets a count of pointers of classes instantiated
Public Property Get PtrCnt() As Long
    PtrCnt = mcolPtr.Count
End Property

'Gets a count of names of classes instantiated
Property Get NameCnt() As Long
    NameCnt = mcolName.Count
End Property

The two methods called from each classes Term() or Terminate().

'
'Removes a class from the collection
'Called from each class' Term()
'
Public Function PtrDel(obj As Object)
    On Error Resume Next
    mcolPtr.Remove (obj.NameInstance)
End Function

'
'Removes a class name from the collection
'Called from each class' Terminate() event
'
Public Function NameDel(strName As String)
On Error Resume Next    'We may have the name stack disabled
    mcolName.Remove strName
End Function

If you ever want to get a pointer to a specific class that is loaded.

'return a pointer to the object
Public Function Ptr(strName As String) As Object
On Error Resume Next
    'We may have the pointer stack turned off
    Set Ptr = mcolPtr(strName)
End Function

These two functions return strings of the names of all the objects stored in
the collection.

Public Function PtrNames() As String
On Error GoTo Err_PtrNames
Dim obj As Object
Dim str As String
    If blnEnblPtrStack Then
    For Each obj In mcolPtr
        If Len(str) > 0 Then
            str = str & "; " & vbCrLf & obj.NameInstance
        Else
            str = obj.NameInstance
        End If
    Next obj
    End If
    PtrNames = str
Exit_PtrNames:
Exit Function
Err_PtrNames:
        MsgBox Err.Description, , "Error in Function
basClassGlobal.PtrNames"
        Resume Exit_PtrNames
    Resume 0    '.FOR TROUBLESHOOTING
End Function

Public Function Names() As String
On Error GoTo Err_Names
Dim str As String
Dim strName As Variant

    If blnEnblNameStack Then
        For Each strName In mcolName
            If Len(str) > 0 Then
                str = str & "; " & vbCrLf & strName
            Else
                str = strName
            End If
        Next strName
    End If
    Names = str
Exit_Names:
Exit Function
Err_Names:
        MsgBox Err.Description, , "Error in Function clsInstanceStack.Names"
        Resume Exit_Names
    Resume 0    '.FOR TROUBLESHOOTING
End Function

Below is the Register function that saves the class pointers and names in
the respective collection.

Note: We want to be able to switch this stuff on and off by using SysVars
but the SysVar class is a Service Class loaded by the Framework class. Thus
since this loads first SysVars aren’t directly available to control this
functionality.  We therefore use Booleans that we just set true in the
init() of this class.

Further we then provide two methods that can set these Booleans, which will
be used by the Framework class to turn on/off this troubleshooting
processing using SysVars.

Public Property Let EnblPtrStack(lblnEnblPtrStack As Boolean)
    blnEnblPtrStack = lblnEnblPtrStack
End Property
Public Property Let EnblNameStack(lblnEnblNameStack As Boolean)
    blnEnblNameStack = lblnEnblNameStack
End Property

What that means though is that the functionality cannot be turned off until
the Framework loads and the SysVar classes load, thus all of those classes
will load into the collections even if the SysVars later turn off this
functionality.  If you want to prevent loading the Framework class and
SysVars into this troubleshooting class you need to set the Booleans = FALSE
in init() of this class.  You can then turn on the logging for all classes
after the SysVars load.  The downside is that you can’t see any service
classes load that the framework class may load.

'THIS FUNCTION GENERATES A UNIQUE NAME FOR THIS INSTANCE OF THIS CLASS USING
THE
'CLASS MODULE NAME AND A RANDOM VALUE AND LOGS THIS INSTANCE IN THE TWO
COLLECTIONS
'THE POINTER COLLECTION AND THE NAME COLLECTION.
'
'This function will check to see if the instance name is already in place,
i.e.
'if the calling class has its own method of generating an instance name.  If
it
'is in place (length > 0) then we will attempt to save the name in the
InstanceName
'in the name collection keyed on the instance name.  If it saves we will
then save
'the pointer in the pointer collection, also keyed on the instance name.
'
'If it fails to save, or if the len(InstanceName) = 0 then this function
will build
'a name for the class instance.
'
'The algorithm for building a new name will be to take the parent's instance
name
'(if it exists), add the instances MODULE name, and try to save that.  If
the save
'fails, then a fixed length 9 digit random number is added to the whole and
attempted
'to save.
'
Public Function Register(obj As Object)
On Error GoTo Err_Register
Dim strInstanceName As String
    If blnEnblNameStack Or blnEnblPtrStack Then
        'if the instance name is not already initialized by the calling
class, attempt
        'to build an instance name from the parent lineage plus module name
        If Len(obj.NameInstance) = 0 Then
            On Error Resume Next
            strInstanceName = obj.Parent.NameInstance & ":" & obj.NameModule
            'if the err <> 0 then the parent did not exist so just go with
the object
            'module name.
            If Err <> 0 Then
                strInstanceName = obj.NameModule
            End If
            obj.NameInstance = strInstanceName
        Else
            strInstanceName = obj.NameInstance
        End If

        'attempt to add the ame of the object to the name collection
        On Error Resume Next
        mcolName.Add item:=strInstanceName, key:=strInstanceName

        'if the err <> 0 then the name collided with an existing object so
add a random number
        'it is remotely possible to get a random number that collides as
well so loop if that
        'happens and select another random number
        While Err <> 0
            Err.Clear
            strInstanceName = obj.NameInstance & ":" & Random(999999,
100000)
            mcolName.Add item:=strInstanceName, key:=strInstanceName
            If Err <> 0 Then MsgBox Error$
        Wend
        'When we finally have a unique instance name (it stores in the name
collection)
        If blnEnblNameStack Then
            'store that instance name back into the object InstanceName
proeprty.
            obj.NameInstance = strInstanceName
        End If
        If blnEnblPtrStack Then
            'Now add the object into the framework object tracking system
            mcolPtr.Add obj, strInstanceName
        End If
    End If
Exit_Register:
On Error Resume Next
Exit Function

Err_Register:
    Select Case Err
    Case 0      'insert Errors you wish to ignore here
        Resume Next
'    Case 91     'no parent exists
'        PtrAdd obj
'        Resume Exit_Register
'    Case 2465   'no child collection exists in the parent object
'        PtrAdd obj
'        Resume Exit_Register
    Case Else   'All other errors will trap
        Beep
        MsgBox Err.Description, , "Error in function
basClassGlobal.Register"
        Resume Exit_Register
    End Select
    Resume 0    'FOR TROUBLESHOOTING
End Function

That is really all there is to the troubleshooting class, two collections to
hold name strings and pointers to the classes loading and a few methods to
register the classes (if enabled) and return strings of names and counts of
the collections.
Framework code:
Having done that we need to modify the framework class to read the sysvars
and pass them along to this class.  We must load the SysVar class and
initialize it, then we can read the two Enbl SysVars out and pass them to
the methods of the cIS troubleshooting class.

Public Sub Init(ByRef robjParent As Object)
    mclsSV.Init Nothing, gfwcnn, "usystblFWSysVars"
    cIS.EnblPtrStack = SV("EnblPtrStack")
    cIS.EnblNameStack = SV("EnblNameStack")
End Sub
Generic class code:

In order to use this troubleshooting system any class must have some
specific things.  First it must have a constant that is the class module
name.  It must also have a string that will hold the instance name.  In the
class header:

Private Const mcstrModuleName As String = "dclsFrm"
Private mstrInstanceName As String

The class’ terminate() event remove its own name from the name collection.

Private Sub Class_Terminate()
    'Remove this class' name from the troubleshooting class
    cIS.NameDel mstrInstanceName
End Sub

The Init() method must call the register method to place its name in the
name collection and its pointer in the pointer collection (if enabled by the
SysVars).

Public Sub Init(ByRef robjParent As Object, lfrm As Form)
    'Register this class in the troubleshooting collections
    cIS.Register Me
End Sub

Term() must call the method that removes the class’ pointer from the pointer
collection.

Public Sub Term()
    cIS.PtrDel Me
End Sub

There must be a property to read the module name.

Property Get NameModule() As String
    NameModule = mcstrModuleName
End Property

And we need a property to read and set the InstanceName.

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

That’s pretty much it.  As a class instantiates its Init() method calls the
Register method of the troubleshooting class, passing a pointer to itself.
If the storage of the name and /or pointer are enabled by the SysVars,
Register will try to save the name and/or pointer keyed on the name.  If
that fails Register() will add a random number to the end of the name and
try again until the save works.

It is possible to build your own InstanceName if you wish.  I do this in the
control classes by taking the parent object’s instance name (the form’s
instance name) adding a “:” and then the name of the control.

Function Init(ByRef robjParent As Object, lfrm As Form, lcbo As ComboBox,
lintDataType As Integer)          'Pass in a pointer to a specific control
    Set mfrm = lfrm
    Set mcbo = lcbo                     'Save that pointer to a private
variable here in the class
    mstrInstanceName = mobjParent.NameInstance & ":" & mcbo.Name
    'Register this class in the troubleshooting collections
    cIS.Register Me
End Function

In the case where the form only loads one time this works without
modification and I get names that just make sense such as
frmPeopleV5:txtDOB.  If the form is loaded more than once, not common but
possible, then the form class would end up with a random number embedded in
it and the same control would have a name something like
frmPeopleV5:832735:txtDOB.  Obviously two controls on the same form can
never have the same name so we never have an issue there.

It is often helpful when it comes time to troubleshoot things to have names
that make sense if you can ensure that they will be unique.  You really don’
t need to worry however as the Register() method will add a random number as
needed to force the name to be unique, and then saves that name back into
the class’ InstanceName variable.
To test the system:
•	Open frmTestClsInstanceStack.  The name collection will be displayed with
all of the names of all classes loaded so far, including classes for the
form and control on this form.
•	Go to the database window and load another form, perhaps frmPeopleV5.
Switch back to frmTestClsInstanceStack and press the “Read class names”
button at the bottom of the form.  Notice more class names in the text box.
•	Close that form you just opened,  switch back to frmTestClsInstanceStack
and press “Read Class Names”.  Notice that the class names for all classes
loaded by the form you just closed are now gone.
Summary
Classes can cause memory leaks and shutdown problems in Access if not
unloaded correctly, particularly if the class holds a pointer to a physical
object such as a form, control or record set.  Since the whole point of many
of our classes is to wrap such physical objects in a class so that we can
extend the functionality of that object, we run a very real risk of causing
such problems inadvertently.  The troubleshooting class that I have
described gives us some tools to troubleshoot these problems should they
arise.

As you design your application, or even your framework, it is imperative
that you stop after designing every class and test the load and unload of
that class and all its instances.  By looking at the count of the names
collection and the pointer collection as well as the actual names of the
objects still in the names collection after we think our classes have
unloaded, we can determine if problems exist, and if so at least which
classes have not unloaded correctly.  Using the system described in this
article, if the name is in the name collection then the object didn’t
unload.

I can’t really help you discover why it didn’t unload, but I can at least
tell you that it didn’t so you can go looking for the cause.



John W. Colby
www.ColbyConsulting.com





More information about the AccessD mailing list