Most of the time I write about things I think you will want to know about now or in the near future. Some times though I write about what I get asked about. The reasoning is that if several people are asking about a specific topic then there are probably many more wondering that didn't ask. Recently I received several emails about events and event handlers in VB6. Hence, events in VB6 are the topic of this article.
Windows is an event-driven operating system. That is to say events are central to how Windows works. Without events in your programs you are missing out on a key aspect of dynamic, loosely coupled programming. Although terms like loosely coupled are tossed around they really mean something in the context of application development. In the case of events, loosely coupled means that you can provide any consumer with a means of tapping into instances of your classes without knowing what those consumer classes are in advance. For example, define a changed event for a text box and any consumer can respond to the text value changing in a text box. Of course, events like Change already exist for the TextBox control in Visual Basic 6, but you can add events to your classes to create the same loosely coupled relationship between your classes and any consumer. The first essential ingredient is knowing the mechanics of defining and consuming events in VB6; the second ingredient is creating opportunities where it is apropos to add events.
In this article I will demonstrate the mechanics of declaring an event in a class, a convenient way to raise that event, and how to consume the event. Knowing when to add an event to a class takes some practice and experimentation. There are six basic steps that we need to declare and consume an event that we will be covering in this article, they are:
- Declare the event property in a class
- Implement a method that is delegated to raise the event
- Add code to raise the event in convenient locations in your class
- Declare a WithEvents statement in a client application
- Implement an event handler in the client
- Assign an instance of the class containing the event to the object defined in the WithEvents statement
To demonstrate I will be implementing a class that raises an event when the value of a string property is changed. This event handler works much like the Change event works for the TextBox control in VB6. The class contains a private field and a property to set and get the value of the private field. The property is named Text. Listing 1 contains the class before we add any event code to it.
Listing 1: The class that we will be adding an event to.
Option Explicit Private FText As String Public Property Get Text() As String Text = FText End Property Public Property Let Text(ByVal Value As String) FText = Value End Property
As you can see the class isn't very exciting. It was intentionally left pristine to allow you to clearly see the revisions added to support the event. The field is named FText-F for field, a convention I used from Pascal-and I drop the F-prefix to create the property named Text. Note that there is a property to set and get the value of the underlying field.
Declaring an Event Member
The event statement looks identical to the declaration of a subroutine except for one difference. Where you would have used Sub to indicate a subroutine you use the keyword event. The revised code in listing 1 is shown in listing 2, which now contains the event declaration.
Listing 2: The class now contains an event declaration.
Option Explicit Private FText As String Public Event OnChange(ByVal Text As String) Public Property Get Text() As String Text = FText End Property Public Property Let Text(ByVal Value As String) FText = Value End Property
The event is named OnChange and is defined as a subroutine that takes one string argument. This means that handlers will be subroutines that have exactly the same signature. Next we need to add code to raise the event.
Raising the Event
You can add code to raise an event by writing RaiseEvent followed by the event name and arguments anywhere you want to. However, this makes your code look a bit sloppy, and if you need to create arguments for your events or program in other languages then the sloppiness is exacerbated. As an alternative we can employ a convention that works very well in languages like Visual Basic .NET, C#, C, C++, and Delphi too.
What we'll do is wrap the code that raises the event into a subroutine and call that subroutine. The result is that if we need to change the way the event is invoked we only need to change it in a single place, and our code looks a bit tidier. Listing 3 demonstrates a method that handles raising the event for the class from listing 2.
Listing 3: Implementing a proxy method to raise an event for us.
Option Explicit Private FText As String Public Event OnChange(ByVal Text As String) Private Sub Change(ByVal Text As String) RaiseEvent OnChange(Text) End Sub Public Property Get Text() As String Text = FText End Property Public Property Let Text(ByVal Value As String) FText = Value Call Change(Value) End Property
I made two changes to listing 3. The first change is that I implemented the proxy method Change whose sole function is to raise the OnChange event. The second change is that I now invoke the Change method in the Property Let statement for the Text property. Now when Text changes I can easily raise the OnChange event.
A reasonable person might ask why go to the trouble of creating the proxy method for the OnChange event. The answer is not a simple one. On the surface the answer appears to be one of style. However, there are several factors, many of which relate to multi-language programming. Let me take a moment to explain briefly.
When you raise an event you may need to write some additional code that is passed to the caller, for example, when one of the event arguments is an object. Using the proxy method I only need to write the additional code in one place, instead of everywhere the event is raised. Another reason is that some languages treat events like function pointers and you have to check to make sure that the address of a function has been assigned to the function pointer; this is the case with Delphi, C#, C, and C++. I don't want to write the conditional check all over the place when I can consolidate the check into one place, the proxy method. A third reason is even more advanced. Languages that support inheritance-based polymorphism allow me to implement the proxy method as virtual, which allows me to override the behavior of the event-raising proxy method in subclasses. The result is that a base class may simply raise the event, but a child class has the opportunity to extend the behavior of the proxy method. Capabilities such as those described are available in C++, Delphi, C#, and Visual Basic .NET. Finally, what I have learned in the last fifteen years is that all programmers are multi-language programmers because no language sticks around forever. By resolving to use specific strategies that work successfully in most languages I can program much faster in all languages that I use. Using strategic best practices can make your code more reliable and reduce the effort involved in writing the code.
Consuming the Event
For our purposes the class is finished as of listing 3. The next step is to create a client application and consume the event. Again, for demonstrative purposes, the client application is simple. I added a TextBox and a Command button to a form. The form has a reference to the class containing our event in listing 3. When the Command button is pushed the underlying value of the object's Text property is changed.
Declaring the WithEvents Statement
To tie the class' event to our form we need to declare a WithEvents statement. Listing 4 contains a WithEvents statement that declares a reference name for our class.
Listing 4: An example of a WithEvents statement for our generically named Class1 class.
Public WithEvents Instance As Class1
As you can see the statement in listing 4 looks almost exactly like a variable declaration. By adding the WithEvents keyword all of the events declared in Class1 will show up in the code editor in VB6. Figure 1 shows the selected Object and Procedure in the comboboxes at the top of the code editor, available after the WithEvents statement was added to the form's code.
Figure 1: Select the reference name from the Object combobox and the event name from the Procedure combobox in the code editor to generate an event handler.
When you select the Object and Procedure the code editor will automatically generate the event handler for the procedure selected. This is precisely how event handlers are created for components in VB6 too.
Implementing the Event Handler in the Consumer
If you look closely to figure 1, you will see the rest of the code that demonstrates how to consume the event from our class. The remaining steps include generating the event handler, adding code to the event handler, and associating an object instance with the WithEvents reference. You can see all of these features in the form's code in listing 5.
Listing 5: Implementing the event handler.
1: Public WithEvents Instance As Class1 2: 3: Private Sub Command1_Click() 4: Instance.Text = Text1.Text 5: End Sub 6: 7: Private Sub Form_Load() 8: Set Instance = New Class1 9: End Sub 10: 11: Private Sub Instance_OnChange(ByVal Text As String) 12: Call MsgBox("Changed to: " + Text) 13: End Sub
The line numbers were added for reference only. Line 1 shows you the WithEvents statement in context. Lines 3 through 5 demonstrate an example where we are reading the Text from the TextBox.Text property and assigning it to the Instance.Text property. Lines 7 through 9 provide an example that demonstrates how to create an instance of the object and associate it without WithEvents reference variable, and lines 11 through 13 demonstrate a simple event handler that displays the value of the Text argument passed to our event handler. That is all there is to it.
There are some neat uses for event handlers, as well as other idioms. If you find defining a class B that has a reference to a class A, ask yourself what the relationship is between A and B. If the answer is that B needs to know when something happens in A then consider revising the relationship by using an event to pass the information back and forth. By using an event you make room for a future class C or D.
Events provide you with a convenient way of eliminating special knowledge between classes while allowing them to dynamically interact. A not so good alternative is that every class that might possibly interact would have to know about every other class, which of course is impossible to manage.
The event-driven model is quite old in computer terms and is not unique to Windows. Becoming an expert in how the event model works is very important. As you may already know, events are called delegates in Visual Basic .NET and take on a new role, including being at the center of the multithreading model in Visual Basic .NET.
About the Author
Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for his most recent book, Visual Basic .Net Unleashed, at a bookstore near you. Also look for his upcoming book "Advanced C# Programming" from Osborne/McGraw-Hill. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at firstname.lastname@example.org.