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.

25 Comments

  • This is very similar to the dojo approach which truth be told was one of the biggest reasons of my choice to use dojo instead of atlas on my projects. It looks like you guys are on the right track though. Nice work.

  • I like how you can wire multiple events for the same handler. Is there a similar way to wire multiple elements to the same handler? Ideally it'd be great to do some sort of CSS-Event Selector or Behaviour type of thing.

  • Jon: so the other frameworks can use short aliases, but we can't? All $ aliases are shortcuts for a properly namespaced function, as the post explains. That means that if any of those collide, you can still use the fully-qualified name and include the conflicting library after ours (that's why the aliases are not used from within the core library's code). It seems like we're the only ones to actually care about not colliding with existing aliases from competing frameworks, and we don't even get some credit for that?

  • > so the other frameworks can use short aliases, but we can't?

    No, you can (and should) use alias', you just should not choose the same prefix as other frameworks...

    > It seems like we're the only ones to actually care about not colliding with existing aliases from competing frameworks,

    In my opinion, if you cared enough about colliding, you would not have chosen the same prefix. (Also, you are not the only ones caring about colliding.)

    > and we don't even get some credit for that?
    Sure you do, but you are Microsoft. I would think that you guys expect the bar to be higher for you than anyone else, being the leading software company in the world...

  • Jon: fair enough, but what other (short) prefix could we have used (MS is not an option for legal reasons)? It's becoming a common convention that such aliases start with $ (which is the only special character allowed with underscore which is more used to denote internal/private) to avoid most collisions with global functions defined by the page developer. Anyway, we checked that we didn't collide with any known alias in the known major frameworks. For the future, nothing can really guarantee that you don't have collisions, even if you use namespaces like we do, save for having a standardization group that coordinates everybody's efforts. That's exactly what OpenAjax is trying to do.

  • http://ajaxian.com/archives/dom-events-in-the-microsoft-ajax-library-formerly-known-as-atlas

  • It's not at all clear to me why Microsoft, which was very late to the Ajax party to start with, felt the need to try to reinvent the wheel and develop their own "brand" of Ajax to begin with. Both Prototype and Dojo are very well established, mature libraries, and by choosing one of those, you wouldn't be stepping on any well established namespaces either. Oh, wait... Microsoft can't use open source software because it's opposed to it, right? Well, you could have licensed Google's or Yahoo's, then. But wait... they're competitors. It wouldn't do to behave in any manner that isn't simply crushing competitors. The same reason Microsoft couldn't use Java, PostScript or PDF either, I guess.

    I guess the question I'll try to answer when I get around to evaluating the "new" Atlas is, What does it bring to the Ajax party that wasn't there before? How does it improve the development, delivery, and useability of Ajax-enabled web applications? If you've indeed made Atlas more cross-browser friendly, that would certainly be a good thing. But it still seems to me that it was an unnecessary effort to begin with, given the large number of top-notch Ajax frameworks and toolkits that already exist. At least Adobe's Spry framework is taking a startingly different approach to the whole thing, one that also works seamlessly across browsers and platforms and whose benefits to developers and end-users seem very clear.

  • Jon: it's not just Prototype who's using the $ prefix. It's just about everyone.

    Leland: Were we really that late? What about Outlook Web Access, arguably the first major Ajax application? What about inventing XmlHttpRequest in the first place? What about callbacks in ASP.NET?
    If we were "opposed to" open source like you claim, how do you explain the the AJAX toolkit is fully open-source (and already includes external contributions)? How do you explain CodePlex as a whole?
    Anyway, *none* of the frameworks you mention are nicely integrated with ASP.NET. The Microsoft AJAX Library not only is very well integrated with ASP.NET, it can also be used without it, say with PHP or Java.
    And by the way, Microsoft *has* used Java in the past, but Sun didn't see it this way. In the same way, Microsoft wanted to include PDF support into Office, but again Adobe didn't see it this way so now it's a separate (free) download from Microsoft.
    So what does Atlas bring to the party? UpdatePanel, xml-script, tight integration with ASP.NET to name a few. Our customers seem to like it.

  • Oh snap! You just got served a cold plate of humiliation Leland!

  • Jon: I completely see your point and it's something we've always taken seriously, as the $get rename attests. We've had lots of discussions on this very subject, both internally and with early adopters. There are tradeoffs we have to make between keeping the majority of our users happy and reducing the risks of collisions with other frameworks. As a matter of facts, when we asked, the majority of our users told us that they preferred to keep the $ function even if there was a known collision with just about any other existing framework. We actually went against that feedback and did the rename to $get. Similarly, we went against the convenience of extending the Array prototype and moved everything to Array statics. By the way, the built-in type extensions have a much higher risk of collision than the $ functions, but in the same way, we think that if we (and other frameworks) are going to keep an acceptable level of usability, the only way to avoid stepping on each other's toes in the future is just to talk to each other. OpenAjax is proof that other framework authors feel the same way.
    Again, we take these considerations very seriously, so thanks for the feedback.

  • > All $ aliases are shortcuts for a properly namespaced
    > function, as the post explains.

    According to the ECMA spec, "$" is conventionally reserved for machine generated variables.

  • Sort of a simplistic question after the discussion above; but does the MouseButton enumeration actually work? I'm currently developing an app that when run in Firefox works fine, when run in IE6 the buttons are mixed up, this occurs when I do something in a mouseup event, in a click event of course it returns left all the time (presumably because in IE window.event.button is always 0 in the click event.)

    Regards,
    Chad.

  • Chad: this is a known problem that is fixed in the RTM version. Thanks for reporting it.

  • Hmm, RTM version eh? So you guys are pretty close then :)

  • This helped clear a few things up I am looking forward to the article on AJAX class events since that is where I am stuck.

  • Jacques: sorry, $get takes two parameters. One is the id, the second, optional parameter is the parent element. Passing it an element that is in a different window is an abuse of the API and something that's entirely unsupported. The reason is mainly that $addHandler not only abstracts the browser differences but also helps components dispose of these events, which depends on the window where it happens. How exactly is it failing? Does it just not work or do you get any kind of error message?
    I'll make sure this is documented. I think you need to hook up your events yourself and handle dispose yourself, not using $addHandler in this case.

  • Thx for your response.

    Internet Explorer reports the following JS error on the page:
    Error: object required
    Code: 0

    I think this is a serious limitation and I am not sure I understand its justification considering:

    1) the following works in IE (addEventListener works too)
    g_FileUploadTextBox.attachEvent("onkeyup", updateFileName);
    g_FileUploadTextBox.attachEvent("onpropertychange", updateFileName);

    2) In the Ajax framework, $addHanlder/$removeHandler maintains an array of events which help the dispose process but I am not sure why this depends on the "window where it happens".

    3) I think scripting components in an IFrame is a fairly common Ajax scenario, epecially for file uploads, and you this should have been considered.

  • Jacques: the problem is that it would be very complex to subscribe to the unload event of all windows you attached events in and dispose of them properly. We just couldn't ensure the level of functionality that we can ensure in the same window.
    If you're using an iframe, what I recommend is that it has its own copy of the AJAX library and pretty much works in isolation, with just the level of communication you need between JavaScript components (which you can achieve by exchanging delegates between components in different windows. Avoid DOM-based communication like you're trying because that will be a nightmare to clean up and avoid memory leaks. If you are using an iframe, that's usually because you want the iframe to be able to post and navigate independantly, which means that the dispose logic will need to be taken care of. That will be a lot easier with the iframe working as a full page, with its own copy of the framework.

  • Is what your doing is making AJAX work even if the browser has JavaScript turned off? It looks like it is not going as well as you had hoped. Does that mean that your idea is not going to make it out here in the real world?

  • Stephen: yes, potentially, unobtrusively adding events like described here enable you to build a web page that could work without JavaScript. It requires some care but it absolutely can be done and it is the direction more and more people are taking. But to be perfectly clear, it doesn't "make Ajax work" if JavaScript is off, it makes the page work reasonably if it's off. By definition, Ajax only works with JavaScript on.
    I don't understand your question though. What idea are you referring to? What is not going as well as I had hoped?

  • Are you still planning to do a post about Class events or can you point me at a good resource?

  • Peter: yes, absolutely. I'll try to get that done next week.

  • Dave: If I'm not mistaken, that jQuery feature appeared after that comment was written. It is true that jQuery does an excellent job at keeping to its namespace, as does Dojo.

  • Well I have started to play little bit with the ATLAS and I want to add DOM event handler to TR element.
    Code works fine for IE , but in a firefox load event is happend , but events are not hooked to dom element ...
    I'm using asp 2.0 , please give me a tip , relly dont have a clue what is going on......
    function pageLoad()
    {
    alert('atload');
    for(var i = 0 ; i<testTable.rows.length;i++)
    $addHandler(testTable.rows(i),"click",RowClick);
    }
    function RowClick(e)
    {
    alert(this.innerHTML);
    }

  • @Lokas: you need to define testTable, for example var testTable = $get("testTable");. You also need to use square brackets instead of parentheses to get the rows: testTable.rows[i].

Comments have been disabled for this content.