Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Incremental Page Display Pattern for User Controls

3.96/5 (8 votes)
30 Oct 2007CPOL4 min read 1   485  
A solution for loading user controls asynchronously via AJAX with no custom JavaScript required.

Loading...

Screenshot - loading.gif

Contents

Introduction

I'll be the first to admit that, even as a developer, I "ooh" and "ahh" over web sites with really clean and usable AJAX implementations. I'm often frustrated that most of the cool AJAX-esque libraries aren't targeted at ASP.NET. In particular, I've come across several sites recently that use an incremental page display pattern to display content after the page is loaded. I know that ASP.NET AJAX and the Control Toolkit have support for calling web services to display HTML output, but I dislike this approach for quite a few reasons (too many for this article). Plus, calling web services doesn't give developers a way to render user controls and other truly dynamic content. That's far too limiting to be very useful.

Purpose

My goal in this article is to lay the groundwork for loading content, specifically user controls (.ascx controls), on the fly. To give credit where it's due, some of the inspiration for this article comes from Scott Guthrie's UI templating article. I like Scott's approach to avoiding the UpdatePanel as well as using templated user controls. His idea is a good starting point, but I'm interested in having a server control I can drag onto a page and be done. I'm especially interested in one that lets me write zero JavaScript. Like most developers, I'd prefer to spend my time writing code, not hard-to-maintain client scripts.

A Working Example

The Server Control

Let's dive right into the example code. The code is primarily made up of two parts; a server control, and an HTTP request handler. Let's start with the server control. The purpose of the server control is simply to generate a client-side callable JavaScript used to call the server for the contents of a user control. All the server control needs in order to render the script is the relative path of the user control that will be rendered. Here's how the code works:

C#
public class IncrementalLoader : System.Web.UI.WebControls.CompositeControl, IScriptControl
{
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        // JavaScript registration - only happens for the first server control loaded

        if (!this.Page.ClientScript.IsClientScriptIncludeRegistered(
                              "__IncrementalLoaderScript"))
        {
            string resource = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), 
                "ControlLoading.IncrementalLoader.js");
            
            this.Page.ClientScript.RegisterClientScriptInclude(this.GetType(), 
                "__IncrementalLoaderScript", resource);
        }
    }
    // Gets a path for the javascript to query.  This points to the HTTP handler

    public string GetCallbackPath()
    {
        string path = this.Page.Request.Path;

        if (!string.IsNullOrEmpty(path))
        {
            path = path.Replace(".aspx", HandlerExtension);
        }

        string controlLocation = this.Page.ResolveUrl(this.ResourceToLoad);
        string query = Utility.EncryptString(controlLocation);

        path += "?" + ControlQuery + "=" + HttpUtility.UrlEncode(query);

        return path;
    }
    // Each server control instance renders a single line of JavaScript to load 

    // the user control when the browser is done loading the page.

    public string RenderLoadScript()
    {
        return "LoadContent('" + GetCallbackPath() + "', '" + 
               _contentPanel.ClientID + "', '" 
               + _loadingPanel.ClientID + "', '" + 
               _faultPanel.ClientID + "');";
    }
    // Registers the startup script 

    private void RetisterClientScript()
    {
        StringBuilder script = 
          new StringBuilder("<script type="\"text/javascript\""></script>\r\n");

        this.Page.ClientScript.RegisterStartupScript(this.GetType(), _
                               contentPanel.ClientID + 
                               "LoadingScript", script.ToString());
    }
}

There's not much to it. The server control just writes out a little bit of JavaScript. The server control also provides templates to define the mark up to be shown while the control is loading, and if an error is encountered. Note that the query string built by the server control is encrypted. You wouldn't want prying eyes seeing the relative paths to your user controls.

The Handler

Let's move on to the HTTP Handler. The hander is responsible for rendering the output for the specified user control. The handler takes incoming HTTP requests (for .ashx extensions in this example) and parses the encrypted query string to determine which user control to load. The user control is loaded by creating an empty Page object, adding the user control to the Page's control collection, and executing the Page. The resulting HTML is sent back to the browser to be rendered. For example purposes, the handler simulates latency up to 3 seconds.

C#
public void ProcessRequest(HttpContext context)
{
    // *** Simulate latency ***

    Random r = new Random();
    int wait = r.Next(500, 3000);
    System.Threading.Thread.Sleep(wait);

    if (context.Request.QueryString[IncrementalLoader.ControlQuery] != null)
    {
        try
        {
            // Create a page instance to host the user control

            Page page = new Page();

            string controlLocation = 
              Utility.DecryptString(
              context.Request.QueryString[IncrementalLoader.ControlQuery]);
            UserControl control = (UserControl)page.LoadControl(controlLocation);

            page.Controls.Add(control);

            // Execute the page and return the output
            // (only the control's output is returned)

            StringWriter output = new StringWriter();
            context.Server.Execute(page, output, false);

            context.Response.Clear();
            context.Response.Write(output.ToString());
        }
        catch { }
    }
}

All seems simple, right? It is a clean and easy way to get the output HTML needed to populate the UI, and it takes next to no code.

Still, there are already some fundamental flaws with this code.

Pros and Cons

On the up side, pages load extremely fast since there's only minimal data to load on post back. I'm of the opinion that when the user can see even a little content, it's a better experience than waiting for a whole page to load. Also, based on my initial metrics, loading dynamic content takes as much time (overall duration of loading all controls) as a standard page load.

Also, if your page contains a long-running user control, other parts of the page are usable while the long-running content is being processed.

There are, of course, down sides to the approach. The most obvious problem is that each dynamic user control costs one additional round trip to the server. While the traffic is minimal and asynchronous, there's still a higher overall volume for the server to deal with. Obviously, this is a serious consideration to take into account before employing this pattern.

The most frustrating down side right now is the inability to use ASP.NET controls that require a form tag. Hyper links, for example, don't need to be contained inside a form tag since they don't necessarily post back to the current page. A button, however, requires a form tag to operate. Until someone comes up with a way to manage state and event registration, this solution has a limitation as to which controls can be used.

Note: the sample download does contain one possible work around for the form tag issue. I got all the controls to render, but the view state and events are lost.

A Call for Help

I simply have not had time to dig into the ASP.NET pipeline and page architecture to tackle how view state and events can be maintained outside of having a page to post back to. I'm officially asking, for anyone interested, to help complete the project by finding a way to maintain state and events for the dynamically loaded controls! Any help or ideas are welcome!

Enjoy!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)