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!
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!