Declaring Client-Side ASP.NET AJAX Controls : Part I

I want to build “pure” client-side ASP.NET AJAX web applications and I want to get the full benefits of a declarative framework. Currently, the ASP.NET AJAX framework does not support a good method of creating declarative client-side controls. In this blog entry, I examine different strategies for implementing declarative client-side controls that target the Microsoft ASP.NET AJAX framework.

The Problem in Need of a Solution

I like the ASP.NET framework. Declarative server-side controls are a good thing. I would much rather add a GridView control to a page than script out all the necessary logic for rendering the user interface for sorting and paging a set of items. Likewise for the TreeView, Menu, and all of the other controls in the toolbox. In general, I believe that it is better to declare than to code.

I’ve been writing web applications for a long time. I used to build huge websites using classic Active Server Pages. Active Server Pages did not support declarative controls and the pages were not pretty. You had to swim in a sea of vbscript/jscript script when reading the page. The pages were hard to understand and maintain. The technical name for the code contained in this type of page is spaghetti code.

I like building user interfaces with declarative controls. Unfortunately, the ASP.NET framework currently only supports declarative server-side controls. The ASP.NET framework does not support declarative client-side controls. Since I want to build rich interactive responsive Ajax web applications, I need client-side declarative controls.

I’ve made a list of the features that I want in a declarative client-side Ajax framework:

· Declarative Controls – I want to be able to add a client-side control to a page in exactly the same way as I currently can add a server-side control to a page. You should be able to declare a rich client-side control simply by adding a tag to a page.

· Composite Controls – In some cases, a client-side control should get replaced by a rich set of HTML elements. Think of the ASP.NET Login control. This control renders TextBox, Validation, and Button controls. Some declarative client-side controls need to be composite controls.

· DOM Accessible – You should be able to interact with a client-side control using standard DOM methods such as getElementById(). As we’ll see later in this blog entry, this requirement is more challenging than you might think.

· Templates – Many of the server-side controls in the ASP.NET framework support templates. Templates are huge. Templates enable you to get rid of the majority of your spaghetti code. However, templates pose special issues for a declarative control framework. A template does not represent a set of controls. A template represents a pattern for a repeated set of controls. Templates introduce special issues about control IDs (you can’t repeat the same ID).

· Libraries – Different client-side controls might live in different client-side script libraries. The framework needs to be able to load the right client-script library for the right control. It would be even better if the framework could determine library dependencies and load the right dependencies automatically and non-redundantly.

· Scriptless – In the perfect world, a page would contain nothing but tags. It would be a valid XHTML document (both the source code and the rendered content). It should not contain scripts or even data-binding expressions. A person entirely ignorant of the ASP.NET framework should be able to load it up into an application like Dreamweaver and start modifying it without endangering any of the page logic (you know, the designer should be able to design without breaking your page’s functionality).

· Notepad-able – I don’t trust any code that I can’t write in notepad. I get nervous whenever I hear anyone say that the Visual tools will make some code easy to write (for example, creating entity associations in LINQ to SQL is currently much too hard). One of the things that I really liked about the original ASP.NET framework was that I could do everything in Notepad. That gave me the feeling that I could understand the code.

The list above is a lot to want, but it describes what I want. The rest of this blog entry will describe different strategies for implementing this type of declarative client-side framework.

Creating a Client-Side ASP.NET AJAX Control

Before we do anything else, let’s start by creating a really, really simple ASP.NET AJAX client-side control. The code in Listing 1 defines a simple client-side control named the MyControl control. The MyControl control renders the string “blah”.

Listing 1 – MyControl.js

   1:  /// <reference name="MicrosoftAjax.js"/>
   2:   
   3:  Type.registerNamespace("Code");
   4:   
   5:  Code.MyControl = function(element) {
   6:   
   7:    Code.MyControl.initializeBase(this, [element]);
   8:   
   9:    element.innerHtml = "blah";
  10:   
  11:  }
  12:   
  13:  Code.MyControl.prototype = {
  14:   
  15:    initialize: function() {
  16:   
  17:      Code.MyControl.callBaseMethod(this, 'initialize');
  18:   
  19:      // Add custom initialization here
  20:   
  21:    },
  22:   
  23:    dispose: function() {
  24:   
  25:      //Add custom dispose actions here
  26:   
  27:      Code.MyControl.callBaseMethod(this, 'dispose');
  28:   
  29:    }
  30:   
  31:  }
  32:   
  33:  Code.MyControl.registerClass('Code.MyControl', Sys.UI.Control);
  34:   
  35:  if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

If the code in Listing 1 does not make sense to you, don’t worry. The code defines a very minimal client-side control. So, let’s look at different ways that we can add this control to a page.

Creating AJAX Controls: The Windows Forms Approach

You create an ASP.NET AJAX control by calling the client-side $create() method. The $create() method can be used to convert a normal HTML element in a page into a client-side Ajax control.

For example, the page in Listing 2 instantiates the MyControl control that we created in the previous section:

Listing 2 – Way1.aspx

   1:  <%@ Page Language="C#" %>
   2:   
   3:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   4:   
   5:  <html xmlns="http://www.w3.org/1999/xhtml">
   6:   
   7:  <head id="Head1" runat="server">
   8:   
   9:    <title>Way 1</title>
  10:   
  11:    <script type="text/javascript">
  12:   
  13:    function pageLoad() 
  14:   
  15:    {
  16:   
  17:      $create(Code.MyControl, null, null, null, $get("ctl"));
  18:   
  19:    }
  20:   
  21:    </script>
  22:   
  23:  </head>
  24:   
  25:  <body>
  26:   
  27:  <form id="form1" runat="server">
  28:   
  29:  <div>
  30:   
  31:  <asp:ScriptManager ID="ScriptManager1" runat="server">
  32:   
  33:  <Scripts>
  34:   
  35:    <asp:ScriptReference Path="~/MyControl.js" />
  36:   
  37:  </Scripts>
  38:   
  39:  </asp:ScriptManager>
  40:   
  41:    <div id="ctl"></div>
  42:   
  43:  </div>
  44:   
  45:  </form>
  46:   
  47:  </body>
  48:   
  49:  </html>

There are two things to notice about this page. First, notice that the MyControl.js JavaScript file is imported with the server-side ScriptManager control. The ScriptReference loads the MyControl.js file.

Second, notice the page includes a JavaScript pageLoad() function. This function gets called by the ASP.NET AJAX framework automatically after all of the scripts are loaded and the DOM has been parsed. The call to the $create() method is contained in this pageLoad method.

The $create() method converts a normal, unassuming HTML element into a client-side ASP.NET AJAX control. The script above converts a DIV tag declared in the body of the page into an ASP.NET AJAX control.

The $create() method accepts the following set of parameters:

· Type – Indicates the type of control to create. For example, Code.MyControl

· Properties – Indicates a list of initial property values for the control. You pass this list as a JavaScript object literal.

· Events – Indicates a list of initial event handlers for the control. You pass this list as a JavaScript object literal.

· References – Indicates a list of references to other controls. You pass this list as a JavaScript object literal.

· Element – Indicates the DOM element where the control will be initialized.

I almost never use any of the parameters except for Type, Properties, and Element.

I call this method of instantiating an AJAX control the Windows Forms approach. I call it the Windows Forms approach since it reminds me of the way that you must code the instantiation of all of the controls in a Windows Forms form.

Nobody writes Windows Forms applications in notepad because this method of instantiating controls is just too awkward and time consuming. Obviously, this is not a good way to create AJAX controls. This approach is a non-declarative approach. It does not work well in the case of pages with a lot of controls.

Creating AJAX Controls: The ASP.NET Controls Approach

The ASP.NET framework currently supports one method of creating declarative client-side controls. You can create a declarative client-side control by launching it from a declarative server-side ASP.NET control.

To make this easy, the ASP.NET 3.5 framework includes a base control class for this very purpose: the ScriptControl. The code in Listing 3 defines a server-side control named MyControl that launches the client-side control also named MyControl.

Listing 3 – MyControl.cs

   1:  using System;
   2:   
   3:  using System.Web;
   4:   
   5:  using System.Web.UI;
   6:   
   7:  using System.Collections.Generic;
   8:   
   9:  namespace Code
  10:   
  11:  {
  12:   
  13:    public class MyControl : ScriptControl
  14:   
  15:    {
  16:   
  17:      protected override HtmlTextWriterTag TagKey
  18:   
  19:      {
  20:   
  21:        get
  22:   
  23:        {
  24:   
  25:          return HtmlTextWriterTag.Div;
  26:   
  27:        }
  28:   
  29:      }
  30:   
  31:      protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors()
  32:   
  33:      {
  34:   
  35:        ScriptDescriptor descriptor = new ScriptControlDescriptor("Code.MyControl", this.ClientID);
  36:   
  37:        return new List<ScriptDescriptor>() { descriptor };
  38:   
  39:      }
  40:   
  41:      protected override IEnumerable<ScriptReference> GetScriptReferences()
  42:   
  43:      {
  44:   
  45:        ScriptReference refer = new ScriptReference("~/MyControl.js");
  46:   
  47:        return new List<ScriptReference>() { refer };
  48:   
  49:      }
  50:   
  51:    }
  52:   
  53:  }

The server-side control defined in Listing 3 has one property named TagKey and two methods named GetScriptDescriptors() and GetScriptReferences. The TagKey property determines the tag that the server-side control renders to the browser. In the code above, the control renders a DIV tag.

The GetScriptDescriptors() method is responsible for generating the $create() method call that creates the client-side control. The control in Listing 3 creates a client-side control named MyControl. The control generates the following $create statement which is executed on the client:

$create(Code.MyControl, null, null, null, $get("ctl"));

Finally, the GetScriptReferences() method enables you to add the JavaScript library that contains the definition for the client-side control to the page. In Listing 3, the GetScriptReferences() method is used to add a reference to the MyControl.js JavaScript library that contains the definition of the client-side MyControl control.

After you create the server-side script control, you can use the control in a page just like any other ASP.NET control. The page in Listing 4 takes advantage of the server-side MyControl control to launch the client-side MyControl control.

Listing 4 – Way2.aspx

   1:  <%@ Page Language="C#" %>
   2:   
   3:  <%@ Register TagPrefix="ajax" Namespace="Code" %>
   4:   
   5:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   6:   
   7:  <html xmlns="http://www.w3.org/1999/xhtml">
   8:   
   9:  <head runat="server">
  10:   
  11:    <title>Way 2</title>
  12:   
  13:  </head>
  14:   
  15:  <body>
  16:   
  17:  <form id="form1" runat="server">
  18:   
  19:  <div>
  20:   
  21:    <asp:ScriptManager ID="ScriptManager1" runat="server" />
  22:   
  23:    <ajax:MyControl ID="ctl" runat="server" />
  24:   
  25:  </div>
  26:   
  27:  </form>
  28:   
  29:  </body>
  30:   
  31:  </html>

The advantage of this approach to creating client-side controls is that it integrates very well with the existing ASP.NET framework. For example, the very popular AJAX Control Toolkit uses this approach (more or less) for all of the toolkit controls such as the DragPanel and AutoComplete controls (see http://www.asp.net/ajax/ajaxcontroltoolkit/samples/). This is a very good approach to take when you want to incrementally add Ajax functionality to an existing server-side ASP.NET application.

However, I’m interested in building “pure” Ajax applications with the Microsoft ASP.NET framework. If you want to abandon all of the baggage of server-side ASP.NET -- such as view state, the postback model, and the page and control event model -- then you won’t want to use heavy weight server-side controls as a launch mechanism for your client-side controls.

In particular, the ScriptControl base control requires you to add a ScriptManager control to a page. The ScriptManager control, in turn, requires you to add a server-side form control to a page. Therefore, as soon as you start using a ScriptControl to launch your client-side controls, you’ve already committed yourself to the web form model.

Furthermore, it is important to realize that a ScriptControl derives from the base WebControl class. The base WebControl class has a rich set of events, methods, and properties tied to the server-side Web Forms page execution model. A ScriptControl participates in the normal page execution lifecycle.

When building a “pure” Ajax application, this is the very stuff that I want to go away:

· Postbacks – The whole point of an Ajax application is to avoid submitting entire pages back to the server. From the perspective of an Ajax developer, posting a page back to the server is just an opportunity to create a bad user experience. Imagine, for the moment, freezing a desktop application and making the screen shake whenever a user performed any action; that would be crazy. The holy grail of a “pure” Ajax application is the single page application.

· View State – View state is great for a server-side ASP.NET web application. However, view state is a nightmare when you are building a client-side application. Keeping view state synchronized between the client and server is a huge pain. In any case, there is no point in maintaining view state when you are not performing postbacks (see previous bullet point). Since view state must be pushed back and forth across the wire in a hidden form field, view state just hurts performance.

· Page and Control Execution Lifecycle – ASP.NET provides developers with a rich server-side page execution lifecycle which is completely useless in the case of a "pure" client-side application. Who cares about Init, Load, PreRender, and Unload events when these events happen on the server-side? Just render my client-side code please.

Now, I want to emphasize that the features listed in the bullet points above are what makes ASP.NET such a fantastic framework for building server-side web applications. Be that as it may, I want my fast Ajax fighter jet of a client-side application.

The .NET and ASP.NET frameworks provide many useful features that can be used in a pure Ajax web application. For example, I want to take advantage of ASP.NET services such as the authentication, role, and profile services. I also want to take advantage of .NET framework features such as LINQ to SQL. I just don’t want to be forced to adopt a heavy-weight server-side page model just to take advantage of these useful framework features.

Creating AJAX Controls: The Lightweight ASP.NET Page Approach

If server-side ASP.NET controls are too heavy to launch client-side Ajax controls, then why not build lightweight server-side controls? Why not create server-side controls that don’t assume view state, the postback model, or a server-side page execution lifecycle? This is the approach to creating declarative client-side controls that we will consider in this section.

When I started to investigate this approach, I quickly realized that heavyweight web controls are baked very deeply into the ASP.NET framework. Every control used in an ASP.NET page must derive from the base Control class (System.Web.UI.Control class). The base Control class already assumes a heavy-weight server-side page model. Therefore, if you want to create lightweight controls, you must also abandon ASP.NET pages.

One of the nice features of the ASP.NET framework is that it was designed to be very flexible. If you want to abandon ASP.NET pages as they currently exist, then you can do this. You simply need to remap ASP.NET pages to a new HTTP Handler.

I want my handler to do something really simple. I want it to parse a page and generate $create() method calls for any page elements that do not inhabit the XHTML namespace. My HTTP Handler is contained in Listing 5:

Listing 5 – AjaxPageHandler.cs

   1:  using System;
   2:   
   3:  using System.Web;
   4:   
   5:  using System.Xml;
   6:   
   7:  using System.Text;
   8:   
   9:  public class AjaxPageHandler : IHttpHandler
  10:   
  11:  {
  12:   
  13:    public void ProcessRequest(HttpContext context)
  14:   
  15:    {
  16:   
  17:      // load page
  18:   
  19:      string pagePath = context.Request.PhysicalPath;
  20:   
  21:      XmlDocument doc = new XmlDocument();
  22:   
  23:      try
  24:   
  25:      {
  26:   
  27:        doc.Load(pagePath);
  28:   
  29:      }
  30:   
  31:      catch
  32:   
  33:      {
  34:   
  35:        throw new Exception("Could not load " + context.Request.Path);
  36:   
  37:      }
  38:   
  39:      // find Ajax elements
  40:   
  41:      XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
  42:   
  43:      nsManager.AddNamespace("default", "http://www.w3.org/1999/xhtml");
  44:   
  45:      XmlNodeList controls = doc.SelectNodes(@"//*[namespace-uri(.) != 'http://www.w3.org/1999/xhtml']", nsManager);
  46:   
  47:     if (controls.Count > 0)
  48:   
  49:     {
  50:   
  51:      // Add Microsoft AJAX Library Script reference
  52:   
  53:      AddScriptTag(context, doc, "/Microsoft/MicrosoftAjax.js");
  54:   
  55:      // Add libraries
  56:   
  57:      AddLibraries(context, doc);
  58:   
  59:      // build $create method calls
  60:   
  61:      AddCreates(doc, controls);
  62:   
  63:    }
  64:   
  65:    // Render XML doc
  66:   
  67:    doc.Save(context.Response.Output);
  68:   
  69:  }
  70:   
  71:  private void AddLibraries(HttpContext context, XmlDocument doc)
  72:   
  73:  {
  74:   
  75:    foreach (XmlAttribute att in doc.DocumentElement.Attributes)
  76:   
  77:    {
  78:   
  79:      if (att.Name.StartsWith("xmlns:"))
  80:   
  81:      {
  82:   
  83:        AddScriptTag(context, doc, att.Value);
  84:   
  85:      }
  86:   
  87:    }
  88:   
  89:  }
  90:   
  91:  private void AddScriptTag(HttpContext context, XmlDocument doc, string path)
  92:   
  93:  {
  94:   
  95:    XmlElement script = doc.CreateElement("script", "http://www.w3.org/1999/xhtml");
  96:   
  97:    script.SetAttribute("type", "text/javascript");
  98:   
  99:    script.SetAttribute("src", context.Request.ApplicationPath + path);
 100:   
 101:    script.InnerText = ""; // don't create minimal element
 102:   
 103:    doc.DocumentElement.AppendChild(script);
 104:   
 105:  }
 106:   
 107:  private void AddCreates(XmlDocument doc, XmlNodeList controls)
 108:   
 109:  {
 110:   
 111:    // Build $create method calls
 112:   
 113:    StringBuilder sb = new StringBuilder();
 114:   
 115:    sb.AppendLine();
 116:   
 117:    sb.AppendLine(@"//<![CDATA[");
 118:   
 119:    sb.AppendLine("Sys.Application.initialize();");
 120:   
 121:    sb.AppendLine("Sys.Application.add_init(function() {");
 122:   
 123:    foreach (XmlElement el in controls)
 124:   
 125:    {
 126:   
 127:      if (!el.HasAttribute("id"))
 128:   
 129:        throw new Exception("Element " + el.Name + " missing id");
 130:   
 131:      sb.AppendFormat("$create({0}.{1},null,null,null,$get('{2}'));\n", el.Prefix, el.LocalName, el.GetAttribute("id"));
 132:   
 133:    }
 134:   
 135:    sb.AppendLine("});");
 136:   
 137:    sb.AppendLine("//]]>");
 138:   
 139:    // Add script element
 140:   
 141:    XmlElement script = doc.CreateElement("script", "http://www.w3.org/1999/xhtml");
 142:   
 143:    script.SetAttribute("type", "text/javascript");
 144:   
 145:    script.InnerXml = sb.ToString();
 146:   
 147:    doc.DocumentElement.AppendChild(script);
 148:   
 149:  }
 150:   
 151:  public bool IsReusable
 152:   
 153:  {
 154:   
 155:    get { return true; }
 156:   
 157:  }
 158:   
 159:  }

The HTTP handler in listing 5 actually does 3 things:

· It adds a reference to the Microsoft AJAX Library. The assumption is that this library is located at the path /Microsoft/MicrosoftAjax.js. You can download the standalone Microsoft AJAX Library from http://msdn.microsoft.com/en-us/asp.net/bb944808.aspx

· It adds a reference to each library needed by the client-side controls.

· It calls the $create() method for each client-side control.

You can apply the HTTP Handler to any aspx page in a folder named AjaxPages with the web configuration file in Listing 6 by adding this configuration file to the AjaxPages folder.

Listing 6 – /AjaxPages/Web.config

   1:  <?xml version="1.0"?>
   2:   
   3:  <configuration>
   4:   
   5:  <system.web>
   6:   
   7:    <httpHandlers>
   8:   
   9:      <remove verb="*" path="*.aspx"/>
  10:   
  11:      <add verb="*" path="*.aspx" type="AjaxPageHandler"/>
  12:   
  13:    </httpHandlers>
  14:   
  15:  </system.web>
  16:   
  17:  </configuration>

Finally, Listing 7 contains a page that contains two declarations for client-side controls. The page contains the declaration for a control named MyControl and a control named Boom.

Listing 7 – /AjaxPages/Page.aspx

   1:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   2:   
   3:  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:Code="/MyControl.js" xmlns:Ajax="/Boom.js">
   4:   
   5:  <head runat="server">
   6:   
   7:    <title>Ajax Page</title>
   8:   
   9:  </head>
  10:   
  11:  <body>
  12:   
  13:  <div>
  14:   
  15:    <Code:MyControl id="ctl" text="Hello World!"></Code:MyControl>
  16:   
  17:    <Ajax:Boom id="boo1"></Ajax:Boom>
  18:   
  19:  </div>
  20:   
  21:  </body>
  22:   
  23:  </html>

The page in Listing 7 is a valid XHTML page. Notice that the page’s document element contains three xmlns attributes. It has xmlns attributes for the following namespaces: http://www.w3.org/1999/xhtml, MyControl.js, and Boom.js. The last two namespaces do double duty as paths to client control libraries.

Notice, furthermore, that the page contains the declarations for two client-side controls named MyControl and Boom. The declarations use the namespace prefixes from the xmlns attributes.

When you request the page, the AjaxPageHandler HTTP Handler executes and parses the page. The rendered page in Listing 8 gets sent to the browser.

Listing 8 – Page.aspx (rendered content)

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:   
   3:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"[]>
   4:   
   5:  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:Code="/MyControl.js" xmlns:Ajax="/Boom.js">
   6:   
   7:  <head runat="server">
   8:   
   9:  <title>Ajax Page</title>
  10:   
  11:  </head>
  12:   
  13:  <body>
  14:   
  15:  <div>
  16:   
  17:  <Code:MyControl id="ctl" text="Hello World!">
  18:   
  19:  </Code:MyControl>
  20:   
  21:  <Ajax:Boom id="boo1">
  22:   
  23:  </Ajax:Boom>
  24:   
  25:  </div>
  26:   
  27:  </body>
  28:   
  29:  <script type="text/javascript" src="/Code/Microsoft/MicrosoftAjax.js">
  30:   
  31:  </script>
  32:   
  33:  <script type="text/javascript" src="/Code/MyControl.js">
  34:   
  35:  </script>
  36:   
  37:  <script type="text/javascript" src="/Code/Boom.js">
  38:   
  39:  </script>
  40:   
  41:  <script type="text/javascript">
  42:   
  43:  //<![CDATA[
  44:   
  45:  Sys.Application.initialize();
  46:   
  47:  Sys.Application.add_init(function() {
  48:   
  49:  $create(Code.MyControl,null,null,null,$get('ctl'));
  50:   
  51:  $create(Ajax.Boom,null,null,null,$get('boo1'));
  52:   
  53:  });
  54:   
  55:  //]]>
  56:   
  57:  </script>
  58:   
  59:  </html>

This seems like a great approach to solving the problem of declaring client-side controls. This solution appears to enable us to build “pure” client-side Ajax applications. Our lightweight pages do not shackle us to view state, the postback model, or the server-side page execution lifecycle.

In fact, when I first started writing this blog entry (days ago), I thought that this solution would be the best solution. However, the devil is in the details. There are some real problems with this solution that you encounter as soon as your declarative controls get more complicated.

You quickly encounter problems with the solution when working with Firefox. The problem is that Firefox interprets all of the custom control declarations using its tag soup processor. This processor mangles the tags in weird ways. You might have noticed that I declared the two tags in Listing 7 using explicit opening and closing tags:

<Code:MyControl id="ctl" text="Hello World!">

</Code:MyControl>

<Ajax:Boom id="boo1">

</Ajax:Boom>

If, instead, I had declared the tags using self-closing tags, then the page would not have been interpreted correctly by Firefox:

<Code:MyControl id="ctl" text="Hello World!" />

<Ajax:Boom id="boo1" />

The Firefox tag soup processor interprets the above tag declarations like this:

<Code:MyControl id="ctl" text="Hello World!">

<Ajax:Boom id="boo1"></Ajax:Boom>

</Code:MyControl>

Notice that the second tag has gotten gobbled up by the first tag. When you have a series of custom client-side controls in a row, they all get swallowed up by the first custom tag in the series.

This problem might seem minor, but it gets more acute when you start adding multiple client-side tags to a page. Eventually, I want to be able to declare client-side controls that contain templates that contain multiple client-side controls. This will never happen if Firefox always insists on re-arranging all of my tags.

There is a way to fix this problem when working with Firefox. You need to serve your pages as XHTML pages (using the "application/xhtml+xml" MIME type) instead of the normal text/html MIME type. When a page is served to Firefox using the "application/xhtml+xml" MIME type, Firefox does not use its normal tag soup processor. Instead, it uses its stricter XML parser to parse the page. No more garbling the page.

You can serve pages to Firefox using the "application/xhtml+xml" MIME type by using the Global.asax file in Listing 9:

Listing 9 – Global.asax

   1:  <%@ Application Language="C#" %>
   2:   
   3:  <script runat="server">
   4:   
   5:  void Application_PreSendRequestHeaders(object sender, EventArgs e)
   6:   
   7:  {
   8:   
   9:    HttpContext context = ((HttpApplication)sender).Context;
  10:   
  11:    if (context.Request.Path.ToLower().EndsWith(".aspx"))
  12:   
  13:    {
  14:   
  15:      if (Array.IndexOf(context.Request.AcceptTypes, "application/xhtml+xml") != -1)
  16:   
  17:      {
  18:   
  19:        context.Response.ContentType = "application/xhtml+xml";
  20:   
  21:      }
  22:   
  23:    }
  24:   
  25:  }
  26:   
  27:  </script>
  28:   

Notice that the Global.asax file in Listing 9 only serves a page as XHTML when the browser accepts it. Microsoft Internet Explorer cannot handle XHTML pages – so don’t attempt to serve an XHTML page to this browser.

Unfortunately, this solution introduces yet another problem. When a page is served as "application/xhtml+xml", you can no longer use the DOM getElementById() method with custom tags. Because the Microsoft AJAX Library $get() method is a shorthand for the getElementById() method, and we use the $get()method with the last parameter of the $create() method call, this means that our $create() method calls will no longer work. Drats!

So, what do we do now? If we want to be able to use getElementById() with our declarative client-side controls, then we must replace the custom tags with standard XHTML tags. For example, we can replace any custom client-side tags, such as the Code:MyControl tag, with a standard XHTML DIV tag. If we perform this replacement, then getElementById() works again.

We could modify our AjaxPageHandler HTTP Handler to perform this replacement on the server-side for us. However, at this point, I’m going to abandon the server-side approach to parsing declarative controls and turn to a client-side approach.

Creating AJAX Controls: The Client-Side Approach

The final solution for parsing client-side declarative controls that I want to examine in this blog entry is the client-side approach. When following this strategy, you perform all of your control parsing within your client-side JavaScript code.

The basic idea is that you perform a DOM walk. You walk through all of the elements in a page and you execute a call to the $create() method whenever you encounter any custom tags. You can use the JavaScript library in Listing 10 to perform a DOM walk.

Listing 10 – DomWalk.js

   1:  /// <reference name="MicrosoftAjax.js"/>
   2:   
   3:  Sys.Application.add_init( appInit );
   4:   
   5:  function appInit()
   6:   
   7:  {
   8:   
   9:    // Find Ajax controls
  10:   
  11:    var controls = [];
  12:   
  13:    var els = document.getElementsByTagName("*");
  14:   
  15:    var el;
  16:   
  17:    for (var i=0;i < els.length;i++)
  18:   
  19:    {
  20:   
  21:      el = els[i];
  22:   
  23:      if (isControl(el))
  24:   
  25:        controls.push( el ) ;
  26:   
  27:    }
  28:   
  29:    // Create controls
  30:   
  31:    for (var k=0;k < controls.length;k++)
  32:   
  33:    {
  34:   
  35:      el = controls[k];
  36:   
  37:      $create(Type.parse( getControlType(el) ), null, null, null, el);
  38:   
  39:    }
  40:   
  41:  }
  42:   
  43:  function isControl(el)
  44:   
  45:  {
  46:   
  47:    if (el.tagUrn)
  48:   
  49:      return true;
  50:   
  51:    if (el.namespaceURI && el.namespaceURI != "http://www.w3.org/1999/xhtml")
  52:   
  53:      return true;
  54:   
  55:    return false;
  56:   
  57:  }
  58:   
  59:  function getControlType(el)
  60:   
  61:  {
  62:   
  63:    return (el.tagUrn || el.namespaceURI) + "." + (el.localName || el.tagName);
  64:   
  65:  }
  66:   
  67:  Sys.Application.notifyScriptLoaded();

The code in Listing 10 marches through all of the elements in a page searching for elements that are not part of the default XHTML namespace. Next, the code calls the $create() method for each of non-XHTML elements in order to instantiate a client-side control. The page in Listing 11 uses the DomWalk.js JavaScript library.

Listing 11 – Way3.aspx

   1:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   2:   
   3:  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ajax="Code">
   4:   
   5:  <head runat="server">
   6:   
   7:    <title>Way 3</title>
   8:   
   9:    <script type="text/javascript" src="Microsoft/MicrosoftAjax.js"></script>
  10:   
  11:    <script type="text/javascript" src="SuperControl.js"></script>
  12:   
  13:    <script type="text/javascript" src="DomWalk.js"></script>
  14:   
  15:    </head>
  16:   
  17:  <body>
  18:   
  19:  <div>
  20:   
  21:    <ajax:SuperControl id="ctl" />
  22:   
  23:  </div>
  24:   
  25:  </body>
  26:   
  27:  </html>

There are three things that you should notice about the page in Listing 11. First, notice that the HTML document element includes an xmlns attribute that has the name ajax and the value Code. Next, notice that each of the required script libraries -- MicrosoftAjax.js, SuperControl.js, and DomWalk.js-- are manually imported with <script> tags. Finally, notice that the body of the page contains the declaration for a single control named the SuperControl.

In order for the page in Listing 11 to work correctly, it must be served as an XHTML document to Firefox. In other words, you need the Global.asax file contained in Listing 9. One consequence of this requirement is that the page must be an aspx page instead of an html page (otherwise, the Global.asax file won’t execute when you request the page).

The SuperControl is contained in Listing 12.

Listing 12 – SuperControl.js

   1:  /// <reference name="MicrosoftAjax.js"/>
   2:   
   3:  Type.registerNamespace("Code");
   4:   
   5:  Code.SuperControl = function(element) {
   6:   
   7:    // create replacement element
   8:   
   9:    this.originalElement = element;
  10:   
  11:    element = document.createElement("div");
  12:   
  13:    element.id = this.originalElement.getAttribute("id");
  14:   
  15:    this.originalElement.parentNode.replaceChild(element,this.originalElement);
  16:   
  17:    Code.SuperControl.initializeBase(this, [element]);
  18:   
  19:    // add inner HTML
  20:   
  21:    element.innerHTML = "Super!";
  22:   
  23:  }
  24:   
  25:  Code.SuperControl.prototype = {
  26:   
  27:    initialize: function() {
  28:   
  29:      Code.SuperControl.callBaseMethod(this, 'initialize');
  30:   
  31:      // Add custom initialization here
  32:   
  33:    },
  34:   
  35:    dispose: function() {
  36:   
  37:      //Add custom dispose actions here
  38:   
  39:      Code.SuperControl.callBaseMethod(this, 'dispose');
  40:   
  41:    }
  42:   
  43:  }
  44:   
  45:  Code.SuperControl.registerClass('Code.SuperControl', Sys.UI.Control);
  46:   
  47:  if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
  48:   

Notice that the constructor for the SuperControl includes logic to replace the original element (the ajax:SuperControl element) with a new element (a DIV element). There are two reasons why you need to do this. First, you cannot use document.getElementById() with a custom element like the ajax:SuperControl element. Second, innerHTML does not work with custom elements. If you want your custom controls to work like normal XHTML elements, then you need to replace them with normal XHTML elements.

Conclusion

So which approach is the best approach for parsing client-side Ajax controls? I’m leaning on the side of the client-side approach described in the last section. I have two reasons for preferring the client-side approach over the lightweight server-side approach.

First, I know that I need to replace the custom elements that represent Ajax controls in a page with standard XHTML elements. Otherwise, I can’t interact with the controls as normal elements (getElementById() and innerHTML don’t work). I could perform this replacement on the server-side. However, I might want to dynamically inject controls into a page from JavaScript code. In that case, it would make more sense to parse the elements on the client-side.

Imagine, for example, a client-side Login control. This is an example of a composite control. I would want to replace the ajax:Login element with a set of normal XHTML elements like DIV and INPUT elements. I don’t want to do these replacements in server-side code since I might want to add a Login control to a page from JavaScript code dynamically.

Second, I don’t want to perform a getElementById() call for each element that I want to instantiate with the $create() method since the getElementById() method is notoriously slow. However, if I build my $create() method calls on the server-side, I don’t know of any way of avoiding calling getElementById() for each control. When rendering from the server, I am forced to represent an element with its Id string and not with the element itself.

If, on the other hand, I perform the parsing on the client-side, then there are many ways of avoiding getElementById(): I can use getElementsByTagName(), I can use XPATH, I can perform an XSLT transformation, and so on. I don't know a priori which method is faster, but it seems like I have much more flexibility.

2 Comments

  • With that amount of efforts you can develop your own Ajax framework, from ground up just using ASP.NET Callbacks.

    By the way, "declarative" is not always good in my opinion, "visual" is.

  • "We could modify our AjaxPageHandler HTTP Handler to perform this replacement on the server-side for us. However, at this point, I’m going to abandon the server-side approach to parsing declarative controls and turn to a client-side approach."

    I was with you until that step. To me this seems like the point to throw in the towel and abandon custom client tags. Allow the server to render pure XHTML and call it a day.

    Sure, you lose strict getElementById functionality for your controls. But that's ok, IMHO. The client only speaks XHTML and you can't really change that. After all, this is your presentation layer. Every graphical application must eventually bow to a presentation layer at some point. You can still do rich client side manipulation of your controls if you build client side datastructures to manage the state of each control. You could even keep your own DOM if you want to - much like the Control heirarchy in ASP.NET. In other words you just need a smarter controller. If you want to insantiate your controls from the client, have the controler callback to a standard creation mechanism on the server that returns pure XHMTL for the control in question.

    Great work though. I think this general direction in good and will become the prefered way to develop web applications.

Comments have been disabled for this content.