|
MICROSOFT ACCESS®
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 FactsThere 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 comboWithEvents
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
FunctionNotice 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
SubOf 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 ClassEnough 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 Class1. 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 DatabaseOption ExplicitDim WithEvents mcbo As
ComboBoxPublic
Function init(lcbo As
ComboBox) Set
mcbo = lcbo
mcbo.AfterUpdate = "[Event
Procedure]"End
FunctionPrivate
Sub mcbo_AfterUpdate() MsgBox "This is the combo After Update
event"End
Sub5. 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 DatabaseOption ExplicitDim fMySimpleCombo As
MySimpleCboPrivate
Sub Form_Open(Cancel As
Integer) Set
fMySimpleCombo = New
MySimpleCbo
fMySimpleCombo.init Combo0End Sub8. 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
WorksThat's all there is to
WithEvents! We built a simple class that dimensions a combo
control WithEvents.Dim
WithEvents mcbo As ComboBoxWe 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
FunctionFinally 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
SubIn the form's class
module we dimension a fMySimpleCombo class
variable.Dim fMySimpleCombo
As MySimpleCboThen 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 Combo0End SubWe 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 ClassNow 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
Class1. 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 DatabaseOption ExplicitDim WithEvents mcboRecSel As
ComboBoxDim mtxtRecID
As TextBoxDim mfrm As
FormPublic
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
FunctionPrivate
Sub Class_Terminate() 'clean up pointers to form and control
objects Set
mcboRecSel = Nothing Set mtxtRecID =
Nothing Set mfrm
= NothingEnd
SubSub
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 WithEnd Sub4. 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 = NothingEnd SubPrivate Sub Form_Open(Cancel As
Integer) Set
fMyRecordSelector = New
MyRecordSelector
fMyRecordSelector.init Me, cboRecSel,
txtRecIDEnd
Sub10. 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
DatabaseOption
ExplicitPrivate
WithEvents fclsMsg As
clsMsgPrivate Sub
Form_Close() Set
fclsMsg = NothingEnd
Sub Private
Sub Form_Open(Cancel As
Integer)
InitClsMsg Set
fclsMsg = gclsMsgEnd
SubPrivate Sub
txtSend_AfterUpdate() fclsMsg.Send "frm1", "frm2", "Just a test",
txtSend.ValueEnd
SubPrivate Sub
fclsMsg_Message(varFrom As Variant, varTo As Variant,
_
varSubj As Variant, varMsg As
Variant) If
varTo = "frm1" Then txtReceive =
varMsg End
IfEnd
Sub5. 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.ValueEnd
SubPrivate Sub
fclsMsg_Message(varFrom As Variant, varTo As Variant,
_
varSubj As Variant, varMsg As
Variant) If
varTo = "frm2" Then txtReceive =
varMsg End
IfEnd
SubSave 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
WorksPublic 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
FunctionFunction
SendSimple(varMsg As Variant) RaiseEvent
MessageSimple(varMsg)End FunctionThen
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 DatabaseOption ExplicitPublic gclsMsg As
clsmsgFunction
InitClsMsg()
Static blnInitialized As
Boolean If
blnInitialized = False Then Set gclsMsg = New
clsmsg
blnInitialized = True End IfEnd FunctionNotice
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
DatabaseOption
ExplicitDim WithEvents
fclsMsg As clsmsgIn 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 =
gclsMsgEnd
SubClose simply cleans up
behind us.Private Sub
Form_Close() Set
fclsMsg = NothingEnd
SubThe 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.ValueEnd
SubAnd 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
IfEnd
SubAnd 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.SummaryEvents 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
- 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.
- 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.
- Implement a NotInList handler class for
combos to open a form to edit the data in the table behind
the combo.
- 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/. |
 |
EXPERT ADVICE ON
BUSINESS TECHNOLOGY How To & What With |
ADVISOR.com |
|