Multi-cast delegates the evil way

Multi-cast delegates the evil way

  • Comments 11

A lot of people have asked me over the years how various kinds of event binding work.  Basically, event binding works like this:

1)     Someone clicks on a button,
2)     then a miracle happens, and...
3)     the button's event handlers execute.

It's that second step that people struggle with.

First, some terminology.  I studied applied mathematics, and somethings we talked about quite a bit were sources and sinks. Sources produce something -- a faucet produces water at a certain rate, for example.  A sink takes that water away.  We'll borrow this terminology for our discussion of events.  An event source is something that produces events, like a button or a timer.  An event sink is something that consumes events, like an event handler function.  (Event sinks are also sometimes called "listeners", which mixes metaphors somewhat, but that's hardly unusual in this profession.)

This terminology leads to a rather unfortunate homonymy -- when I first heard "this method sinks the click event", I heard "this method syncs the click event".  When we talk about event sinks, we're talking about the consumer of something, not about synchronizing two things in time.  (Sinks, of course, can be asynchronous...)

The miracle actually isn't that miraculous.  Implementing event sources and sinks requires two things: first, a way to wrap up a function as an object, such that when the source wants to "fire" the event, all it does is invokes the sink's wrapper.  Second, a way for the thread to detect that the button, or whatever, has been pressed and thereby know to trigger the sink wrappers. 

An explanation of the magic behind the latter would take us fairly far afield.  Suffice to say that in IE, the details of how that mouse press gets translated into windows messages and how those messages are dispatched by the COM message loops behind the scenes are miracles that I don't want to talk about in this article.  I'm more interested in those wrappers.

In the .NET world, an object that can be invoked to call a function is called a delegate.  In JScript Classic, all functions are first-class objects, so in a sense, all functions are delegates.  How does the source know that the developer wishes a particular delegate (ie, event sink) to be invoked when the event is sourced?

Well, in IE, it's quite straightforward:

function doSomething() {  }
button1.onclick = doSomething;  // passes the function object, does not call the function

But here's an interesting question -- what if you want TWO things to happen when an event fires?  You can't say

function doSomething() {  }
function doOtherThing() {  }
button1.onclick = doSomething;
button1.onclick = doOtherThing;

because that will just replace the old sink with the new one.  The DOM only supports "single-cast" delegates, not "multi-cast" delegates.  A given event can have no more than one handler in this model.

What to do then?  The obvious solution is to simply combine the two.

function doSomething() {  }
function doOtherThing() {  }
function doEverything() { doSomething(); doOtherThing(); }
button1.onclick = doEverything;

But what if you want to dynamically add new handlers at runtime?  I recently saw an inventive, clever, and incredibly horribly awful solution to this problem.  Some code has been changed to protect the guilty.

function addDelegate( delegate, statement)
  var source = delegate.toString() ;  
  var body = source.substring(source.indexOf('{')+1, source.lastIndexOf('}')) ; 
  return new Function(body + statement);
}

Now you can do something like this:

function dosomething() { /* whatever */ }
button1.onclick = dosomething;
// ... later ...
button1.onclick = addDelegate(button1.onclick, "doOtherThing();") ;

That will then decompile the current delegate, extract the source code, append the new source code, recompile a new delegate using "eval", and assign the new delegate back.

OK, people, pop quiz.  You've been reading this blog for a while.  What's wrong with this picture?  Put your ideas in comments and I'll discuss them in my next entry.

This is a gross abuse of the language, particularly considering that this is so easy to solve in a much more elegant way.  The way to build multi-cast delegates out of single-cast delegates is to -- surprise -- build multi-cast delegates out of single cast delegates.  Not decompile the single-cast delegate, modify the source code in memory, and then recompile it!  There are lots of ways to do this.  Here's one:

function blur1(){whatever}
function blur2(){whatever}

var onBlurMethods = new Array();

function onBlurMultiCast()
  for(var i in onBlurMethods) 
    onBlurMethods[i]();
}
blah.onBlur = onBlurMultiCast;
onBlurMethods.push(blur1);
onBlurMethods.push(blur2);

I'll talk about VBScript and JScript .NET issues with event binding another time. 

I'm on vacation for the next three weeks, so I might have lots of time for blogging, or lots of other things to do.  So if you don't hear from me, I'll be back in the new year.  Have a festive holiday season!

  • There is a great comic illustrating your intro in Daniel Dennett's book "Consciousness Explained" [or possibly "Darwin's Dangerous Idea". FYI.
  • I was making a deliberate reference to this famous cartoon: http://www.sciencecartoonsplus.com/miracle.gif Is that the one you're thinking of?
  • just thought I'd blog my take instead of leaving comments: http://weblogs.asp.net/asmith/posts/43205.aspx
  • Funny you should mention this topic...I've just released the beginnings of an ECMAScript BCL called f(m)...and the centerpiece of this kit is....drumroll...a common Event system that handles this exact problem. Except I basically copied the .NET guys :) You can find it at http://fm.dept-z.com, and to be honest, I'd love some feedback. Why did I do it that way? Because I've seen a number of examples like the ones you showed above, and it looks SO ugly...
  • Here's my solution. It allows the delegating object to behave just like both a method AND an array of delegate methods. I used the "fn.call" syntax to allow contained methods of MulticastDelegate to access its "this" property (i.e., they become methods of the parent object as well). function println(v) { WScript.StdOut.WriteLine(v) } function MulticastDelegate() { var $d = function() { for( var ix in arguments.callee.delegates ) arguments.callee.delegates[ix].call(this, arguments ) } $d.delegates = new Array(); $d.push = function(f) { this.delegates.push(f) } // add other Array methods to taste return $d; } example = new MulticastDelegate(); example.push( function() { println( "One" ) } ) example.push( function() { println( "Two" ) } ) example(); Or, if talking about a web page: button1.onclick = new MulticastDelegate(); button1.onclick.push( function() {whatever} ); button1.onclick.push( function() { a more complex whatever } );
  • If I were really putting this into production, I would also have MulticastDelegate examine it's "arguments" and push all such elements onto the constructed array. This would allow multiple delegate functions to be added at construction time. Jay.
  • Assuming a script running in a browser, there is a MUCH easier way to do all of this. But first i'll highlight why these kinds of ideas are not always useful. Not everybody writing script that attaches to events, controls all the code on the page. assigning a custom multicast delegate doodad to SomeElement.onclick will be destroyed if some other chunk of code that doesn't have any clue about your code just comes along and assigns it's own function to SomeElement.onclick. Note that the code examples here do exactly that. You may be interfering with somebody else's event handling system. If you are wondering about a real-world scenario... think about asp.net server controls development. If I package some behavior in a control with script, I want to make sure that my event handling system will not be affected by the page developer or some 3rd party. This is why I think it's better to leverage the browser here, and use addEventListener (for W3C DOM compliant browsers) and/or attachEvent ( for IE). I've blogged about this, with my current methodology, here: http://weblogs.asp.net/asmith/posts/30744.aspx
  • First a nitpick, the code should be: > button1.onclick = addDelegate(button1.onclick, "doOtherThing();" ); and > blah.onBlur = onBlurMultiCast; And not as written. Of course, had you been using the BeyondJS library you would have functional composition available, in which case you could simply write: blah.onBlur = blur1.andThen(blur2); andThen is implemented in such a way that you can modify the function chain dynamically. An added advantage, though not in this case, is that the value returned by blur1 is passed as an input arg to blur2. Another way to approach this problem using BeyondJS is event streams. Streams are objects that behave like containers, only in "time" instead of in "space". This allows you to use list comprehensions on events: var st = eventStream(blah, "onBlur"); st.foreach(blur1); st.foreach(blur2); Unsurprisingly this looks a lot like your onBlurMultiCast example, only streams support lots of other list comprehensions such as filer. I can think of various reasons why you would dislike the addDelegate code, primarily the fact that it's eval. Another problem is that it won't always work. For example: buttons1.onclick = alert; button1.onclick = addDelegate(button1.onclick, "doOtherThing();"); would break because window.alert.toString() just won't work. You could try "" + window.alert which would run, but the result still won't work. A third problem is that the original delegate function looses its scope (closure). And yes, for the browser you should simply use addEventListener or attachEvent.
  • attachEvent has always left something to be desired, but I stopped using it awhile ago so I don't recall what bugged me about it.
  • Andy- Your method still requires people to conform to your function call, which is no different than the other methods. This doesn't really fix the problem of 3rd party integration you speak of.
  • Do people still attach events that way? attachEvent has been around for years. Why reinvent the wheel?
Page 1 of 1 (11 items)