Introduction
This article describes a control which can host a UserControl
. It allows to Ajaxify a user control without any code change. The main benefit is that when refreshing its content, it does not instantiate the full page but only the contained user control.
Most of the ASP.NET features (at least the ones that I tested) are supported: viewstate, controlstate, postback events, validators... Moreover, one of the cool features of this control is allowing UserControl
to do cross-domain Ajax postback.
Background
When I wanted to put some Ajax in my ASP.NET project, I started by looking at the Microsoft Ajax Toolkit. However, one drawback of the UpdatePanel
is that when it refreshes its content, it instantiates not only the UpdatePanel
but also the entire page. It's great in some cases when you have several parts of the page interacting. However when your UpdatePanel
is independent (a menu for example) from the main content and your page is heavy, the side effect is that it takes ages to refresh only a small part of your page.
I could have used some webservice to update my content, but I don't like to output HTML from a C# function and it's not easy to design content this way. Microsoft gave us the user controls which are great to design and code: so I started to think of a kind of an UpdatePanel
which will host a UserControl
and allow it to be refreshed without reloading the whole page.
The main difficulty was to allow a developer to Ajaxify a UserControl
without any code change.
Implementation
Here is a quick overview of how the Control is implemented and how it works:
- After the initial page request, the client receives a standard HTML page
- When the client fires a postback event on a control hosted by the
DynamicUserControl
, the event is trapped by some JavaScript code and redirected to the DUCHandler
- On the server side, the
DUCHandler
instantiates a dummy Page containing only the DynamicUserControl
and the custom UserControl
- From the
UserControl
point of view, it's a standard page postback - The output result is then sent back to the client, and the browser updates only the
DynamicUserControl
which fired the postback - Note: All other postback events fired from outside the
DynamicUserControl
are handled by the Page handler
I will try to explain some of the hacks I used to make it work on the server and client side.
Server Side
The main difficulty in this part was to override the mechanism used to handle the viewstate, the controlstate and all the internal stuff of the Framework. Unfortunately, most of the ASP.NET classes are internal or sealed (or both), which makes it difficult to plugin.
The solution (hack) was to use the reflection API to access all these methods and members. The drawback of using reflection is that the code is really dependent on the Framework version and that it could break upon any minor update of it.
The DUCHandler
is really just a wrapper around the Page handler: It creates a page containing only a DynamicUserControl
with the UserControl
inside. However there are some noticeable hacks:
- The
DUCPageWrapper
: which overrides the FindControl
mechanism of the Page
. It allows the internal control to have the same ID as in the full page control tree but without all the surrounding controls. - The
DUCScriptManagerWrapper
: which overrides the RegisterArrayDeclaration
needed by controls using script arrays (especially the validators). - The
DUCRedirectModule
: which intercepts the redirection response before sending it to the client (i.e. It allows the DynamicUserControl
to handle the Response.Redirect
method).
Client Side
This is really the heart of the DynamicUserControl
.
The first thing the script does is to override the __doPostBack
function. So when the client fires a postback event, the code checks if it's originating from inside a DynamicUserControl
. In this case, the script parses the control to find the form's inputs that it contains. Then it makes a POST
request with all the data to the DUCHandler
and replaces the control content with the HTML output.
One of problems I had was integrating the script codes and script includes outputted by the DUCHandler
inside the existing page: simply putting the HTML tags in the innerHTML
was not sufficient. I had to create the script includes elements the DOM way (i.e. using document.createElement
) and call window.eval
with some delay to correctly evaluate the JavaScript blocks in the current page scope. (see DynamicUserControl._updateContainer()
and DynamicUserControl._scriptExecutor()
in the DynamicUserControl.js file).
Also, to allow the control to post data to another sub-domain, I needed to use a proxy iframe. Using the iframe proxy was the solution to bypass browser security both in IE and Firefox. You can see how it works by looking at the DynamicUserControl._doPostBackXSS()
function in the JS file and you can compare it with the standard way of posting data using XmlHttpRequest
in the DynamicUserControl._doPostBackXHR()
function.
Using the Dynamic User Control
In order to use this control, your project must be in ASP.NET 2.0 and must reference the Microsoft Ajax extensions DLL.
Here is the configuration to put in your Web.config file:
<pages>
<controls>
...
<add tagPrefix="jp" namespace="DUCExtension" assembly="DUCExtension"/>
</controls>
</pages>
...
<httpHandlers>
<add verb="*" path="*.duc"
type="DUCExtension.Modules.DUCHandler, DUCExtension" />
</httpHandlers>
...
<httpModules>
<add name="DUCRedirect"
type="DUCExtension.Modules.DUCRedirectModule, DUCExtension"/>
</httpModules>
If you want to enable the cross-domain postback, you also need to add the following lines:
<appSettings>
<add key="DUCDomain" value="mydomain.com"/>
</appSettings>
Then, if you use a UserControl
this way in your page:
<%@ Register TagPrefix="uc" TagName="Test" Src="~/UserControl/Test.ascx" %>
...
<uc:Test runat="server" ID="ucTest" />
you can replace your code with this:
<jp:DynamicUserControl runat="server" ID="ducTest"
UserControlPath="~/UserControl/Test.ascx"
EnablePostBackRegistration="true" >
</jp:DynamicUserControl>
You may also add a ProgressTemplate
which will be shown during the control update:
<jp:DynamicUserControl runat="server" ID="ducTest"
UserControlPath="~/UserControl/Test.ascx"
EnablePostBackRegistration="true" >
<ProgressTemplate>
<div>
<asp:Image runat="server" ImageUrl="~/img/ajax-loader.gif" />
</div>
</ProgressTemplate>
</jp:DynamicUserControl>
At last, here is a quick overview of the properties available:
UserControlPath
: Path to the user control to instantiate UserControl
: Get the embedded user controlProgressTemplate
: Content to be shown while updating the control UrlMap
: Prefix to the user control path (used in cross-domain postback) EnablePostBackRegistration
: If set to true
, embedded controls are able to register themselves for PostBack data (i.e. Page.RegisterRequiresPostBack
) UserControlProperties
: Set embedded UserControl
properties. Here is a sample:
<jp:DynamicUserControl runat="server" ID="ducTest"
UserControlPath="~/UserControl/Test.ascx"
EnablePostBackRegistration="true" >
<UserControlProperties>
<jp:UserControlProperty Name="TestString" Value="Toto"/>
<jp:UserControlProperty Name="TestInt" Value="123"/>
<jp:UserControlProperty Name="TestDouble" Value="45.26"/>
</UserControlProperties>
</jp:DynamicUserControl>
(Assuming TestString
, TestInt
and TestDouble
are public properties of the embedded UserControl
)
Limitations
I have tested it quite a lot, however I'm sure it isn't bug-free. Apart from the bugs you may find, here are some limitations you should be aware of:
- Event Validation is disabled for all controls inside the
DynamicUserControl
and MUST be disabled in some cases at the page level (i.e. by setting EnableEventValidation="false"
in the Page
directive) - You can't set properties on the user control in the declarative part of your pages
- The
Page
can depend on the user control embedded inside the DUC, however the user control CANNOT depend on the page it lives in! - The side effect of using an iframe is that it creates an entry in the browser history (only when using the cross-domain feature)
- It was only tested under Firefox 2, IE6 SP2 and IE7
- Validators Hack does not work with the new Framework 3.5 Beta 2
History
- 2007 October 8th
- 2007 October 11th
- Fixed bug with Framework 3.5 Beta 2 (2.0.50727.1378)
- 2007 October 30th
- Added support for dynamically added stylesheet file (see this thread)
- Minor correction of
_scriptExecutor
(DynamicUserControl.js) - Added support for ASP.NET theme
- 2007 October 31
- Fixed bug with Kevin's help (see this thread)
- Corrected some bugs with Validators (especially with IE)
- Added support for any ScriptMode in the ScriptManager
- 2007 November 1st
- Corrected a bug on control state not loaded for dynamically created controls
- 2007 November 14
- Fixed bug when using
Response.Redirect
- Added support for Multiline textboxes
- Added support for user control properties (Thanks to Kevin)