Control/Class arrays - WithEvents!

Squirm
01-21-2004, 09:27 AM
This is a problem I once had a few years ago, and since learning more about COM and OOP in general, I've developed a sound understanding of how it can be circumvented.

It is quite a common scenario - you need an array or collection of controls or classes which can raise events. On a form, a control array can be used, but this is not helpful when dealing with an array of classes, or when a class is being used to receive the events. The WithEvents keyword allows us to declare an object so that we may receive events, but this only works for single variables, not for arrays or dynamically-created objects. This document provides a solution.

First we need to understand more about OOP and how VB's event system works before we can break it. ;)

There's no such thing as an event
Quite a profound statement, but technically true. VB fools us into thinking events differ from functions and subs, but in practice they are the same! Just like one object calls a method in another object to do something, so events are merely the second object invoking a hidden method in the first! The RaiseEvent statement causes a hidden function to be called in the object which is receiving events.

Imagine 2 objects ClassA and ClassB. These 2 examples are functionally identical (almost):

Example 1:
'***** Class A *****
Public Event Finished()

Public Sub Start()
'Do something
RaiseEvent Finished
End Sub

'***** Class B *****
Dim WithEvents myA As ClassA

Set myA = New ClassA
myA.Start

Private Sub myA_Finished()
MsgBox "Done"
End Sub

Example 2:
'***** Class A: *****
Public myB As ClassB

Public Sub Start()
'Do something
myB.Finished()
End Sub

'***** Class B: *****
Dim myA As ClassA

Set myA = New ClassA
Set myA.myB = Me
myA.Start

Public Sub Finished()
MsgBox "Finished"
End Sub

In the second example, ClassB is passing a reference to itself to ClassA, which then calls a function in ClassB when it has completed. The exact same thing happens in the first example, you just don't see it! :D There is a little more to it than that, but for the purposes of this tutorial, knowing this much is enough (for more information you may wish to check out Thinker's awesome VB, COM Interfaces and Plugins (http://www.visualbasicforum.com/showthread.php?threadid=26521) tutorial).

What if class A needs to raise events to many different objects, not just class B?
While the above example works fine if ClassA only needs to ever notify objects of type ClassB, what if a third type, ClassC, may also be receiving events from ClassA? The second example breaks down but the first would still work. Again, we look at what VB is doing behind the scenes - it is manipulating interfaces. What is an interface? An interface is basically a contract - a list of members (properties, subs and functions) which must be implemented by a class who uses the interface.

Imagine a stranger on the street told you he'd bought a new car. Not knowing anything about this person, you'd have no idea what car he's bought, but you know what a car is - a motor vehicle with doors and seats. We can call this a 'car interface' - although a Ferrari differs from Skoda, they're both cars and as such, have certain things in common. This can be translated into code:

'***** Car Interface class: *****
Public Property Get MaxSpeed() As Long
End Property
Public Sub Accelerate()
End Sub
Public Sub Brake()
End Sub

'***** Ferrari class: *****
Public Property Get MaxSpeed() As Long
MaxSpeed = 170
End Property
Public Sub Accelerate()
'Make the car speed up - good acceleration
End Sub
Public Sub Brake()
'Make the car slow down
End Sub

'***** Skoda class: *****
Public Property Get MaxSpeed() As Long
MaxSpeed = 65
End Property
Public Sub Accelerate()
'Make lots of noise but don't go anywhere
End Sub
Public Sub Brake()
'Do nothing - wasnt going fast anyway
End Sub

Now that both cars implement the same interface, we can devise functions which will take a generic car type and which will be able to manipulate anything which implements the car interface, whether it be a Skoda, Ferrari, Ford, or whatever. As long as it as a MaxSpeed property and Accelerate and Brake methods, it is a car and can be treated the same.

How is this useful?
Whenever an object has events, VB creates a hidden event interface which contains all of the event functions which are called when an event is raised, as explained above. When you place a control on a form, or use the WithEvents keyword, VB forces the parent object to implement this hidden event interface, and passes a reference to it to the object raising the events. If we go back to the first example, this could translate to:

'***** Interface class IAEvents: *****
Public Sub Finished()
End Sub

'***** Class A: *****
Public eventSink As IAEvents

Public Sub Start()
'Do something
eventSink.Finished()
End Sub

'***** Class B: *****
Implements IAEvents

Dim myA As ClassA
Set myA = New ClassA
Set myA.eventSink = Me
myA.Start

Private Sub IAEvents_Finished()
MsgBox "Finished (in B)"
End Sub

'***** Class C: *****
Implements IAEvents

Dim myA As ClassA
Set myA = New ClassA
Set myA.eventSink = Me
myA.Start

Private Sub IAEvents_Finished()
MsgBox "Finished (in C)"
End Sub

The Implements keyword tells VB that we wish to make the object fulfill the contract of the interface by placing all of the needed members within the object. Since both ClassB and ClassC both implement class IAEvents, they can pass themselves to ClassA without ClassA caring which is which - all it cares about is the IAEvents part, which they both implement. A class can implement as many interfaces as it needs to, so can receive events from many different kinds of objects.

Alright, so how does this help us?
Background information out of the way, we can get down to business. Lets take a look at the example again:

Dim myA As ClassA
Set myA = New ClassA
Set myA.eventSink = Me '****
myA.Start

All of the above code could be placed in a function/sub. myA does not need to be declared in the General Declarations section. The line marked with the asterisks is the real key - where we tell the ClassA object where to send the 'events' to. Since this can be done at runtime this allows us to create objects at runtime and receive their events:

'***** Class B: *****
Implements IAEvents

Private Sub Command1_Click()
Dim myA As ClassA
Set myA = New ClassA
Set myA.eventSink = Me
myA.Start
End Sub

Private Sub IAEvents_Finished()
MsgBox "Finished!"
End Sub

The above code would work fine. However, there are still drawbacks. Firstly, there is no way of telling how many ClassA objects are currently in use - this is easily corrected with an array or a collection. The main problem is that in the Finished event, it is impossible to tell which instance of ClassA is causing the event. This is where we copy VB's control arrays and make the Finished sub take one parameter - Index As Long. By passing a unique index number to the event, the recipient will be able to determine which object sent the event. If the index corresponds to an array index, then the whole thing works exactly the same as a control array. Alternatively it could represent an index into a collection with similar results!

Squirm
01-21-2004, 09:32 AM
I'm lost. What are you talking about?
Okay, I think it's example time. Lets imagine you need to have an array of class A all raising events to class B. Using VB's standard event system this would be impossible or at the very least extremely cumbersome (individual variable names for each array element). However, using our system it is easy. Take the step-by-step approach:

1: Decide what events the class needs to raise, then turn them into subs/functions and place them all in a blank class with no other code. The first parameter of each method should be ByVal Index As Long:

Public Sub TheEvent(ByVal Index As Long, ...)
End Sub

Name the class I[nameofyourclass]Events ... so if your class is going to be StringMath your event class should be called IStringMathEvents

2: In the main class, declare an object of your interface class and write Property Set and Property Get subs to change this object:

Dim m_EventSink As IStringMathEvents

Public Property Set EventSink(newEventSink As IStringMathEvents)
Set m_EventSink = newEventSink
End Property
Public Property Get EventSink() As IStringMathEvents
Set EventSink = m_EventSink
End Property

3: Still in the main class, declare a variable of type Long to hold the index, and code properties to change it:

Dim m_Index As Long

Public Property Let Index(ByVal newIndex As Long)
m_Index = newIndex
End Property
Public Property Get Index() As Long
Index = m_Index
End Property

4: Code the main class as normal, but whenever you wish to raise an event, call the appropriate method of the m_EventSink object. Always pass the index property as the first parameter:

'Code code code
m_EventSink.TheEvent m_Index, ......
'Code code code

Your class and events are now ready for use in a project.

5: In the object which is receiving the events (eg form), implement the event interface by placing the line Implements [interfacename] in the General Declarations section and then using the top dropdown boxes to complete all of the functions and properties which make up the interface - all must be included, if even you don't intend to use them.

Implements IStringMathEvents

Private Sub IStringMathEvents_TheEvent(ByVal Index As Long, ...)
'Code goes here
End Sub

Declare an array of the type of the class. It can be fixed or dynamic:

Dim stringThings() As StringMath

6: Every time you instantiate a new StringMath object, set the Index and EventSink properties so that you can identify it once an event is raised and so that events are raised to the correct object:

Set stringThings(i) = New StringMath
Set stringThings(i).EventSink = Me
stringThings(i).Index = i

That is basically it. The array can be used just like a conventional control array, with very minor modifications! :cool:

A working example is attached.

Squirm
01-21-2004, 09:36 AM
Cool, but I can't edit this control/class I wish to use like that
One of the more obvious applications which might require dynamic objects with events is some kind of server application. With the Winsock control, since it is already coded and compiled, the above technique would seem out of reach. However, we can be sneaky and create a stub object to forward events from the control to the recipient and everything will work just fine. I'll let the example speak for itself.

:p

EZ Archive Ads Plugin for vBulletin Copyright 2006 Computer Help Forum