WithEvents Wizardry
Use WithEvents feature of MS Access 97 and VB 5.0/6.0 to implement reusable components dynamically attached to forms' and controls' events on run-time.

Shamil Salakhetdinov
e-mail:
shamil@marta.darts.spb.ru
All rights reserved. Terms of use.

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.

Contents

Sample form description

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.

Sample form standard implementation

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.

Reworking sample form using custom classes

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:

  1. Use event procedure assigned standard way to Form's Open event. This procedure creates a custom object instance, which is used to process form's and controls' events. (See clsDEEP1 custom class in sample database).
  2. Write a custom class, which gets form instance passed to it in Init procedure parameter or creates a form instance (object way) by itself. (See clsDEEP2 custom class in the sample database).

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 -

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 of textboxes. Using WithEvents and "differential event-driven programming" you can build specialized custom class implementing just this functionality or you can combine this custom class with navigation buttons functionality, or write two different custom classes and combine them by aggregation into parent DEEP-object etc. You can find by yourself a lot of very useful and sometimes tricky applications of this new programming style.

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:

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).

Termination Problem

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:


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
frmTerminator stores DEEP-Object reference in Termination Queue and sets TimerInterval to 1.

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.

Form Open Time

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…

Summary

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.
All rights reserved. Terms of use.

Last updated: June 4, 1999