AJAX, RemoteScripting.Net,
Script Callbacks and Other Goodness

by Peter A. Bromberg, Ph.D.

Peter Bromberg

"if the door to success doesn't open to polite knocks, Kick it In!" -- Jackson Clark

It's interesting how the "AJAX" (Remote Scripting) phenomenon has energized the web development community. That's a good thing, although like any other technology implementation, you are going to have developers who just go "ape" attempting to put no-reload script callbacks into everything they can think of, whether it really makes sense for the user experience or not. That's not a good thing. Above everything else, it's important to think about why you want to use something and whether or not and how it will benefit the consumer of your creation. This "going ape" phenomenon is particularly evident in the non-Microsoft developer space, since these poor souls have only recently acquired an XMLHTTPRequest object in their browsers, something we as classic ASP Internet Explorer geeks have had since 1998! Even then, the one in Firefox is a bit stunted, being unable to make a request to a resource from another domain (such as, for example, a crummy RSS feed!) for security reasons.

You are also going to see (correction: you are seeing) a number of implementations of Remote Scripting for .NET, which is our focus here. And, as one would expect, you will see good ones and not-so-good ones. You'll see implementations that are complex and difficult to use, and those that are elegant and simple. And of course, you can count on seeing people attempt to exploit the technology for gain by putting together the "Super Ajax for .NET whatever" library that's for sale, usually at some ridiculous multiple of what it's actually worth, or the "How I made $476,000 from my web site with AJAX" seminar, etc., ad-nauseum. Dart has a nice package of script-callback enabled ASP.NET controls, but at $500 a pop for a single server license, I just don't know. I've been rolling my own just fine, thanks!

Remote Scripting is just the process of making an out - of - band call (e.g., not with the normal page POST / RELOAD cycle) to some external resource, getting back the result either synchronously or asynchronously, and updating the content of the page that initiated the call, usually using client-side HTML DOM methods. This new moniker "AJAX" really does a disservice to the technique, which as far as I know, was originally invented by Microsoft last century with their Remote Scripting release that was subsequently used in Outlook Web Access, a pretty advanced implementation at the time. I say "does a disservice" because it confuses the developer landscape to the point where newer developers may not even be aware that there is already a well-established remote scripting codebase and set of techniques prior to "AJAX", that AJAX really does nothing particularly new at all (except perhaps make more money for seminar promoters!), and consequently they get all caught up in this marketing hype. Don't get me wrong, Microsoft does this too -- often changing the names of products, seemingly just to get everybody confused as hell! I don't want to beat a dead horse; if they had simply called it what it really is -- Remote Scripting -- and forgotten about the buzzword hype and BS, and then promoted it, I'd be fine.



Fact of the matter is, cross-browser remote scripting was probably made most popular by Brent Ashley, who's been doing it since 2000 (by Internet standards, that's a very long time). In fact, his site has a link back to an article I wrote on it published right here, back in 2001. Unfortunately, the buzzword "AJAX" means a lot more to soccer fans, grocery store owners, and students of Greek History than it does for web development. But, hey! Who am I to interfere with progress!

Other stuff I've written on this subject, for those interested in its evolution as seen via the "Bromberg" view, can be found here, and here. For those who aren't aware, script callbacks are "built-in" to ASP.NET 2.0, and in a previous article, I showed how one could "backPort" the methods and script necessary to duplicate it almost exactly, for ASP.NET 1.1.

So in the last week or so Scott Guthrie posted an announcement on his blog about their ATLAS project, which is basically intended to provide a strongly typed script callback model that is easy to use. From what I've read, it sounds like it will be first-class. In fact, Scott even goes so far as to explain that the ATLAS moniker comes from a natural progression of internal Microsoft naming conventions, rather than "marketing hype". The comments that appeared on his blog aren't surprising - it seems every Tom, Dick and Harry developer posted a comment with a link to their own "creation", screaming "me, me"! One that keeps popping up is Michael Swartz' "AJAX.Net" which is now on Sourceforge.net. One commenter even sniped "why can't Microsoft support this", when in fact this particular library was indeed written in and for the .NET Framework! But, I bet they never even have used it, to make a comment like that. It's like, "Hey, it doesn't matter what you do, you are Microsoft, and therefore I am gonna bitch about something and try to make you look bad"!

OK, so, fine -- I looked at the AJAX.NET library. He said he was going to post the source code but as of the last time I looked, he never did. So I decompiled it and took a good hard look. This is an awful lot of code, its very sophisticated, and it is also very complex. My take on it? Too complex for most developers, and it also uses a separate IHttpHandler implementation to process the callback requests.

This approach requires not only configuring the custom handler (e.g., a "phantom page extension") in one's web.config but, what is worse, it prevents the callback methods from accessing the values of any of the server-side control values on the page since the callback is actually made to a different URL. Not good. At least, not what I want. Here's the crux of the issue, according to Bertrand Leroy of Microsoft:

Leroy states that Microsoft's callbacks are an integral part of the page lifecycle and thus carry with them the weight of the state of the page. He goes on to explain that most "AJAX" users seem to expect to just query data out-of-band, not necessarily trigger server-side events in the stateful context of the page.

According to Leroy, "If you are a control developer, nothing outside of your control should be considered an asset simply because it's not responsible for it. Many Ajax control developers think like page developers and expose properties that will point the control to some service url that will feed them data."

Leroy continues, "We avoid that kind of architecture because we want our page developers to be able to drag and drop the control to the page in the designer and have it working immediately. We don't want them to have to write a separate page, handler or web service for that. We want the control and the page to be self-contained components. To achieve that goal, we need the out-of-band calls to go back to the page itself and not to some external url." Leroy goes on to state that they've been listening to the feedback and expect to address both scenarios in the new ATLAS framework.

So, for now, I tossed out AJAX.NET and looked around some more. Then I found Jason Diamond's "AJAX.Net Library". Ah! This is elegant and simple! It doesn't require any custom handlers, is easy to understand and use, and it does virtually everything the other library does, but in only about 400 lines of code!

So in keeping with my credo of "understand everything under the hood before you use it", the first thing I did was change the namespace to "RemoteScripting", and start tracing through the code. (I'm not intending to come off as "pompous" as I've already been accused of, but this made me feel better immediately!) What I really like about Jason's approach is that you only need to adorn your server-side method with an attribute, and this makes his library reflect on it, generating the javascript matching callback code automatically on the fly. It can handle DataSets, DataTable, and primitive types with ease. And since it's not a lot of code, you should find it very easy to extend to custom types if needed. By the way, it also works with controls perfectly well. That alone, to me, is a major reason why developers should consider this approach to Remote Scripting, or one like it, in .NET. And, another thing that should not be lost on professional .NET developers: Jason Diamond put out all of his source code from day one, in the public domain, with his humble blessings. That's the mark of a professional.

What follows in the rest of this article is an ASP.NET 1.1 example using Jason Diamond's library (forgive me, Jason, but if it looks like Remote Scripting, walks like Remote Scripting, and acts like Remote Scripting ...- then the namespace shall remain "RemoteScripting" --at least, in my code). We will grab an MSN Search RSS Feed with our search query through RemoteScripting, load it into a DataSet, use it to populate a dropDown list with the titles and urls of the search results, then when you select a title, it will use another RemoteScripting method to request the page and display it in an IFRAME in the same page. All this will be done with no Postbacks or page reloads. Noobs, once you get the hang of how I've done this you are on a roll as far as putting together your own stuff. At that point if you want to call it AJAX, or Remote Scripting, or Hamburger, it's your choice!

One of the things we learn when starting to do Remote Scripting is that no matter how sophisticated the back-end library we use, if you expect to be able to update the page and manipulate it with script, you are going to have to write that script yourself. No "library" is going to be able to do this for you. Bertrand Leroy's "RefreshPanel" for ASP.NET 2.0 comes close, but this is about ASP.NET 1.1. So first, let's take a look at the client side script in my page:

<script>
  function PopulateListWithDataTable(elem) {
   if(elem.value.length <3) return;
   var select = document.getElementById("_tableList");
    select.options.length=0;   
    document.getElementById("Label1").innerText = "Getting Data...";         
   My.Page.GetDataTable(elem.value,DoTableCallBack);    
  }
  
  function showDetail()
  {
  document.getElementById("details").innerHTML=""; // clear any previous display
  var strUrl=My.Page.GetHtml(document.getElementById("_tableList").value);
   var sHTML="<IFRAME width=100% height=1000 src='" + 
             document.getElementById('_tableList').value +"'></IFRAME>";
// add the IFRAME to the page
  document.getElementById("details").insertAdjacentHTML('afterBegin',sHTML); 
  }
  
  
  function DoTableCallBack(result) {
   var select = document.getElementById("_tableList");    
   var table = result.value;
   if(null==table)
   {
      var option = document.createElement("option");
    option.text = "No Results";
    option.value = "0";
    if (window.navigator.appName.toLowerCase().indexOf("microsoft") > -1) {
     select.add(option);
    } else {
     select.add(option, null);
    }
     document.getElementById("Label1").innerText = "Enter Search Term and TAB out.";     
    return;   
   }
   
   if(table.Rows.length>0)
   {
   var opt =document.createElement("option");
   opt.text="--Select an Item--";
   opt.value="";
   if (window.navigator.appName.toLowerCase().indexOf("microsoft") > -1) 
    {
     select.add(opt);
    } 
    else 
    {
     select.add(opt, null);
    }
   
   for (var i = 0; i < table.Rows.length; ++i) 
   {
    var option = document.createElement("option");
    option.text = table.Rows[i].title;
    option.value = table.Rows[i].link;
    if (window.navigator.appName.toLowerCase().indexOf("microsoft") > -1) 
    {
     select.add(option);
    } 
    else 
    {
     select.add(option, null);
    }
   }
    document.getElementById("Label1").innerText = "Enter Search Term, TAB out.";     
  }   
   
 } 
  </script>

I have three methods:

PopulateListWithDataTable is used to populate the listbox.

DoTableCallback is the callback target of the PopulateListWithDataTable method, and receives and processes the results returned by the matching server-side method that is called.

Finally, ShowDetail is used to get the page contents from the target url when a listbox item is chosen.

Now let's see how we use Jason's library to implement this in the server side code, and what his library actually does with it "under the hood". Remember, the difference between just a "developer" and a "professional level developer" is that the "pro" not only understands just how to do something; he / she understands how and why it works!

using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Net;
using System.Text.RegularExpressions;

namespace RemoteScriptingTest
{
 public class WebForm2 : System.Web.UI.Page
 {
  protected System.Web.UI.WebControls.DropDownList _tableList;
  protected System.Web.UI.WebControls.Label Label1;
  protected System.Web.UI.WebControls.Label Label2;
  protected System.Web.UI.WebControls.TextBox TextBox1;
 
  private void Page_Load(object sender, System.EventArgs e)
  {
   RemoteScripting.Manager.Register(this, "My.Page", RemoteScripting.Debug.None);
  }

  [RemoteScripting.Method]
  public DataTable GetDataTable(string srch)
  {
   DataTable table=null;
   try
   {
   string query="http://beta.search.msn.com/news/results.aspx?q=" +srch+ "&format=rss";
    DataSet ds = new DataSet();
    ds.ReadXml(query);
    table=ds.Tables[2];      
    table.TableName = "Items";  
   }
   catch(Exception ex)
   {
    System.Diagnostics.Debug.WriteLine(ex.Message+ex.StackTrace);
   }
   return table;
  }
  [RemoteScripting.Method]
  public string GetHtml(string url)
  {
          WebClient clnt =new WebClient();
   byte[] b=clnt.DownloadData(url);
   string result=System.Text.Encoding.ASCII.GetString(b);
   result=CleanHtml(result);
   return result;
  }
  private string CleanHtml(string htmlToClean)
  {
          return Regex.Replace(htmlToClean, @"</?(?i:meta|script|link|strong)(.|\n)*?>", "");
  }

  #region Web Form Designer generated code
  override protected void OnInit(EventArgs e)
  {
  
   InitializeComponent();
   base.OnInit(e);
  }

private void InitializeComponent()
  {     
   this.Load += new System.EventHandler(this.Page_Load);

  }
  #endregion  
 }
}

Note that there are 2 things going on in your server-side code:

1) in the Page_Load handler, we register our page with the library and give it a manager namespace:

"RemoteScripting.Manager.Register(this, "My.Page", RemoteScripting.Debug.None);"

I'll have more about this in a moment in the javascript code it generates.

2) In each of our server-side business logic methods, we decorate the method with the [RemoteScripting.Method] attribute. This tells the library it must reflect the method and generate all the appropriate matching javascript code (which gets injected into our page) "on the fly".

So what happens in the library when this page is requested? Let's take a look - the library creates the following:

<script>
function RemoteScripting_GetXMLHttpRequest() {
  var x = null;
 if (typeof XMLHttpRequest != "undefined") {
  x = new XMLHttpRequest();
 } else {
  try {
   x = new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) {
   try {
    x = new ActiveXObject("Microsoft.XMLHTTP");
   } catch (e) {
   }
  }
 }
 return x;
}

function RemoteScripting_CallBack(target, args, clientCallBack, debugRequestText, 
debugResponseText, debugErrors) { var x = RemoteScripting_GetXMLHttpRequest(); var url = document.location.href.substring(0, document.location.href.length - document.location.hash.length); x.open("POST", url, clientCallBack ? true : false); x.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if (clientCallBack) { x.onreadystatechange = function() { if (x.readyState != 4) { return; } if (debugResponseText) { alert(x.responseText); } var result = eval("(" + x.responseText + ")"); if (debugErrors && result.error) { alert("error: " + result.error); } clientCallBack(result); } } var encodedData = "RemoteScripting_CallBackTarget=" + target; if (args) { for (var i in args) { encodedData += "&RemoteScripting_CallBackArgument" + i + "=" + encodeURIComponent(args[i]); } } if (document.forms.length > 0) { var form = document.forms[0]; for (var i = 0; i < form.length; ++i) { var element = form.elements[i]; if (element.name && element.value) { encodedData += "&" + element.name + "=" + encodeURIComponent(element.value); } } } if (debugRequestText) { alert(encodedData); } x.send(encodedData); var result = null; if (!clientCallBack) { if (debugResponseText) { alert(x.responseText); } result = eval("(" + x.responseText + ")"); if (debugErrors && result.error) { alert("error: " + result.error); } } delete x; return result; } </script> <script> var My = { "Page": { "GetDataTable": function(srch, clientCallBack) { return RemoteScripting_CallBack('ASP.Default_aspx.GetDataTable', [srch], clientCallBack, false, false, false); }, "GetHtml": function(url, clientCallBack) { return RemoteScripting_CallBack('ASP.Default_aspx.GetHtml', [url], clientCallBack, false, false, false); } }}; </script>

As can be seen, Jason's library has reflected each of our methods and constructed matching client-side script functions, including a cross - browser implementation of XMLHTTPRequest to handle the out of band calls "behind the scenes", and injected it all into our page. So when a client - side method is called (say the "onblur" method of my Textbox, which makes a call to the client-side PopulateListWithDataTable(elem) method), the code to handle the client-side remote scripting call is all there and matches perfectly.

Two options are now available to the curious:

1) Try out a live version here. (Firefox users, I am sorry. In their infinite wisdom, the Firefox developers decided that an XMLHTTP call to a resource on different domain is a security issue. If this disappoints you, complain to them. I like FireFox, but I believe that security decisions should be empowered to the user). IE users, if you get any security issues, just add the remote url of http://beta.search.msn.com/news/results.aspx to your trusted sites.

2) Download the Visual Studio .Net 2003 Solution here.

And finally, let's try to remember that it's not "Microsoft gets hip to AJAX". Microsoft INVENTED AJAX, and its called Remote Scripting. In ASP.NET 2.0, they call it "Script Callbacks". Do good things with it!

 

 

 


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: