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

13Nov/066

Convert your scraps of script to reusable AJAX Control Extenders

If you have been developing web applications over the last few years, then chances are you’ll have put together a whole set of JavaScript code to enhance your users' experience when using your sites, adding behavior that HTML cannot provide.

In this article you’ll see how Microsoft’s AJAX Control Toolkit gives you a fantastic opportunity for you to repackage that JavaScript code into reusable components that can be used within the server-side ASP.NET framework, so that you can easily share your JavaScript behavior with other developers without them having to get into the guts of your JavaScript code and script libraries.

If you want to run through the steps I describe here, you’ll need to download and install the ASP.NET AJAX kit, and the AJAX Control Toolkit.   There is a download link at the end of the article, from which you can download the complete code.

Before diving into the details of the AJAX Control Toolkit, I’d like to first share with you a situation will probably strike a chord.

A couple of years ago I had a user of our web site that complained that the standard behavior of the ASP.NET ListBox control, which by default renders as an HTML SELECT, was broken.

As far as she was concerned when she typed a sequence of characters there should be an incremental search within the contents of the ListBox, rather than the default behavior which simply jumps to the first element that starts with each character. If you type “mi” in a ListBox then you’ll end up at “IBM” rather than “Microsoft”:

 

 So I put together some JavaScript code which added the behavior she was looking for:

  

I added a couple of event handlers to detect when the user gives the SELECT focus, and to detect when they typed characters within the SELECT. When the user gives the SELECT the focus I create a DIV with a prompt that tells them that they can search, and then when they type characters I replace the prompt message with the characters that were typed, and I select the first item that starts with the characters that were typed.

This had nothing to do with ASP.NET AJAX, or the Atlas Control Toolkit – it was just plain old JavaScript.

There are a few issues with this implementation:

  • Firstly, whenever any I or any other developers want to use this, they’ll have to hook up the JavaScript event-handlers, which can be painful when dealing with server-side controls such as the ASP.NET ListBox, which generates the SELECT.
  • Secondly, if you wanted to customize the behavior in some way, perhaps to use a different prompt message, or to apply a CSS style to the DIV, then you’d have to edit the JavaScript. Yuck.
  • Finally, if I wanted to ensure that this behavior worked properly across all browsers then I’d have to do a lot of work – for example different browsers have subtly different ways of hooking up event handlers.

Microsoft’s ASP.NET AJAX and the Atlas Control Toolkit address all of these issues.

Of course you probably won’t have implemented exactly this behavior, but chances are you’ll have written other scraps of JavaScript to do other things to make your users lives easier. I’ll show you how easy it is to repackage code into an AJAX Control Extender.
 

Creating an AJAX Control Extender

Once you’ve installed the AJAX Control Toolkit, you’ll need to double-click on the AjaxControlExtender.vsi file in the AjaxControlExtender subdirectory under the kit. Doing this will install a Visual Studio Template which you can use to create AJAX Control Extenders.

If you then fire up Visual Studio 2005, and hit File|New|Project you will be presented with the option of creating a new ASP.NET AJAX Control Project. Select this option and give your project a suitable name – I’ve used ListSearch. Once you hit OK you should find yourself with a project with three files:

  • The first file, ListSearchBehavior.js, is where JavaScript code that actually implements your behavior will sit.
  • The second file, ListSearchDesigner.cs, is used to ensure that the extender works well within the Visual Studio design-time environment. You can safely ignore this file.
  • The third and final file, ListSearchExtender.cs, is the server-side of the equation. This is where you can specify what kind of ASP.NET control your extender will target, and what properties your extender will expose.

The Server Side

If we start first on the server side in ListSearchExtender.cs, then there were two things wanted to do. I wanted to indicate what kind of ASP.NET control my extender would target, and I wanted to indicate the properties my extender exposes.

In my case I wanted my ListSearch extender to target both ASP.NET ListBoxes and also ASP.NET DropDowns. Both of these classes derive from the ListControl class. I modified the generated class to specify the appropriate target control instead of the default, which is the top level Control class:

[Designer(typeof(ListSearchDesigner))]
[ClientScriptResource("ListSearch.ListSearchBehavior",
                      "ListSearch.ListSearchBehavior.js")]
[TargetControlType(typeof(ListControl))]
public class ListSearchExtender : ExtenderControlBase
{

I also wanted to indicate what properties I wanted to be available on my Extender. A couple of useful properties might be the text of the prompt message, and the CSS class to be used for the prompt message. These properties will be available on the server-side on the ASP.NET design surface. There is an example property created by default. I deleted it and replaced it with the two properties I wanted to use. The first was called PromptText, and looks like this:

[Description("The prompt text displayed when user clicks the list")]
[DefaultValue("Type to search")]
[ExtenderControlProperty()]
public string PromptText
{
   get
   {
       return GetPropertyStringValue("PromptText");
   }
   set
   {
       SetPropertyStringValue("PromptText", value);
   }
}

I used the standard ComponentModel Description attribute so that the property has a description when people use it in the designer, and I marked it with the ExtenderControlProperty attribute so that the AJAX Control Toolkit knows to transfer it down to my JavaScript code.

I also indicated the default value. Please note that this is not the default value that the property will get automatically set by the AJAX Control Toolkit if the user does not specify a value in the ASP.NET designer. You’ll still need to initialize this property to the default value in the JavaScript code. What the DefaultValue attribute does, is tell the AJAX Control Toolkit runtime what the default value you have given the property is on the browser side, so that it knows when it doesn’t need to pass the property value down to the browser - in other words, an optimization.

The implementation of the properties calls into the AJAX Control Runtime, since it knows how to pass the property values over to the browser.

I also created a second property called PromptCssClass, along the same lines:

[Description("CSS class applied to prompt when user clicks list")]
[DefaultValue("")]
[ExtenderControlProperty()]
public string PromptCssClass
{
    get
    {
        return GetPropertyStringValue("PromptCssClass");
    }
    set
    {
        SetPropertyStringValue("PromptCssClass", value);
    }
}

That is all that I needed to do on the server side.

The Browser Side

In order to implement my browser-side functionality I edited the ListSearchBehavior.js file.

The first thing I did here is to set up the JavaScript side to handle the properties I just declared on the server side. The default boilerplate generated JavaScript has a handy TODO indicating where the properties have to be inserted. I replaced the default _myPropertyValue initialization with my two properties:

    this._promptCssClass = null;
    this._promptText = 'Type to search';

Further down there is a second TODO where the accessors for my properties can be defined. Again, I replaced the generated example accessor with my own:

    // Property Accessors
    get_PromptText : function() {
        return this._promptText;
    },

    set_PromptText : function(value) {
        if (this._promptText != value) {
            this._promptText = value;
            this.raisePropertyChanged('PromptText');
        }
    },

    get_PromptCssClass : function() {
        return this._promptCssClass;
    },

    set_PromptCssClass : function(value) {
        if (this._promptCssClass != value) {
            this._promptCssClass = value;
            this.raisePropertyChanged('PromptCssClass');
        }
    }

These will be called by the framework when the behavior is first initialized, with the values specified in the designer on the server side..

Now came the fun part – actually implementing my behavior.

The initialize method is called by the framework to let my behavior initialize itself. The default implementation simply calls into the base class. In my case I wanted to also hook up event handlers on the generated HTML SELECT so that I know when the user clicks on the control, etc:

initialize : function() {
   ListSearch.ListSearchBehavior.callBaseMethod(this, 'initialize');
   $addHandlers(this.get_element(),
                 { 'focus' : this._onFocus, 'blur' : this._onBlur,
                   'keydown' : this._onKeyDown,
                   'keypress' : this._onKeyPress },
                 this);
},

As of Beta 2 of the ASP.NET AJAX it is much easier to wire up event handlers.  In prior versions a separate call to $addHandler had to be made to add each event, and each handler method had to be wrapped in its own delegate.  The get_element call gets a reference to the HTML SELECT object, to which the event handlers are being hooked up.  The second parameter indicates the names of the events, and the corresponding event handler methods.  The final parameter indicates which object should be the this when the event handlers are called -- if you leave it out then the SELECT DOM object itself will be the this.

Note that I’m calling a utility function called $addHandlers provided by ASP.NET AJAX to handle the adding of the event handlers. This means that ASP.NET AJAX has to deal with all the nastiness associated with different browser implementations of event hookup.

What goes up, must come down, and since I’ve set up all these event handlers in the initializer then it stands to reason that I should tear them down when the behavior unloads. Indeed this is what I do in the dispose method:

dispose : function() {
    $clearHandlers(this.get_element());

    ListSearch.ListSearchBehavior.callBaseMethod(this, 'dispose');
},

Once again, the second ASP.NET AJAX beta has made life a lot easier.  Whereas previously we had to call $removeHandler for each event handler, there is now one handy call that releases the lot.

What is left is the actual event handlers that do the work.

The _onFocus and _onBlur methods create and delete the DIV with the prompt text and the text typed so far:

_onFocus : function(e) {
    // Create a DIV that shows the text typed so far
    var element = this.get_element();
    this._promptDiv = document.createElement('div');
    this._promptDiv.innerText = this._promptText;
    this._promptDiv.showingPromptText = true;
    if(this._promptCssClass) {
        this._promptDiv.className = this._promptCssClass;
    }
    this._promptDiv.style.left = element.offsetLeft +
                                 element.offsetWidth + 10;
    this._promptDiv.style.top = element.offsetTop;
    document.body.appendChild(this._promptDiv, element);
},

_onBlur : function(e) {
    // Remove the DIV showing the text typed so far
    document.body.removeChild(this._promptDiv);
    this._promptDiv = null;
},

This took care of creating and hiding the DIV, but I also needed to handle keystroke related events.  The _onKeyDown is called before the _onKeyPress event, which I also handle. The reason we handle the _onKeyDown is to remove the prompt message, if I am showing it, and also to handle the user pressing the backspace key, since this isn’t passed to the keyPress event.  Note that there are now symbolic names for the keys such as backspace so that you don't have to hardcode the corresponding constant (8).

_onKeyDown : function(e) {
    var element = this.get_element();
    var promptDiv = this._promptDiv;
    if(promptDiv.showingPromptText) {
        promptDiv.innerText = "";
        promptDiv.showingPromptText = false;
    }

    // Backspace not passed to keyPressed event, so handle it here
    if(promptDiv.innerText.length > 0 &&
       event.keyCode == Sys.UI.Key.backspace) {
        promptDiv.innerText = promptDiv.innerText.substring(0,
                                   promptDiv.innerText.length - 1);
        this._searchForTypedText(element, promptDiv.innerText);
    }
},

The _onKeyPress simply adds the character that was typed to the promptDiv, and searches the target SELECT:

_onKeyPress : function(e) {
    // Add key pressed to the displayed DIV and search for it    
    this._promptDiv.innerText += String.fromCharCode(e.charCode);
    this._searchForTypedText(this.get_element(),
                             this._promptDiv.innerText);
    e.preventDefault();
    e.stopPropagation();
},

By calling the preventDefault and stopPropagation methods on the event object we let ASP.NET AJAX handle the subtle differences in browser handling of the cancellation of event propagation so that the default behavior of going to the entry starting with the last character that was typed is not invoked.

Finally the _searchForTypedText method is a simple (but very inefficient!) search.  Note that by wrapping the option text in the ASP.NET AJAX String type I could call its handy startsWith method.

_searchForTypedText  : function(element, searchText) {
    var options = element.options;
    var text = searchText.toLowerCase();

    for(var index = 0; index < options.length; index++) {
       var optionText = new String(options[index].text.toLowerCase());
       if(optionText.startsWith(text)) {
            options[index].selected = true;
            return;
       }
    }
},

I then built my new ListSearch extender project.

Using the new extender

To use my new AJAX Control Extender, I added a new “ASP.NET AJAX Enabled Project” to my solution, which I called “ListSearchWebSite”. I then went to the design view of the Default.aspx page that was created, and looked at the toolbox. In my case there was a tab created automatically called ListSearchExtender, but it was empty. I had to right-click on the tab, select “Choose Items …” and the click “Browse…” and browse to the ListSearch.dll that I built. Once I did this my ListSearchExtender appeared in the tab.

I dragged a ListBox Control onto the Default.aspx and went to its properties to add a few items to the ListBox under the Items property.  I added "Compaq", "Digital Equipment Corp", "HP", "IBM", "Maplin", "Microsoft" and "Motorola".

I then dragged the ListSearchExtender over to the Default.aspx page and set its TargetControlID property to the ListBox's ID:

Once I'd done this I could go to the properties of the ListBox itself and set the properties of the newly associated ListBoxExtender:

To make the prompt message display a little better, I added a StyleSheet.css item to the web project and set that as the StyleSheet property of the Default.aspx document.  These are the simple contents of the style sheet -- it just defines the listSearch CSS class used for the PromptCssClass property.

.listSearch {
	position:absolute;
	background-color:Orange;
	font-family:Lucida Console;
	font-weight:bold;
}

Now I had my behavior packaged up in a simple reusable form that can easily be configured within ASP.NET, without other developers having to know anything about the JavaScript implementation under the hood.

Summary

The complete ASP.NET Solution I've shown above can be downloaded here.

You've seen how straight-forward it is to create AJAX Control Extenders and package up JavaScript behavior so that it can be reused by developers without their having to get to grips with JavaScript.

There is however one additional step you can take.  You can contribute your Control Extenders to the AJAX Control Toolkit so that they become part of the standard kit.  There is a complete test framework and more.  Please leave a comment if you'd like me to cover this additional step (assuming they accept this control extender) in a new article. 

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 (6) Trackbacks (3)
  1. Finally, an article about the MS Ajax and the toolkit that I can learn something from.

    Thanks you, colin

  2. I was just looking for keyup/keydown tips/tricks using MS ajax 1.0, good thing i found your blog first, really saved me bunch of time. Nice work.

  3. Ich besichtige deinen Aufstellungsort wieder bald fur sicheres!

  4. How to position the Type Search next to the list item instead of bottom or top

    Please let me know. Is it in the stylesheet.

    Thanks
    Sriman

  5. Hello,

    I’m doing some stuff with the Ajax server control project. I have to know which browser I’m dealing with.

    Let’s say I have a key press event and I need to know which character I have. However in IE I can get window.event.keyCode but for others I have to get the event itself to get the key code. In normal HTML files we pass parameter to event but when I tried to do it in this project I couldn’t achieve it.

    Do you have any suggestion?

    Thanks,
    Gokce


Leave a comment