DotNetSlackers: ASP.NET News for lazy Developers

Saturday, January 8, 2011

Dynamic User Control, Ajaxify Your Controls


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 theUpdatePanel 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 aUserControl 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:
Dynamic user control overview
  • 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 theUserControl 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 DynamicUserControlto 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 POSTrequest 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. (seeDynamicUserControl._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:
 Collapse
<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:
 Collapse
<appSettings>
    <add key="DUCDomain" value="mydomain.com"/>
</appSettings>
Then, if you use a UserControl this way in your page:
 Collapse
<%@ Register TagPrefix="uc" TagName="Test" Src="~/UserControl/Test.ascx" %>
  ...
<uc:Test runat="server" ID="ucTest" />
you can replace your code with this:
 Collapse
<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:
 Collapse
<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 control
  • ProgressTemplate: 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:
     Collapse
    <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 TestStringTestInt 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

No comments:

Post a Comment