Drawing this subject to a close (finally!), here’s the concluding post I promised, including the fully documented and finished implementation that has been serving me well for almost 2 years. The finished implementation incorporates a number of refinements to the core framework, and those are what we shall briefly look at in this final post.
TOnDestroy – Taking Care of IOn_Destroy
The IOn_Destroy mechanism used to manage the relationship between listeners and events is useful, but a little cumbersome. Fortunately, Delphi itself provides a means to greatly simplify the use of this mechanism – interface delegation.
By implementing a class that takes care of the IOn_Destroy interface, we can add IOn_Destroy support to any class much more simply as shown below, using a form class as an example:
TMainForm = class(TForm, IOn_Destroy) // Published declarations private fOn_Destroy: IOn_Destroy; public constructor Create(Owner: TComponent); override; property On_Destroy: IOn_Destroy read fOn_Destroy implements IOn_Destroy; end; constructor TMainForm.Create(Owner: TComponent); override; begin inherited; fOn_Destroy := TOnDestroy.Create(self); end;
That’s it – done.
There are a couple of things to note:
Firstly, the private member declaration fOn_Destroy holds an interface reference, NOT an object reference. This is essential for one reason, and useful for another.
It is essential because otherwise the reference counting system in Delphi will cause our aggregated object to be prematurely destroyed (the multicast events framework does not hold on to any references to IOn_Destroy – references are obtained, used to access the exposed On_Destroy event, and then discarded).
It is useful because – unless you are doing something beyond the scope of this series of posts – this will be the ONLY reference kept to the class implementing On_Destroy for our class. So when any TMainForm instance is destroyed, this sole reference to it’s IOn_Destroy implementation is released, automatically freeing the object providing that implementation. In otherwords, we don’t have to worry about remembering to free the aggregated object.
This comes with a small price though. When using interface delegation to add IOn_Destroy support to a class the previous specification for the IOn_Destroy interface makes it a little cumbersome to work with, requiring code that accesses the On_Destroy event to go through some awkward indirection:
var mainform: TMainForm; mainform.On_Destroy.On_Destroy.Add( handler );
To alleviate this, the original IOn_Destroy interface is renamed IOn_Destroy_ and a new IOn_Destroy interface introduced, extending the original:
IOn_Destroy_ = interface function get_On_Destroy: TMultiCastNotify; property On_Destroy: TMultiCastNotify read get_On_Destroy; end; IOn_Destroy = interface(IOn_Destroy_) procedure Add(const aHandler: TNotifyEvent); procedure Remove(const aHandler: TNotifyEvent); end;
The TMultiCastEvent implementation is modified to use the base interface, so if you have a class with an existing On_Destroy event, you can still expose that to the framework by directly implementing IOn_Destroy_ if you wish and the mechanism continues to work as before.
However, if you use the TOnDestroy class to add On_Destroy support, you have the choice of exposing it either as IOn_Destroy_ or the more user-friendly IOn_Destroy. The Add() and Remove() methods exposed by this interface allow your code to use the exposed IOn_Destroy as if it were the On_Destroy event itself:
var mainform: TMainForm; mainform.On_Destroy.Add( handler );
Destruction – Only Half The Story
So implementing On_Destroy support is made a great deal less tiresome.
But adding support for a number of events is still a pretty long-winded affair. Imagine some class providing multicast events for create, change and destroy events:
fOn_Create := TMultiCastNotify.Create(self); fOn_Change := TMultiCastNotify.Create(self); fOn_Destroy := TMultiCastNotify.Create(self);
Surely there is something we can do about this?
Fortunately, yes. Unfortunately, it’s not something that we can handle generically in the base implementation. We can however introduce some assistance in the TMultiCastNotify event class, along with another useful helper routine. So let’s take a look at the additions to the final declaration for the TMultiCastNotify event class:
TMultiCastNotify = class(TMultiCastEvent); : class procedure CreateEvents(const aSender: TObject; const aEvents: array of PMultiCastNotify); procedure DoEventFor(const aSender: TObject); : end;
CreateEvents is a basic factory method – it will instantiate a new instance of TMultiCastNotify for each reference passed to it. The references have to be passed by address, but that is only a slight inconvenience, compared to the alternative:
TMultiCastNotify.CreateEvents(self, [@fOn_Create, @fOn_Change, @fOn_Destroy]);
There is a slight risk introduced by such a method – what if we passed the same reference more than once, or a reference that has already been assigned?
This we can take care of in the base class.
TMultiCastEvent now supports a CheckReferences() method. This method inspects a supplied array of pointers to methods (actually pointers to pointers to methods, to be precise). It inspects each method reference and checks for two things:
1. that each reference is unique
2. that each reference is currently unassigned
The implementation of the CreateEvents() method includes an initial call to CheckReferences() to ensure we don’t make these mistakes when using TMultiCastNotify, and as a result, both of these example will result in a runtime error (EInvalidPointer exception):
// Reference already assigned - this will fail... fOn_Create := TMultiCastNotify.Create(self); TMultiCastNotify.Create(self, [@fOn_Create]); // Duplicate references - this will fail... fOn_Create := NIL; TMultiCastNotify.Create(self, [@fOn_Create, @fOn_Create]);
Since CheckReferences() is little more than a more-sophisticated-than-usual ASSERT(), it is subject to an {$ifopt C+} conditional compilation directive. i.e. it is only part of the class if compiling with ASSERTs turned on. If you implement your own multicast event classes with similar factory methods you will need to respect this compiler setting, just as TMultiCastNotify does.
Do Me A Favor
The last small addition to TMultiCastNotify that you may have noticed is the addition of a DoEventFor() method.
Just as with the unicast form of TNotifyEvent, occasionally we need to fire an event and have that event represent a different sender from the usual. DoEventFor() simply satisfies this occasional need on our multicast event.
TMultiCastEvent vs The Alternatives
A number of attempts have been made to implement, or at least approximate, multicast events in Delphi over the years – more so I think since C# and .NET arrived on the scene.
Most of those that I have come across either make no attempt to provide a framework for implementing event types not supported “out of the box”, other than the “copy+paste+modify” reuse paradigm, or place rather obscure demands on implementers of events, and/or consumers of those events.
The overriding aim in my implementation was to make working with multicast events as close as possible to working with existing Delphi events. 100% compatability of existing handlers with multicast events is, I think, a pretty good achievement.
🙂
MultiCast Events In The Future
Mention has recently been made of possible work in this area in future versions of Delphi.
Whatever comes out of that exercise I for one hope that we don’t end up with a solution that mimics C#/.NET events. It’s an approach that I don’t think “fits” Delphi at all.
If nothing else, having had multicast events available to me for the past 2 years, one thing I have learned is that not all events need to be multicast. This is most especially true in the GUI. Multicast events have proven to me to be most useful in business, and framework, objects. (My examples have used GUI controls simply because they provide a convenient and visual test bed for demonstrations).
As promised at the outset, the fully documented implementation is now available for download. This final version includes a help file generated from the source (plus some additional topic content) using the excellent Doc-o-Matic.
[dm]3[/dm]
UPDATE: An updated version with bug fix and minor enhancements is now available. Full details of changes in this update are available here.
[dm]7[/dm]
The download contains a help file (Win32 Help format) and two versions of the unit. One named MultiCast.pas, the other Deltics.MultiCast.pas. That’s the only difference. I primarily use Delphi 7 and so my library of some 200+ units are named with this dot-notation, but the code should be usable in earlier versions of Delphi where this dot-notation is not supported, so this renaming task has been taken care of for the convenience of users of those earlier versions.
🙂
I should confess that, as I alluded to in a couple of the posts in this series, the process of blogging about this work gave me some fresh insights into what I had thought was a very well settled implementation. As a result the finished unit provided here is slightly modified from the version I’ve been using. The changes are neither extensive nor significant, but still, in the interest of full disclosure….
If anyone has any problems (or for that matter, further suggestions or even just comments of any sort) about the implementation, please do not hesitate to get in touch.
Interesting code – overall, it’s a nice, clean approach. A couple minor observations though…
1. I personally don’t like the way your setter for the Enabled property works. Why not just use a Disable/Enable procedure pair, if only for consistency with the VCL (BeginUpdate/EndUpdate and DisableAlign/EnableAlign)?
2. You could avoid the IOn_Destroy_/IOn_Destroy semi-hack by putting the needed methods on IOn_Destroy directly and having TOn_Destroy derive from TMultiCastNotify.
Hi CR – Yes, I’m not totally happy with the Enabled property myself. The reason I didn’t go with an Enable/Disable pair was simply to keep the number of methods to a minimum, but I agree it isn’t particularly pretty.
If I found myself enabling/disabling methods a lot I would probably have changed it already, and now that someone else has pointed it out … peer pressure is a powerful force.
🙂
I’m not entirely sure what you mean by #2 – I think I see what you are driving at, but I’ll need to take a look.
Thanks for the comments.
Good job! Jolyon, I want send you my multicastevent source code to you, can you give me a email? My email address is [snip].
And I use the TypeInfo to get the parameters count, then I can use different version of event firer to call the events chain correctly.
Hi George – I have emailed you. I hope you didn’t mind, I thought you’d prefer it if I removed your email addy from the comment.
Jolyon, I mean this:
1. Change the ancestor of TMultiCastEvent to TInterfacedObject (if it were my own code I would use the COM aggregation pattern here, though that would require uses of TOn_Destroy to hold an object rather than an interface reference to it, so let’s not complicate matters…).
2. Comment out IOn_Destroy_ and the ancestor spec for IOn_Destroy.
3. Remove all but the destructor from TOnDestroy.
4. In TMultiCastEvent.Destroy change the type of ‘listener’ to IOn_Destroy, update the Supports call to check for IOn_Destroy, and edit the call to Remove to be on ‘listener’ directly.
5. Do the same things in the previous step for TMultiCastEvent.Add.
6. Edit TObDestroy.Destroy (now the class’ only method) to be simply
. destructor TOnDestroy.Destroy;
. begin
. DoEvent;
. inherited;
. end;
That’s it. Note a few lines of code have been trimmed in process, which is a bonus I think.
Urgh, minor typo (‘TObDestroy’ for ‘TOnDestroy’), and my full stop ploy didn’t work, but you should be able to see what I meant…
Hi CR, OK, I think I see now.
One thing I was trying to be careful of was to keep a multicast event as lean as possible. Bringing TInterfacedObject into the ancestry adds a little extra “weight” to the class (a refcount) for very little benefit afaict, other than making TOnDestroy a little simpler to implement.
I can see that your changes save a little bit of code in the framework implementation but it doesn’t seem to make all that much difference – if any – in the code that consumes the framework, other than to add that additional “weight” to each event instance.
Having said that, I think maybe TOnDestroy could be made a little cleverer – I may take another look, but I was hoping to move on to other subjects this week, so I may have to come back to this later.
Thanks for all the comments and taking time to think about this stuff though. It’s gratifying to know that it has engaged the interest of others.
🙂
“Bringing TInterfacedObject into the ancestry adds a little extra “weight” to the class (a refcount) for very little benefit afaict”
Actually, it doesn’t add any runtime ‘weight’ at all, since TOnDestroy remains the only class actually used through an interface reference. Changing the root ancestor was just for convenience.
“I can see that your changes save a little bit of code in the framework implementation but it doesn’t seem to make all that much difference – if any – in the code that consumes the framework”
Correct, and that was the intention. Nevertheless, my (minor) alterations save more code than would be added if you were to implement Disable/Enable methods instead of your existing funky Enabled property setter. 😉
I’m a little confused now then, as I thought you were suggesting deriving TMultiCastEvent from TInterfacedObject…?
And yes, the “funky” Enabled property needs to be de-funked.
🙂
I’m currently trying to decide on a source hosting solution to avoid having to post updated downloads the whole time. Any suggestions?
I’ve looked at googlecode and sourceforge and neither of them appealed to me, not least because I don’t use SVN.
“I’m a little confused now then, as I thought you were suggesting deriving TMultiCastEvent from TInterfacedObject…?”
Sure. But just because a class is descended from TInterfacedObject doesn’t mean you have to use it via interface pointers. Hell, TGraphic descends from TInterfacedPersistent and that doesn’t mean much (if anything) in day-to-day code, does it…? (And yes, I realise TInterfacedPersistent doesn’t implement its own reference counting, but then what it does implement instead, namely a version of the COM aggregation pattern, isn’t something TGraphic uses either.)
Perhaps not, but if a class derives from TInterfacedObject it DOES get a ref count member variable.
And I still don’t see what benefit this would give other than, by using an interface reference, removing the need to explicitly destroy events. But if that is what you are suggesting, then exposing events using interface references would then require an interface be declared for each event type as well:
IMultiCastNotify = interface
Add(TNotifyEvent);
Remove(TNotifyEvent);
end;
TMultiCastNotify = class(TMultiCastEvent, IMultiCastNotify)
Which isn’t too much of an additional burden I guess, although I wonder how this is likely to fit if I use generics in D2009. Is this going to work, for example:
TMultiCastNotify = class(TMultiCast<TNotifyEvent>, IMultiCastNotify);
Or will that interface cause problems?
Or perhaps I’ve misunderstood exactly what you have in mind. Feel free to email me some code (jsmith at this domain) showing your envisaged changes if you think that would help.
I’m totally open to suggestions, I think I’m just not “getting” this one yet.
Urgh – no, I’m not suggesting exposing all events as interfaces, hence the analogy with TGraphic. The steps listed in my second reply to modify your original source are complete. I tagged it as a *minor* suggestion remember…
That said, you are nevertheless correct to infer I have thought about suggesting using interfaces throughout (I just haven’t intended this to come out in my comments so far). Thinking about it a bit more, the benefit of doing that is that what events an object supports would be semi-discoverable at runtime, each event kind (On_Paint, On_Click, etc.) having its own GUID. So…
{ in Deltics.MultiCast }
type
TMultiCastEvent = class(TObject, IInterface)
//etc. – implement COM-style aggregation rather than
//inherit from TInterfacedObject
end;
INotifyEvent = interface //no need for a GUID
procedure Add(const aHandler: TNotifyEvent);
procedure Remove(const aHandler: TNotifyEvent);
end;
IOn_Destroy = interface(INotifyEvent)
[‘{D2AD6882-0CB7-40A6-839D-F527071918FC}’]
end; //no need to redeclare the methods
IOn_DblClick = interface(INotifyEvent)
[‘{A2F70511-2B46-4935-83A8-B5E5C3F7DC8C}’]
end; //no need to redeclare the methods
{ in a unit implementing an event owner }
type
TMyEventOwner = class(TForm, IOn_Destroy, IOn_DblClick)
private //using aggregation now, so use an object ref for FOn_Destroy
FOn_Destroy: TOnDestroy;
FOn_DblClick: TMultiCastNotify; //as before
public
//other stuff, e.g. constructor and destructor
property On_Destroy: TOnDestroy read FOn_Destroy implements IOn_Destroy; //change of property type
property On_DblClick: TMultiCastNotify read FOn_DblClick implements IOn_DblClick; //added ‘implements’ thingy
end;
{ example code for querying for an event at runtime }
procedure TTest.WatchForDblClick(Control: TControl);
var
On_DblClick: IOn_DblClick;
begin
if Supports(Control, IOn_DblClick, On_DblClick) then
On_DblClick.Add(MyDblClickHandler);
end;
I might get round to typing this up and sending you some code this evening. A ‘proof of concept’ is currently working…
I guess I’m not familiar enough with COM-style aggregation – I’ll be interested to look at any code you send.
I’m not sure I see the point of discoverable events, except in very limited and fairly abstract cases (an generic “event logger” perhaps?).
In more common usage, if I want to respond to an event in some other object then I have to have a response but I also have to know that that object will trigger that response in circumstances where the response is required and appropriate.
In other words, I need to know more about the other object than simply that it supports some event. And if I know that much about an object, I won’t need to discover it’s events, because I surely know what events it supports. Don’t I?
“I guess I’m not familiar enough with COM-style aggregation”
Well, it just means passing on the IInterface calls to the owning object. In the present case I’m not sure whether the ‘aggregated’ or ‘contained’ behaviour is preferred (i.e., whether QueryInterface is passed on or not), given an IOn_Destroy instance is at once both a property of its owner and queried on the owner. (The difference is in whether, having queried for IOn_Destroy, you then want to be able to query for other interfaces from the just retreived IOn_Destroy reference.)
“I’m not sure I see the point of discoverable events, except in very limited and fairly abstract cases”
Possibly, but making it so brings consistency between On_Destroy and any other multicast event. For, in your original implementation, On_Destroy is ‘special’ – it needs its own special class and managed in a particular way, using an interface rather than an object reference; in my revised one (I’ve emailed you the code BTW), it is not, to the extent that a distinct TOnDestroy object is not really necessary – an event owner could just use a regular TMultiCastNotify for FOn_Destroy and call FOn_Destroy.DoEvent manually in its destructor.
Well, to be fair, On_Destroy *is* a special case – it has special meaning, and purpose, for the multicast event framework itself that no other event does.
Implementing On_Destroy is entirely optional – if a class reliably removes any handlers from all events before being/when destroyed then you don’t even need to bother.
TOnDestroy isn’t *needed* to support IOn_Destroy, it just made it easier to do so, *if* that is what you choose to do (rather than choosing to be careful to remove handlers, as above).
Thanks for the code.
🙂
“TOnDestroy isn’t *needed* to support IOn_Destroy”
Of course not, but my (second) mod almost entirely removes its specialness from the POV of its owner, since using TOnDestroy now saves merely a single line of code in the destructor. Moreover, in your original code, On_Destroy properties only *look* like any other On_XXX event, since you expose a wrapper class through interface rather than the TMultiCastXXX instance directly:
var
Event: TMutiCastEvent;
begin
Event := Obj.On_DblClick; //compiles
Event := Obj.On_Destroy; //compiles in my mod but not your original code
Sure, this isn’t a big deal (or even a small deal!) in practice. Also, I’m willing to concede that this lack of a practical difference holds too for the other added consistency my mod gives, with On_Destroy now longer being special in its being discoverable at runtime through a Supports call. I just find the consistency more aesthetically pleasing I guess…
Delphi has out of box mechanism for destroy notification, but this is only available for TComponents. Both subscribers and publishers must be TComponents:
procedure TComponent.FreeNotification(AComponent: TComponent);
procedure TComponent.RemoveFreeNotification(AComponent: TComponent);
With regards to alternative mechanism of destroy notification, I’d better use published property instead of interface.
Warning about TMethod to TObject cast: TMethod can be class function as well. And I don’t know a reliable way to distinguish TObject from TClass.
Even more, there is a Contnrs.TComponentList that tracks its subitems.