The other day I had a little chat with a colleague. It was about one of his worker components and how it used events to communicate intermediate state and the final result. About event registration and deregistration, and the ungainly code resulting from it. When I suggested using callbacks instead of events, he quickly jumped on the bandwagon; a few day later I got a single note on Messenger: „Callbacks are WAY COOL!“.
That got me thinking. Why did I recommend callbacks? When and why did I abandon events? What’s wrong with events anyway?
Well, there’s nothing wrong with events at all. It’s just that Silverlight, asynchronous, and multithreaded processing have changed the picture (and I may have worked too much in those areas lately 😉 ). And this is the deal:
- Until recently I used to “see” (read: write code for/against) events mainly from the event consumer side. WinForms, WebForms; register handler, react to something. That kind of stuff.
- Since “recently” I kind of had to do that less often. Why? Silverlight data binding solved many of the demands I previously used to address with event handlers. Making a control invisible for example. (Events still drive databinding, but at the same time databinding shields them away from me.)
- Also since “recently” I have had to implement the providing part quite a bit more often. Why? Silverlight databinding relies on certain interfaces that contain event declarations, namely INotifyPropertyChanged, INotifyCollectionChanged, and INotifyDataErrorInfo.
- And the “event-based asynchronous pattern”. Yep. We’ll get to that one.
OK, let’s try to classify these scenarios.
Broadcasts
The first two points are just two sides of the same coin: The radio broadcasting scenario.
- Some component wants to advertise interactions or state changes; hence it broadcasts them by way of events.
- Some client code needs to get notified about one or the other of these events; hence it subscribes to the event by way of an respective event handler, to consume it from there on.
Same as radio, the broadcaster broadcasts and doesn’t care whether anyone listens. Same as radio, the receiver is turned one and listens as long as something comes in. Well, the analogy stops at the life time: Event source and consumers tend to have similar life time.
Passing the Baton
The 3rd point is actually a quite different scenario: Start some work and have an event notify me about the result (and sometimes about intermediate state). Once I receive the result I let go of the participant and pass the baton on to the next piece of work.
Same as in a relay run, each participant does one job and once it’s done, he is out of business. Same as in a relay run, participation is obligatory – take someone out (or put something in his way) and the whole chain collapses.
Needless to say that this is nothing like the broadcasting scenario…
Usually the reason for the event approach (rather than simple return values) is asynchronous processing; and in fact this is not a particularly new pattern – BackgroundWorker works accordingly. On the other hand the pattern is still evolving, as the usual pattern for asynchronous work has been no pattern at all (i.e. leave it to the developer, as Thread or ThreadPool do), or the IAsyncResult pattern (relying on a wait handle). New developments however start to employ events more often, and Microsoft has actually dubbed this pattern as “event-based asynchronous pattern” (see MSDN).
One area which relies heavily on this pattern is server requests in Silverlight, via WebClient or generated proxies. But it doesn’t stop there, as Silverlight is asynchronous by nature, rather than by exception: Showing a modal dialog, navigating to another page, (down-)loading an assembly. And quite often these single incidences are chained together to form a bigger logical program flow, for example:
- The user clicks the delete button –> the application shows the confirmation dialog –> a call to the delete operation is made –> IF it succeeds (the code navigates to a list page –> …) OTHERWISE (an error message box is shown –> …)
Any arrow represents an event based “hop” to bridge some “asynchronicity gap” – essentially turning the logically sequential chain into a decoupled, temporary register and deregister event nightmare.
Coming back to the beginning of the post: This is the scenario I was discussion with my colleague. And doing this with a whole bunch of events and respective handler methods is simply awkward, especially if you even have to provide the event sources, usually with respective EventArgs classes. And the issue of having to consistently deregister the event handlers in order to avoid memory leaks becomes more prevalent.
Changing the Picture…
Inevitably I got annoyed with the setup/teardown orgies, and eventually I began to abandon events in this case and started passing simple callbacks along. Like this:
void DeleteBook(Book book)
{
string msg= "Delete book #" + book.Inventory + ", ‘" + book.Title + "’ ?" ;
MessageBoxes.Instance.ShowConfirm(null, msg,
ok => { if (ok) BeginDeleteBook(book); }
);
}
void BeginDeleteBook(Book book)
{
this.DeleteBookCall.Invoke(book.BookID,
ea => NavigateToBooks());
}
And actually I’m not the only one following this approach. The Task Parallel Library TPL for example has already started to make heavy use of callbacks. So this is definitely not limited to Silverlight…
Note: Also this lays the ground for a next evolutionary step: Coroutines.
Caliburn has a nice example of what this looks like; a little weird at first glance actually, but it collects all that logically sequential but technically asynchronous control flow in one method. Jeremy digs a little deeper into the topic in his post “Sequential Asynchronous Workflows in Silverlight using Coroutines”.
Anyway, even without going into coroutines, the callbacks over events approach has its merits in terms of coding efficiency. I’ll provide a motivating example, next post.
That’s all for now folks,
AJ.NET
Good job, AJ!
I just took an alike solution in a previous project to handle imports of large amounts of data. Instead of all those events, I used a command-pattern alike approach with callbacks and composite commands that worked as workflows to orchestrate several other atomic commands.
Thanks for sharing
Flo
Comment by Florian Reischl — July 18, 2010 @ 9:15 pm