Raising event from object in custom collection class Raising event from object in custom collection class vba vba

Raising event from object in custom collection class


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 clsPersonPublic Sub AddPerson(p As clsPerson)    Set aPerson = p    ' this automagically registers p to the aPerson event-handler `End SubPublic 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 SelectedChangePublic Sub Raise_SelectedChange    RaiseEvent SelectedChangeEnd 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:

  1. 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.

  2. 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.

  3. 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.


Example Implementation

Responding to requests for a 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 RaiseSelectedChange(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 clsPersonsDelegatePublic Property Let Selected (newVal as boolean)    pSelected = newVal    'RaiseEvent SelectedChange'    colDelegate.RaiseSelectedChange(Me)End PropertyPublic Property Get Selected as boolean    Selected = pSelectedEnd 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 CollectionPrivate WithEvents colDelegate as clsPersonsDelegatePrivate Sub Class_Initialize()    Set colPeople = New Collection    Set colDelegate = New clsPersonsDelegateEnd Sub' 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 our 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