jwcolby
jwcolby at colbyconsulting.com
Thu Mar 5 07:18:30 CST 2009
clsGlobalInterface It is often the case that we need common code in a set of classes. As an example I have found instances where classes contained in a collection were not destroyed properly by the garbage collector when the pointer to the collection was simply set to nothing. I have run into places where I had to have a term event of a class, and specifically call the term event in order for the cleanup in that term to run to allow the class to be destroyed. So one “common code” thing I do is to build a function that accepts a collection as a parameter, and then iterates the collection getting a pointer to whatever is in the collection, calling the term event (if any) and then deleting that object from the collection. The fact that Access does not have inheritance poses problems when trying to add such common code to all classes. In languages with inheritance we would simply back up the chain to the class that is the parent to all of the classes we want to have the common functionality, and add the functionality in that parent class. That functionality would then be available to all of the descendents. In Access, where we do not have inheritance, there are several strategies for dealing with this “common code” issue. One is to simply embed the code in each class. This method has the obvious “fix the bug in one place” problem. Another method is to place the common code in a module where it is called by all classes. This is a workable method but one of the precepts of class programming is that the class should be portable, and preferably stand-alone. Having code for the classes in modules immediately creates the issue of “what module has to go with this class”. Another method is to use a “helper class” which is referenced by every class that needs the helper code. While this violates the “stand-alone” concept it does at least start to centralize all of the helper code into a specific place, which soon becomes engrained in the developer’s mind. Over time I have used all of the strategies mentioned above, and in fact still do use all of the strategies. However I want to discuss the helper class strategy in more detail in this lecture. Create the Class • Open the demo database. • In the menu, click Insert / Class Module. • Immediately save the module as clsGlobalInterface. • In the header of the module insert the following code: Header Private mobjParent As Object This provides clsGlobalInterface a method of manipulating the parent class if desired. Private mcolPreviouslyHookedEvents As Collection Provides a collection to hold the event property value for previously hooked events Private Const cstrEvProc As String = "[Event Procedure]" The constant to be placed in the event property. Initialization • In the body of clsGlobalInterface insert the following code: Private Sub Class_Initialize() Set mcolPreviouslyHookedEvents = New Collection End Sub Private Sub Class_Terminate() Set mcolPreviouslyHookedEvents = Nothing End Sub Function mInit(lobjParent As Object) Set mobjParent = lobjParent End Function The class event stubs initialize and destroy the collection. mInit stores the passed in pointer to the parent object into a private object variable. Properties • Immediately below mInit() place the following code: Property Get colPreviouslyHookedEvents() As Collection Set colPreviouslyHookedEvents = mcolPreviouslyHookedEvents End Property This property allows the parent class to access the collection of functions found in its properties (if any). Events Because this class does not sink events for any objects, there will be no event sinks. Methods ' 'Allows hooking a property by passing back "[Event Procedure]". 'If the property already has a hook using the old style =SomeFunction() 'Then we don't hook the property but instead pass back the value of the property Function mHookPrp(prp As Property) As String If Left(prp.Value, 1) = "=" Then mHookPrp = prp.Value mcolPreviouslyHookedEvents.Add prp.Name & prp.Value, prp.Name Else prp.Value = cstrEvProc End If End Function This code is going to provide common functionality for all classes which use clsGlobalInterface. A subset of all the classes we will design will be wrappers for forms and controls. These classes will likely sink events, and as such they must hook the events that they want to sink. This code performs this common event hooking process, and makes sure that if an event is already hooked with a call to a function, that call is not deleted in favor of our event hook. It also stores the values of all properties which are already hooked with functions using the =MyFunction() method common in early Access databases. ' 'Empties out a collection containing class instances ' Public Function ColEmpty(col As Collection) On Error GoTo Err_ColEmpty Dim obj As Object On Error Resume Next For Each obj In col obj.mTerm Next obj On Error GoTo Err_ColEmpty While col.Count > 0 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 This function simply iterates a collection calling the mTerm event. Once all objects in the collection have had their mTerm method called, the collection is emptied. • Compile and save clsGlobalInterface So, we now we have a class specifically to hold code that may be used in other classes. In order to use the class we simply dimension a private variable at the top of each class that will use clsGlobalInterface, initialize it, and start calling the methods as desired. Modifications to clsFrm In order to use clsGlobalInterface we have to modify each class that will use it. We will start with clsFrm. • Open clsFrm • In the header of clsFrm insert the following code: Header Private mcolCtls As Collection Private mclsGlobalInterface As clsGlobalInterface This simply adds a variable to hold a pointer to an instance of clsGlobalInterface to the already existing header code.. Properties Next, add the following property code immediately below the existing mInit(). Property Get cGI() As clsGlobalInterface Set cGI = mclsGlobalInterface End Property This gives us a property to return a pointer to our new clsGlobalInterface instance. I placed this code here in the doc because in the Initialization you will refer to this property. Initialization • Add the following code to the body of clsFrm Private Sub Class_Initialize() Set mcolCtls = New Collection Set mclsGlobalInterface = New clsGlobalInterface End Sub Private Sub Class_Terminate() Set mcolCtls = Nothing Set mclsGlobalInterface = Nothing End Sub Here we added code to create and destroy an instance of clsGlobalInterface. The next thing we are going to do is modify the code in the mInit of clsFrm. Originally we used the following code which you should still see in clsFrm.mInit: mfrm.BeforeUpdate = cstrEvProc mfrm.OnClose = cstrEvProc Now we want to change this to: Function mInit(lfrm As Form) cGI.mInit Me Set mfrm = lfrm With mfrm cGI.mHookPrp .Properties("BeforeUpdate") cGI.mHookPrp .Properties("OnClose") End With The first thing we do is to initialize cGI by calling .mInit and passing the init code a reference to Me. Notice that Me is a reference to the current class, in this case clsFrm. You are probably accustomed to using Me as a reference to a form however in fact even there it is really a reference to the form’s class. We set up a “with end with” construct to manipulate the mfrm.Properties collection, then pass in the same two properties we had previously hooked. The difference now is that the cGI.mHookPrp will handle the hook and handle leaving the property alone if it already has a function hooking the property. Summary In this lecture we have discussed the problem with reusable code and classes, and a couple of different strategies for handling the problem. We then examined the strategy of a helper class which is used by every class that needs some common functionality. We created clsGlobalInterface, designed to implement a helper class, and provided a couple of methods, one for a consistent method of hooking object events as well as a method for emptying a collection. While there is no optimum solution to the common code problem in Access, the helper class is one solution that works well for staying with the class encapsulation strategy. It gives you a place to put common code and more importantly variables that are required for every class.