Jim Lawrence (AccessD)
accessd at shaw.ca
Thu Mar 11 22:14:25 CST 2004
Fascinating :-) Jim -----Original Message----- From: accessd-bounces at databaseadvisors.com [mailto:accessd-bounces at databaseadvisors.com]On Behalf Of John W. Colby Sent: Thursday, March 11, 2004 6:52 PM To: Access Developers discussion and problem solving Subject: [AccessD] Framework Discussion - Class Interface 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 -- _______________________________________________ AccessD mailing list AccessD at databaseadvisors.com http://databaseadvisors.com/mailman/listinfo/accessd Website: http://www.databaseadvisors.com