Loading...
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:
public class IncrementalLoader : System.Web.UI.WebControls.CompositeControl, IScriptControl
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
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);
}
}
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;
}
public string RenderLoadScript()
{
return "LoadContent('" + GetCallbackPath() + "', '" +
_contentPanel.ClientID + "', '"
+ _loadingPanel.ClientID + "', '" +
_faultPanel.ClientID + "');";
}
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.
public void ProcessRequest(HttpContext context)
{
Random r = new Random();
int wait = r.Next(500, 3000);
System.Threading.Thread.Sleep(wait);
if (context.Request.QueryString[IncrementalLoader.ControlQuery] != null)
{
try
{
Page page = new Page();
string controlLocation =
Utility.DecryptString(
context.Request.QueryString[IncrementalLoader.ControlQuery]);
UserControl control = (UserControl)page.LoadControl(controlLocation);
page.Controls.Add(control);
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!