Damian Mehers' Blog Xamarin from Geneva, Switzerland.

27Nov/066

Tip/Trick: Using ScriptManager ScriptPath to load MicrosoftAjax.js from file system

This isn't particularly tricky but it can be non-obvious.

If you don't want to use the embedded resources to serve the Microsoft AJAX JavaScript runtime, but instead you want to have them served from the file system, to make JavaScript debugging more pleasant, then this is what you need to do:

Create a directory structure under your web application's root directory

You'll need to create a directory structure that corresponds to the attributes of the assembly that normally contains the script files.  In your case you'll can create a subdirectory structure in your web site like this:

Scripts\Microsoft.Web.Extensions\1.0.61025.0

The top directory doesn't have to be called Scripts you can call it anything you wish.  Since MicrosoftAjax.js is in the Microsoft.Web.Extensions assembly, this is why the second directory level is called Microsoft.Web.Extensions.  The final directory is the version number of the DLL.  You can see this from your web.config:

<compilation debug="false">
  <assemblies>
     <add assembly="Microsoft.Web.Extensions, Version=1.0.61025.0, ..."/>
  </assemblies>
</compilation>

Copy the ASP.NET AJAX JavaScript runtime files

When you installed ASP.NET AJAX you'll have had the ASP.NET AJAX JavaScript source files installed, by default into:

C:\Program Files\Microsoft ASP.NET\ASP.NET 2.0 AJAX Extensions\v1.0.61025\ScriptLibrary\Debug

and

C:\Program Files\Microsoft ASP.NET\ASP.NET 2.0 AJAX Extensions\v1.0.61025\ScriptLibrary\Release

Copy MicrosoftAjax.js from the Debug directory into your Scripts\Microsoft.Web.Extensions\1.0.61025.0 directory, and rename it to Microsoft.Web.Resources.ScriptLibrary.MicrosoftAjax.debug.js.  Then do the same for MicrosoftAjaxTimer.js and MicrosoftAjaxWebForms.js following the same pattern of adding "Microsoft.Web.Resources.ScriptLibrary." to the start of the filename, and then adding ".debug" in front of the ".js".

Also copy the same files from the Release directory but this time rename them without adding the ".debug" in front of the ".js".

Now the source files will be picked up whether or not you are running in debug mode.

Set the ScriptPath on your ScriptManager

Finally, set the ScriptPath property on ASP.NET ScriptManager to tell it to source the ASP.NET AJAX runtime from your new directory:

<asp:ScriptManager ID="sm1" runat="server" ScriptPath="~/Scripts">
</asp:ScriptManager>

Verifying that the JavaScript files are being served from files

If you do a "View Source" in Internet Explorer you should see the JavaScript files being included from the appropriate location:

<script src="Scripts/Microsoft.Web.Extensions/1.0.61025.0/
Microsoft.Web.Resources.ScriptLibrary.MicrosoftAjax.js"
type="text/javascript"></script>
 

About Damian

Filed under: AJAX 6 Comments
23Nov/0611

Using the Active Scripting Debug API to log all JavaScript that IE executes

You can download the free TraceJS program here. Run it, select the Internet Explorer instance you are interested in, and you'll be able to see all JavaScript it executes.  If you get an error starting it up, it is likely you do not have Microsoft's Active Scripting Debug API installed.  You can download the Microsoft Script Debugger, which will install it, here

Sometimes I get the germ of an idea and I won't let go until I have implemented it. In this case I wanted to see every line of JavaScript code that was being executed inside Internet explorer. There are plenty of library routines that let you instrument your code, but I wanted something different.

I wanted to actually hook into Internet explorer somehow. A debugger API seemed like the obvious way to go. But which debugger the API? There are so many to choose from. In the end I discovered that the Active Scripting API was what I needed. Unfortunately it's been a long time since anyone has used that it API. Especially from .NET. There were a smattering of articles on the web but none did exactly what I wanted.

In order to use it from .NET I needed a type library in order to generate a COM Interop assembly. Eventually I found one in an old version of the Internet explorer SDK. Having generated to the Interop assembly I found I needed to make some corrections. So I generated an IL file corrected it and regenerated the assembly.

For a long time when I first used the API it did not seem to be working. I could not connect to Internet Explorer. The call to connect failed every time. I eventually discovered that it had actually worked the first time and that subsequent attempts to connect were failing because there was already a debugger attached. DOH!

From there on it was pretty plain sailing. As soon as I attached the debugger I requested a breakpoint. I received a call back when the breakpoint occurred and then logged all the information about the current statement including the filename, the function name, the current stack depth, the line number, and of course the current chunk of JavaScript that was being executed. This is the callback code that gets executed when a breakpoint occurs:

// Called on breakpoint, which is for each statement in the target app
public void onHandleBreakPoint(IRemoteDebugApplicationThread prpt,
                               tagBREAKREASON br,
                               IActiveScriptErrorDebug pError)
{
    try
    {
        // Properties of the current breakpoint which we will be fetching
        string fileName;
        string functionName;
        string code = "";

        // Various interfaces and structs used for active script debugging
        IEnumDebugStackFrames enumDebugStackFrames;
        tagDebugStackFrameDescriptor debugStackFrameDescriptor;
        IDebugCodeContext debugCodeContext;
        IDebugDocumentContext debugDocumentContext;
        IDebugDocument debugDocument;

        // Get the function name from the first stack frame
        prpt.EnumStackFrames(out enumDebugStackFrames);
        uint stackFrameCount;
        enumDebugStackFrames.RemoteNext(1, out debugStackFrameDescriptor,
                                out stackFrameCount);
        if (stackFrameCount != 1)
        {
            return;
        }

        debugStackFrameDescriptor.pdsf.GetDescriptionString(0,
                                                            out functionName);

        // Get the file name
        debugStackFrameDescriptor.pdsf.GetCodeContext(out debugCodeContext);
        debugCodeContext.GetDocumentContext(out debugDocumentContext);
        debugDocumentContext.GetDocument(out debugDocument);
        debugDocument.GetName(tagDOCUMENTNAMETYPE.DOCUMENTNAMETYPE_URL,
                              out fileName);

        // If we are not in the same file and function as previous
        // breakpoint, then find the stack depth
        if (fileName != lastFile || functionName != lastFunction)
        {
            stackDepth = 0;

            while (stackFrameCount == 1)
            {
                tagDebugStackFrameDescriptor tmpDebugStackFrameDescriptor;
                enumDebugStackFrames.RemoteNext(1,
                                                out tmpDebugStackFrameDescriptor,
                                                out stackFrameCount);
                stackDepth++;
            }
        }

        // The DebugDocumentText lets us get at the document text
        IDebugDocumentText ddt = debugDocument as IDebugDocumentText;
        if (ddt == null)
        {
            return;
        }

        // Find out the location of the chunk of code is currently
        // being stepped through
        uint charPosition = 0;
        uint nrChars = 0;
        ddt.GetPositionOfContext(debugDocumentContext,
                                 out charPosition,
                                 out nrChars);

        // Find out what line it is on
        uint lineNr = 0;
        uint offsetInLine;
        ddt.GetLineOfPosition(charPosition, out lineNr, out offsetInLine);

        // Get the chunk of code that is being stepped through.
        if (nrChars >= 0)
        {
            if(nrChars > codeBuffer.Length - 1) {
                nrChars = (uint)codeBuffer.Length - 1;
            }

            uint charsReturned = 0;
            ddt.GetText(charPosition, codeBufferPtr, IntPtr.Zero,
                        ref charsReturned,
                        nrChars);
            code = new String(codeBuffer, 0, (int)charsReturned);
        }

        // Tell any listeners about the new trace message
        TraceEventArgs traceEventArgs = new TraceEventArgs(fileName,
                                                           functionName,
                                                           stackDepth,
                                                           (int)lineNr,
                                                           code);
        OnTraceStep(traceEventArgs);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
    finally
    {
        try
        {
            // Resume the process, indicating we want to break at the next available point
            debuggableApplication.RemoteDebugApplication.ResumeFromBreakPoint(prpt,
                  tagBREAKRESUME_ACTION.BREAKRESUMEACTION_STEP_INTO,
                  tagERRORRESUMEACTION.ERRORRESUMEACTION_AbortCallAndReturnErrorToCaller);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error resuming: " + ex);
        }
    }
}

Of course this has an impact on performance, but it does let you see every line of code that has run. It can be quite illuminating to trace through the Microsoft ASP.NET AJAX runtime and see exactly how much work it does on your behalf. You can also use it to see if your JavaScript code is doing what you think it is doing, without having to step through it in the debugger.

For example this is the trace from the first update panel example in the ASP.NET AJAX documentation.

I put a simple graphical interface on top of the program:

TraceJS ScreenShot

You can download the program here, and the complete source is available here.

I can imagine that this could be extended to include filtering, displaying parameters, code coverage and possibly performance metrics but as it stands I think it's already quite useful to see every line of code that has run without having to manually step through the debugger. I'm happy to contribute it to CodePlex if anyone wants to take it further.

About the author.

Filed under: AJAX 11 Comments
17Nov/064

ScreenCast: How to create and use an ASP.NET AJAX Control Extender

In this 13 minute video I walk through creating and deploying an example ASP.NET AJAX Control Extender.

The example extender automatically disables buttons when they are clicked, to stop people clicking them twice (for example on a purchase form).  Implementing this isn't as obvious as you might think, and involves cloning.

Screenshot from the video

I do absolutely everything from scratch including writing the server-side and browser-side code, and creating a web site to test the extender.

The video can be viewed/downloaded here.

Please make sure you have sound playing since I talk through a lot of what I am doing.

Filed under: AJAX 4 Comments
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 12 Comments
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 6 Comments