Microsoft Ajax events - part 2: exposing events from custom classes

 

In part 1, I showed how to subscribe to events exposed by JavaScript classes built on Microsoft Ajax. In this post, I'll show how to expose new events from your own classes.

Theoretically, the only things you have to do to expose an event are to implement "add_myEvent" and "remove_myEvent" methods that add or remove handlers, and to call all subscribers when relevant. In practice, managing the list of handlers for each event is really boilerplate code that you would have to reproduce for every event. This is why the Sys.Component base class exposes an events property that is an instance of Sys.EventHandlerList, which makes the event handler management a lot easier, and also has a few performance advantages. This is not a new pattern, System.Web.UI.Control also has an Events property of type System.ComponentModel.EventHandlerList, so if you know the .NET pattern you should feel right at home.

Let's look at a very simple example for which we'll build a very simple timer component. Our timer will expose a single event: tick (in addition to the events the base class supports, but we'll get back to that in a moment). The code for the add and remove event accessors is really simple, and this is all you have to do to expose any event:

add_tick: function(handler) {
    this.get_events().addHandler("tick", handler);
},
remove_tick: function(handler) {
    this.get_events().removeHandler("tick", handler);
},

The only thing that will vary in this code is the event name ("tick" here).

Calling subscribers is done from the _tick private function of our Timer class:

_tick: function() {
    var handler = this.get_events().getHandler("tick");
    if (handler) handler(this, Sys.EventArgs.Empty);
    // [...]
}

What's important in this method is how the _tick function gets a unique function reference for the "tick" event that will forward the calls to all subscribers. This makes Ajax events similar to .NET's multicast events. The function reference that getHandler returns can be null (in the case there are no subscribers) so it's important to test it before using it, but calling it is as simple as calling a regular function: the multicasting is entirely and internally handled by Sys.EventHandlerList.

Note that this _tick function is wired using window.setTimeout from the start method as can be seen in the full source code. I also omitted the code that rewires the timeout for the next tick.

It should be noted that the signature for the handlers is always (sender, arguments) where arguments are an instance of a class derived from Sys.EventArgs. Here, there's really no relevant information that we want to send to the handlers, so we're just using Sys.EventArgs.Empty, which is a special, empty instance of Sys.EventArgs.

For your own events, you can build your own class that derives from Sys.EventArgs. You should clearly document that this new class works with your event. Typically, the new argument class should take a constructor parameter for each of the pieces of information you want to transmit to the handlers. There should also be a property getter for each of those, but there usually won't be a setter. The reason is that these arguments should most of the time be considered immutable. There are a few cases though when this is not true and the handlers should be able to communicate back to the event publisher. One example of that is cancellable events (see Sys.CancelEventArgs and its cancel property). What's important in those case is that handlers should never rely on the values of those mutable properties. Only the event publisher can do anything depending on them. In the cancellable case, for example, what setting cancel to true does is prevent an action that would have happened *after* all the handlers have been called, but it doesn't prevent the remaining handlers from being called.

In the case of the Timer component, the event is raised by an externally happening source (the timeout feature exposed by JavaScript), which is why _tick is a private function. In some cases, it may be desirable to make it possible for public consumers of the class to raise the event. In this case, the convention is to expose a public method named "raise[event name]". For example, the base Sys.Component class exposes a raisePropertyChanged method that is typically called by derived classes to raise the propertyChanged event from the setters of properties that wish to advertise their changes.

Most of the time though, the event will be raised as part of a larger operation, which is what's being exposed publicly, and there's no point in having a "raise..." method. Once more, an example of that can be found in Sys.Component: dispose raises the disposing event before it actually does its cleanup work.

This pretty much covers what you need to know to implement your own custom events. In the next and final post in this series, I'll show how to integrate Microsoft Ajax events with the OpenAjax hub.

Full code for the timer component:
Timer.js.txt (rename to Timer.js before using that file)

A sample page that uses the component:
Clock.aspx.txt (rename to Clock.aspx and include in a Microsoft Ajax site with Timer.js to use)

Read part 1 of this post:
Microsoft Ajax events - part 1 - subscribing

3 Comments

  • Great blog entry - good job of relating this to a well known and recognized eventing pattern. It helps my understanding of how the AJAX developers intended the framework to be used (and some ideas of how to extend some of the core components.)

    One question regarding >> I also omitted the code that rewires the timeout for the next tick.

    What did you mean by this? In my testing the event fires at every interval and the folowing code seems to "rewire it":

    >>this._pollCookie = window.setTimeout(this._pollDelegate, this._interval);

    Thanks,

    Jeff

  • Jeff: I omitted it from the code in the blog post, not in the full source code.

  • Can i define a Custmon Event on the ASMX Webservice, and run a javascript function on the client site with this event??

Comments have been disabled for this content.