Shamil Salakhetdinov
e-mail:
Written in November, 1998
This article was submitted to
one of the very famous magazines in
November 1998
and was accepted by its editorials that time. But after FIVE(!)
months of editions and e-mail
ping-pong I've got the following message from the managing editor: "I appreciate all of your
work on this article and am sorry that we can't
reach an agreement. The article needs
these changes in order to be suitable
for publication. Prior to your submission one of the
tech editors offered to
write on this topic, but Dana wanted to give you the opportunity.
It is a
difficult subject and I will let the tech editor write on this topic in a
future article..."
I expected this article had been printed in
February 1999 but it is published here only in June 1999.
It would be very sad if upcoming MS Access 2000 will have a bug which is
described in this article...
WithEvents feature of VBA is scarcely described in the public sources. All the descriptions I know usually qualify WithEvents as a feature used to get events fired by the server components to the client ones. In this article the opposite control flow direction is discussed, namely, events fired by forms and controls are processed by custom object instances dynamically attached (sunk) to forms' and controls' events. I call this concept of programming Dynamic External Event Processing (DEEP-concept) and the instances of custom classes used to support this concept - DEEP-objects.
Sample form used to discuss the main subject of this article has the following layout:
Figure 1. Sample form
Being opened the form has etched rectangle, one label, one command button and activated timer event firing every second to diminish counter value embedded in message text. The title of the form shows the form's Hwnd (form's window handle assigned to form on open) and the name of the class module used to implement form's functionality. When you click on rectangle's border its appearance is changed from etched to sunken. When you click on label's text its font's appearance is changed from normal to underlined italic. When you click on command button the message "Button [OK] clicked " is showed and the form closes itself. If you don't click [Ok] button form closes itself when the counter value becomes equal to zero.
Standard way to implement sample form is to use Code Behind Form (CBF) to react on the user's actions. This way of programming is well known and is described in a lot of publications. However I shortly describe here an advanced variation of this implementation method, which uses object method to open forms, to compare it later with the new one using "CBF-less" forms and DEEP-objects. The sample CBF code has detailed comments, so I write only a few comments in the text of the article to clarify some of the code lines. The name of the sample form with CBF is frmCBF. You can find it in the sample MS Access 97 database file samp4ww.mdb.
First of all I'd like to note that the code is split into several sections which facilitate code understanding and support. The section starts from apostrophe (VBA comment sign), followed by asterisk (*) and plus (+) sign and ends with apostrophe, asterisk and minus sign. In some cases, when the new section starts immediately after the current one, I don't use the end section line. In the article's text I've also dropped some of the end- section and comments lines to make the text shorter you can find full code in the sample database.
In the "Declaration" section (Listing 1) I define symbolic constants and private module level variables. They are rather trivial to be discussed in this article except one, which is called mobjSelfRef:
Private mobjSelfRef As Object
This variable is used to not let a form to be closed when it is opened object way. Before I proceed with my explanation how mobjSelfRef solves this problem I'd like to remind you shortly that a MS Access 97's form with CBF is a special kind of custom class module. You can open such form not only using DoCmd.OpenForm "frmMyForm", but also using several object methods. Here is the first method:
Form_frmMyForm.Visible = True
Having been executed this line opens one form instance. If you run it again while the first form instance is opened (or there is a frmMyForm opened by DoCmd) then it does nothing. When you use this method MS Access 97 creates global object variable somewhere in its internal structure which keeps form "alive" on sub exit. To close a form opened this way you can use DoCmd.Close or the following code line:
Set Form_frmMyForm = Nothing
If you create your own global object variable or a collection then you can open one or multiple form instances using second method:
Public gcolForms As New Collection gcolForms.Add New Form_frmMyForm gcolForms(gcolForms.Count).Visible = True
To close a form opened this way you have to remove the form's object reference from the global collection:
gcolForms.Remove
Second method is quite good but it needs to create a global object variable or a global collection. If you use a global collection you have to store in your program an index of the item of this collection corresponding to the form instance to close form instance by removing it from collection. (For example, you can store such index in form's hidden control). You cannot rely on DoCmd.Close acForm, "frmMyForm" - if you have multiple instances of the form opened in the same time, DoCmd.Close acForm, "frmMyForm" closes the first instance of the public Forms collection not the one having the focus.
And here is the third method to open a form instance object way:
Dim obj As Form_frmCBF Set obj = New Form_frmCBF obj.Visible = True set obj = Nothing
As you see it does not use global collection. What then keeps form "alive" when obj object variable is set to Nothing? The answer is - mobjSelfRef object variable which is set in Form_Open event procedure (see Listing 2 - "Form events" section):
Set mobjSelfRef = Me
Without such self-referencing form will be closed when its object reference is set to Nothing. You can try frmCBF~SelfRef sample form, which does not Set mobjSelfRef = Me on open and it isn't kept opened when its object reference is set to Nothing.
You can ask why I write here about different ways to open forms object way? Because, as you'll see later in this article, I use the similar to the third method technique/feature to create and "keep alive" multiple instances of custom objects. The only difference is that they do not use self-referencing object variable, they create and store in their private object variables object references of aggregated or referenced objects. These object variables instances do not let the parent (entry) custom object instance to be destroyed on sub exit. This is one of the rules of COM (Component Object Model) the foundation of all the modern apps and development tools running under MS Windows.
"Form events" section has nothing special to highlight, except custom method call mtdClose in Form_Timer event procedure used to close form when counter becomes equal to zero:
Public Sub mtdClose() Set mobjSelfRef = Nothing End Sub
It just sets mobjSelfRef to Nothing freeing-up form's object anchor and form closes. I'd like to repeat that DoCmd.Close acForm,"frmCBF" being executed closes the first form instance in Forms collection not the one having the focus. So I have to create and use frmCBF's custom method to close exactly the instance I want to close. The form to close can be in focus or not, does not matter, e.g., in the following code sample I close the last opened frmCBF form, if any:
Dim i As Integer If Forms.count >0 then For i = Forms.Count 1 To 0 Step -1 If Forms(i).Name = "frmCBF" Then Forms(i).mtdClose Exit For End If Next I End If
The third section is "Controls' events section" (see Listing 3). Again, nothing special and tricky in it, but I'd like to explain here the following non standard way to refer to the Form's lblMsg label control used in cmdOk_Click event procedure:
Form![lblMsg].Caption = strMsg
The standard way being:
Me![lblMsg].Caption = strMsg
I use Form object property of Me (form's CBF instance) object to make easier the process of CBF's code moving into custom classes.
The next section is "Custom properties" (see Listing 4). In this section you can see a problem which arises because of the fact that Form and its CBF are implemented using integrated typelib "Member identifier already exists in object module from which this object module derives". I cannot create custom property named TimerInterval form already has such a property by default. You can say that this isn't a big problem. Probably, but you have to be very careful assigning the names of the custom properties what if the future versions of MS Access will introduce such your custom properties as Form's default ones? Yes, you can use naming conventions prefixing all the custom properties but in my opinion such properties' names look a little bit ugly.
Property Let from "Custom Properties" section call BusinessRules validate sub from "Business Rules" interface section (see Listing 5). Business rules are simple and used to prevent setting TestDuration custom property less than 0 or greater than 30 seconds and to prevent setting FormTimerInverval less than 1000 and greater than 2000 milliseconds.
The last section is "Form custom commands" interface (see Listing 6). The form's CBF has two custom commands: mtdReset and mtdClose. The first resets FormTimerInterval and TestDuration custom properties and the second closes the form. Again I cannot use "Close" as the name of custom method ("Expected:identifier" compile time error), so I use "mtd" prefix recommended by LRNC.
Imagine now that I'd like to create another form having the same controls and functionality as frmCBF but different layout. And even more, I'd like to use, e.g., the functionality of frmCBF's "Form events" section or other its sections for other forms. Obvious solution is to create custom classes implementing part of frmCBF's functionality one custom class implementing one or a subset of frmCBF's sections. Would it be possible I could reuse them for many different types of forms and custom objects.
Well, it is easy - copy and paste frmCBF's sections in custom classes but how can I capture form's events and process them in external custom classes? Here is where WithEvents feature does its very useful work.
The description of the technique used to link to forms' and control's events from custom object instances of an MS Access 97's custom class is described in Marek Kepinski's article (December 1998 issue of Smart Access). My article (to be published in January's 1999 issue of Smart Access magazine) presents an advanced extension of WithEvents usage known as DEEP-Object Concept.
In short, to dynamically link to forms' and controls' events in MS Access 97 using external to form custom classes (DEEP-Objects) you have to do the following:
Here is an example:
Private WithEvents cmdOK As CommandButton Public Sub Init (ByRef rfrm as Form) Set cmdOK=rfrm![cmdOk] ' - the next line is used only in MS Access 97; ' - MSAccess97 is a custom project level compilation constant #If MSAccess97 then If cmdOK.OnClick <> "[Event Procedure]" then cmdOK.OnClick = "[Event Procedure]" End If #End If End Sub Private Sub cmdOK_Click() MsgBox "[OK] click processed in external custom object!" End Sub
As you see in the code above, this advanced WithEvents feature application can be used in both MS Access 97 and VB 5.0/6.0 a custom compile time constant MSAccess97 facilitates code porting to VB. Very tricky and useful code using WithEvents-enabled objects can be written also in MS Excel 97 and in MS Word 97.
Unfortunately, Open and Load form's events of MS Access form cannot be sunk. You cannot also sink Resize, Activate and Current events while form open procedure is in progress. This fact forces you to select one of the two possible solutions to set external event processing objects:
Both methods mentioned above use only one custom class to implement all the form's functionality. This seems to be not a big difference from the form's CBF the only advantage I get is that I can now reuse such custom classes with other forms, which implement the same functionality as frmCBF. But I want to get more modular solution I want to have the frmCBF's CBF functionality be split into smaller "bricks" each of which implements only part of frmCBF's functionality. So I use the CBF's sections described in the beginning of this article as these bricks prototypes. I move each section in a separate custom class. What I get as the result of this operation is presented on Figure 2.
Figure 2. Custom classes hierarchy built from CBF
On the left side you see custom classes hierarchy and on the right Form and its controls. MS Access 97's VBA does not support classical inheritance nor it supports Implements feature of VB 5.0/6.0 used to implement interfaces. So I use aggregation to implement custom classes hierarchy. Solid lines show "aggregation links" with upper class aggregating lower which it is linked to by solid line. In the sample database I implemented three aggregation levels to show that the presented technique can be easily generalized to any depth of aggregation hierarchy. Dash lines with arrows show "sink links" the class to which arrow points sinks the events of the form/control from which this line starts.
I don't use delegation. Instead of it in my sample custom classes I create object properties (Property Get) returning object references of their own aggregated object instances
Public Property Get ICmd() As Object Set ICmd = Children("ICmd") End Property
or object references of aggregated object instances of parent object:
Public Property Get ICmd() As Object Set ICmd = mobjParent.ICmd End Property
I mainly use late binding with custom classes to be able to move them freely "back-and-forth" between front-end and library databases.
All the component custom classes have several interfaces used to support hierarchical structure and inter- components communication. These are:
Each custom class has also a declaration section used to specify constants and plain, object and WithEvents variables. These variables and constants are encapsulated and can be accessed from other components through public properties and methods.
clsProperties (see Figure 2) custom class has two special interfaces:
You may say that dedicated Properties custom class is an overkill but in my opinion it isn't. Having been moved into special custom class, custom properties interface is easy to extend and to use in other custom classes. I think that a class module code shouldn't be more than 300-400 lines long to be manageable and easy to edit. I even plan to move Identity and Parent/Child and other generic interfaces I have in my apps into separate custom classes. Yes, this technique adds (at least) one additional level of indirection and hits application performance but this performance slowdown can be neglected comparing it with the flexible and easy-to-maintain application's object structure I get.
clsDEEP is an entry custom class. Its task is to get Parent's and Form's object references and to initialize itself and all its children. This task is performed in Init sub:
Public Sub Init(Optional ByRef robjParent As Object = Nothing, _ Optional ByRef rfrm As Form = Nothing)
First action is to store parent object reference in private object variable mobjParent:
Set mobjParent = robjParent
If parent isn't specified then this code line just sets mobjParent to Nothing. The next action is to get form's object references and to set a value of the private object variable named Form. Please remember that I used Form instead of Me in CBF. In custom classes Me refers to the object instance of this class. If I have used Me in CBF I have been forced now to reedit all the code moved into custom classes from CBF.
If Not rfrm Is Nothing Then Set Form = rfrm Else #If MSAccess97 Then Set Form = New Form_frmNoCBF1 #Else Set Form = New frmNoCBF1 #End If End If
To assign the value to the Form object variable I use input parameter rfrm. If it isn't Nothing it is assigned to Form, if it is Nothing then I create new instance of the form frmNoCBF1.
The next step is to create and initialize aggregated objects:
Set mobjChildren = New Collection Set mobjPrps = New clsProperties IPrp.Init Me, Form Children.Add New clsForm, "IForm" IForm.Init Me, Form Children.Add New clsFormCommand, "ICmd" ICmd.Init Me, Form Children.Add New clsBusinessRules, "IBRule" Children("IBRule").Init Me, Form Children.Add New clsTerminator, "Terminator" Children("Terminator").Init Me, Form
This code creates a new collection and assigns its reference to the mobjChildren object variable. Then it creates a new instance of clsProperties custom class and assigns its reference to the mobjPrps object variable. Next line:
IPrp.Init Me, Form
calls initialization sub of mobjPrps instance which is accessed through the object property IPrp:
Public Property Get IPrp() As Object Set IPrp = mobjPrps End Property
The following lines create and call initialization procedures of aggregated (children) objects. These objects do not have dedicated object variables in clsDEEP - they are added to the mobjChildren collection accessed through the object property named Children. They are also assigned with a mnemonic name (collection index) used to get their reference in the case they aren't associated with a special object property:
Children("IBRule").Init Me, Form
Now I perform the last steps -
With Form ' Init timer interval Me.IPrp.FormTimerInterval = gclngTimerInterval ' Init test duration Me.IPrp.TestDuration = gcintTestDuration ' store form's caption in .Tag If .Tag = "" Then .Tag = .Caption End If ' set form's caption .Caption = " - Class module -> " & mcstrModuleName End With
IForm.Form_Open intCancel
ICmd.mtdReset gclngTimerInterval, gcintTestDuration
If intCancel = False Then Form.Visible = True Else Terminate End If
What is important in this init procedure is the sequence in which aggregated objects's instances are created and initialized. As you see on the Figure 2 (dash lines with arrows) Form events are sunk by several custom classes, and lblMsg label control's events are sunk by two classes. The obvious questions you may ask when you see such a picture are:
Yes, the event procedures' call sequence is defined by initialization sequence but unfortunately this sequence is different for MS Access 97 and VB 5.0/6.0. For MS Access 97 it is:
For VB 5.0/6.0 it is:
This implementation difference isn't a big problem but you should take it into account when you plan to use the same custom classes for both MS Access 97 and VB 5.0/6.0.
What you get when you use several custom classes to sink events of the same form or control is a very useful and brand new for VBA programming style I call it "dynamic structured event-driven programming". Another variation of this new programming style can be called "differential event driven programming". Namely, you can do the following:
For example, almost every MS Access developer programs his/her own procedures to simulate
Limited space of magazine's publication does not let me to comment in details all the "atomic" custom classes presented on Figure 2 I built from CBF of frmCBF. I hope you find them quite easy to look through and understand. Here I comment slightly only two of them clsTimer and clsTerminator they are the most interesting in my opinion.
clsTimer custom class is interesting because from its implementation you can get the idea how to write classes which use different features of MS Access97 and VB but implement the same functionality. As it is known MS Access 97's Form object has Timer event which is used to implement asynchronous "time-fired" event procedures. VB's Form object does not have such an event VB has a dedicated control type called Timer. clsTimer custom class uses conditional compilation to activate one or another feature depending on the development tool:
#If MSAccess97 Then Private WithEvents Form As Form #Else Private Form As Form Private WithEvents timTimer As Timer #End If
Set Form = rfrm #If MSAccess97 Then Form.OnTimer = "[Event Procedure]" #Else Set timTimer = Form![timTimer] #End If
and in private events sections this code:
#If MSAccess97 Then Private Sub Form_Timer() #Else Private Sub timTimer_Timer() #End If
Of course, you can do it another way but what is important in my opinion and what I wanted to show by this code is that you can start to write cross-development tools code right now using MS Access 97 and VB 5.0/6.0. When custom events and Implements features will be available in MS Access (I guess in the next version) you'll be able to "move" clsTimer class one level deeper in your custom class hierarchy. (By the way, this is possible even with MS Access 97 you can rather easy simulate custom events and even implement asynchronous events but this is another story).
clsTerminator custom class is used to solve what I call "Termination problem". I was informed about first symptoms of this problem by Marek Kepinski who had found that when WithEvents Form object variable is set to Nothing (from within Form_Close event procedure), i.e. while form close procedure is in progress, MS Access 97 crashes. I had started to investigate this problem in details and I found that the obvious workaround (never set WithEvents Form object variable to Nothing) solves MS Access crash problem but does not solve the "memory leakage problem". You can find the graphical representation of the latter problem on Figure 3.
Figure 3. Termination problem
MS Access 97 and VB 5.0 do not release correctly memory allocated by a set of cross-referenced custom objects' instances when an entry object instance goes out of scope/is set to Nothing nor they call (some of) Class_Terminate event procedures of aggregated/entry object. On the Figure 3 you see the results of the tests which show Memory Usage of the two cases when you use Terminator class and when you don't use it. In the latter case after 1000 cycle of the test Form Open/Close you "loose" about 6MB of (virtual) memory (the test digits are received from Windows NT Task Manager, "Processes" page, "MS Access" Image Name - "Mem Usage" column intersection cell. The test code was stopped on breakpoint after every 100 cycles to write down current value of "Mem Usage").
The complete workaround which solves both MS Access crash and "memory leakage" problems is implemented by clsTerminator custom class, TerminateDEEP public sub and frmTerminator form. clsTerminator's object instance is sunk to the form's Close event. When this event fires clsTerminator calls Parent's (clsDEEP) Terminate procedure,
Private Sub Form_Close() Parent.Terminate End Sub
which in turn calls TerminateDeep public sub passing to it its own object reference - Me:
Public Sub Terminate() TerminateDeep Me End Sub
TerminateDEEP sub opens hidden frmTerminator form (if it was not opened before) and passes to it (marshals) the reference of the DEEP-Object instance to terminate calling its Terminate custom method:
frmTerminator stores DEEP-Object reference in Termination Queue and sets TimerInterval to 1.Public Sub TerminateDeep(ByRef robj As Object) #If MSAccess97 Then Dim frm As Form_frmTerminator On Error Resume Next Set frm = Forms("frmTerminator") If Err <> 0 Then Err.Clear DoCmd.OpenForm "frmTerminator", acNormal, _ , , , acHidden Set frm = Forms("frmTerminator") End If #Else Dim frm As frmTerminator On Error Resume Next Set frm = frmTerminator If Err <> 0 Then Err.Clear Load frmTerminator Set frm = frmTerminator End If #End If If Err <> 0 Then On Error GoTo 0 Err.Raise vbObjectError + 505, mcstrModuleName, _ "Unable to activate terminator form." End If On Error GoTo 0 frm.Terminate robj End Sub
Public Sub Terminate(ByRef robj As Object) mcolTerminationQueue.Add robj Form.TimerInterval = 1 End Sub
When all of the clsDEEP object instance's (event) procedures are finished, frmTerminator's Timer event fires (it is important to have all sink chains calls to be finished) calling Form_Timer event procedure, which processes Termination queue and calls DEEP-Object's DEEPDestroy method:
Private Sub Form_Timer() If mcolTerminationQueue.Count > 0 Then For i = mcolTerminationQueue.Count To 1 Step -1 Set obj = mcolTerminationQueue(i) mcolTerminationQueue.Remove i obj.DEEPDestroy Next End If End Sub
DEEPDestroy method calls Termination methods of all its children down to the hierarchy. As a result memory is released and Class_Terminate private event procedures of all the custom objects' instances are called.
This solution seems to be a little bit tricky but it works I run the crash test, which worked for fifteen hours opening and closing different implementations of event procedures including clsDEEP, which was called 100,000 times without any "memory leakage".
And in VB 6.0 Microsoft solved both problems! Hopefully the next release of MS Access will also be free of these problems I think that the roots of these problems aren't in MS Access 97 or VB 5.0 but in some COM's DLLs. This is my opinion. I can be wrong. Just guessing.
You, probably, have already asked yourself how much it affects, this DEEP-Object concept, the form open time. Well, it hits performance (see Figure 4) but not a lot you gain much more in code development and support processes than you loose in performance. Remember, that VBA does not unload code having been loaded once if you keep code in CBF, Form is loaded faster (tens of milliseconds faster as you can find on Figure 4 for sample forms and custom classes) than when you keep it out of CBF in external custom classes. But if you have a lot of forms in your application you'll find that during application session's lifetime performance slows down while you open more and more new types of forms. This is CBF which "steals" (virtual) memory
Figure 4. Form Open Time for different implementation methods.
I ran another crash test attaching more that 200 DEEP-objects to the form with about 200 controls it took about half a second (Pentium 166/64MB) to initialize and assign them as sinks. This test results showed me that I can neglect performance hit. Of course, having read this article and having run the tests from sample database you'll find by yourself is it a considerable performance hit or not and is it worth your efforts and time to start moving DEEP-Object way
DEEP-Object concept provides the foundation for developing MS Access 97's applications as a set of reusable object components. It can be used also to prototype/simulate multi-tier applications which are easily portable to "cousin" development tools VB 5.0/6.0. It gives the "high-start" to several new styles of VBA programming and it drafts an Object Access Development Framework/Technology. Being based on advanced features of Microsoft's COM technology, DEEP-Object concept can only grow and get more power with each new release of MS Windows, MS Access and VB. And I hope this concept will find its applications in other VB/VBA-based development tools.
Shamil Salakhetdinov is both the general- and software- manager of DARTS Ltd., St.Petersburg, Russia. Having been born in this mysterious wonderland and living all his life there he is still thinking that Russia is the country of the future - it was so yesterday, it is so nowadays, but it shouldn't be so forever... You can reach him by e-mail: shamil@marta.darts.spb.ru to try to break up this and other his dreams
Click to download sample database (samp4ww.zip - 169895 bytes)| HOME |
Copyright (c) 1998,1999 by Shamil Salakhetdinov.
|
| Last updated: June 4, 1999 | |
|
|