Asynchronous Callbacks and Ajax based UI Experience in Web Applications


Introduction

Web applications work on the underlying Http protocol and Http is stateless by nature. It uses the Request - Response pattern between the client and the server, to process the logic at the server and present the information in HTML to the client browser. In the case of long running applications, it would be helpful to the end user to see some status on the browser when the application is processing some complex logic on the server. This article shows a convenient way of showing status information in such cases. The sample demos show how to achieve this in .NET 2.0 using the CallBack infrastructure provided in the framework and also a means to achieve the same effect using the .NET 1.1 framework.

Background

There are lots of Progress bars available on the Internet but this example gives an additional benefit of having a "modal" like behavior for the progress bar. This way, the user is not only given an indication of an ongoing task running at the server, but also is prevented from interacting with other elements on the screen.

Ajax! Old Wine in a new bottle?

The principles on which majority of the Ajax platforms are based have been available for a long time, notably the XmlHttpRequest (XHR) object. One of the main advantages of using a framework as opposed to developing these applications from the scratch is not to worry about writing browser dependent code. Some of the available popular frameworks are given below:

  1. Prototype
  2. Script Aculous
  3. Taconite
  4. Rico
  5. DWR
  6. Yahoo Developer Framework
  7. Microsoft Atlas Framework... to name a few

Implementation with 2.0 Framework

The current code sample was developed using .NET 2.0 framework and the Yahoo developer framework. The idea behind the example is to simulate a long running operating by making the current executing thread sleep for a predetermined time interval. This call is made asynchronously via a callback. Let us look at some code to understand the internals of a callback.

In the .NET 2.0 framework, implementing Client Callback functionality requires three steps at the server side code and three JS functions. 1

On the Server Side:

  1. Inherit the Page from ICallbackEventHandler
  2. Provide implementation for the RaiseCallbackEvent method
  3. Provide implementation for the GetCallbackResult method

public void RaiseCallbackEvent(String eventArgument)

{

    System.Threading.Thread.Sleep(2000);

    returnValueToClient = string.Empty;

}

 

public string GetCallbackResult()

{

    return returnValueToClient;

}

In addition to these, you would of course, need the code to associate the UI controls with some JS code based on a client side event.

#region Register Client Scripts

YahooStatus.Attributes.Add("onClick","return YahooStatus();");

CustomStatus.Attributes.Add("onClick","return CustomStatus();");

#endregion Register Client Scripts

On the Client Side:

  1. A function (CallServer in this case) which sends a request to the server. This is injecting into the client browser via the RegisterClientScriptBlock method of ClientScriptManager.
  2. A function(ReceiveServerData in this case) which handles the result from the server after a callback.
  3. A function which makes the actual call (using a XmlHttp Request internally. This is taken care by the framework). This gluing is made possible by the method GetCallbackEventReference.

The below code snippet shows how the three client side functions are registered on the page.

#region Client Callback

 ClientScriptManager csmanager= Page.ClientScript;

 String callBackReference = csmanager.GetCallbackEventReference(this,
                             "arg","ReceiveServerData", "context");            

String callbackScript = "function CallServer(arg, context) {" +   callBackReference + "; }";

csmanager.RegisterClientScriptBlock(this.GetType(),
                   "CallServer", callbackScript, true); 

#endregion Client Callback

You can update the user screen with a "temporary message" which shows that there is a task in progress before making a call to the CallServer function. The CallServer invokes a Callback to the server asynchronously, keeping the "temporary message" unfrozen on the browser. When the processing is finished, the ReceiveServerData function is called which removes the "temporary message" and restores the UI. Download the first example (AjaxDemo) provided in the example for a detailed analysis of the code.

A high level flow is shown below wherein the pseudo code in shown on the left and the actual code snippet on the right.

It is worth noting that the .NET 2.0 framework provides support to pass a context information to the function which processes the result after a callback function. This enables us to dynamically change the behavior of the receiving function. In the sample provided, the JS function 'eval' is used to dynamically delegate a call to the function whose name is passed in as the context key.

 

The first screenshot shown below (of the temporary wait screen) is the one obtained by using the Yahoo framework

Major part of the above functionality is made possible by using the "Yahoo Widget Panel".  The sample code shows the way it is invoked and hidden.

function ShowYahooScreen(title)

{

    yahooPanel = new YAHOO.widget.Panel("WaitPanel",

        { width:"240px",

          fixedcenter:true,

          underlay:"shadow",

          close:false,

          visible:true,

          draggable:false,

          modal:true});

    yahooPanel.setHeader(title);

    yahooPanel.setBody("<img src=\"Images/progress_bar.gif\"/>");

    yahooPanel.render(document.body);

    yahooPanel.show();

}

 

function HideYahooScreen()

{

    if (yahooPanel)

    {

        yahooPanel.hide();

    }
}


More information about its usage can be obtained from the link provided in the References section.

The second screenshot shown below (of the temporary wait screen) is with a custom script with a behavior similar to the Yahoo message screen. This can be easily modified to meet any domain specific requirement.

The visibility styles of 'Progress Bar div' and the 'Page Background div' are alternately changed to get the above effect. The main ingredient of the recipe to show the partial opaque screen is the style element filter:alpha(opacity=x); opacity:y.

Challenges with the 1.1 Framework

With the 1.1 Framework, much of the work of making a silent call to the server (callback) has to be implemented by our code. The main steps are very similar to the ones listed above, but are listed here in the context of .NET 1.1

  1. Before making a call to the server, the screen is updated with the temporary status message.
  2. A XHR object makes a call to the server (HTTP GET) with a QueryString parameter so that the server can differentiate a callback from a postback,
  3. The XHR object is associated with an eventhandler for the event onreadystatechange.  In the event handler the state of the property "readyState" is checked till its state is "complete". This read only property returns a 4 bit integer and has the possible values of a) uninitialized = 0 b)loading = 1 c)loaded =2 d)interactive =3 and e) complete =4.
  4. In the event handler of the XHR object, when the readyState =4, the callback is supposed to be complete and at this point, the temporary status message can be removed. In an actual world application, the server would return some response in the form of XML with which the client side DOM can be updated at this time.  The useful properties to check are a) XHR object status set to 200, b) statusText set to "OK" and c) responseText or responseXML (accessible via DOM) which the server sends back.

Code Sample for 1.1 Framework

On the Server Side:

  1. Associate the UI controls event with a client event and JS code.

    CustomStatus.Attributes.Add("onClick", "return invokeServerCall('Custom');");
    YahooStatus.Attributes.Add("onClick", "return invokeServerCall('Yahoo');"); 

  2. If this is a CallBack, process the callback event

    private bool IsCallback()
    {
              ifRequest.QueryString["callback"] == null)

              {

                       return false;

              }

              Response.Write(RaiseCallbackEvent());

              Response.Flush();

              Response.End();

              return true;

    }

    private string RaiseCallbackEvent()
    {

              System.Threading.Thread.Sleep(2000);

              Response.ContentType = "text/xml";

    return "blahfromserver";

    }

On the Client Side:

var xmlRequest;

var returnFunction;

 

function invokeServerCall(returnFunctionArg)

{

if (returnFunctionArg == "Yahoo")

                   ShowYahooScreen("Loading, Please wait...");

          else if (returnFunctionArg == "Custom")

                   ShowCustomScreen();

          var date = new Date();

          var pageUrl = "Default.aspx?callback=true&date="+date;

         

xmlRequest = getXHRObject();

             

          returnFunction = returnFunctionArg;        

                            

          xmlRequest.onreadystatechange = ReceiveServerData;

          xmlRequest.open("GET", pageUrl, true);

xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

          xmlRequest.send(null);

          return false;

} 

 

function ReceiveServerData()

{

          if (xmlRequest.readyState==4)

          {

                   if (returnFunction == "Yahoo")

                             HideYahooScreen();

                   else if (returnFunction == "Custom")

                             HideCustomScreen();

          }

}

Gotchas!

There are a couple of things to watch out for when implementing a callback from the scratch

  1. Browser Incompatability
    Until recently (IE 7), different browsers implemented XHR in different ways. So, a helper function has been provided to ensure that the code works across different platforms.

    function getXHRObject()

    {

              if (window.XMLHttpRequest)

              {

              // For IE7 and Mozilla browsers

              return new XMLHttpRequest()

              }

              else if (window.ActiveXObject)

              {

             // for IE 5.0 and IE 6.0 browsers

              return new ActiveXObject("Microsoft.XMLHTTP");

        }

        else

              return null;

    }

  2. Caching Issue
    If you observe carefully, the pageUrl has been appended with a second parameter which is the current date. This is done to avoid any caching between subsequent calls to the server. If you remove this, you will observe that the callback works for the first time, but fails from the second time onwards.

Using the Sample Code

In the attached "Code Samples" folder, there are two subfolders, AjaxDemo and AjaxIn1_1. The AjaxDemo is built with .NET 2.0. Extract the contents of the folder and run the Default.aspx.  The AjaxIn1_1 is built with .NET 1.1. Extract the contents of the folder and create a Virtual folder for it to run the application. This code has been verified to work in both the IE and the Mozilla browsers.

Summary and Conclusion

  1. We saw how to show status screens to users when running longer operations on the server side. This enhances the overall UI experience. 
  2. The two options we saw were using the Yahoo framework and a custom implementation of the same.
  3. We also saw how we can do these in the .NET 1.1 and .NET 2.0 frameworks.

References

  1. http://msdn2.microsoft.com/en-us/library/ms178208.aspx
  2. http://developer.yahoo.com/yui/docs/container/YAHOO.widget.Panel.html

Future Scope and Directions

Ajax has been around for a long time and although it has significant features, it's far from being the rich interface users would like to see. The next wave of rich applications being able to support rich 2D and 3D support comes from the Windows presentation Foundation (WPF) initiative of Microsoft...more about this later.