DOM events in the Microsoft AJAX Library
In previous CTPs, the client-side DOM event model was the IE model. You would use attachEvent and get the event data from window.event. In other words, we had just implemented the IE model in Firefox and Safari. This didn't fly as well as we expected for a number of reasons. For instance, it wasn't very well received from a philosophical point of view: making standards-compliant browsers behave like the one non-compliant browser was interpreted by some as a malicious attempt by the Evil Empire to undermine the standardization of the Web by enforcing proprietary APIs. It wasn't. It just seemed at the time like a smart way to build cross-browser compatibility and the reason we did it this way and not the other way around is that both Safari and Firefox have extensible DOM Element prototypes whereas IE doesn't. In other words, there was no way we could make IE behave like the standard, but we could make the others behave like IE. Any other way to make a library cross-browser has to introduce a third API that abstracts the standard and proprietary APIs. This third API is of course just as proprietary as the IEism, whereas our previous approach had the advantage of not introducing a new one. Still, the implementation was fairly complex and relied on the presence of extensibility points that we had no guarantee we would find on other browsers that we may want to support in the future. Another problem with our first implementation was its reliance on the server to detect browsers and selectively send the compat scripts to the client. So we decided to change our compat layer and come back to a more conventional approach that will be easier to adapt to new browsers and that doesn't rely on the server, let alone on browser detection.
The new model for DOM events is thus introducing a new API, but at least it's closely modeled after the standard APIs so it should feel pretty familiar. There are many differences in the implementations of DOM events that we needed to abstract. The first one is in the names of the methods that you call to add an event. In standard browsers, you use add/removeEventListener, in IE it's attach/detachEvent. The event names themselves are different: "click" is "onclick" in IE. Then, you have to abstract the signature of the event handlers themselves: in IE the parameters come from the global window.event object, in other browsers they are passed as a parameter. Finally, the contents of the event parameter object are themselves widely divergent from one browser to the other: mouse buttons don't have the same values for example, and some very useful stuff like mouse positions is missing altogether from the standard.
Here's how you register a click event handler in the AJAX Library now:
$addHandler(myDomElement, "click", someFunction);
As you can see, we use the standard event name here. $addHandler is an alias for Sys.UI.DomEvent.addHandler. You can unhook an event using $removeHandler. For instance, you should do that from your dispose methods to break circular references between your JavaScript objects and the DOM and to prevent memory leaks. From $addHandler, we wrap your function pointer into a closure that will be what will really be attached as the DOM event handler to abstract browser differences. What's nice is that you don't need to worry about that, you just provide a function that takes the event parameters object as its argument, and you write this function exactly the way you would write it for a standard-compliant browser. That means that even in IE, the event parameter object will contain standard fields: the key codes will be the right ones, as will be the mouse button values. By the way, so that you don't need to use integers when testing keys and mouse buttons, we have two enums, Sys.UI.Key and Sys.UI.MouseButton, that you can use in your event handlers:
function myClickHandler(e) { if (e.button === Sys.UI.MouseButton.leftButton) { //... } } function myKeyUpHandler(e) { if (e.keyCode === Sys.UI.Key.enter) { //... } }
From the event handler, it's worth mentioning what the "this" pointer means. Just like in a standard event handler, it represents the DOM element the event was attached to, not necessarily the element that triggered the event. Those are different if the event bubbled up. For example, you may have subscribed to the click event of a div element and what was really clicked was a button inside of it. In this case, "this" represents the div, not the button, but you can still get to the button using the target field of the event parameter object. Now, if you're wiring events from a component, chances are you're using delegates as your handler functions. In this case, "this" still refers to your component, not to any DOM element. One more thing to note is that the native, proprietary event object can still be got from the rawEvent field of the event parameter object.
Another "interesting" divergence is the way you cancel an event or prevent it from bubbling up. In IE, you set returnValue to false (resp. set cancelBubble to true), whereas the standard is to call preventDefault (resp. stopPropagation). The event parameter object that you get as the argument of your handler has the two standard methods (preventDefault and stopPropagation) so you can use them without having to worry about IE.
The last things I'd like to show on the new DOM event model are some of the helpers we've added to make component developers' lives easier. In a control or behavior, you typically have to wire up multiple handlers. For example, an accessible hover behavior might want to subscribe to mouseover, mouseout, focus and blur. To do that, you'd typically create delegates to your handlers and then wire up these delegates to the DOM events one by one. From your "dispose" method, you'd also have to remove those handlers one by one and get rid of the delegates. Seeing that this pattern was repeated over and over again in almost any control or behavior sample, we decided to add helpers to batch those operations. So here's how you would wire up all those events:
$addHandlers(this.get_element(), { mouseover: this._onHover, mouseout: this._onUnhover, focus: this._onHover, blur: this._onUnhover }, this);
No need to create delegates here, this will be done for you under the hood. From dispose, it gets even simpler as we are keeping track of everything that was added using the $add APIs:
$clearHandlers(this.get_element());
In a future post, I'll also look at AJAX class events, which are events that you can expose from your own objects, and that are closer to .NET events than to the DOM events I've been showing here.