ASP.NET: Hamburger, Yes! AJAX, "NOT!", Redux!
Retrofitting Script Callbacks to ASP.NET 1.1
by Peter A. Bromberg, Ph.D.

Peter Bromberg

" If builders built buildings the way programmers wrote programs, then the first woodpecker
that came along would destroy civilization. ."
-- Harry Weinberger

As described in my previous articles, Remote Scripting has been used by classic ASP developers since the earliest days of Internet Explorer 3.0 when Microsoft put out their Remote Scripting applet and sample source code. Since various developers and seminar promoters who have recently "got religion" have decided to give it a new buzzword name of "AJAX", I thought it would be appropriate to join the fray and call my implementations "Hamburger". After all, AJAX is really a foaming cleanser, and I don't think it would be right to use an acronym that conflicts with a trademarked name. "Hamburger", of course, is in the public domain and seems descriptive enough. Besides, the "X" in Ajax supposedly stands for "XML" and we all know that hardly any remote scripting uses client-side XML anyway. (You can click on the image below with your speakers or headphones on and get the whole story). And, soccer fans probably would not appreciate the misappropriation of Ajax either ("football" for you blokes across the pond). Hmm, maybe I'll just call it "H" for short. Now if you really want to get back to the history, Ajax was the son of Telamon, king of Salamis. After Achilles, he was the mightiest of the Greek heroes in the Trojan War. It's kind of amusing when you think about it. I mean, we could resurrect the Hula Hoop, call it the "Anakin Skywalker Levitation Belt", and the naivete of the public would have them rushing out to buy it in droves, truly believing it is the "next big thing".

 

At any rate, I plan to hold my first "Hamburger Summit" soon, so stay tuned for details! I guarantee that you will see a lot of old tricks and techniques with brand new names, but -- le plus ça change, le plus c'est la même chose (if you aren't sure what that means, don't worry -- I'm pretty sure my drift made it over the wire). Now, let's take a look at what the Script Callback mechanism of ASP.NET 2.0 is "under the hood" and see if we can "retro" it into ASP.NET 1.1 (kind of a reverse twist on that AJAX thing, as here we will be taking something new but calling it something old!). I don't think it's necessary to explain my annoyance at seeing somebody misappropriate a technology or technique, give it a new name, and then hype it for gain. Interestingly, the Google developers who've attracted so much attention with their creations based on these techniques do not refer to them either as "AJAX" or even "Remote Scripting" They call it simply "Javascript". Bravo!

By the way, what does Microsoft have to say about all this AJAX stuff? "It's a little depressing that developers are just now wrapping their heads around these things we shipped in the late 20th century," said Charles Fitzgerald, Microsoft's general manager for platform technologies. "But XAML is in a whole other class. This other stuff is very kludgy, very hard to debug. We've seen some pretty impressive hacks, but if you look at what XAML starts to solve, it's a major, major step up." OK guys, its "XAML" now - is it just another buzz word? Umm, I don't think so. Your first question should be, "What is XAML?". XAML is the XML-based declarative markup language for user interface development. Extensible Application Markup Language was created by Microsoft for rapidly building user interfaces for .NET Framework applications. "Longhorn," Microsoft's next version of Windows, is designed to separate presentation layer code from application logic. XAML is Longhorn's default language for user interface programming. You can get a pretty quick idea of where some developers are taking XAML by visiting the XAMLON site here.



Now, back to our Remote Scr- er, Hamburger implementation: First we should probably take a look at a couple of new items from ASP.NET 2.0 that we can make use of:

WebForm_DoPostBackWithOptions
This is a replacement for the old __doPostback javascript method. It has more functionality, supports client side validators, keeps scroll bar position, autopostback, and more. Also added is a server side method to generate the javascript call to this method using the Page.GetPostBackEventReference(options) method. It is not a total replacement since the ASP.NET 2.0 team tried to keep backwards compatibility as much as possible If you use the default options, you aren't using the new Maintain Focus or the autopostback features, and it actually renders the same __doPostBack call that is present in ASP.NET 1.1.

This method is defined in WebForms.js which is now an embeddedresource inside System.Web.dll, and it is served using the new WebResource.axd handler. WebResource.axd alleviates many of the problems that are present in ASP.NET 1.1 with the asp.net script files folder where you used to have to serve the client script (.js) files that are required for things like validators. There are a number of other javascript embedded resources in System.Web. If you are curious what Microsoft have been doing, and you'd like to see them all download this text file.

In ASP.NET 2.0 there are many more scripts since they are leveraging more client side functionality, for example GridView can now do sorting and paging without doing a PostBack. This is all done with the new Script Callback mechanism.

WebResource.axd
WebResource.axd is the new Handler that enables control and page developers to download resources that are embedded in an assembly. This is a valuable and welcome tool, since control developers may want to include a javascript file (.js) or images (.jpg, .gif) for their controls and there is now a built-in method to access these. So now you can do things like: <script src='<PUT THE STRING RETURNED BY Page.GetWebResourceUrl(typeof(YourControl), "YourScript.js")> HERE'>
Another advantage is that you can use localized assemblies and you will have the correct image or script localized based on the deployment.

ASP.NET 2.0 has a new Interface which is required to handle Script Callbacks, "ICallbackEventHandler" which is extremely simple:

public interface ICallbackEventHandler
{
string RaiseClientCallbackEvent(string eventArgument);
}

So, now that we have the ingredients we need from ASP.NET 2.0, lets see if we can't make them "retrofit" into ASP.NET 1.1 without a lot of hassle. Obviously, we are going to define our new ICallbackEventHandler interface somewhere, and we'll need to make our ASP.NET 1.1 pages derive from a base Page class that implements this. However, that's no big deal since we can have our new base Page class itself derive from System.Web.UI.Page, and get back all the functionality - plus our new script callback infrastructure!

Here is my first implementation of the CallbackPage class, which effectively duplicates 100% of the functionality of the Page class's requirements to implement script callbacks at the Page level (not for controls, as mentioned above). In fact, this uses
the identical javascript web resource that you will get by requesting the WebResource.axd handler that can be found in the view source of any ASP.NET 2.0 page that implements a script callback. Note that the GetWebResourceUrl allows us to pull a resource out by name, OnInit sets up the Callback interface call to "handleCallback", along with everything else we need to exactly mimic the operations of the ASP.NET 2.0 script callback mechanism using the identical javascript generated in ASP.NET 2.0 (actually, it's not 100% identical - I shortened the number of underscores at the beginning of one of the callback method names -- I mean, how many underscores do you need to make your point!). Please note that the main reason I haven't invested more time in backporting this for the webcontrol infrastructure as well, is simply because I fully expect to be using ASP.NET 2.0 for production work in just a few short months. And, so should you!

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Reflection;
using System.IO;
using System.Text;

namespace WebUtil
{
 public class CallbackPage : System.Web.UI.Page
 {
  public string GetWebResourceUrl( string resourceName)
  {
   Assembly executingAssembly = Assembly.GetExecutingAssembly(); 
   string asName=executingAssembly.FullName.Substring(0,executingAssembly.FullName.IndexOf(","));   
   Stream resourceStream = executingAssembly.GetManifestResourceStream(asName+"."+ resourceName);
   using(StreamReader reader = new StreamReader(resourceStream, Encoding.ASCII))
   {
    return reader.ReadToEnd();
   }
 }

  protected override void OnInit(EventArgs e)
  {
   base.OnInit(e);
   // __CALLBACKID will be in the Request dictionary with script callback.

   if (Request["__CALLBACKID"] != null) 

   {
    _isCallback = true;
    handleCallback();
   }
  }

  private void handleCallback() 
  {

   // Get the UniqueID of the control.  
   Control _control;
   string controlUniqueID = Request["__CALLBACKID"];
   if (controlUniqueID == "__PAGE") 
   {
    _control = this;
   } 
   else 
   {
    _control = FindControl(controlUniqueID);
   }

   // Get the interface reference.
   ICallbackEventHandler callbackHandler = _control as ICallbackEventHandler;
   if (callbackHandler != null) 
   {
    Response.Clear();
    string result;
    try 
    {
     // Fire the callback handler method.
     result = callbackHandler.RaiseClientCallbackEvent(Request["__CALLBACKPARAM"]);
     // 's' for success.This is further processed at client side.
     Response.Write('s');
    } 
    catch (Exception ex) 
    {
     // 'e' for Exception.
     Response.Write('e');
     result = ex.Message;
    }

    // Write back to client.
    Response.Write(result);
    // Prevent caching at client side.
    Response.Cache.SetExpires(DateTime.Now);
    Response.End();
   }
  }
 

  // Is this a client script callback?
  private bool _isCallback = false;

  public bool IsCallback 
  {
   get {return _isCallback;}
  }

 
  // name of JavaScript file containing the client callback functions
  private const string CALLBACKSCRIPT_KEY   = "Callback.js"; 

  // Get the client-side callback JavaScript function name.
  // Now we can attach the function to any client side event.
  public string GetCallbackEventReference(
   Control _control, // Target control that handles callback at server side.
   string argument, // String argument.
   string clientCallback, // Client side callback function that will process the result.
   string context, // Optional context.
   string clientErrorCallback, // Optional client side error handler.
   bool useAsync // Sync or Async 
   ) 
  {

   string target;
   if (_control is ICallbackEventHandler) 
   {
    if (_control is Page) 
    {
     target = "__PAGE"; // Page gets special handling.
    } 
    else 
    {
     target = _control.UniqueID;
    }
   } 

   else 
   {
    throw new ArgumentException("The control must implement ICallbackEventHandler interface.");
   }

   if ((clientCallback == null) || (clientCallback == string.Empty))
   {
    throw new ArgumentException("The clientCallback argument cannot be null or empty");
   }

   if (((Request != null)) && Request.Browser.JavaScript)
   {
    // Register the callback initialization scripts.
    if (!IsClientScriptBlockRegistered("CallbackInitializationScript")) 
    {     
    string scr=String.Concat("<script language=Javascript>", 
this
.GetWebResourceUrl("Callback.js"),"</script>"); RegisterClientScriptBlock("CallbackInitializationScript",scr); } if (!IsStartupScriptRegistered("PageCallbackScript")) { RegisterStartupScript("PageCallbackScript", "<script language=JavaScript>var pageUrl='"
+ Request.Url.PathAndQuery + "';\r\n WebForm_InitCallback();</script>"); } } else { throw new NotSupportedException("Browser does not support JavaScript?"); } if (argument == null) { argument = "null"; } else if (argument.Length == 0) { argument = "\"\""; } if (context == null) { context = "null"; } else if (context.Length == 0) { context = "\"\""; } // Constructor for the client side callback function reference. string[] textArray1 = new string[13] { "WebForm_DoCallback('", target, "',", argument,
",", clientCallback, ",", context, ",", (clientErrorCallback == null) ? "null" : clientErrorCallback,
",", useAsync ? "true" : "false", ")" } ; return string.Concat(textArray1); } } }

Finally I have a WebForm page that pretty much mirrors the example provided some time ago by Dino Esposito in his August, 2004 MSDN Magazine article, since this may be familiar to developers The difference is, of course, that my page derives from this new CallbackPage base class, using the exact javascript resource that is present in ASP.NET 2.0, and in exactly the same manner, and that it functions perfectly in ASP.NET 1.1. This is an "inline page" so your server side code is right there in it:

<%@ Page language="C#"  Inherits="WebUtil.CallbackPage" %>
<%@ import namespace="System.Data" %>
<%@ import namespace="callbackpageweb" %>
<%@ implements interface="WebUtil.ICallbackEventHandler" %>
<HTML>
 <script language="javascript">
    function UpdateEmployeeViewHandler(result, context) 
    { 
        var o = result.split(",");
        e_ID.innerHTML = o[0]; 
        e_FName.innerHTML = o[1]; 
        e_LName.innerHTML = o[2]; 
        e_Title.innerHTML = o[3]; 
        e_Country.innerHTML = o[4]; 
        e_Notes.innerHTML = o[5]; 
    }
 </script>
 <script runat="server">
    public virtual string RaiseClientCallbackEvent (string eventArgument)
    {
        // Get more info about the specified employee
        int empID = Convert.ToInt32 (eventArgument);
        EmployeesManager empMan = new EmployeesManager();
        EmployeeInfo emp = empMan.GetEmployeeDetails (empID); 
        
        string[] buf = new string[6];
        buf[0] = emp.ID.ToString ();
        buf[1] = emp.FirstName; 
        buf[2] = emp.LastName; 
        buf[3] = emp.Title; 
        buf[4] = emp.Country; 
        buf[5] = emp.Notes; 
        return String.Join(",", buf);
    }
    
    void Page_Load (Object sender, EventArgs e)
    {
        // Populate the drop-down list
        EmployeesManager empMan = new EmployeesManager();
        DataTable dt = empMan.GetListOfNames();  
        cboEmployees.DataSource = dt;
        cboEmployees.DataTextField = "LName";
        cboEmployees.DataValueField = "ID";
        cboEmployees.DataBind();

        // Prepare the Javascript function to call
        string callbackRef = GetCallbackEventReference(this,
            "document.all['cboEmployees'].value",
            "UpdateEmployeeViewHandler", "null", "null",true);
        
        // Bind it to a button
        buttonTrigger.Attributes["onclick"] = 
            String.Format("javascript:{0}", callbackRef);
    }
 </script>
 <body>
  <form id="Form1" runat="server">
   <h1>Select a&nbsp;Name and click for details</h1>
   <asp:dropdownlist id="cboEmployees" runat="server" />
   <button runat="server" id="buttonTrigger" type="button">See Detail</button>
   <br>
   <table>
    <tr>
     <td><b>ID</b></td>
     <td><span id="e_ID" /></td>
    </tr>
    <tr>
     <td><b>Name</b></td>
     <td><span id="e_FName" /></td>
    </tr>
    <tr>
     <td><b>Last Name</b></td>
     <td><span id="e_LName" /></td>
    </tr>
    <tr>
     <td><b>Title</b></td>
     <td><span id="e_Title" /></td>
    </tr>
    <tr>
     <td><b>Country</b></td>
     <td><span id="e_Country" /></td>
    </tr>
    <tr>
     <td><b>Notes</b></td>
     <td><i>
       <span id="e_Notes" /></i></td>
    </tr>
   </table>
  </form>
 </body>
</HTML>

I don't reproduce the EmployeeManager class here as it is very similar to Esposito's example and is included in the downloadable solution. And that's it! Hamburger, or Ajax? I report, you decide! Download the ASP.NET 1.1 Remote Scripting page implementation solution below and have fun with it. Finally, if you want to see an ASP.NET 2.0 implementation of a strongly typed script callback mechanism done "right", I encourage you to visit Bertrand Leroy's blog and try out his RefreshPanel, which enables script callbacks with virtually no coding, along with the ECMAScriptObject classes that replicate the client-side Javascript functionality on the server, not on the client -- which is where it should be done in order to promote a truly service - oriented design.

 

Download the Visual Studio.NET Solution that accompanies this article

 

 


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: