Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Raising event from object in custom collection class

Tags:

events

vba

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 parent class, and then call a public method within the parent class within the child class, however that would result in a circular reference, which as I understand it would make it so the garbage collector would not ever get rid of either object.

Details: I have two classes, one a person named clsPerson, and the second a custom collection class named clsPeople. clsPerson has a public boolean property named Selected. If selected is changed, I call an event SelectedChange. At that point, I need to do something in clsPeople. How can I trap the event in the custom collection class clsPeople? The person class can be changed from outside of the scope of People, otherwise I would look at another solution.

<<Class clsPerson>>
Private pSelected as boolean

Public Event SelectedChange()

Public Property Let Selected (newVal as boolean)
  pSelected = newVal
  RaiseEvent SelectedChange
End Property

Public Property Get Selected as boolean
  Selected = pSelected
End Property

<<Class clsPeople>>
Private colPeople as Collection

' 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 a person, do something
Public Sub ???_SelectedChange
  ' Do Stuff
End Sub
like image 503
lfrandom Avatar asked Oct 09 '13 19:10

lfrandom


1 Answers

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:

  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 clsPersonsDelegate

Public Property Let Selected (newVal as boolean)
    pSelected = newVal
    'RaiseEvent SelectedChange'
    colDelegate.RaiseSelectedChange(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

Private Sub Class_Initialize()
    Set colPeople = New Collection
    Set colDelegate = New clsPersonsDelegate
End 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
like image 185
RBarryYoung Avatar answered Nov 16 '22 02:11

RBarryYoung