How does Timer.CallLater match the passed delegate's method AND object reference?

I would like to create some functionality like Timer.CallLater for an event system.

Currently, I return a key for each subscription to the event so that it can be cleaned up later by a call to unsubscribe. This requires me to keep a key in every object that uses the event system. The more places I use the system the more chances I get something wrong by not recording or releasing the key correctly.

I tried to use just the delegate returned from the AddressOf as the key to a dictionary, however, it does not seem to equal the previous call so it cannot be used in Dictionary.Remove(key). I was able to get something working by converting the delegate to a ptr, but then the object reference is stripped making it useless for multiple objects calling subscribe with the same method.

How does Timer.CallLater handle this with calls from different objects?
Is there a way to get the object reference from the delegate or find equality between 2 calls to AddressOf on an object method?

Thanks!

A delegate is an object. So setting a delegate to a key should be unique.
You can also make it a varant and use the variant.Hash (it’s unique for each object in runtime).

It’s difficult to advise without seeing your code, but this seems like something you’d handle with a new Subscription class. Creating the class will subscribe as needed while the class’s Destructor would handle unsubscribe.

The objects that would need to subscribe might be able to share a common superclass that handles tracking of these subscriptions.

1 Like

Yes, I assumed that Dictionary would only be storing the object ID or Ptr as the key so they would only match if they were the same delegate object. Variant.Hash appears to also have the same issue.

Most of the classes are already a subclass so I would not be able to share a subclass. I could probably use a class interface, however, it still means repeating myself for the implementation side of things.

I could also probably pass in the object as an argument to the subscribe method and use the object id and function ptr as the key. Would be nice to avoid extra steps though, and it does seem possible at least on a framework level.

I can’t believe this - I literally just solved this exact problem a few days ago.

In my system, objects can subscribe to messages based on a string constant - for example, kMessageSelectionChanged = "selectionChanged". More than one message subscription is available per object so they can receive multiple different messages. Objects and also unsubscribe from specific messages, or unsubscribe from all messages in a single call (useful in an object’s Destructor event).

I also discovered that Delegates can’t be used as keys as you did. So I designed my data to be stored thusly:

Dictionary MessageIDToDelegateLookup is a dictionary that associates a message ID to an array of Pairs:

MessageIDToDelegateLookup.Value("selectionChanged")=[Pair, Pair, Pair ... ]

Each Pair looks like this:

currentPair=new Pair(theDelegate, theObject)

…where theDelegate is the delegate used to deliver the message, and theObject is the object receiving it. When a message of a particular ID needs to be sent, the framework does this:

dim delegatePairArray() as Pair

delegatePairArray= MessageIDToDelegateLookup.Value(requestedMessageID)

for each currentPair as Pair in delegatePairArray
    MessageDelegate(currentPair.Left).Invoke(someParameters)
next

To unsubscribe an object from a particular message:

dim delegatePairArray() as Pair

delegatePairArray=MessageIDToDelegateLookup.Value(requestedMessageID)

dim delegateCount as integer

delegateCount=ubound(delegatePairArray)

for i as integer from delegateCount downto 0
    currentPair=delegatePairArray(i)

    if currentPair.Right=theObject then
        delegatePairArray.Remove I
    end
next

To remove the object from all message subscriptions, you could iterate through all possible MessageIDs, or use an optimized function:

dim allDelegatePairArrays() as variant

allDelegatePairArrays=MessageIDToDelegateLookup.Values

dim currentDelegatePairArray() as Pair

for each currentArrayVariantValue as Variant in allDelegatePairArrays

currentDelegatePairArray= currentArrayVariantValue

[code proceeds as before]

This system was optimized for message delivery - I sacrificed the speed of unsubscribing, because in my case that’s a much less common call. There might be a way to maintain a reverse lookup based on the object itself that would speed up unsubscribing if that is a desirable feature.

There is subtle flexibility in this system. For example, an object may subscribe to a message more than once, using different delegates. Delegates don’t have to be methods of the object requesting the messages. Objects may use the same delegate to handle multiple message types. This turned out to be useful in my application.

Hope this is useful, and note that the code above is somewhat pseudocode, if you find any oddities. :slight_smile:

Anyone looking for a great event bus that works out of the box should check out @Ian_Jones’ GitHub - ianmjones/EventBusIJ: A simple synchronous Event Bus for Desktop and Web Edition Real Studio projects.

I use it in many of my projects.

2 Likes

Sorry, but am I the only one imagining a bug full of drunken teens in vegas!

Sorry, I’ll get my coat. :slight_smile:

1 Like

This all seems very complicated compared to how it needs to be…

Another way to do this is to use an extension method and a class interface. You’ll need a property to store the receivers but we’ll use WeakRefs. As long as they exist, they’ll receive messages.

Property mReceivers() as WeakRef

Interface myReceiver
  Sub message(msg as string)
End interface

Sub Subscribe(extends o as myReceiver)
  mReceivers.Add(new weakref(o))
End Sub

Sub SendMessage(msg as String)
  For I as integer = 0 to ubound(mReceivers)
    Dim ref as WeakRef = mReceivers(I)
    If ref = nil or ref.value = nil then
      mReceivers.remove(I)
    Else
      myReceiver( mReceivers(I)).message(msg)
    End if
  Next I
End sub

It auto-removes receivers, doesn’t hold references so it won’t leak and you can have more than one method on the interface for different reasons.

All of this would exist in a module

1 Like

There wouldn’t be a way to selectively unsubscribe to messages in your system without destroying the object on the business end of the WeakRef. That might not be desirable behavior, although I confess that at the moment I can’t think of an obvious application for it. In my case, I wanted to optimize around sending messages, so I didn’t want to do any extra work in that loop. Ensuring that the entire array of receivers is valid removes that requirement.

Also: you have to count down from Ubound(mReceivers) or you’ll throw an OutOfBounds exception at the end of the loop. :wink:

Thanks for the code snippet, the reason for using delegates was so that they could be used by both modules and classes.

Indeed. My solution (above) is a second-generation system; my first one used a class interface, which was fine but did make it somewhat limiting. I also have a hunch that delegates are faster, but I haven’t done any research to prove it.

That’s an important point. Using Greg’s solution, a module could never unsubscribe from messages.

I mainly use the event system for theme updates (dark/light/night), frames from CAN bus nodes, and when some element needs to be updated to reflect the current state of an object.

It is nice to be able to unsubscribe when an element does not need to update but is not going to be destroyed.

I’m using mine to propagate data updates, user-triggered events, etc. to floating palette windows. There will be a variable number of palettes; there may be more than one instance of a particular palette; and other considerations.

Ideally, there would be some way to get the signature of a reference:

For a module, it would be something like Delegate(nil) and for a class, it would be something like Delegate(class)

I will probably end up adding a subscribe method that takes in either a pair or the delegate.

Sub Subscribe(handler as SomeDelegate, object_reference as Object = nil)

Modules would just omit the object argument.

Considering I wrote the code while sitting in a train station, I think I remembered pretty well.

Yes, I neglected to decrement i when removing refs, so just add i=i-1 there.

Yes, you can unsubscribe by making another extension method that loops through the array and removes any items where the WeakRef is not nil and its value = the object

Sub UnSubscribe(extends obj as myReceiver)
  For i as integer = ubound(mReceivers) downto 0
    Dim ref as WeakRef = mReceivers(I)
    If ref<>nil and ref.value = obj then
      mReceivers.remove(i)
    End if
  Next i
End sub

Thanks, however, I do not see how this works with modules unless I am mistaken how interfaces work?

Oh right, your code doesn’t permit modules to receive messages. I was wracking my brain trying to figure out how you pass a module as a parameter and of course you can’t.

No, you understand correctly. His code is fine, it just accomplishes the task using a class interface, which can’t be used with a module.