Programming WithEvents
Marek Kepinski
By using the WithEvents keyword,
you can extend the possibilities of Access's classes. Marek presents new
ideas on programming in Access.
In the Access 97 Developer's Handbook by Litwin, Getz, and Gilbert
(Sybex), there's an example of how to use the WithEvents keyword with OLE
Automation objects like Microsoft Word. It turns out that this just scratches
the surface of what can be done with the WithEvents keyword. Generally,
there are six areas where the WithEvents keyword can be used:
-
The creation of classes with reusable functionality
-
"Listening" to some object's events
and responding to them
-
Direct communication between Access controls
(outside forms' modules)
-
Communication between forms and their controls
in Access (including subforms and ActiveX controls)
-
Communication between Access applications and
other applications that support OLE Automation (Excel, Word, PowerPoint,
Outlook, and so on). See the sidebar "
Excel-ent
Events
" for an example of the kinds of
events that you can intercept in this scenario.
-
Dynamic attachment of events to controls
In this article, I'm going to provide some simple examples of the first
three areas. I'm going to start with a simple example that shows how you
can create a reusable class module that responds to a form's events. The
first step is to insert a new class module (which I've called cTextBox in
this example) into an Access 97 database and then add the following code:
Option Compare Database
Option Explicit
Private WithEvents mTextBox As TextBox
Private Sub mTextBox_Exit(Cancel As Integer)
MsgBox "You're in the Exit event of " & _
"the cTextBox class."
End Sub
|
The objects that are created from this Class module will respond to the
Exit event of any TextBox control, in any form in your database. To use
the object, you'd create a form with two TextBox controls -- I'll call them
txtText1 and txtText2. What I'll do now is have the routines in the cTextBox
class intercept the events fired by these two text boxes. To do that, I
have to add some code to the class to accept a reference to a text box.
That code looks like this:
Public Property Set Control _
(ByVal ctlNewControl As TextBox)
Set mTextBox = ctlNewControl
End Property
|
This routine will define a property that accepts a TextBox control. The
property is called Control, and it sets the mTextBox variable to point to
the TextBox passed to the routine. I can now use the cTextBox object in
my form as follows:
Dim cTB1 As cTextBox
Dim cTB2 As cTextBox
Private Sub Form_Load()
Set cTB1 = New cTextBox
Set cTB2 = New cTextBox
Set cTB1.Control = Me!txtText1
Set cTB2.Control = Me!txtText2
End Sub
|
As you can see, there are just four lines of code in the Form_Load event.
These lines first create a copy of the cTextBox object for each text box
on the form. The lines then tie each text box to one of those cTextBox objects.
I then save the form, open it in the Form View, and try to tab out of either
of the text boxes. When I do, the message "You are in the Exit event
of the cTextBox class" appears.
What does this mean? It means that, with the WithEvents keyword, it's possible
to create Class modules that contain standard code for working with form
controls and reuse them in different Access projects. You can even place
these Class modules in a library database so that the code can be reused
without having to cut and paste the Class modules from project to project.
The preceding example is very simple, but it can be extended easily. I'll
add this code to my cTextBox object:
Public Property Let Length(ByVal intNewLength As Integer)
intLength = intNewLength
End Property
Private Sub mTextBox_KeyPress(KeyAscii As Integer)
If Len(mTextBox.Text) >= intLength Then
Beep
KeyAscii = 0
Exit Sub
End If
End Sub
|
With this code, I've created a property called Length for my object. The
Length property is used to set the value of the intLength variable. I also
have a KeyPress event that fires every time the user presses a key in the
text box. The code in the KeyPress text box checks the length of the Text
property in the form and sees whether it exceeds the length specified in
the intLength variable. If it does, the event sounds a beep and exits the
routine after setting the KeyAscii parameter to 0. Setting the KeyAscii
parameter to 0 causes the last keypress to be discarded.
Put it all together and my cTextBox object allows me to specify a maximum
length for the text entered into my text boxes. Should the user try to type
more characters, he or she will get a warning beep and be prevented from
doing so. The only change I have to make in my form is to set the new Length
property of the cTextBox object:
cTB1.Length = 6
cTB2.Length = 4
|
Listening to forms
For my next example, I'm going to create
an object that captures the events fired by a form. I'll begin by saving
the following code as a class module called cFormListener:
Private WithEvents frm As Form
Public Sub BindForm(NewForm As Form)
Set frm = NewForm
End Sub
Private Sub frm_Activate()
MsgBox "You are in the frm_Activate event " & _
"of the cFormListener class.", vbInformation
End Sub
Private Sub frm_Resize()
MsgBox "You are in the frm_Resize event of " & _
" the cFormListener class.", vbInformation
End Sub
|
This object has a BindForm method that accepts a Form object variable. The
cFormListener object's frm variable is then set to the Form that's passed
to this method. As a result, the frm_Activate and frm_Resize routines will
now execute when the Form passed to the BindForm method fires its Activate
or Resize events.
To test the cFormListener object, I'll create a form with its BorderStyle
property set to Sizable and put the following code in it:
Dim FListener As cFormListener
Private Sub Form_Open(Cancel As Integer)
Set FListener = New cFormListener
FListener.BindForm Me
End Sub
|
This time I used the Form_Open event to create a cFormListener object and
then bind the Form to the object using my BindForm method. Now when I try
to resize the form, I'll get the message boxes that show that the cFormListener
is responding to my form. I could use this technique to create a reusable
Class that would be able to resize any form. To tie the object to the form,
I'd need only two lines of code (one of which is the declaration of a variable).
Communicating between controls
For my final example, I'm going to create
an object that automatically passes information from one control to another.
The cTwoObject object allows you to bind one control to another. I'm going
to begin this example with the form that uses the object (see Figure 1
).
The form consists of one command button (called cmdTest1) and one option
group with two option buttons (called fraOption1). The form's code module
contains the following code:
Option Compare Database
Option Explicit
Dim cTwoCtl As cTwoControls
Private Sub Form_Load()
Set cTwoCtl = New cTwoControls
Set cTwoCtl.BindCommandButton = Me!cmdTest1
Set cTwoCtl.BindOptionGroup = Me!fraOption1
End Sub
|
In this form, clicking on the Command button causes the option group to
change, and selecting one of the options in the group changes the Command
button. The code that does this is in the cTwoControls object, outside the
form's module.
The code in the cTwoControls object can be found in
Listing
1
. The code creates BindCommandButton and a
BindOptionGroup property that accept a reference to a Command button and
an Option group. The two event routines in the listing are fired when either
the Command button is clicked or the Option group is updated. In the Command
button code, the Mod operator causes the option group to toggle between
its two settings. In the Option button code, the caption on the Command
button is swapped between bold and normal.
Listing 1: The code for the cTwoObjects object.
Option Compare Database
Option Explicit
Private WithEvents mCommandButton As CommandButton
Private WithEvents mOptionGroup As OptionGroup
Private iState As Integer
Public Property Set BindCommandButton _
(ctlNewCommandButton As CommandButton)
Set mCommandButton = ctlNewCommandButton
End Property
Public Property Set BindOptionGroup _
(ctlNewOptionGroup As OptionGroup)
Set mOptionGroup = ctlNewOptionGroup
End Property
Private Sub mCommandButton_Click()
If ValidateControls() = False Then Exit Sub
iState = mOptionGroup.Value
mOptionGroup.Value = iState Mod 2 + 1
End Sub
Private Sub mOptionGroup_AfterUpdate()
If ValidateControls() = False Then
Exit Sub
End Sub
If mCommandButton.FontBold = False Then
mCommandButton.FontBold = True
Else
mCommandButton.FontBold = False
End If
End Sub
Private Function ValidateControls() As Boolean
ValidateControls = False
If mCommandButton Is Nothing Then
Exit Function
End If
If mOptionGroup Is Nothing Then
Exit Function
End If
ValidateControls = True
End Function
|
The ValidateControls routine checks to make sure that the references to
the Command and Option buttons have been set. If the relevant object variables
aren't set, ValidateControls exits with an error code.
Conclusion
The WithEvents keyword gives you power
that you never had before. You can move basic functionality from forms into
classes and share those classes among Access projects. It's also possible
to mix the functionality of two different classes, creating a kind of simple
inheritance.
As a bonus, the sample database that comes with this article is set up so
that all the classes in it are available to other Access databases (it's
available in the accompanying Download File
). If you add a reference in your Access database
to my sample database, you'll be able to use all the classes defined in
it, just as if they were defined directly in your project. The simple trick
to expose classes from library databases to other Access projects is described
on the ATTAC Consulting Group Web server at http://ourworld.compuserve.com/homepages/attac-cg/acgsoft.htm
.
[Next month, in another article on WithEvents programming, Shamil Salakhetdinov
will present some techniques that build on this foundation to dynamically
attach events to controls in Access without adding any code to the corresponding
forms. -- Ed.]
Download WITHEVNT Sample.
Marek Kepinski works as a technical
consultant at Impaq Technology Center, a Microsoft Solution Provider based
in Warsaw, Poland. He's currently working on a major database project for
a Swiss bank. MKepinski@Impaq.com.pl.
Sidebar: Excel-ent Events
The WithEvents keyword can also be used
to communicate between Access and other Microsoft products. As an example,
you can use this code to not only gain control of Excel but also to respond
to events fired by Excel:
Dim WithEvents xcl As Excel.Application
|
Excel fires a number of events, which I've listed below, that you can respond
to from within your Access Basic modules. These events are as follows:
-
NewWorkbook
-
SheetActivate
-
SheetBeforeDoubleClick
-
SheetBeforeRightClick
-
SheetCalculate
-
SheetChange
-
SheetDeactivate
-
SheetSelectionChange
-
WindowActivate
-
WindowDeactivate
-
WindowResize
-
WorkbookActivate
-
WorkbookAddinInstall
-
WorkbookAddinUninstall
-
WorkbookBeforeClose
-
WorkbookBeforePrint
-
WorkbookBeforeSave
-
WorkbookDeactivate
-
WorkbookNewSheet
-
WorkbookOpen
Sidebar: Foundation Concepts
Class modules in Access allow you to define
objects by writing VBA code. In a Class module, subroutines and functions
appear as methods of the object defined by the Class module. Property Lets,
Gets, and Sets allow you to create properties for your objects.
The techniques presented in this article can be used not only in Access,
but in all the other Microsoft applications that support Visual Basic for
Applications. The version of VBA that's used by Access 97 contains some
new Visual Basic keywords, which can't be used as identifiers (these include
AddressOf, Assert, Decimal, DefDec, Enum, Event, Friend, Implements, RaiseEvent,
and WithEvents). If you try to use these keywords in your code, you'll get
a compile error -- that is, except for the WithEvents keyword, which can
be used in Access 97.
The WithEvents keyword is used as follows:
Dim WithEvents varname As objectname
|
This statement specifies that varname is an object
variable that's used to respond to events triggered by the object specified
by objectname. In other words, if you create an object and point the varname
variable at it, you can use WithEvents to respond to events fired by the
object.
The following code defines the object variable obj that points at the object
Fred. The object Fred fires an event called Alert. To create an event procedure
that would run when the Fred object fires the Alert event, you'd write this
code:
Dim WithEvents obj As Fred
Set obj = New Fred
Public Sub obj_Alert
MsgBox "Fred has had an Alert event."
End Sub
|
To get more information on WithEvents, see the "ItemAdded Event"
topic in the Access Help file (References collection).
Download sample code for this article here.