Jason Read

August 22, 2006

Ajax, Sychronization, and Object Oriented Design

Filed under: Ajax — Jason Read @ 4:15 pm

For the past 8 months I have been working on a very ajax heavy rich web application. In doing so I have become somewhat acquainted with the advantages and disadvantages of this new web development paradigm. Overall, I appreciate the improved performance and usability it can provide over traditional web applications. However, this approach also brings with it significant development baggage in the form of additional complexity and increased page size. The purpose of this post is to discuss some of these complexities and my approach to handling them.

If you are not familiar with ajax, it is an acronym for “asynchronous javascript and xml”. Basically, it is just a way for javascript code within a rendered html page to communicate with a web server without requiring the browser to reload the page. Most modern browsers support this functionality via a built-in javascript class name XMLHttpRequest (standards compliant browsers), or Microsoft.XMLHTTP for Internet Explorer. To simplify instantiation of the appropriate object, I implemented the following static utility method:

/**
 * a cross browser compatible method of retrieving a reference to an
 * XMLHttpRequest object that can be used to invoke an ajax request
 * @param Function handler the method that should be invoked when the request
 * is completed. required only if invocation is asynchronous
 * @access  public
 * @return XMLHttpRequest
 */
SRAOS_Util.getXmlHttpObject = function(handler) {
  var objXmlHttp=null;

  if (navigator.userAgent.indexOf("MSIE")>=0) {
    var strName="Msxml2.XMLHTTP";
    if (navigator.appVersion.indexOf("MSIE 5.5")>=0) {
      strName="Microsoft.XMLHTTP";
    }
    try {
      objXmlHttp=new ActiveXObject(strName);
      if (handler) {
        objXmlHttp.onreadystatechange=handler;
      }
      return objXmlHttp;
    }
    catch(e) {
      return null;
    }
  }
  if (navigator.userAgent.indexOf("Mozilla")>=0) {
    objXmlHttp=new XMLHttpRequest();
    if (handler) {
      objXmlHttp.onload=handler;
      objXmlHttp.onerror=handler;
    }
    return objXmlHttp;
  }
};

Because XMLHttpRequest supports both synchronous and asynchronous requests, Ajax is really just an approach to using that class in your javascript code. Using it to make synchronous requests limits concurrent invocations, may hang the application, and really does not fit the ajax definition. Because of this, I chose to use asynchronous invocations.

Another interesting fact about XMLHttpRequest is that there really does not have to be any xml involved. It basically just fascilitates allowing your javascript code to send a standard http request to a web server. The xml only comes into play if the response from that request is xml formatted. However, this does not necessarily have to be the case. The request may also return simple delimited data or javascript code that can be invoked using the eval function. Of course, this does deviate from the definition of ajax, but it is in fact a fairly common approach that can still be classified as ajax. The advantage to this approach is simplification of the client-side javascript code in the sense that it does not have to deal with xml parsing and object reconstruction.

When using XMLHttpRequest to make an asynchronous request, you will typically want to respond to the results of that request when it is complete. To do so, you provide it with a callback handler. This callback handler is just a reference to a javascript function that will be invoked when the server has responded to the request. This is where there is a difference between the XMLHttpRequest and Microsoft.XMLHTTP api. With XMLHttpRequest multiple handlers can be specified for different types of events including onload and onerror (the 2 you will generally be concerned with), while Microsoft.XMLHTTP only allows you to specify a single handler for the onreadystatechange event. To further complicate this matter, the handler function will not be passed a reference to the XMLHttpRequest or Microsoft.XMLHTTP instance it has been invoked for. Additionally, the handler can only be invoked statically and not as a member of an object. These factors and api limitiations introduce some very significant problems, particularly when attempting to incorporate ajax functionality into an object oriented javascript environment and in dealing with synchronization accross concurrent requests. In order to reduce this complexity I implemented a more OO friendly encapsulated api for invoking and responding to ajax requests.

The api that I implemented consists of 2 methods. The first is a public method used by other classes to invoke ajax requests, and the second is a private method used as the response handler for those request. The ajax invocation method includes the following parameters (and a few others not relevant to this post):

  1. target: the object to invoke callback on. if null, callback will be invoked statically
  2. callback: the name of the callback method to invoke. If target is specified, this method will be invoked as a member of that object, otherwise it will be invoked statically. this method should have a single parameter which will be a hash with the following key values:
    • results: an object representing the results of the invocation as returned by the server when the request was successful
    • requestId: the value of the requestId parameter if it was specified when the request was invoked
    • status: a status code indicating whether or not the request was successful
  3. requestId: an optional request specific identifier that will be included in the response parameter when the callback method is invoked

The ajax invocation method includes the following steps:

  1. Reference to a new XMLHttpRequest for this request is obtained
  2. Request parameters are stored in a request queue
  3. http request is constructed and submitted to the server

When the request is complete, the private response handler method is invoked which includes the following steps:

  1. Using a semaphore, attempt to obtain an exclusive lock to process the next response. If critical section is currently blocked, wait for 100 milliseconds before trying again using the setTimeout function
  2. Once a lock is obtained, iterate through all of the requests in the queue looking for the first request that is currently in a completed state. When found, remove that request from the queue and release the blocking semaphore
  3. Create the response object that will be included in the callback invocation including the results object (created by eval‘ing the server response string, the status code and the requestId
  4. Invoke the callback method either statically or as a member of target if specified in the request

By creating this encapsulation layer on top of the XMLHttpRequest api I was able to significantly reduce the complexity of incorporating ajax functionality into the software. The following is the full code used in the methods previously described. Some of the functionality of the ajax invocation method was not explained in this post.

  /**
   * used to retrieve invoke an asynchronous ajax service from the server. this
   * method will automatically switch between ajax and standard form posting to
   * a hidden layer based on the parameter types specified in constraintGroups
   * and params. if any of those parameters specify a
   * SRAOS_AjaxConstraint.CONSTRAINT_TYPE_GET or
   * SRAOS_AjaxConstraint.CONSTRAINT_TYPE_POST value, the latter method will be
   * utilized. this may be useful for posting files for example.
   * @param String service the id of the ajax service to invoke as defined in
   * the base or a plugin entity model
   * @param Object target the object to invoke callback on. if null, callback
   * will be invoked statically
   * @param String callback the name of the callback function in target to call
   * when this service invocation is completed. the function should contain 1
   * input parameters which will be an associative array containing the
   * following values:
   *  count:          the total # of results regardless of limit/offset. for
   *                  create/update actions this will be 1 on success. for delete
   *                  actions it will be 0
   *  limit:          the limit that was applied to the request
   *  offset:         the offset that was applied to the request
   *  results:        an array representing the output of the service request.
   *
   *                  for entity ajax service requests this will be an array of
   *                  associative arrays containing the entity attributes returned
   *                  or an array of strings if a view was specified. if the
   *                  action invoked by the server is create or update, the
   *                  results will be an array of those entities (or their
   *                  corresponding views) that were created/updated. if the
   *                  action is delete, results will be empty. if a validation
   *                  error fails duing a create or update action, results will be
   *                  an array of error messages applicable to the validation
   *                  error
   *
   *                  for global service requests, the output will depend on the
   *                  service type. (see api and dtd documentation for more info)
   *  requestId:      the requestId specified when this method was invoked
   *  status:         the result code for the service request. this value will be
   *                  equal to one of the SRAOS.AJAX_STATUS_* constants. if
   *                  the request fails, ONLY the status code will be returned in
   *                  the output. if the request was for an entity write
   *                  transaction (create, delete, or update), the results will
   *                  contain a single entity instance for the entity that was
   *                  modified
   *  time            the time in seconds (w/ 3 decimal places) that this
   *                  request took to fulfill
   * If target is null, this function will be invoked statically
   * @param SRAOS_AjaxConstraintGroups[] constraintGroups the constraint groups
   * to apply to this service request (see class api comments for more info)
   * @param SRAOS_AjaxRequestObj requestObj if this request is being made to
   * create, delete or update an object, this parameter should reference that
   * corresponding object
   * @param SRAOS_AjaxServiceParam[] params service invocation params. these
   * apply only to global ajax services
   * @param String requestId a request specific identifier. this value will be
   * returned in the "response" if the invocation is successful
   * @param String[] excludeAttrs attributes to exclude from the results of this
   * service invocation. these will be added to those defined in the entity
   * model for the service
   * @param String[] includeAttrs a restricted set of attributes that should be
   * included in the output of the service request. these attributes can ONLY be
   * those already permitted in the service definition (a sub-set of the allowed
   * attributes)
   * @param int limit the request limit
   * @param int offset the request offset
   * @param int timeout the amount of time in milliseconds to wait for a
   * response before invoking the callback function with the
   * SRAOS.AJAX_STATUS_TIMEOUT status code. if not specified, the callback
   * method will not be invoked until a resonse is received
   * @access  public
	 * @return void
	 */
	this.ajaxInvokeService = function(service, target, callback, constraintGroups, requestObj, params, requestId, excludeAttrs, includeAttrs, limit, offset, timeout) {
    var formAction = SRAOS.getFormAction(constraintGroups, params, requestObj);

		this._ajaxRequests.push({});
    var id = this._ajaxRequests.length - 1;
    if (this._ajaxRequests[id] && this._ajaxGateway && this._appId) {
      var req = this._getAjaxRequestParams(service, constraintGroups, requestObj, params, excludeAttrs, includeAttrs, limit, offset);
      this._ajaxRequests[id].ajaxTarget = target;
      this._ajaxRequests[id].ajaxCallback = callback;
      this._ajaxRequests[id].ajaxRequestId = requestId;
      this._ajaxRequests[id].ajaxParams = "app=" + this._appId + "&id=" + id + (!formAction ? "&request=" + req : '');
      this._ajaxRequests[id].ajaxParams += requestId ? '&requestId=' + SRAOS_Util.urlEncode(requestId) : '';

      this._ajaxSubmitRequest(id);
      if (timeout) { setTimeout("SRAOS._ajaxAbortRequest(" + id + ")", timeout); }

      // submit silent request when formAction is required
      if (formAction && this.getAjaxTargetWindow(target)) {
        var form = this.getAjaxTargetWindow(target).getForm();
        form.action = this._ajaxGateway + '?' + this._ajaxRequests[id].ajaxParams + '&silent=1&request=' + req;
        form.method = formAction;
        form.submit();
      }
    }
    else {
      OS.msgBox(SRAOS.ICON_ERROR, this.getString(SRAOS.SYS_ERROR_RESOURCE));
    }
	};

/**
 * handles responses to ajax requests invoked through ajaxInvokeService
 * @access private
 * @return void
 */
SRAOS._ajaxResponseHandler = function() {
  if (!OS) { return; }

  // semaphore to support concurrency
  if (SRAOS._ajaxWait) {
    return setTimeout("SRAOS._ajaxResponseHandler()", 100);
  }
  SRAOS._ajaxWait = true;
  var reset = false;
  // look for a completed request
  for(id in OS._ajaxRequests) {
    if (OS._ajaxRequests[id] && (OS._ajaxRequests[id].xmlHttp.readyState==SRAOS.AJAX_READYSTATE_COMPLETE || OS._ajaxRequests[id].xmlHttp.readyState=="complete")) {
      var request = OS._ajaxRequests[id];
      OS._ajaxRequests[id] = null;
      var failedResponse = { status: SRAOS.AJAX_STATUS_FAILED };
      SRAOS._tempAjaxResults[id] = failedResponse;
      SRAOS._ajaxWait = false;
      reset = true;
      try {
        if (request.xmlHttp.status == SRAOS.AJAX_STATUSCODE_VALID) {
          // alert(request.xmlHttp.responseText);
          eval(request.xmlHttp.responseText);

          // results not available yet, check again later
          if (response.status == SRAOS.AJAX_STATUS_RESULTS_NOT_AVAILABLE) {
            OS._ajaxRequests[id] = request;
            setTimeout('OS._ajaxSubmitRequest(' + id + ')', OS.getAvgAjaxResponseTime(3) * 1000);
            return;
          }
          SRAOS._tempAjaxResults[id] = response ? response : failedResponse;
          SRAOS._tempAjaxResults[id].requestId = request.ajaxRequestId;
          request.ajaxTarget ? request.ajaxTarget[request.ajaxCallback](SRAOS._tempAjaxResults[id]) : eval(request.ajaxCallback + '(SRAOS._tempAjaxResults[' + id + '])');

          if (!response) {
            alert(OS.getString("error.ajax"));
          }
          else {
            OS._ajaxRequestSuccess++;
            OS._ajaxTotalResponseTime += response.time;
            OS._ajaxLastTime = response.time;
          }
        }
        // invalid response from server
        else {
          request.ajaxTarget ? request.ajaxTarget[request.ajaxCallback](SRAOS._tempAjaxResults[id]) : eval(request.ajaxCallback + '(SRAOS._tempAjaxResults[' + id + '])');
        }
      }
      catch (e) {
        var tmp = "";
        for(i in e) {
          tmp += i + ": " + e[i] + "\n";
        }
        OS.displayErrorMessage(OS.getString(SRAOS.SYS_ERROR_RESOURCE) + "\n\n" + tmp);
        request.ajaxTarget ? request.ajaxTarget[request.ajaxCallback](SRAOS._tempAjaxResults[id]) : eval(request.ajaxCallback + '(SRAOS._tempAjaxResults[' + id + '])');
      }
      OS._ajaxRequests[id] = null;
    }
  }
  // reset the semaphore if it has not already been done. this is the result
  // of an error or timeout, because if this method was called, there should
  // have been a completed request
  if (!reset) {
    SRAOS._ajaxWait = false;
  }
};

Powered by WordPress