by Peter G. Aitken (c)1998
Originally published in Visual Developer magazine .
I wonder how many of this magazine's readers remember the introduction of the original Visual Basic back in well, to tell you the truth I don't remember exactly when it was, but it seems like it must have been at least 7 or 8 years ago. Many of you have, I suspect, gotten into programming relatively recently and so cannot remember what it was like before tools such as Visual Basic came along. In those not-so-good old days, everything, and I do mean EVERYthing, that a program did had to be hand-coded. Things that today are so trivial that nobody gives them a thought, such as detecting mouse activity or keyboard input, posed significant programming problems. Many authors, myself included, made a few dollars writing articles that explained how to perform such tasks.
This kind of programming was rendered obsolete the moment Visual Basic came on the scene. Visual Basic gave us two enormously significant innovations, the one relevant to this column being the event-driven programming model (the other, of course, is drag-and-drop visual interface design). For the first time, programmers did not have to exert any effort to detect user input via the mouse or keyboard. Detection of such input, or events, was done automatically behind the scenes. All you had to do was to write the code you wanted to be executed when particular events occurred. If you have never programmed the "old" way you cannot realize what an amazing innovation this was.
As powerful as Visual Basic's original event-driven programming was, it did have limitations (although these did not become apparent for a while). Most important was the fact that the list of available events, and of event sources and sinks, was fixed. Certain objects, namely controls and forms, could serve as event sources (that is to say, could generate a defined set of events), and form objects could serve as event sinks (could detect these events). That was it there was no provision for customizing the event driven model. In Visual Basic 5, however, you have three keywords that open up the power of event-driven programming. You are no longer limited to those events and the event sources and sinks that are built into Visual Basic. You can now define your own events, fire those events whenever needed, and respond to those events, and to Visual Basic's built-in events, as dictated by the needs of your program.
What sort of events am I talking about? The events you can define yourself are different from Visual Basic's built-in events in that they are not connected, at least directly, to the system hardware or to user actions. In other words we are not talking about mouse actions and key presses. Rather, a user defined event is any occurrence in your code that you care to define as an "event." Here are some examples:
- Completion of an FTP transfer.
- 12:00 noon on Tuesday.
- Conclusion of a long series of calculations.
- Receiving a "disk full" error when writing to a file.
- Completion of a database query.
In essence, an event is whatever you say it is. This is one reason why Visual Basic's user-defined event capability is such a powerful tool, because there are few if any restrictions on how you use it. Even better, it's very easy to do. Let's take a look at the fundamentals of how it's done. The first step is to declare the event. You do this at the module level in a class module:
MyEvent is the event name, and follows standard Visual Basic naming conventions. arglist is the optional event argument list, which follows most of the same rules as arguments for procedures. The exceptions are that events cannot have named arguments, ParamArray arguments, or Optional arguments. If the event has no arguments, you should include an empty set of parentheses after the event name in the declaration. You can stick the Public keyword in front of the event declaration, but it makes no difference because events are public by default. As with other Visual Basic procedures, you can use the Tools|Add Procedure command to insert an event declaration.
Once an event has been declared you raise it with the RaiseEvent statement:
You can raise an event only from within the module where it is declared. If the event takes no arguments, use the event name by itself, without parentheses.
What happens when you raise an event? As with Visual Basic's built-in events, user-defined events have an effect only if the project contains one or more event procedures associated with that event. The association between an event procedure and an event is defined by the procedure name, which must consist of the name of the object in which the event is raised followed by an underscore and the name of the event itself. This is the same naming convention used for other event procedures. For example, the following event procedure will respond to the event MyEvent raised in the object Obj1:
Private Sub Obj1_MyEvent(arglist)
So far events sound pretty simple. You raise, or call, the event in code which causes a discrete chunk of code located elsewhere to be executed. But wait a minute this sounds just like a procedure! What then is so special about raising an event? Why not simply call a procedure instead? Put the code that you want executed when the "event" occurs in a regular sub or function, and call it as needed.
There are two things that give events more flexibility than a standard procedure call. First, when you call a procedure it must find a valid target or an error occurs. In other words, a procedure of the specified name must exist, and be in scope. This is not the case with events. You can raise an event and if there is no sink for the event no corresponding event procedure it is not a problem. This is the same way Visual Basic's built-in events work. If you do not write a Click event procedure for a particular Text Box control, no big deal. Clicks to that control are simply ignored. Likewise, you can raise a user-defined event without concern for whether or not there's a corresponding event procedure. When you write the code for an object, you can raise events that might be needed, but you do not have to actually use them.
Second, a user-defined event can be "sunk" in more than one location. This means that your program can respond to a raised event in two or more locations in your code. This may not be a technique you'll use frequently, but it can be very powerful when needed.
Where can you raise events? In class modules and in ActiveX elements. What about Form modules? This is a bit of a mystery, to be honest. I have not found any claim in the Visual Basic documentation or Books on Line to the effect that events cannot be declared and raised in a Form module. In fact, Event and RaiseEvent statements in a Form module compile and run just fine. The only problem is that the event seems to disappear into a black hole, and to be undetectable. There may be a way to do this that I have not tried, but for now we must consider Form modules as off limits for raising events. Standard modules (what used to be called Basic modules) are definitely off limits.
Working with user-defined events differs slightly between class modules and ActiveX modules. Let's look at a simple class module example.
Raising Events in a Class Module
There are three steps to defining and using an event in a class module:
1. Declare the event with the Event keyword.
2. Raise the event as needed with the RaiseEvent statement.
3. Declare an instance of the object using the WithEvents keyword.
Steps 1 and 2 are done within the class module, while step 3 is done in a form module. In the following example we will define an event that permits a class module that performs a lengthy calculation to report its status back to the calling form, which then displays an indication of how much of the calculation has been completed.
Start the demonstration project by creating a standard Visual Basic EXE project and naming the form frmUserEvents1. We'll program the class first. Use the Project|Add Class Module command to add a class module to the project, and name the class ClassWithEvents. Add the code shown in Listing 1. This code declares an event named ReportStatus that takes a single type String argument, and also defines a method named Calculate. This method doesnt do anything useful, but only performs some time-consuming calculations. Each time through the loop, the ReportStatus event is raised and passed a string indicating the current status of the calculations.
Listing 1. Code in the ClassWithEvents module.
' Declare the user-defined event.
Event ReportStatus(Status As String)
' The Calculate method.
Public Sub Calculate(NumReps As Long)
Dim i As Long, j As Long, t As Double
For i = 0 To NumReps
RaiseEvent ReportStatus("Completed " & i _
& " of " & NumReps & " calculations.")
For j = 1 To 30000
t = Tan(Rnd())
Now we must program the form. On the Form place two Text Box controls named txtStatus1 and txtStatus2, and a Command Button named cmdStart. Add a couple of labels as shown in the figure. Add the code in Listing 2 to the form module. Note that the two event procedures follow standard Visual Basic event procedure naming conventions: the name of the object followed by an underscore and the name of the event. The event in this case is ReportStatus, which we defined when we wrote the code for the class ClassWithEvents.
Once you have completed writing the code for the form module, pull down the object list at the top left of the code editing window. You'll see that the two objects you declared, MyObject1 and MyObject2, are listed there. Note also if you select one of these two type ClassWithEvents objects, the class's events will be displayed in the event list at the top right of the code window. In this case of course it is only the single event ReportStatus. This shows you that Visual Basic is "aware" of the events that are defined in classes regardless of whether it is a predefined class or one you created yourself. As with any object, you can use the object and event lists to insert the skeleton of event procedures and to navigate among event procedures that have already been created.
Listing 2. Code in frmUserEvents1.
' Declare the objects WithEvents.
Private WithEvents MyObject1 As ClassWithEvents
Private WithEvents MyObject2 As ClassWithEvents
Private Sub cmdStart_Click()
' When the user clicks Start, call the
' Calculate method in both objects.
Private Sub Form_Load()
' When the form loads create two
' object instances.
Set MyObject1 = New ClassWithEvents
Set MyObject2 = New ClassWithEvents
Private Sub MyObject1_ReportStatus(Status As String)
' Event procedure for the ReportStatus
' event of MyObject1.
txtStatus1.Text = Status
Private Sub MyObject2_ReportStatus(Status As String)
' Event procedure for the ReportStatus
' event of MyObject2.
txtStatus2.Text = Status
It is important to note that WithEvents can only be used when declaring a variable as a specific object type. You cannot use it when declaring a generic object variable. Thus, this is OK:
Private WithEvents Obj1 as SpecificObjectName
Set Obj1 = New SpecificObjectName
But the following will not work:
Private WithEvents Obj1 as Object
Set Obj1 = New SpecificObjectName
Raising Events in an ActiveX Control
Defining user-defined events in an ActiveX control is not any different from the process outlined above for a class module. You must declare each event with the Event keyword, then raise the events as needed using RaiseEvent. Detecting the events is a bit different. If the ActiveX component is a visual object that you drop on a form during program design, then its events are automatically available (and listed in the Events list) without any need to use the WithEvents keyword. This, of course, is just like you would expect things to work. If, however, the ActiveX component is not dropped on a form during design but rather created with the New keyword, then the variable that is used to refer to the ActiveX object must be declared with the WithEvents keyword just like an object based on a regular class.
Writing Generic Event Handlers
Events can go both ways. What I mean by this is that classes can not only raise their own events, but can also respond to events generated by other objects. You could, for example, create a class that contains code to respond to Click events received by a Text Box control. What is the advantage of doing this? You can write a generic event handler complete with all the required error checking, the reuse the code time after time for different instances of the control type without time-consuming recoding. Suppose, for example, that you frequently write applications that require a Combo Box control populated with a list of the 50 states. When the user tabs to the control and presses a key, you want the list to scroll to the corresponding section of the list. You can create a class to perform these tasks, and use it for any Combo Box in any project. When and if Puerto Rico becomes the 51st state, you'll have only one section of code to modify.
To illustrate I will create a simple application that contains a form with a single List Box control. When the user clicks the List Box it will be filled with a list of vegetables. The work will be done by a class that could be applied to any other List Box. Create a standard EXE project, and add a class module, assigning the class name CFillListBox to it. The class module requires a variable of the proper type declared with WithEvents, as shown here:
Private WithEvents MyList As ListBox
Also required is a way to associate an instance of the class with a specific List Box. This is done by means of a property, and since the property value will in this case be an object we must use a Property Set procedure:
Public Property Set ListBoxControl(c As ListBox)
Set MyList = c
The final class ingredient is the event procedure that will be executed when the associated List Box is clicked. As before we use standard event procedure naming conventions:
Public Sub MyList_Click()
Now return to the project's form and add a List Box, leaving the Name property at the default value of List1. At the module level, the form needs a variable for the CFillListBox instance:
Private cFillList As CFillListBox
We'll use the form's Load event procedure to create the instance of the class CFillListBox, and to associate it with the form's List Box. We also need to put one item in the List Box because it seems that an empty List Box control will not respond to Click events.
Private Sub Form_Load()
Set cFillList = New CFillListBox
Set cFillList.ListBoxControl = List1
' This next line is required because an
' empty list box cannot respond to clicks.
That's all the project is ready to try. When you run it you'll see the List Box with the single entry "dummy." Click on the text in the List Box and the original entry will be erased and replaced with the list of veggies. The event that was received by the List Box has been responded to by code in the class we created. You could easily associate the same instance of this class with multiple List Box controls, in turn, to populate each one. There are many other possibilities, including writing generic code to sort the list, to search, and so on. The beauty of this approach, again, is that you can write generic reusable code to handle events received by controls.
What happens if there is also an event procedure defined within the form module? Then when the event occurs, both event procedures the one in the form module and the one in the class module are executed. You can see this for yourself by adding a List1_Click() event procedure to the form in the project that we just completed.
Visual Basic Newsgroups
It has been said that two heads are better than one. Assuming that each head is on its own body, I agree! It follows that several hundred heads may be better yet, and when you're facing a thorny programming problem it is certainly true. But where can you get these "heads?" Most of us are lucky if we have one or two other Visual Basic programmers to consult, and if you're hidden away in a cabin in Montana you may be limited to asking the bears and elk (who, I understand, prefer Delphi anyway). Fortunately, with a phone line, modem, and newsreader software you can tap into a huge source of programming knowledge.
Where is this treasure trove? I am talking about Usenet, more popularly known as newsgroups. I expect that most readers of this column know about newsgroups already, but you may not know just how much Visual Basic information is out there. I count 29 different newsgroups that are devoted to various aspects of Visual Basic programming, and there may be more because there is no guarantee that my news server carries all of them. Let's take a look.
First there are those that fall in Usenet's comp hierarchy. Newgroups are arranged by topic, and comp is the top-level hierarchy devoted to computer-related topics. These newsgroups are moderated, which means that one or more people have the task of screening incoming posts. This should not be thought of as censorship, but rather serves to keep the messages on-topic, to weed out spam, to keep flames within reasonable limits, and so forth. There are 5 newsgroups under comp that are of direct interest to Visual Basic programmers:
Why, you may ask, are there both comp.lang.basic.visual and comp.lang.visual.basic newsgroups? To be honest I do not know, as the articles posted to the two groups do not seem to differ in topic.
Second are the newsgroups hosted by Microsoft on their public news server at msnews.microsoft.com. There are 24 distinct Visual Basic groups, all listed as microsoft.public.vb.xxxx. The xxxx parts are listed here:
You can see that just about any Visual Basic programming topic you can image receives coverage in the Microsoft public newsgroups. An added advantage of these groups is that some of Microsoft's own Visual Basic people seem to hang out there, and what better source of information that the horse's own mouth? Be aware that these newsgroups are not an official support mechanism, but they are often the quickest way to get an answer to your programming questions.
I find myself browsing a few of the Visual Basic newsgroups on a regular basis even when I do not have a specific question that needs answering. I often pick up useful tidbits of information, learn about better ways of doing things, or just have fun chatting with other programmers. I also feel that it is important to contribute to the groups. If everyone only asked questions and never answered any, where would we be? You may feel that you're not enough of an "expert" to be answering questions, but there's almost always someone who is more of a beginner than you are who can use your help.