ADVISOR   EXPERT ADVICE ON BUSINESS TECHNOLOGY
How To & What With
ADVISOR.com 
  
MICROSOFT ACCESS®
ADVISOR


BOOK EXCERPT
Beginning Access 2002 VBA: WithEvents and RaiseEvent
One of the best-kept secrets in Microsoft Access, WithEvents lets you handle an object's events inside classes other than the form classes. This book chapter explains what it can do for you.

By Ian Blackburn, John Colby, Mark Horner, Martin W. P. Reid, Robert Smith, David Sussman, Paul Turley, and Helmut Watson

ARTICLE INFO
MICROSOFT ACCESS ADVISOR GUIDE
Doc # 13276
5 November 2003
Length 24 pages


Back to standard article layout
This article is an excerpt from the book Beginning Access 2002 VBA from Wrox Press. For more information, visit http://www.wrox.com.

Events are the life-blood of Windows and of Access forms and controls. Responding to events is an integral part of Access development. You have already seen dozens of examples where we ran our own code on a button click event or a combo after update. In a sense, writing our own event handlers to make our forms or controls do useful things is one of the key differences between a developer and a power user. Access has many wizards that can build up command buttons and similar controls for us, but to be able to do that ourselves, to really understand event handlers, is the key to really controlling our applications.

WithEvents is one of the best-kept secrets in Access. Do a keyword search of WithEvents in any of the Access or VBA help files and you get nothing! If you find anything at all, it will be simply that it is a keyword -- no explanation of what it does, where or why you would use it. Start looking in all of the books in the bookstores. Most of them don't even mention the term anywhere. So what are they and why would we use them? Once you learn the how and why, you soon realize that handling events in classes is a tremendously powerful encapsulation tool, making it difficult to understand why they have been kept so quiet.

In Chapter 12 we learned how to build our own classes to encapsulate behaviors that would be useful to implement in multiple places in our databases. What if we could somehow marry classes and object events into a tightly integrated, completely encapsulated system that performed some useful behavior for us? Imagine being able to build a class that could cause a control to do exactly the same thing on any form we wanted. For example, we could build a class that allows us to handle the Enter and Exit events for textboxes. As the textbox gets the focus we could do something like change the font or font color or background color. That is the exact purpose of WithEvents -- handling an object's events inside of classes other than the form classes.

WithEvents requires us to think about events a little differently than we have before. Events have more power than we commonly believe and can be used in ways not understood by the average Access developer. In order to learn these things we need to do a little reviewing of what we currently understand, learn some new terms, and stretch our imaginations just a bit. The next couple of pages will require a little more concentration, but once you learn the terms and concepts, the actual implementation is simple. When we say simple, we mean that we only need to use about eight lines of code in a class module and five lines in the form's module, as shown below, to start using WithEvents.

Class code:

Private WithEvents mcbo As ComboBox

Public Function init(lcbo As ComboBox)
  Set mcbo = lcbo
  mcbo.AfterUpdate = "[Event Procedure]"
End Function

Private Sub mcbo_AfterUpdate()
  MsgBox "this is the combo After Update event"
End Sub

Form code:

Dim fclsCboSimple As clsCboSimple

Private Sub Form_Open(Cancel As Integer)
  Set fclsCboSimple = New clsCboSimple
  fclsCboSimple.init Combo0
End Sub

As you can see from the above code, the implementation is really simple, so bear with me. WithEvents provides the developer with considerable capabilities, and learning how to use this simple concept will literally move you to a new level of development ability.

In this chapter we will be looking at the following:

  • Reviewing events
  • What WithEvents is
  • Why we use WithEvents
  • How to build a simple class that uses WithEvents
  • How to build a Record Selector class that demonstrates reusability
  • How to have our class raise an event of its own!

Event Review, Some Terms and Facts

There are some terms that we will need to learn in order to get a handle on WithEvents, and be able to discuss event handling among ourselves. As you know, objects can cause events to happen. This process is called sourcing an event. One thing that may not be obvious is that an event acts very similar to a radio broadcast of a message. The following is a simplified version of what really happens.

An event occurs, which could be the click of a mouse. Windows takes that click and determines what program of all those currently running "owned" the piece of screen that the click occurred on, and notifies that program (Access in this case) that the click occurred, passing in a "handle to window" of the object clicked on. That program (for our purposes Access, but also Word or Excel, or almost any other Windows program) discovers which of its controls corresponds to that "handle to window" and notifies that control that the mouse clicked on it. Everything that happens from the click to the instant the event is broadcast is irrelevant to us. What we care about is that the control "broadcasts" its click event! And by broadcast we do mean broadcast. Any class inside of Access can (potentially) hear the event. It is important to understand the concept of an event being broadcast because it is possible (and even common) to handle an event in more than one class.

The process of "listening" or receiving program control from an object event is called sinking the event. Only classes can sink events. This is the really weird part of this whole "secret" thing -- only classes can sink events, and only the WithEvents keyword allows a non-form class to sink events. (And the WithEvents keyword isn't explained anywhere in all of Access documentation!) Form classes don't need the WithEvents keyword to sink events for objects physically placed on the form, but they will need to use the WithEvents keyword to sink events generated by any objects that cannot be physically placed on the form, such as DLLs, OCXs, and classes that can be directly referenced.

Up to this point, the only place you have handled events has been in the module of a form. It turns out that form modules are classes, and the event handlers that we create in these classes are also called event sinks. So an object sources events, and a class sinks events. An event handler and an event sink are just different terms for the same thing. As an interesting aside, the terms source and sink come from electronics where transistors source and sink current.

By the way, a class is an object, and as such it too can source an event! Yes, your own classes can raise (source) events that other classes in your application can sink. That is powerful stuff as we will see towards the end of this chapter.

But first let's review a little of what we already know. A combo on a form has many possible events that it can source; Click, GotFocus, AfterUpdate, and so on. Back in Chapter 3 we learned how to use the property box of a control to select an event property, then click the builder button (the ellipsis to the right of the property) and select code builder. This caused Access to do two things. First of all it built an event handler shell sub for us in the form's module. The empty event handler shell is also known as an event stub. It is called a stub because it contains only the Sub and End Sub lines with no real code inside of it. It also placed the text [Event Procedure] in the event property of the property sheet.

Having created the event stub, we can now place code into the sub that the code builder built for us. When that event fires for that object, program control is transferred to the event handler sub and our code begins to run.

An interesting and little known fact is that it is the very existence of the text [Event Procedure] in the object's event property that causes (or allows) Visual Basic to allow the event to fire. We can literally turn off the broadcasting of an individual event simply by deleting that [Event Procedure] text in the event property, and turn it back on by setting [Event Procedure] back in the property. Even if you have an event stub for a combo's click event (for example) in the form's module, simply delete the [Event Procedure] in the combo's Click event property and the event will never fire (source the event). As the event never even fires, the click event code will never run. Put [Event Procedure] back in the property and the event starts firing again. We will be using this little known fact to turn on an object's event broadcasting from right inside our classes from code in our class's Init event.

What is WithEvents?

WithEvents is a Visual Basic keyword, and is declared as below:

Private WithEvents mcbo As combo

WithEvents tells the class that an object will be sourcing events, and that we want to sink those events in this class. By adding the WithEvents keyword to a statement that would normally just declare mcbo As combo, the keyword is literally telling the class to dimension the object "with events". In other words, this object is capable of sourcing events and we will be sinking at least some of those events inside of this class. Once an object is declared using the WithEvents keyword, any or all of its events can be sunk (handled) in the class. Which you sink will depend entirely on which events you need to handle to perform the task.

Since only classes can sink events, the WithEvents keyword can only be used in classes. You cannot declare a control WithEvents in a normal module, or you will get a compile error when you attempt to compile the project.

Of course you are probably wondering how the class knows which combo to sink events for. The answer to that comes in the Init event we define for the class.

Public Function init(lcbo As ComboBox)
  Set mcbo = lcbo
  mcbo.AfterUpdate = "[Event Procedure]"
End Function

Notice that we are passing in a combo box to the function, and inside the init event we set our own mcbo to the lcbo passed in. Thus we have told the class to sink events for whatever combo is passed in. By the way, there is nothing magic about the name Init for the function; it can be whatever you wish. Simply understand that real classes are going to be more complex than the simple examples you have seen so far in this book and it is common practice to initialize all of the variables for the class in an Init statement immediately after setting the class variable.

Having dimensioned the combo WithEvents in the class and told the class which combo to sink events for, you can now create event handlers in the class, and control will be transferred to your event handler when the control fires that event.

Private sub mcbo_AfterUpdate()
  msgbox "this is the after update event"
End Sub

Of course you can sink as many of the object's events in the class as you wish. To keep things simple we are just showing you one for now.

An event handler that you write in your class is identical in every way to an event handler that the code builder writes. It must be private, it must be a sub, and it must use the name of the object sourcing the event (mcbo in this case) with an _ (underscore) and then the event property name (AfterUpdate). If the event passes in any parameters you must also recreate these parameters as well. The easiest way to get an event handler built is to simply place a control of the type you desire in a form, name the control exactly what you will call it in your class, open the properties box for that control, then use the code builder to build the event stub. Now cut the event stub out of the form's module and paste it into your own class. The code builder will also build any parameters that Access requires for that event. After a while you will get used to seeing the event stub and will be able to build your own. If you ever have problems though, just remember you can always fall back on Access to build one for you.

Once you learn and start using WithEvents you are going to discover all kinds of classes written by other developers that raise events that you can sink in your own classes. Want to zip and unzip files? Yep, there's a class out there and guess what, it raises events when it is done doing various things. You will learn all about raising events at the end of this chapter.

Why Use WithEvents?

Simply put, WithEvents allows us to sink events for objects in classes of our choosing instead of just in form modules. All of a sudden we can create a class that provides our project with all of those advantages that classes give us -- encapsulation of properties, methods, and behaviors (including object event sinks) -- but apply them to forms, controls, and other objects as well!

For example, in a project for an insurance company, the client wanted a set of 5 checkboxes to record properties of a claim. It could be a worker's compensation claim, car-related (an accident), an illness, an accident / injury, or a maternity claim. That's what they wanted! Thinking about it and discussing it with the client it became obvious that certain rules had to be applied, that is, that these couldn't just be clicked willy-nilly without affecting other check boxes. Some of the rules were that if it was an illness it wasn't an accident (and vice versa), if it was maternity, it was listed as an illness and (therefore) wasn't an accident (don't go there!), if it was workers comp it wasn't an illness but was an accident, and so on. So when the auto-related checkbox was checked the accident had to be checked and the illness had to be unchecked. If maternity was checked, then the accident and auto had to be unchecked and illness checked, and so on. Using WithEvents (the click event) for all of the checkboxes, we were able to encapsulate the entire set of rules into a single class that was then used on several different forms (see frmChkBoxes in WithEvents.mdb).

A form has its own class module, but even in this case you really don't want to place common functionality in each form's module. We can build a class to sink the form's events and have all form functionality that is common across all (or a set of) our forms encapsulated in that class. This makes it much easier to maintain, and one place to go to update the behaviors.

Controls on the other hand don't have a class of their own, which is one of the big drawbacks to Access controls! Suppose you wanted to have a common behavior for a combo's NotInList event. Perhaps you simply want the combo to inform the user that they can't edit the list, or perhaps you want to open a form that allows the user to edit the data behind the combo. Sure you can create functions that sit in a library to handle these things, but by wrapping them up in a class, you know exactly where to go to edit combo functionality, and of course you can move the class from project to project (or store it in a library!).

A couple of things to know about event sinks:
  • For physical objects (controls) on forms, you don't need the WithEvents keyword. To sink an object's events in any other class or even a non-control's events in a form's class module, you must use the WithEvents keyword.
  • If you have an event stub in a form class for a control event, that event stub will get control first, before your class.
  • It is possible to sink object events in multiple classes! You can literally sink an event for a control on form A over on form B! Or you can define a class that handles events for an object, and then create multiple instances of that class passing in a reference to the same object. Thus you can have one, five, ten, or a hundred instances of a class all sinking the click event for the same button, or the after update event of the same combo. This can actually be quite useful since your own classes can raise events and in fact we will show you an example of that later.
  • If you sink events in multiple instances, control passes to the classes in the order instantiated for Access 97, but in reverse order of instantiation for Access 2000 and Access 2002. What this means is that each class will eventually get control but the order that the classes gets control depends on the order that the classes are instantiated.
  • Visual Basic is a single-threaded language. Each event sink must finish processing before the next event sink can get control. If the code in the class sinking the event takes 30 seconds to do something, your program will appear to hang for thirty seconds before control passes on to the next event sink for that control's event. No other processing will occur anywhere in your program until the event handler finished its processing! This isn't a reason not to use events (or WithEvents), but it is a warning to make sure event handlers -- all event handlers everywhere -- finish what they are doing as rapidly as possible. If they are going to take very long, let the user know or you will have users rebooting their machine because it's "hanging".

The Simple Combo Class

Enough theory, let's have some fun. In order to demonstrate just how simple WithEvents really is, we are going to build a very simple class that handles the AfterUpdate event of a combo box. Don't get too excited though; all this class will do for us is put up a message box when the after update fires, but that is enough to demonstrate the process. It also allows you to see exactly how simple this stuff really is. Once we understand what we are doing and how and why it works, we will then move on to a slightly more complex combo class that is truly useful -- a combo record selector. Ok, let's get to work.

Try It Out: Creating the Simple Combo WithEvents Class

1. Open up the IceCream.mdb database and switch to the VBA IDE by hitting Alt+F11.

2. Insert a new class module. You can do this either by selecting Class Module from the Insert menu or by hitting the Insert Class Module button on the toolbar.


3. A new class called Class1 should now appear in the Project Explorer window. If the Properties window is not visible, make it so by hitting F4 and then change the name of the class to MySimpleCbo.


4. Now, in the code window, add the following code to the class module. Then save the class module.

Option Compare Database
Option Explicit

Dim WithEvents mcbo As ComboBox

Public Function init(lcbo As ComboBox)
  Set mcbo = lcbo
  mcbo.AfterUpdate = "[Event Procedure]"
End Function

Private Sub mcbo_AfterUpdate()
  MsgBox "This is the combo After Update event"
End Sub

5. Next, switch to the database window and create a new blank form in design view. Add a combo control to the form. The default combo name will be Combo0.

6. Now open the properties box for the combo, set the row source type to Value List, and set the row source to 1;2;3. This will provide us some simple data for the combo, so that the AfterUpdate event can be generated.


7. Save the form as frmSimpleCombo. Open the module for the form and insert the following code:

Option Compare Database
Option Explicit

Dim fMySimpleCombo As MySimpleCbo

Private Sub Form_Open(Cancel As Integer)
  Set fMySimpleCombo = New MySimpleCbo
  fMySimpleCombo.init Combo0
End Sub

8. Close the form, and then reopen it. Drop down the combo and select one of the numbers. A message box will pop up saying "This is the combo After Update Event".


How It Works

That's all there is to WithEvents! We built a simple class that dimensions a combo control WithEvents.

Dim WithEvents mcbo As ComboBox

We built an Init function that we will use to initialize the class, which tells the class exactly which combo we want to respond to. This Init function stores the pointer (a pointer is a variable that holds a memory address that allows you direct access to the data held in that address) of the combo passed in to the combo variable that we declared using WithEvents in the class header. Remember we also said that simply setting the object's event property to [Event Procedure] causes the object to start sourcing the event, so in order to make sure that the combo is sourcing the AfterUpdate, we simply use code to set the control's AfterUpdate property to [Event Procedure].

Public Function init(lcbo As ComboBox)
  Set mcbo = lcbo
  mcbo.AfterUpdate = "[Event Procedure]"
End Function

Finally we build an event stub for the event we want to sink - the AfterUpdate in this case.

Private Sub mcbo_AfterUpdate()
  MsgBox "this is the combo After Update event"
End Sub

In the form's class module we dimension a fMySimpleCombo class variable.

Dim fMySimpleCombo As MySimpleCbo

Then in the form's Open event we set our class variable to be equal to a new instance and initialize the class instance.

Private Sub Form_Open(Cancel As Integer)
  Set fMySimpleCombo = New MySimpleCbo
  fMySimpleCombo.init Combo0
End Sub

We then saved the form, closed it and reopened it, so that when we select something from the combo the combo's AfterUpdate event fires. The biggest difference between what you already knew and what we have learned in this chapter is that the event is sunk in our own class - fMySimpleCombo instead of in the form's module as we would normally expect to happen. Notice that nowhere in the form's module is there any event sink for the combo, but we still get the event to perform work for us.

Think about this... if we had another form (or simply another class), and we dimensioned the same class in that form, but we passed in the combo form's (frmSimpleCombo) Combo0... event what do you think would happen? That is correct, the class on that other form would sink the event for the combo on frmSimpleCombo. In effect you can spy on a control on a completely different form using this technique. We have no idea why you would want to do that but if you ever did, now you know how!

The Record Selector Class

Now that you have seen how simple it is to use WithEvents, it's time to build a class that is actually quite useful, amazingly simple, and yet clearly demonstrates the whole reusability thing.

The record selector class will take control of a combo's AfterUpdate event and use the event to find the primary key (PK) of a record, and use the PK to find and display that record in the form. In other words use a combo to select a record for display. Once we have this class working you will discover that we can then use the class on virtually any form that displays data from a table, such as frmIceCream, frmIngredients, frmCompany, and so on, as long as it uses a single field PK. From that we can clearly see the reusability and maintenance advantage of doing this in a class rather than with functions scattered in a library, or, even worse, in code scattered throughout your forms.

Try It Out: Creating the Simple Combo WithEvents Class

1. Open up the IceCream.mdb database and switch to the VBA IDE by hitting Alt+F11. Insert a new class module.

2. Change the name of the class to MyRecordSelector using the properties window.

3. Now, in the code window, add the following code to the class module. Then save the class module.

Option Compare Database
Option Explicit

Dim WithEvents mcboRecSel As ComboBox
Dim mtxtRecID As TextBox
Dim mfrm As Form

Public Function init(lfrm As Form, lcboRecSel As ComboBox, ltxtRecID As TextBox)
  Set mfrm = lfrm
  Set mtxtRecID = ltxtRecID
  Set mcboRecSel = lcboRecSel
  mcboRecSel.AfterUpdate = "[Event Procedure]"
End Function

Private Sub Class_Terminate()
  'clean up pointers to form and control objects
  Set mcboRecSel = Nothing
  Set mtxtRecID = Nothing
  Set mfrm = Nothing
End Sub

Sub mcboRecSel_AfterUpdate()
Dim strSQL As String
 
  'BUILD AN SQL STATEMENT
  strSQL = mtxtRecID.ControlSource & " = " & mcboRecSel
  With mfrm
    ' Find the record that matches the control.
    .RecordsetClone.FindFirst strSQL
    'SET THE FORMS BOOKMARK TO THE RECORDSET CLONES BOOKMARK
    '("FIND" THE RECORD)
    .Bookmark = .RecordsetClone.Bookmark
  End With
End Sub

4. Switch to the database window and open frmCompany in design view. Add a combo and a textbox to the form in the form header section.


5. Open the properties sheet for the textbox and set the Name property to txtRecID. Then set the Control Source to CompanyID.

6. Select the label for the combo and change it to display Select Company. Then view the properties sheet for the combo box and change the Name property to cboRecSel. Set the Row Source property to:

SELECT tblCompany.CompanyID, tblCompany.CompanyName FROM tblCompany ORDER BY tblCompany.CompanyName;

7. On the Format tab of the property sheet, set the Column Count property to 2 and the column widths to "0cm;1cm".

8. Open the form's code module and place the following code in the header:

Dim fMyRecordSelector As MyRecordSelector


9. Next, go to the bottom of the form's code module and insert the following code into the form:

Private Sub Form_Close()
  Set fMyRecordSelector = Nothing
End Sub

Private Sub Form_Open(Cancel As Integer)
  Set fMyRecordSelector = New MyRecordSelector
  fMyRecordSelector.init Me, cboRecSel, txtRecID
End Sub

10. Save the form, close it, and reopen the form.

11. Select a company name in the record selector. The class module we just created will sink the combo box's AfterUpdate event. Using that event the class module will run code that causes the form to find and display the record for the company selected in the combo box.

How It Works

This time we pass into the class Init function a reference to the form, the combo, and the textbox. Notice that only the combo is dimensioned WithEvents. We will not be sinking any events for the form or the textbox; in fact the only event we will sink is the combo's AfterUpdate event.

Dim WithEvents mcboRecSel As ComboBox
Dim mtxtRecID As TextBox
Dim mfrm As Form

Public Function init(lfrm As Form, lcboRecSel As ComboBox, ltxtRecID As TextBox)
  Set mfrm = lfrm
  Set mtxtRecID = ltxtRecID
  Set mcboRecSel = lcboRecSel
  mcboRecSel.AfterUpdate = "[Event Procedure]"
End Function

Once again we save the object pointers passed into the class global variables for the corresponding objects. And of course, we set the mcboRecSel.AfterUpdate = [Event Procedure] so that we know that event sourcing is enabled by the record selector AfterUpdate event.

The terminate event cleans up the pointers to the objects.

Private Sub Class_Terminate()
  'clean up pointers to form and control objects
  Set mcboRecSel = Nothing
  Set mtxtRecID = Nothing
  Set mfrm = Nothing
End Sub

The combo box's AfterUpdate event sink is where all the action is. As you can see, however, it isn't complicated. Basically we build a SQL statement that we will use to find the record in the form. The control source of the txtRecSel control on the form tells us what the field name is for the PK of the table. The cboRecSel value will be the PK of the record selected. We use these two values to construct our SQL string.

Then with the mfrm object, we use the RecordsetClone.FindFirst method to find the record using the SQL statement we created in the step above. Having found the record, we now set the form's bookmark equal to the RecordsetClone's bookmark. That step causes the form to display the record found.

Sub mcboRecSel_AfterUpdate()
Dim strSQL As String
 
  'BUILD A SQL STATEMENT
   strSQL = mtxtRecID.ControlSource & " = " & mcboRecSel
   With mfrm
    ' Find the record that matches the control.
    .RecordsetClone.FindFirst strSQL
    'SET THE FORMS BOOKMARK TO THE RECORDSET CLONES BOOKMARK ("FIND" THE
    'RECORD)
    .Bookmark = .RecordsetClone.Bookmark
  End With
End Sub

All in all the process is remarkably simple. The nice part though is that you can now add this record selector functionality to any form simply by copying the two controls into any form that displays records from a table or query and which has an AutoNumber PK. Set the SQL statement of the combo to use data from the new form's RecordSource (table / query) and bind txtRecID to the PK field of the RecordSource. Dim the class in the form's header, initialize the class in the form's Open event, and destroy the class in the form's Close event. It is very quick and easy to add this functionality to the next form that you want to have a record selector.

RaiseEvent

So we have now seen how objects such as controls on a form (or a form itself) can source events, and we have discovered how to sink those events inside our own classes using the WithEvents keywords. But how can we create our own events and why might we want to?

The RaiseEvent keyword is the answer to how. The why is just a matter of imagination. Let us give you an example of how we used RaiseEvent one time to get your imagination cranking.

We needed to interface a bar code reader to an Access database and we needed ironclad control over the process so we decided to use a serial port bar code reader.

The bar code reader we selected connected to a COM (serial) port and transmitted a string of characters with a Carriage Return terminator at the end of the bar code. In order to use this however we needed to somehow "talk to" the serial port. We discovered that Visual Basic (that is, Visual Basic such as Visual Basic 6, not VBA) comes with a ComCtl OCX, a control that can be dropped on a form, and which can monitor a single serial port. The ComCtl OCX raises events that basically tell us that a character or set number of characters have been received on the COM port.

In order to provide black box encapsulation and a simple interface to the database system, we created a class that could initialize the baud rate, start and stop bits, and other parameters of the ComCtl OCX, turn on and off receiving data on the COM port, as well as sink the events that the ComCtl OCX generated (using WithEvents of course!).

The problem though is that we wanted the ComCtl and its class to deal only with the data coming from (and going to) the serial port, not deal with the bar coding stuff itself. In other words, we wanted to be able to use that same class for any application that needed to talk to the COM port, not build a class that could only interface the COM port to a bar code reader. Done correctly we would have a ComCtl class that could be used for interfacing a bar code reader to one system, while the exact same class could interface a serial modem to another system, or a gauge multiplexer to a different system.

We could then build a second class that understood bar code stuff and could "listen" for messages from my ComCtl class saying that data had arrived. The ComCtl class handled the port, by setting it up, getting the data from the ComCtl OCX and broadcasting a message that data had arrived, while my bar code class listened for messages saying that data had arrived and processed the data however it wanted to.

In order to do that we needed our ComCtl class to be able to raise its own events when it was finished getting data from the ComCtl OCX. It turns out this is absolutely trivial by using the RaiseEvent keyword!

A Simple Example

In order to keep things simple we're not going to use the ComCtl class. What we will do instead is create a class that can broadcast messages for other classes. Once we have that working we will set up two forms that can talk to each other using this message class. So, when we type in a textbox on FormA, it will appear on FormB. Type in a textbox in FormB and it will be displayed on FormA. This will all be done by using RaiseEvents in our Message class (the event source) and WithEvents in our forms (the event sinks). All this in two lines of code and it will roll into one everything we have learned in this chapter. By the way, we actually use this class in our own systems to solve some tricky inter-form communications problems.

Try It Out: RaiseEvent example

In order to save space and time, we have created a database called WithEvents.mdb to demonstrate the concepts. Let's get started.

1. Create a new class module and enter the following code, then save it, calling it clsMsg:

Option Compare Database
Option Explicit

Public Event Message(varFrom As Variant, varTo As Variant, _
                     varSubj As Variant, varMsg As Variant)
Public Event MessageSimple(varMsg As Variant)

Function Send(varFrom As Variant, varTo As Variant, _
              varSubj As Variant, varMsg As Variant)
 
  RaiseEvent Message(varFrom, varTo, varSubj, varMsg)
End Function

Function SendSimple(varMsg As Variant)
  RaiseEvent MessageSimple(varMsg)
End Function

2. Now insert another module and enter the following code:

Option Compare Database
Option Explicit

Public gclsMsg As clsmsg

Function InitClsMsg()
Static blnInitialized As Boolean
  If blnInitialized = False Then
    Set gclsMsg = New clsmsg
    blnInitialized = True
  End If
End Function

Save the module as basInitMsg.

3. Now create two forms frm1 and frm2, each with just two textboxes in each form. Call one of the textboxes txtSend and the other txtReceive. Change the caption property of each box to Send Message and Receive Message respectively:


4. Open the code view for frm1 and add the following code:

Option Compare Database
Option Explicit
Private WithEvents fclsMsg As clsMsg

Private Sub Form_Close()
  Set fclsMsg = Nothing
End Sub
 
Private Sub Form_Open(Cancel As Integer)
  InitClsMsg
  Set fclsMsg = gclsMsg
End Sub

Private Sub txtSend_AfterUpdate()
  fclsMsg.Send "frm1", "frm2", "Just a test", txtSend.Value
End Sub

Private Sub fclsMsg_Message(varFrom As Variant, varTo As Variant, _
                            varSubj As Variant, varMsg As Variant)
  If varTo = "frm1" Then
    txtReceive = varMsg
  End If
End Sub

5. The code for frm2 is almost identical with just the following lines altered:

...

Private Sub txtSend_AfterUpdate()
  fclsMsg.Send "frm2", "frm1", "Just a test", txtSend.Value
End Sub

Private Sub fclsMsg_Message(varFrom As Variant, varTo As Variant, _
                            varSubj As Variant, varMsg As Variant)
  If varTo = "frm2" Then
    txtReceive = varMsg
  End If
End Sub

Save both the forms and close them.

6. Now, reopen frm1 and frm2 in Form view. In the Send Message box on frm2 type in anything you want. Notice that as soon as you press Enter the message is immediately displayed on frm1. Likewise click into the Send Message textbox on frm1 and type in anything you want. Notice that it is immediately displayed on frm2.


How It Works

Public Event Message(varFrom As Variant, varTo As Variant, _
                     varSubj As Variant, varMsg As Variant)
Public Event MessageSimple(varMsg As Variant)

We declare two different public events, Message and MessageSimple. The Message event has full to / from / subject / message parameters whereas the MessageSimple has only a single simple message parameter.

Function Send(varFrom As Variant, varTo As Variant, _
              varSubj As Variant, varMsg As Variant)
  RaiseEvent Message(varFrom, varTo, varSubj, varMsg)
End Function

Function SendSimple(varMsg As Variant)
  RaiseEvent MessageSimple(varMsg)
End Function

Then there are two methods of the class, the Send and the SendSimple. Notice that each method accepts the same parameters as the event declared in the header and just passes them on to the RaiseEvent inside of the method.

We need to have a global variable for the message class as well as an init function, all this is stored in the basInitMsg module. This will allow us to have a single class instance that gets and sends messages, and everybody that uses the class will just get a pointer to this global variable.

Option Compare Database
Option Explicit

Public gclsMsg As clsmsg

Function InitClsMsg()
  Static blnInitialized As Boolean
  If blnInitialized = False Then
    Set gclsMsg = New clsmsg
    blnInitialized = True
  End If
End Function

Notice that InitClsMsg has a static boolean flag that stores whether or not we have already initialized the class so that we only do it once. This allows many different forms or classes to call the Init so that they can be set up in any order and only the first one that calls the function actually performs the initialization.

Finally the two demo forms, frm1 and frm2, both have pretty much identical code. Notice that we dimension a fclsMsg variable inside the form using the WithEvents keyword. This tells the form's class that the fclsMsg object will be sourcing events and we want to sink those events inside of this (the form's) class.

Option Compare Database
Option Explicit
Dim WithEvents fclsMsg As clsmsg

In the Open event we call the InitClsMsg function, which will initialize the message class if that has not been done yet, and then we get a pointer to that global message class.

Private Sub Form_Open(Cancel As Integer)
  InitClsMsg
  Set fclsMsg = gclsMsg
End Sub

Close simply cleans up behind us.

Private Sub Form_Close()
  Set fclsMsg = Nothing
End Sub

The txtSend textbox is used to transmit messages so in the AfterUpdate we call fclsMsg's Send method, passing a string saying who we are (frm1), who we are sending the message to (frm2), what the message is about (Just a test), and the message itself which is the value of the txtSend (whatever was typed in).

Private Sub txtSend_AfterUpdate()
  fclsMsg.Send "frm1", "frm2", "Just a test", txtSend.Value
End Sub

And finally... we sink the Message event that fclsMsg raises. Notice that every message sent on this message channel will cause this event to fire, even the messages that we send. Since that is the case, we need to only listen to or look for the messages sent to frm1 (if we are looking at the code in frm1). Obviously frm2 would only look for messages sent to frm2. When a message comes in with our name in the varTo parameter we place it into the txtReceive and the message will be displayed.

Private Sub fclsMsg_Message(varFrom As Variant, varTo As Variant, _
        varSubj As Variant, varMsg As Variant)
  If varTo = "frm1" Then
    txtReceive = varMsg
  End If
End Sub

And that's it!

One other thing to think about is that the messages don't have to be text. Because we are passing into variant parameters, the message could be a number, string, even a pointer to a word document or a recordset. As long as the receiving class knows what is coming and how to use it, it can be passed over this message channel. And of course, your class can simply raise its own event directly, it doesn't need to use a message channel like this.

And finally, notice that using WithEvents we don't have the problem of having to have the recipient there to take the message. We could have done this inter-form communication simply by writing code that poked the values directly into the textbox on the other form. But what happens if the other form is closed? Well, we would get a run-time error. Using WithEvents / RaiseEvent, if the other form is closed nothing happens. Again, we can build a system that sends a message without even having the receiver loaded. Clean, encapsulated, and doesn't depend on the existence of the matching interface to work. "I've done my part, if anyone is interested here's the data."

You might be scratching your head asking when you would ever really use this message class… maybe never. This was really just intended as a simple demo without any clutter to demonstrate how WithEvents and RaiseEvents work together to form a complete system. Furthermore it demonstrates that more than one class can sink an event! You could have one or a hundred forms watching the message channel and all of them will receive the message. Which one, if any, actually uses the message is up to you. In fact you could have a class that isn't even on a form watching the message channel and doing something when it gets a message. My bar code processor is a classic example of that.

On the other hand, imagine a form that is hidden when open. Its purpose is to watch the free disk space (using its timer event, once an hour) and transmit a message to the database application when the space gets too low. The form's class simply sends a "Low Disk" message using the message class above. An error logger class that logs the disk usage in a table is sinking events from the message class and when it sees a message addressed to itself, logs the problem and generates an e-mail to the network administrator warning them that they need to free up some disk space before the database fails.

The point is that we are looking at classes as black box systems with complex internal workings but a simple interface. WithEvents and RaiseEvent allow us to communicate directly from one object to another, or from one object to many other objects if that is what we need. Each class (object) doesn't need to know about who will use the event, nor how the event will be used (or even if it will be used). The object simply does its job and raises an event saying "I have finished my task", possibly passing parameters as well. Whether anyone is listening at the moment or using the event to trigger other actions is entirely irrelevant to the object that raises the event. This is no different from a combo box that has a dozen or more events. The combo is capable of raising its events regardless of whether anyone uses them. If no one is sinking those events, no problem, but if someone needs them they are there. Thus the combo doesn't know, nor care, who may be listening to its events. Furthermore the combo doesn't know, nor care how any of its events will be used.

Summary

Events are vitally important to Windows and Access. As developers we quickly learn how to build event handlers in forms for the various form and control events we use to customize our forms and, by now, events are probably old hat.

What we have done in this chapter is taken event handling to the next level and shown you how to sink events directly in a class of your own making. This ability combined with your evolving knowledge of classes will undoubtedly make you a better programmer. Class objects allow you to encapsulate behaviors and properties for objects so that you can use these behaviors throughout your system with just a few simple lines of code to instantiate the class.

We have learned how to:

  • Use the keyword WithEvents to allow our class to sink events for an object.
  • Build an init procedure to accept references to objects and save them in a class's local variable.
  • Build the event sink itself and apply logic to cause the event sink to perform some action.
  • Hook the class into a form, initialize in Open, and clean up in Close.
  • Raise an event of our own in order to signal to a listener that something has happened, passing parameters to the listeners if we need to.

As you've seen, this is mostly just an extension of information you learned in earlier chapters. You already know how to handle events generated by objects, and how to build classes. This chapter simply put the two concepts together and showed how to handle events in any class you want rather than only in form classes. However it truly is a revolutionary step for most developers, giving them the power to do things that simply weren't possible before.

Exercises
  1. Add the record selector controls and class to frmIceCream and frmIngredients. This will demonstrate to you the value of reusability and how easy it is to get a coherent look and feel across your forms using your new class.
  2. Think about other ways you could use WithEvents to encapsulate functionality into a class for use in your projects. Add an event handler for the record selector's combo box to sink the NotInList and display an error message to the user that the data they tried to find in the combo isn't a valid record. This will replace the annoying "the text you entered isn't an item in the list" with a more user-friendly message.
  3. Implement a NotInList handler class for combos to open a form to edit the data in the table behind the combo.
  4. Notice that although the combo control does select a record, it doesn't stay "synched" with the form if you page down through records in the form. Add a public method to the combo's class to set the combo equal to the PK in the textbox and call that method from the form's Current event so that the combo stays synched to the form.

Portions of ADVISOR NETWORK and this Web site copyright ®1983-2005, this article copyright ©2003 ADVISOR MEDIA, Inc. unless otherwise stated. All Rights Reserved. ADVISOR® is a registered trademark of ADVISOR MEDIA, Inc. in the United States and other countries. For ADVISOR NETWORK Terms of Use see http://advisormedia.com/Legal/.
ADVISOR  EXPERT ADVICE ON BUSINESS TECHNOLOGY
 How To & What With
ADVISOR.com