Wednesday, September 3, 2008

Multicast Events - the finale

In my previous two posts I presented a technique using the new generics language feature of Delphi 2009 to create a typesafe multicast event. In the previous post, I showed how you can create a TMulticastEvent<T> instance and assign it to an event handler for an existing event on a TComponent derived type. Using the existing FreeNotification mechanism, you didn't need to worry about explicitly freeing the multicast event object. What if one of the components in the sink event handlers in the multicast event list was freed? The good thing is that the FreeNotification mechanism works both ways. We can leverage this functionality again to handle cleanup from the other direction.

In order to implement the complete cleanup for the TComponentMulticastEvent<T>, we need to know when an event handler was added and when one was removed. To do this I added these two virtual methods to the base TMulticastEvent class (the base non-generic version). There are also helper functions, RemoveInstanceReferences() and IndexOfInstance() that can be used in descendants to remove all event handlers that refer to a specific object instance and check if a specific instance is being referenced within the list.

  TMulticastEvent = class
...
strict protected
procedure EventAdded(const AMethod: TMethod); virtual;
procedure EventRemoved(const AMethod: TMethod); virtual;
protected
procedure RemoveInstanceReferences(const Instance: TObject);
function IndexOfInstance(const Instance: TObject): Integer;
...
end;

They're not marked abstract because the immediate descendant, TMulticastEvent<T> doesn't need to and should not be forced to override them. They just do nothing in the base class. In the corresponding Add and Remove methods on TMulticastEvent, these virtual methods are then called with event just added or just removed. Now we override the EventAdded and EventRemoved methods in the TComponentMulticastEvent<T> class:

  TComponentMulticastEvent<T> = class(TMulticastEvent<T>)
...
private
FSink: TNotificationSink;
strict protected
procedure EventAdded(const AMethod: TMethod); override;
procedure EventRemoved(const AMethod: TMethod); override;
...
end;

We also need to hold a reference to the internal notification sink class in order to use its FreeNotification mechanism. Here's the implementation of these methods:

procedure TComponentMulticastEvent<T>.EventAdded(const AMethod: TMethod);
begin
inherited;
if TObject(AMethod.Data) is TComponent then
FSink.FreeNotification(TComponent(AMethod.Data));
end;

procedure TComponentMulticastEvent<T>.EventRemoved(const AMethod: TMethod);
begin
inherited;
if (TObject(AMethod.Data) is TComponent) and (IndexOfInstance(TObject(AMethod.Data)) < 0) then
FSink.RemoveFreeNotification(TComponent(AMethod.Data));
end;

And then the Notification on the private TNotificationSink class:

procedure TComponentMulticastEvent<T>.TNotificationSink.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if Operation = opRemove then
if AComponent = FOwnerComp then
Free
else
FEvent.RemoveInstanceReferences(AComponent);
end;

In the EventRemoved method we call IndexOfInstance() to ensure that there aren't multiple references to the same instance in the list before we remove the free notification hook. This is because FreeNotification will add the instance to its internal list only once.


So there you go, a multicast event that also performs full auto-cleanup for both the source and the sink instances. If the source instance goes away, the multicast event instance is automatically cleaned up. Likewise, if one of the sink event handlers' instances go away, it will automatically be removed from the list so there are no stale references. Of course, I'll remind the reader that with this implementation, it only works for TComponent derived instances. For non-TComponent derived instances, you can still use a descendant of TMulticastEvent<T> in a manner similar to TComponentMulticastEvent<T> mixed with, for instance, a technique described here.

10 comments:

  1. Nice thing Allen. I must confess that I didn't follow very close your posts now due of lack of time but I intend to do it. However some notes:

    1. Imho, a message queue in TObject it is badly needed, not only for having Notification method for the above purposes but this is also a central piece in parallel programming - for ex. Actor model, Erlang, Oz programming languages etc. Having such a structure at root of the class system also gives us new perspectives in expresing inter-object interactions (think only about Windows's messaging system and what was achieved with it...)

    2. A common usage pattern is:
    - I made my
    TFooComp = class(TComponent)
    property DataSource: TDataSource read FDataSource write FDataSource; //or whatever...
    ...
    end;

    ...and I want to hook/add functionality to (some of) the FDataSource.DataSet's events.

    How can be this done in a 'safe' manner with your implementation, a.) without knowing nothing about the 'outside world' (ie. my TFooComp will be deployed/used in an unknown code) b.) allowing the possibility of using many instances of TFooComp class, preferably without having each instance its MultiCast Manager?

    HTH & TIA,

    m. Th.

    ReplyDelete
  2. RE_1>This is like saying that we need a window-proc in TObject. Allen just posted an example of a (potentially) many to many relation between events and event handlers, implemented via generics (read: c++ templates)
    RE_2>Since DataSource property is known (as datatype: TDataSource) and you expressed premeditation in delegating events of DataSource.DataSet, you already know "something" (a lot) about the outside world. Since TDataSource makes composition with its DataSet property, the only way to handle what you said you would have to use notifications anyway. How can TFooComp be deployed in an "unknown code" since you already subscribed to ancestor class contract and most like to DataSource and further on to DataSource.DataSet via simple composition? (TFooComp "has a"). Of course, in your example TFooComp would rather be a descendent o TDataSource... If you look at the source of TSimpleDataset (interesting coincidence, from what I remember, Allen baptized that component many years ago...) you will see that composition (with or without delegation) go hand in hand... Generic delegation is brilliant and, since you were wondering, it's "safe" (as in early binding, or compiler front-end checked). In a way, your "business as usual" components are dbAware controls... Just in a way... So, lets not judge the model by assuming that helpers are anything but helpers...

    Allen, Thanks again, Cool!

    Yours,
    Daniel

    ReplyDelete
  3. disregard "FooComp would rather be a descendent o TDataSource"

    ReplyDelete
  4. @Daniel:

    #1:
    Somewhat but not quite. Let's get out from Windows box. For example see:

    http://en.wikipedia.org/wiki/Erlang_(programming_language)

    and go to "Concurrency and distribution oriented language" chapter. (there, the definition for 'process' is different for a regular OS process, take care).

    Also, you can have a look at

    http://en.wikipedia.org/wiki/Oz_programming_language

    "Message passing concurrency" part.

    And, perhaps, the accolade at:

    http://en.wikipedia.org/wiki/Message_passing

    (especially the "OOP" part)

    ...and, yes, my request is somewhat outside of the main topic of Allen's post.

    #2:
    Yes, nowadays we must use notifications to implement this. And we must do hard-wired/specialized/"premeditated" :-) code to do this, ie.

    if FDataSourcenil then
    if FDataSet.DataSetnil then
    begin
    FOldBeforePost:=FDataSource.DataSet.BeforePost;
    FDataSource.DataSet.BeforePost:=FNewBeforePost;
    end; //...to attach

    (and similar code to 'detach' it)
    ...also code (as we both said) "somewhere else" to handle the parts lifetime (eg. wh/if FDataSource dies? wh/if FDataSource.DataSet dies/changes?) so we have a whole bunch of (almost) non-reusable code spreaded through the class in order to handle this.

    What I'm asking is:

    Can I have in TFooComp.SetDataSource(Value: TDataSource) something like TDataSource.DataSet.BeforePost add MyNewHandler?

    ...and everything to be managed automatically, preferably /without/ having a TComponentMulticastEvent in each TFooComp instance. Imho, your answer, although quite ok, doesn't address quite well this central (and single) point of my previous post.

    Just my2c & HTH,

    m. Th.

    ReplyDelete
  5. Ouch, the blog engine ate (again) the angular braces. Rewritting the code:

    if Assigned(FDataSource) then
    if Assigned(FDataSource.FDataSet) then
    ...

    ReplyDelete
  6. Is the code for these three blog posts available as a downloadable demo .zip file? That would be handy. I think it would make an excellent Developer Network posting.


    Warren

    ReplyDelete
  7. Any source code available ?

    ReplyDelete
  8. Great set of articles on Multicasting events. It would be immensely helpful if you could make available the source code for your posts to fill in the gaps of understanding. Thanks.

    ReplyDelete
  9. I join to all the requests for source code, will help fill in the gaps and understand, maybe the community could enhance and make a project of it for all to use

    ReplyDelete
  10. Ralf SteinhaeusserApril 12, 2011 at 8:59 PM

    You'll find a working implementation of the 1st article in
    http://stackoverflow.com/questions/1225256/getting-allen-bauers-tmulticasteventt-working

    Cheers,
    Ralf

    ReplyDelete

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.