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.
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:
ReplyDelete1. 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.
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)
ReplyDeleteRE_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
disregard "FooComp would rather be a descendent o TDataSource"
ReplyDelete@Daniel:
ReplyDelete#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.
Ouch, the blog engine ate (again) the angular braces. Rewritting the code:
ReplyDeleteif Assigned(FDataSource) then
if Assigned(FDataSource.FDataSet) then
...
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.
ReplyDeleteWarren
Any source code available ?
ReplyDeleteGreat 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.
ReplyDeleteI 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
ReplyDeleteYou'll find a working implementation of the 1st article in
ReplyDeletehttp://stackoverflow.com/questions/1225256/getting-allen-bauers-tmulticasteventt-working
Cheers,
Ralf