If an object is contained within a collection, can that object still raise events to a parent class?
Clearly you could tell the child class a reference to the pare
You can easily raise an event from a class in a collection, the problem is that there's no direct way for another class to receive events from multiples of the same class.
The way that your clsPeople
would normally receive the event would be like this:
Dim WithEvents aPerson As clsPerson
Public Sub AddPerson(p As clsPerson)
Set aPerson = p ' this automagically registers p to the aPerson event-handler `
End Sub
Public Sub aPerson_SelectedChange
...
End Sub
So setting an object into any variable declared WithEvents
automatically registers it so that it's events will be received by that variable's event handlers. Unfortunately, a variable can only hold one object at a time, so any previous object in that variable also gets automatically de-registered.
The solution to this (while still avoiding the problems of reference cycles in COM) is to use a shared delegate for this.
So you make a class like this:
<<Class clsPersonsDelegate>>
Public Event SelectedChange
Public Sub Raise_SelectedChange
RaiseEvent SelectedChange
End Sub
Now instead of raising their own event or all calling their parent (making a reference cycle), you have them all call the SelectedChange
sub in a single instance of the delegate class. And you have the parent/collection class receive events from this single delegate object.
The Details
There are a lot of technical details to work out for various cases, depending on how you may use this approach, but here are the main ones:
Don't have the child objects (Person) create the delegate. Have the parent/container object (People) create the single delegate and then pass it to each child as they are added to the collection. The child would then assign it to a local object variable, whose methods it can then call later.
Typically, you will want to know which member of your collection raised the event, so add a parameter of type clsPerson
to the delegate Sub and the Event. Then when the delegate Sub is called, the Person object should pass a reference to itself through this parameter, and the delegate should also pass it along to the parent through the Event. This does not cause reference-cycle problems so long as the delegate does not save a local copy of it.
If you have more events that you want the parent to receive, just add more Subs and more matching Events to the same delegate class.
Responding to request for more concrete example of "Have the parent/container object (People) create the single delegate and then pass it to each child as they are added to the collection."
Here's our delegate class. Notice that I've added the parameter for the calling child object to the method and the event.
<<Class clsPersonsDelegate>>
Public Event SelectedChange(obj As clsPerson)
Public Sub Raise_SelectedChange(obj As clsPerson)
RaiseEvent SelectedChange(obj)
End Sub
Here's our child class (Person). I have replaced the original event, with a public variable to hold the delegate. I have also replaced the RaiseEvent with a call to the delegate's method for that event, passing along an object pointer to itself.
<<Class clsPerson>>
Private pSelected as boolean
'Public Event SelectedChange()'
' Instead of Raising an Event, we will use a delegate'
Public colDelegate As clsPersonsDelegate
Public Property Let Selected (newVal as boolean)
pSelected = newVal
'RaiseEvent SelectedChange'
colDelegate.SelectedChange(Me)
End Property
Public Property Get Selected as boolean
Selected = pSelected
End Property
And here's our parent/custom-collection class (People). I have added the delegate as an object vairable WithEvents (it should be created at the same time as the collection). I have also added an example Add method that shows setting the child objects delegate property when you add (or create) it to the collection. You should also have a corresponding Set item.colDelegate = Nothing
when it is removed from the collection.
<<Class clsPeople>>
Private colPeople as Collection
Private WithEvents colDelegate as clsPersonsDelegate
' Item set as default interface by editing vba source code files'
Public Property Get Item(Index As Variant) As clsPerson
Set Item = colPeople.Item(Index)
End Property
' New Enum set to -4 to enable for ... each to work'
Public Property Get NewEnum() As IUnknown
Set NewEnum = colPeople.[_NewEnum]
End Property
' If selected changes on any person in out collection, do something'
Public Sub colDelegate_SelectedChange(objPerson as clsPerson)
' Do Stuff with objPerson, (just don't make a permanent local copy)'
End Sub
' Add an item to our collection '
Public Sub Add(ExistingItem As clsPerson)
Set ExistingItem.colDelegate = colDelegate
colPeople.Add ExistingItem
' ... '
End Sub