Dienstag, August 14, 2007

Extending the OpenAjax hub

Extending events over the network by using an OpenAjax hub compatible approach.

The current version of the OpenAjax hub implementation (Hub 1.0) is focusing on a client-side solution for integrating components into a web application. But it doesn't address any needs that might come with the scenarios where events have to cross the boundaries of the current client side window.

However the hub specification does not explicit exclude these scenarios and is extendible without changing the API itself by introducing a OpenAjax compatible extender component.

Another approach is currently discussed in the OpenAjax member wiki at http://www.openajax.org/member/wiki/OpenAjax_Hub_1.1_Roadmap that is designed by extending the API of the hub itself.

Building an OpenAjax hub extender

The idea behind building an extender is to capture the events in one window and pass it over to other windows.

When the windows exists in the same frameset and share a common top window the message just needs to cross the boundaries. If the window is on another computer the client has to capture the event, and pass it to a server where other clients can then attach and retrieve the events.

1. capturing events

For the local hub implementation the part of the hub extender that captures the local events is just another subscriber. It can register to any local events or can just listen to some of the events that come around. The attribute "capture" is used to specify what events are sent to the server by specifying a semicolon separated list of namespaces using the same syntax as the subscribe method.

The other attribute that we need is the name of the service where the events can be send to using the server site publish method.

// registering for the local event.
OpenAjax.hub.subscribe(this.capture, "_catchLocalEvents", this);

// this function is called whenever a local event was published
_catchLocalEvents: function(eventName, eventData) {
  if (! this._replay) {
    // it's a real local event so publish it.
    var a = [eventName, eventData];
    a.multi = true;
    this.lastLocalAction = eventName + ":" + eventData;
    ajax.Start(this.sendAction, a);
  } // if
}, // _catchLocalEvents

The _replay flag is taking care about the fact that the events that are coming from the server should not be repeated back to the server.

The sendAction is the description how the server should be called. The call should be started at once and multiple calls for publishing an event to the server can be queued. There is no return value in this case. 

// The Ajax action to send an event to the server
sendAction: {
  delay:0,
  queueMultiple:true,
  prepare: function (a) { return(a); },
  call: "proxies.OpenAjaxChat.Publish",
  finish: null,
  onException: proxies.alertException
}, // sendAction

2. listening to events

Listening to events that come from the server is somewhat more complicated because the normal http interaction schema for web applications is client side initiated. The server just has no chance to send messages to "all" or "specific" clients but has to wait until a client asks for an update.

So the client loops by using another Ajax action that is specified with a small delay and calls the server to retrieve new events. The server will return all newer events that have been recorded since the last call of this client that is determined by using a marking counter that can be passed from the client.

// this function is called after retrieving all the events from the server.
_republish: function(events) {
  this._replay = true;
  var aList = events.split('\n');
  for (var n = 0; n < aList.length - 1; n++) {
    if (aList[n] != this.lastLocalAction) {
      var a = aList[n].split(':');
      OpenAjax.hub.publish(a[0], a[1]);
    }
  } // for
  this._replay = false;

  // remember the current marker
  this.marker = aList[aList.length-1];

  // start another pollAction
  ajax.Start(this.pollAction, this);
},

The action is calling the serer by passing the current marker of the client and the asynchronous result will be passed to the _republish method.

// The Ajax action that polls the events from the server
pollAction: {
  delay:200, // wait 200 msec before calling the server (just to be smart).
  prepare: function (obj) { return(obj.marker); },
  call: "proxies.OpenAjaxChat.PullEvents",
  finish: function (ret, obj) { obj._republish(ret); },
  onException: proxies.alertException
} // pollAction

The action and the _republish method will be called repetitive to poll the server.

One thing is left to do in the implementation of this action is the fact that local events already have been published on the client and should not be republished when the server returns them again. To stop publishing an event twice the hub extender records the last event that has been captured locally and sorts this event out.

The Server API

Calling the server for publishing and retrieving the latest events is implemented by using a real SOAP based webservice with the 2 methods Publish and PullEvents.

The communication layer is not as complex as the one defined with Bayeux from the Dojo Foundation but it helps in most scenarios. It is restricted in some way because complex objects cannot be used by the eventData.

The return value of pullevents method is complex object that contains an array of eventname and eventdata and the value of the current marker that is transported using a string too by using the following format:

[namespace.eventname:data\n]*

marker

A sample application

A small sample is showing how all together can be used. It is a kind of chat application where anyone can participate in the same conversation by just opening the url: http://www.mathertel.de/AjaxEngine/S06_AJAXForms/OpenAjaxChat.aspx

You can change your current alias in the configuration section of the page. The large textarea is used to display all the events as the arise and the small textfield beneath can be used by you to type some text. Feel free top open another window and see that your written text also appears on the other side and maybe you can see others typing too. (I suggest talking about animals).
The data itself is a combination of the user's name and the typed text.

The first component is the one that is used for entering a new message and it publishes the event de.mathertel.chatsample.message whenever a line is finished by using the return key. The implementation is quite simple:

<fieldset style="width: 424px"><legend>new message:</legend>
  <input id="newmessage" style="width: 390px;" />
</fieldset>

// the functionality of the new message field.
var newmessageBehavior = {
  onkeypress: function(evt) {
    evt = evt || window.event;
    // send the message when pressing <enter>
    if (evt.keyCode == 13) {
      OpenAjax.hub.publish("de.mathertel.chatsample.message", document.getElementById("username").value
        + " - " + this.value);
      this.value = "";
    }
  } // onkeypress
} // newmessageBehavior
OpenAjax.hub.registerLibrary("newmessageBehavior", "http://www.mathertel.de/OpenAjax/newmessageBehavior");
jcl.LoadBehaviour("newmessage", newmessageBehavior);

The next component implemented is the larger textarea where the all the events will be logged:

<fieldset style="width: 424px"><legend>Chat log:</legend>
  <textarea rows="10" cols="50" id="chatlog" disabled="disabled">
  <textarea />
</fieldset>


// the functionality of the chat log area.
var chatlogBehavior = {
  // registering for the event.
  init: function () {
    OpenAjax.hub.subscribe("de.mathertel.chatsample.**", "_log", this);
  }, // init

  // append another line to the log text and cut the first one if there are too many.
  _log: function(eventName, eventData) {
    var txt = this.value.split('\n');
    if (txt.length > 9)
      txt = txt.slice(-9);
    txt.push(eventData);
    this.value = txt.join('\n');  
  } // _log
} // chatlogBehavior
OpenAjax.hub.registerLibrary("chatlogBehavior", "http://www.mathertel.de/OpenAjax/chatlogBehavior");
jcl.LoadBehaviour("chatlog", chatlogBehavior);

Implementing these 2 components a "local" chat applications ready to run.

The third component is the hub extender that is invisible and is introduced above. The complete source code of the page can be seen here:

http://www.mathertel.de/AJAXEngine/ViewSrc.aspx?file=S06_AJAXForms/OpenAjaxChat.aspx

and you can see the sample live at:

http://www.mathertel.de/AJAXEngine/S06_AJAXForms/OpenAjaxChat.aspx

 

If you are interested in the web service implementation the source is available here:

http://www.mathertel.de/AJAXEngine/ViewSrc.aspx?file=S06_AJAXForms/OpenAjaxChat.asmx

Discussion

The sample above shows how easy it is to extend a hub into other browser windows by not extending the API but by adding a component that does the job.
The huge advantage with this approach is that the additional functionality is strictly separated from the core implementation is and encapsulated into it's own JavaScript file. Therefore it is an optional component and will be not part of the download if not needed in a specific application.

Keine Kommentare: