:::: MENU ::::

Wednesday, December 24, 2008

ASP.NET User Controls are pretty useful.  They allow functional modules of code and markup to be encapsulated in such a way that reuse is convenient and easy, without sacrificing the power or integration of the ASP.NET model.  As we move into an era of AJAX-driven websites, this modularity is still very important. Can the user controls that we all know and (mostly) love still help with this encapsulation, despite being engineered before AJAX techniques emerged?  I think they can.  But at this point in the ASP.NET timeline, user controls are in need of some help.

The Fundamental Problem

With AJAX, more and more content is being dynamically loaded by the client on demand, rather than being included in the original http response.  This fundamental change conflicts with the user control's current usage model of being attached to the control heirarchy during the page lifecycle on the server--either through markup, or using the Page.LoadControl method in code.  For user controls to be useful in the world of AJAX and demand loading, we would need to find a way to load them outside of the normal page lifecycle, and use javascript to get the the rendered HTML and inject it into our page.  Luckily, this isn't too difficult to accomplish.

The following example illustrates a basic scenario in which we have a page that uses jQuery to load a user control when a button is clicked.  The calling page is pretty simple:

As you can see, all I've done in jQuery's ready event handler is wire up the click event of the button to make an ajax call to a web service.  The data result that is returned from the ajax call is then added into the content div on the page.  Let's take a look at the web service that we are calling in that code:

This is a pretty standard WCF Ajax service, which uses a utility class called UserControlUtility by calling its RenderAsString method, which looks like this:

In the helper method above, I'm simply accepting a parameter called path, which allows us to use the LoadControl method in the usual way.  If you are worried about the potential baggage of instantiating a Page object for every User Control that is rendered, don't lose too much sleep over it.  A page object that is instantiated like this is pretty lightweight, and doesn't go through the heavy ASP.NET Page lifecycle that occurs on a normal page load.

This is pretty nifty for simple scenarios, but big challenges arise when the application gets more complicated.  What happens when the user control has javascript of it's own?  Well ordinarily you would have a few options.  One option that I defaulted to when starting out with jQuery was to write all the JavaScript in the calling page, and just apply it to the user control's html when it has been loaded.  This is not the best solution, because you lose the encapsulation that we were trying to maintain with user controls in the first place.  The second solution is to include the javascript within the user control within another jQuery ready handler.  This works out much better, because the client functionality gets to be bundled with the markup for clean encapsulation.  Additionally, the included javascript will be excuted when the control is rendered on the parent page, thanks to jQuery.  But has this solved all of our problems?  Not quite.

Mo Javascript, Mo Problems

To illustrate how problems can arise with that last solution, let me give an example. Say you are developing a real-time stock-screening application.  In this application, you have a user control called StockItemRow.ascx that had quite a bit of javascript associated with it. You also have a page called Screener.aspx that periodically polls a web service for matching stocks, and adds those stocks to the grid via a rendered instance of StockItemRow.ascx.  And suppose the user control had a good deal of javascript bundled with it, and also a few nested user controls of its own (with their own javascript, of course).  What were to happen if you dynamically added 50 or 60 rows over a few minutes? You may see what I am trying to get at here.

The problem is that the JavaScript is being loaded over and over on each successful new request for data, simply because it is bundled inside the rendered user control.  As you load more and more data onto the page, this becomes a bigger and bigger waste.  Plus, unless you write your javascript very carefully, each new dynamically loaded user control could end up applying it's javascript to other user controls that have already been loaded.  Yuck!  In order to solve these problems, it is going to take a little more work.

The first issue we need to solve is the repititious loading of unnecessary javascript.  To do this, we need to separate it out from the user control into it's own js file.  Some may argue that we are losing encapsulation here, but I disagree.  I think just if an aspx page can have both a file for markup and a codebehind file, then a user control can have both a markup file and a js file (and it's codebehind file, for that matter). After we have separated it out, we have freed ourselves to be able to load the javascript file once, while still rendering the user control multiple times.

But just separating the javascript out doesn't solve our problems. We need to somehow "register" a single instance of javascript on the page, and have any dynamically loaded user controls use just that instance.  Additionally, we need to make sure that the javascript is capable of being applied to individual user controls, without affecting other user controls that have already been wired up and loaded on the page. 

Enter jQuery.DynamicLoader

jQuery.DynamicLoader is a simple jQuery plugin I wrote that allowed a parent page to dynamically load User Controls and their corresponding script files on demand. Here is the way it works:

  • You reference jQuery.DynamicLoader on your parent page.
  • Create an ajax service that renders user controls, similar to the example I showed earlier
  • Anytime you want to load a user control on that page, call $.dynamicLoader.loadUC() with the appropriate options.  This will fetch the rendered user control, and its corresponding javascript file. If the javascript is being loaded for the first time, DynamicLoader will register that instance as the singleton for all subsequent user controls of that same type.
  • The javascript instance is then invoked with the rendered user control as its UI context.

Let's jump into the sample project I've created as an example:

DynamicLoader.zip (52.77 kb)

The project contains a single page Default.aspx, and two user controls, TableWidget.ascx and CellWidget.ascx.   The purpose of the project is to demonstrate a page intitally with no content, and how we can dynamically load several tiers of user controls, each with their own scripts.  We start from a single button on Default.aspx that will dynamically load a new TableWidget every time it is clicked.  Inside each TableWidget is a button gets wired up to load its own user controls, this time CellWidgets.  Each CellWidget has its own javascript that needs to execute as well. 

 Here is how the first button is wired up with jQuery: 

As you can see, it is calling DynamicLoader's loadUC function, which takes a few options: ucName is the path to the user control to be loaded, queryString allows you to pass parameters to your UserControl to help render it on the server, and eventBindings allows you to handle events that are fired within the usercontrol.

As I mentioned earlier, the javascript in your user control needs to be registered before it can be used.  Don't get scared off now, it's only two extra lines of code:

We have a standard jQuery ready handler, and inside that we call DynamicLoader's registerUC function.  This will only be loaded once, even if multiple TableWidgets are loaded afterwards.  Also notice the event triggers.  You can create as many different types of events as your heart's desire, as long as the parent knows the name of the event (and references it in the eventBindings option).  I've included ready, busy, unbusy, and finished in the default options.  The ready event is one that I consider critical, because it is the event that the parent will use to attach the user control to the page.

Live Demo | Download Sample Solution (52.77 kb)

You can see that there are buttons on the CellWidget that do some trivial javascript actions, and also a button that demonstrates an event being monitored by the parent user control.

Room for Improvement

DynamicLoader is more of a proof concept than a full-fledged plugin, and there are several areas in which it needs to be improved:

  • The event chaining needs some work.  I haven't really tested it with events that bubble more than two layers up.
  • Right now it doesn't look like jQuery's $.getScript is caching the scripts.  I'd like to rewrite a version of getScript that does.
  • The registration system is very rigid at this point.  It expects you to pass in a user control's path, and the script needs to register itself with that exact path as its key (without the extension). 

So there you have it. This technique allows you to treat your User Controls as neatly encapsulated modules that are loaded and configured on demand.  Plus, there is no limit to nesting your user controls, and they will load efficiently and within their own context.   Finally, you don't have to break communication with your user controls.  The event binding allows a separation of concerns, while still being able to act on important things that happen within the user control.

I hope you find this technique useful, and please let me know if you have suggestions or improvements!

More