HOWTO: Write controls compatible with UpdatePanel without linking to the ASP.NET AJAX DLL

This is another repost from a post I made to the ASP.NET Forums:

Buying Into Microsoft ASP.NET AJAX without Necessarily Paying For It

By Eilon Lipton, Software Design Engineer on Microsoft ASP.NET AJAX

Abstract

Without the release of Microsoft ASP.NET AJAX just around the corner, taking advantage of the new AJAX features is all the buzz. However, what if some of your customers are not yet upgrading to ASP.NET AJAX? This article discusses a technique to allow your ASP.NET server control to function without ASP.NET AJAX installed, while taking advantages of new features such as the UpdatePanel control’s asynchronous postbacks when ASP.NET AJAX is available.

Introduction

The problem being solved here is how to access ASP.NET AJAX functionality when you can’t link against the Microsoft.Web.Extensions.dll assembly installed in the GAC. The primary technique we will use is the .NET Framework’s Reflection feature, which allows us to dynamically call functions without taking a compile-time dependency on them.

The intent of the sample control is neither to show how to write an ASP.NET control, nor the best practices of doing such. The sample control is just to demonstrate patterns and practices for using Microsoft ASP.NET AJAX to write compatible controls.

The techniques demonstrated here are typically only required for controls that do not take advantage of the Microsoft AJAX client libraries and the server interfaces, such as IScriptControl. Controls that use those features typically have to link against the Microsoft.Web.Extensions.dll assembly anyway. Rather, these techniques are intended for controls that were built against ASP.NET 2.0 and wish to merely “play nicely” with ASP.NET AJAX.

The full source code is available here.

Why Write an ASP.NET AJAX-Aware Control?

The ASP.NET AJAX UpdatePanel control places restrictions on what the controls placed inside it are allowed to do. If the control is not intended to be placed inside an UpdatePanel control then are no restrictions on what it may do. The key restrictions for a control inside an UpdatePanel are:

  1. It must perform script registration through the ScriptManager’s registration APIs instead of through the Page.ClientScript APIs. There is a simple one-to-one mapping from the old APIs to the new APIs.
  2. If the control attaches event handlers it must implement dispose functionality. The “dispose” expando technique will be shown, although there are several other ways to do this.
  3. The scripts registered by the control need to be divided into two: 1. the script library code, which contains only function and class declarations, and is shared by all instances of the control; and 2. the initialization code for the control, which is unique to each control instance.

If your customers want to use your control inside an UpdatePanel, you will have to abide by these restrictions.

For more information on how UpdatePanels work, please see this blog post.

The Sample MathWidget Control

The sample control used for demonstration purposes is a simple ASP.NET composite control that performs math operations. Rather than posting back to the server to do the calculations, it includes a client script library that does the work in the browser.

Math Widget

If you’re been following so far, you’re probably thinking to yourself, “oh no, it’s registering client script – that will never work inside an UpdatePanel!” Here’s an outline of the control and the code it uses to perform its script registration:

namespace MathWizard {


public class MathWidget : CompositeControl {
protected override void CreateChildControls() {

}

protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);

// Register script library
Page.ClientScript.RegisterClientScriptResource(
typeof(MathWidget),
"MathWizard.MathWidget.js");

// Register initialization
Page.ClientScript.RegisterStartupScript(
typeof(MathWidget),
ClientID + "_KeyStuff",
"MathWidget_Initialize('" + ClientID + "');",
true);
}
}
}

  

 

Of the restrictions mentioned earlier only the third one is being followed: separation of script library and initialization code. Clearly this control will not work when placed inside an UpdatePanel since the async postback processing will be unaware of the script registrations performed during that postback. Only registrations performed through the ScriptManager APIs will work during an async postback.

Here’s the initial version of the client-side library for the MathWidget control:

 

 

function MathWidget_Add(widgetID) {
// Perform add button operation
var leftOperand = parseFloat(document.getElementById(widgetID + "_LeftOperandTextBox").value);
var rightOperand = parseFloat(document.getElementById(widgetID + "_RightOperandTextBox").value);
document.getElementById(widgetID + "_ResultTextBox").value = leftOperand + rightOperand;
}

function MathWidget_Multiply(widgetID) {
// Perform multiply button operation
var leftOperand = parseFloat(document.getElementById(widgetID + "_LeftOperandTextBox").value);
var rightOperand = parseFloat(document.getElementById(widgetID + "_RightOperandTextBox").value);
document.getElementById(widgetID + "_ResultTextBox").value = leftOperand * rightOperand;
}

function MathWidget_Initialize(widgetID) {
// Initialize a client instance of the MathWidget control
var leftOperand = document.getElementById(widgetID + "_LeftOperandTextBox");
var rightOperand = document.getElementById(widgetID + "_RightOperandTextBox");
leftOperand.attachEvent('onkeypress', MathWidget_KeyPressHandler);
rightOperand.attachEvent('onkeypress', MathWidget_KeyPressHandler);
}

function MathWidget_KeyPressHandler() {
// Allow only numeric keys to go through
if (window.event.keyCode < 48 ||
window.event.keyCode > 57) {
window.event.returnValue = false;
}
}

As you can tell, the MathWidget_Initialize() method hooks up event handlers to DOM elements. This will also break during an async post since we’ll be leaving some old event handlers attached to DOM events, as mentioned in the second restriction. Although this usually doesn’t produce disastrous results, it’s a great way to cause a memory leak. To avoid the memory leak we’ll need to implement dispose functionality.

Making the Control Register Its Scripts

The first step in getting the control to work with UpdatePanels is to write a compatibility layer that hides some of the implementation details. This is especially important since the compatibility layer will use the .NET Framework’s Reflection feature to do the method calls, and we’d rather hide that code from the control’s code.

The compatibility layer’s job is to detect whether ASP.NET AJAX is available. When it is available, it uses the new ScriptManager APIs to perform the script registration. When it isn’t available, it calls the ASP.NET 2.0 Page.ClientScript APIs. This fulfils the first restriction. Here’s an outline of the compatibility layer:

namespace MathWizard {


internal static class ScriptManagerHelper {
private static readonly object ReflectionLock = new object();
private static bool MethodsInitialized;
private static MethodInfo RegisterClientScriptResourceMethod;


private static void InitializeReflection() {
if (!MethodsInitialized) {
lock (ReflectionLock) {
if (!MethodsInitialized) {
Type scriptManagerType = Type.GetType("Microsoft.Web.UI.ScriptManager, Microsoft.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", false);
if (scriptManagerType != null) {
RegisterClientScriptResourceMethod = scriptManagerType.GetMethod("RegisterClientScriptResource");

}
MethodsInitialized = true;
}
}
}
}

public static void RegisterClientScriptResource(Control control, Type type, string resourceName) {

InitializeReflection();
if (RegisterClientScriptResourceMethod != null) {
// ASP.NET AJAX exists, so we use the ScriptManager
RegisterClientScriptResourceMethod.Invoke(null, new object[] { control, type, resourceName });
}
else {
// No ASP.NET AJAX, so we just call to the ASP.NET 2.0 method
control.Page.ClientScript.RegisterClientScriptResource(type, resourceName);
}
}

}
}

 

The basic technique is to try locating the ScriptManager type in the Microsoft.Web.Extensions.dll assembly. If it can’t be found, we know that ASP.NET AJAX is not available. If it was found then we locate specific methods of interest and hold on to them so that we can call them later.

We then change the code in our control to call the new methods:

 

        protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
// Register script library
ScriptManagerHelper.RegisterClientScriptResource(
this,
typeof(MathWidget),
"MathWizard.MathWidget.js");

// Register initialization
ScriptManagerHelper.RegisterStartupScript(
this,
typeof(MathWidget),
ClientID + "_KeyStuff",
"MathWidget_Initialize('" + ClientID + "');",
true);
}

 

That’s two restrictions down, and one to go.

Implementing Dispose

Fortunately, implementing dispose functionality is rather easy. Typically to implement dispose functionality I look at what my initialization functionality does and do it backwards. In the case of the MathWidget script library the initialize function attaches event handlers, so our dispose method must detach those handlers.

Here’s what the client script dispose function looks like:

function MathWidget_Dispose(widgetID) {
    // Initialize a client instance of the MathWidget control
    var leftOperand = document.getElementById(widgetID + "_LeftOperandTextBox");
    var rightOperand = document.getElementById(widgetID + "_RightOperandTextBox");
    leftOperand.detachEvent('onkeypress', MathWidget_KeyPressHandler);
    rightOperand.detachEvent('onkeypress', MathWidget_KeyPressHandler);
}

 

The next problem is how we get the ASP.NET AJAX async postback processing to call the dispose function when the UpdatePanel gets updated. As mentioned earlier, there are several techniques. This technique is the easiest to implement for existing controls, and it involves adding a “dispose” expando to one of our DOM elements, in this case the main <span> tag surrounding our control. When an async postback returns from the server and the UpdatePanels’ contents need to be updated, ASP.NET AJAX will search the contents of the UpdatePanel for DOM elements with a “dispose” expando. If it finds it and the expando is a function, it will call it. Here’s how we register the expando:

 

        protected override void OnPreRender(EventArgs e) {
            …
            // Register dispose if Microsoft ASP.NET AJAX is available
            if (ScriptManagerHelper.IsMicrosoftAjaxAvailable()) {
                ScriptManagerHelper.RegisterStartupScript(
                    this,
                    typeof(MathWidget),
                    ClientID + "_Dispose",
                    @"
document.getElementById('" + ClientID + @"').dispose = function() {{
    MathWidget_Dispose('" + ClientID + @"');
}}
",
                    true);
            }
        }

  

 

Note that we only bother registering the expando if ASP.NET AJAX is available. It doesn’t hurt to always register it, but there’s no point cluttering the page with script that won’t get used.

Summary

We fulfilled all three requirements to be fully compatible with UpdatePanels and we barely had to modify our control. You can also extend the compatibility layer to call other methods on ScriptManager, as necessary.

All the source code here is free for any purposes you have. I’d love to hear your feedback on it too!

4 Comments

  • That post is the "bible" for me.

  • Eilon, is there a way to easily detect whether a control is part of an UpdatePanel? Or at the very least to be able to tell whether you are in an UpdatePanel callback generically?

    Seems to me you wouldn't want to run this code otherwise to avoid the overhead of the Reflection calls.

    I guess we now pay for those sealed types in the framework huh? It'd be a heck of a lot easier to deal with this if ClientScriptManager could have been subclassed!

  • ooo! it's one of the best sites ever! :)

  • This looks incredibly useful. However, how do I achieve the functionality of IsClientScriptRegistered? I was thinking a static dictionary type of deal, but I'm not sure this is the best method.

Comments have been disabled for this content.