Damian Mehers' Blog Android, VR and Wearables from Geneva, Switzerland.

16Nov/0612

Using Control Adapters to automatically attach AJAX Extenders to ASP.NET Controls

They have confusingly similar names, but how can ASP.NET AJAX Control Extenders and ASP.NET Control Adapters be usefully combined?

Imagine that you have discovered  a new ASP.NET AJAX Control Extender that adds a "must have" behaviour to a standard ASP.NET control, for example a ListSearch Extender that lets users search incrementally within ASP.NET ListBox controls.  You are under strict instructions to apply the extender to all instances of the ListBox control throughout your large existing web site that was created long before ASP.AJAX lept onto the scene.  What can you do?

One option would be to spend a few days going through the whole site adding the AJAX extender to your pages. 

A second option might be invest five minutes reading this article.  You'll see how you can use Control Adapters to automatically add the AJAX behavior to all instances of the ASP.NETcontrol, without modifying a single page.

First though, a quick introduction to Control Adapters and Control Extenders.

What are ASP.NET Control Adapters?

ASP.NET Control adapters "shadow" a target ASP.NET control type and let you intercept the target's event lifecycle, substituting your own code for the the standard control implementation.  They are configured declaratively in a ".browser" file, with no coding required to deploy them.

The poor target control doesn't even know that it's event lifecycle has been hijacked and possibly replaced.  Which is good -- you can enhance a standard ASP.NET control such as a ListBox silently just by adding the control adapter in a browser file.  A typical use for control adapter is to render the target control using different HTML than it uses by default, such as using CSS instead of tables.

What are ASP.NET AJAX Control Extenders?

ASP.NET AJAX Control Extenders are something completely different.  A Control Extender typically adds a specific kind of JavaScript behaviour to specific kinds of controls.  The cool thing about extenders is that they can be used and configured on the server side, using the standard ASP.NET designer. 

Combining Adapters and Extenders could lead to a world of pain

A Control Adapter could totally confuse a Control Extender, by rendering HTML that is not at all what the Control Extender expected. 

For example in my ListSearch Extender I assume that the ListBox and DropDown controls are rendered using the HTML SELECT tag.  If they are rendered as something else by a Control Adapter that targets them, then my Extender will break. 

Control Adapters can pull the rug from the feet of Control Extenders by generating different HTML to that which the Extender expects ... the only thing to do here is to code definsively and check that the generated HTML DOM object is as expected.

Control Adapters can automatically add Control Extenders to specific controls

A more productive way of combining the use of Control Adapters and Control Extenders might be to intercept the lifecycle of ASP.NET Controls to automatically add the Extender to the page when the control is used.

This example is based off of the ListSearch extender I introduced previously. In fact the example involves modifying the project created in that article.

There are two steps:

  • Creating and deploying a ControlAdapter to add the Extender
  • Ensuring that all pages have an AJAX ScriptManager by creating and deploying a custom HttpModule

Creating and deploying the ControlAdaper

Creating and deploying a control adapter is simplicity itself.  You create the adapter by deriving from the System.Web.UI.WebControls.Adapters.WebControlAdapter class, overriding the methods that you are interested in.  Then you tell the ASP.NET runtime about your adapter by creating a browser definition file in a special App_Browsers application subdirectory.

I added a new class to my existing ListSearch project.  I called the class ListBoxAdapter.  Because I wanted to add the ListSearchExtender I overrode the CreateChildControls method to add the ListSearchExtender as a child of the target ListBox.

This is the ListBoxAdapter code:

namespace ListSearch
{
    public class ListBoxAdapter : System.Web.UI.WebControls.Adapters.WebControlAdapter
    {
        protected override void CreateChildControls()
        {
            ListSearchExtender listSearchExtender = new ListSearchExtender();
            listSearchExtender.TargetControlID = this.Control.ID;
            listSearchExtender.PromptText = "Click to search ...";
            listSearchExtender.PromptCssClass = "listSearch";
            this.Control.Controls.Add(listSearchExtender);
            base.CreateChildControls();
        }
    }
}

In order to ensure that my new ControlAdapter was picked up on the web site, I right clicked on the web site using the Solution Explorer, selected "Add New Item" and then chose "Browser File" from the list of templates.  I said OK when asked if I wanted to create the special App_Browsers subdirectory.

I used these contents for the browser file:

<browsers>
    <browser refID="Default">
	<controlAdapters>
		<adapter controlType ="System.Web.UI.WebControls.ListBox"
		         adapterType="ListSearch.ListBoxAdapter" />
	</controlAdapters>
    </browser>
</browsers>

This file basically says that I want to use my new ListBoxAdapter with the standard ListBox class, and I want to use it for all browsers.  There is a great deal more to browser files than this -- for example you could apply the adapter to specific browser types that you know it will work with.

Having built the control adapter and set up the browser file, all instances of the ListBox will now have an associated ListSearchExtender so that they can be incrementally searched.  There is however a problem.  Before any ASP.NET AJAX Control Extenders can be used, there must be one (and only one) ScriptManager instance on the page. 

Using an HttpModule to ensure all pages have an AJAX ScriptManager

An HttpModule is an instance of a class that sits outside of the standard ASP.NET page framework.  It operates at a lower level, and can interact with the processing of an Http request by the IIS web server.  To create the module I added a new Class called ScriptManagerAddModule to the ListSearch project, with these contents:

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using Microsoft.Web.UI;

namespace ListSearch
{
    public class ScriptManagerAddModule : IHttpModule
    {
        public void IHttpModule.Dispose() { }

        // This is where you can indicate what events in the request processing lifecycle you want to intercept
        public void IHttpModule.Init(System.Web.HttpApplication context)
        {
           context.PreRequestHandlerExecute += new EventHandler(HttpApplication_PreRequestHandlerExecute);
        }

        void HttpApplication_PreRequestHandlerExecute(object sender, EventArgs e)
        {
            HttpApplication httpApplication = sender as HttpApplication;

            if (httpApplication != null)
            {
                Page page = httpApplication.Context.CurrentHandler as Page;
                if (page != null)
                {
                    // When standard ASP.NET Pages are being used to handle the request then intercept the
                    // PreInit phase of the page's lifecycle since this is where we should dynamically create controls
                    page.PreInit += new EventHandler(Page_PreInit);
                }
            }
        }

        void Page_PreInit(object sender, EventArgs e)
        {
            Page page = sender as Page;
            if (page != null)
            {
                // ScriptManagers must be in forms -- look for forms
                foreach (Control control in page.Controls)
                {
                    HtmlForm htmlForm = control as HtmlForm;
                    if (htmlForm != null)
                    {
                        // Look for an existing ScriptManager or a ScriptManagerProxy
                        bool foundScriptManager = false;
                        foreach (Control htmlFormChild in htmlForm.Controls)
                        {
                            if (htmlFormChild is ScriptManager || htmlFormChild is ScriptManagerProxy)
                            {
                                foundScriptManager = true;
                                break;
                            }
                        }

                        // If we didn't find a script manager or a script manager proxy then add one
                        if (!foundScriptManager)
                        {
                            htmlForm.Controls.Add(new ScriptManager());
                        }
                    }
                }
            }
        }
    }
}

As you can see from the comments, the module intercepts the PreRequest phase of the processing of Http requests. 

In the PreRequest event handler it checks if the Http Handler that is processing the request is the standard System.Web.UI.Page class (which implements IHttpHandler) .  If it is then it hooks into the PreInit phase of the Page's event lifcycle. 

In the PreInit event handler it checks if the page's form already has a ScriptManager or ScriptManagerProxy (new to the second beta of ASP.NET AJAX) and if not, it adds a ScriptManager.  This way a page can add its own ScriptManager with its own specific properties (for example to enable partial rendering), otherwise it could let the HttpModule add a minimalistic ScriptManager.

In order to deploy the new ScriptManagerAddModule the application's web.config must be modified to add the new module to the ScriptModules section:

<httpModules>
	<add name="ScriptManagerAddModule" type="ListSearch.ScriptManagerAddModule"/>
</httpModules>

Summary

In this article you have seen how you can deploy an AJAX Control Extender to an existing web site to modify the behaviour of specific kinds of ASP.NET Controls, without modifying a single page.

Do this by first creating and deploying an ASP.NET Control Adapter which "shadows" instances of the target control type, and adds an instance of the ASP.NET AJAX Control Extender to the page for each instance of the target control type.

Secondly create and deploy an HttpModule which intercepts the PreInit phase of each page, and adds an AJAX ScriptManager to the page if it doesn't already have one.

Now your users can enjoy site-wide enhancements without you having to change a line of code (or aspx), and if you want to remove functionality for any reason you can simply delete the ".browser" file that hooks up the Control Adapter.

About the author

I am an independent consultant based out of Geneva, Switzerland currently specializing in ASP.NET/C#/SQL Server.

In my spare time I recently created the PromptSQL SQL Intellisense tool, acquired in April 2006 by Red-Gate Software, and released as SQL Prompt 2.0.

I also created the J-Integra Java-COM bridge, which involved writing an entire DCOM/DCE-RPC protocol stack over TCP/IP in pure Java.  I sold J-Integra to Intrinsyc Software in Jan 2001 and served as their Chief Software Architect until Jan 2004.  J-Integra’s many customers include BEA Software - they bundle J-Integra with their WebLogic application server.

I can deliver ad-hoc and longer term consulting, and am keen to speak at conferences and seminars in English or French - please contact me for a list of previous speaking engagements.

Contact me by email by adding damian in front of atadore.com, with an @ in between.

Filed under: AJAX Leave a comment
Comments (12) Trackbacks (0)
  1. Wouldn’t it be great to only inject the ScriptManager into the page if some control/extender of the page needs one?

  2. Indeed it would — it would be pretty easy to walk the control tree and see if there are any ASP.NET AJAX Controls … I’ll look at updating it.

    Thanks for the suggestion.

    Regards,
    Damian

  3. I created project step by step according to your article. However, when I ran the website I got the error like:

    Server could not create ListSearch.ListBoxAdapter.

    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: System.Exception: Server could not create ListSearch.ListBoxAdapter.

    Source Error:

    An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

    Stack Trace:

    [Exception: Server could not create ListSearch.ListBoxAdapter.]
    System.Web.Configuration.HttpCapabilitiesBase.GetAdapterFactory(Type adapterType) +442
    System.Web.Configuration.HttpCapabilitiesBase.GetAdapter(Control control) +629
    System.Web.UI.Control.ResolveAdapter() +136
    System.Web.UI.Control.InitRecursive(Control namingContainer) +50
    System.Web.UI.Control.InitRecursive(Control namingContainer) +271
    System.Web.UI.Control.InitRecursive(Control namingContainer) +271
    System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1744

    How can I fix it? Thanks.

  4. BTW, I changed ASP.NET AJAX 1.0 Beta to ASP.NET AJAX 1.0 RC.

  5. Hello Jason,

    Can you ZIP up your project and email it to me and I’ll take a look. I’ve sent you a separate email with my email address.

    Thanks,
    Damian

  6. Thank you, Damian. I didn’t get your email yet though. You could send me email to gmail box if you like. Thanks agian.

    Merry Christmas!

    Jason

  7. Hey, to begin with nice article!

    I’m creating a new control á la Container Panel where I want to use the CollapsiblePanel extender. But I’m having trouble including it since I get this error: “Extender controls may not be registered before PreRender”.

    If you could evolve on how to include an extender in a custom control it would be deeply appreciated.

    Best regards

    Patrik Nordberg

  8. I’ve finally loaded a demo project which you can find here: http://damianblog.com/2007/02/28/adapters-extenders-demo/

  9. Wouldn’t it be possible to inject the Script Manager at the CreateChildControls level of your adapter.

    ala

    if(ScriptManager.GetCurrent(this.Control.Page) == null)
    {
    this.Control.Page.Form.Control.Add(new ScriptManager())
    }

  10. This is a very good solution, but it doesn’t work with DropDownLists because with a DropDownList, you aren’t allowed to add any controls to the Controls collection. There is a way around that, however, but that requires that you override the CreateControlCollection of the DropDownList. This would mean creating a custom control to do this, and using the Adapter with the custom control. Because of this, you would have to go through all your existing code to update instances of DropDownList with your custom DropDownList. Is there an easier way?

  11. Is there a way to disable the loading of the control adapters? I found that I could prevent ‘adaptation’ for your custom control by returning null from Control.ResolveAdapter in the ASP.Net Quickstart Toolkit, but I cant figure out how to do it.

  12. @Jason

    Hi!

    The “Server could not create *Adapter” error results from your class being inaccessible – ensure that you have the “public” keyword before the class declaration and have specified the correct namespace and class name.

    -Nitin


Leave a comment

No trackbacks yet.