Galin Iliev

Software Architecture & Development

How to create ASP.NET AJAX Server Control Extender

Although ASP.NET AJAX has been on the out for a while it is always good to write about it. This is mainly because AJAX is in the root of new fancy web applications that adds very good experience to end-user.

Silverlight is live and version 2 is around the corner it seems AJAX cannot be completely skipped. So this is how you can create your own client-side control (widget). Note that js class we create can be used completely standalone (without xxxExtender.cs file and any compiled code) - see at the bottom of this post how can be done.

The things are described below are regarding:

  • create skeleton of extender - just basic because there is some good sample already
  • create properties
  • create events
  • create control and Initialize class
  • use js object from client-side

To illustrate we will extend Label control and we will create it clock. It will gets updated every second and when clicked will display message box with current time. Also there will be client-side event called OnTimeUpdated which will be fired every time clock is updated.

There are two ways to create control extender:

There are some differences in both ways mainly in connecting js class with .NET Extension class. In VS 2005 class attributes are used while in VS 2008 are used method override. From my experience there are some nasty confusions if you want to include more than one js file in  the attribute. So let's start with VS 2008

Create new project of type "ASP.NET AJAX server control extender"

You will find three items in solution tree: "ServerControl1.cs", "ClientControl1.resx", "ClientControl1.js". Main files are those with extension .js and .cs. Let's see what's in them:

ServerControl1.cs:

1: using System;
2: ...
3: using System.Xml.Linq;
4:  
5: namespace AjaxServerControl1
6: {
7:     /// <summary>
8:     /// Summary description for ServerControl1
9:     /// </summary>
10:     public class ServerControl1 : ScriptControl
11:     {
12:         public ServerControl1()
13:         {
14:             //
15:             // TODO: Add constructor logic here
16:             //
17:         }
18:         protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors()
19:         {
20:             ScriptControlDescriptor descriptor = new ScriptControlDescriptor("AjaxServerControl1.ClientControl1", this.ClientID);
21:             yield return descriptor;
22:         }
23:  
24:         // Generate the script reference
25:         protected override IEnumerable<ScriptReference> GetScriptReferences()
26:         {
27:             yield return new ScriptReference("AjaxServerControl1.ClientControl1.js", this.GetType().Assembly.FullName);
28:         }
29:     }
30: }

In the code above can be seen two methods beside constructor:

  • GetScriptReferences() - this method include embedded js file  into html page in script tags
  • GetScriptDescriptors() - this one is tricky. It actually ensures that $create() client side method uses correct class for correct ClientID. As we are going to extend another's control behavior we will change it to targetControl.ClientID.

Note: ExtenderControl class should be used as base class instead ScriptControl (ScriptControl is used for creating controls like ScripManager and UpdatePanel)

and ClientControl1.js(this one should be embedded resource)

1: /// <reference name="MicrosoftAjax.js"/>
2:  
3: Type.registerNamespace("AjaxServerControl1");
4:  
5: AjaxServerControl1.ClientControl1 = function(element) {
6:     AjaxServerControl1.ClientControl1.initializeBase(this, [element]);
7: }
8:  
9: AjaxServerControl1.ClientControl1.prototype = {
10:     initialize: function() {
11:         AjaxServerControl1.ClientControl1.callBaseMethod(this, 'initialize');
12:         
13:         // Add custom initialization here
14:     },
15:     dispose: function() {        
16:         //Add custom dispose actions here
17:         AjaxServerControl1.ClientControl1.callBaseMethod(this, 'dispose');
18:     }
19: }
20: AjaxServerControl1.ClientControl1.registerClass('AjaxServerControl1.ClientControl1', Sys.UI.Control);
21:  
22: if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

  Note that full class name is used (namespace + class name) when js class is created. We have some things to note here:

  • AjaxServerControl1.ClientControl1 = function(element) - this is class constructor with parameter DOM element
  • AjaxServerControl1.ClientControl1.prototype = class declaration with inherited initialize and dispose methods
  • AjaxServerControl1.ClientControl1.registerClass() - register class in the client namespace
  • and the last line notifies ScriptManager that the file has been loaded.

This is actually how js and extender control are connected.

As server-side class inherits ExtenderControl class we should change js class inheritance from Sys.UI.Control to Sys.UI.Behavior

Setup Extender properly

  1. Create class that inherits ExtenderControl  class
  2. Add class attribute [TargetControlType(typeof(Label))] that describe controls of which types will be extended. Only controls' ID of this type will be allowed to be put in TargetCotnrolID
  3. Override methods GetScriptReferences() and GetScriptDescriptors() to contains similar body:
    protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl){
        ScriptControlDescriptor descriptor = new ScriptControlDescriptor("AjaxControlExtenderLibrary.AjaxClock", targetControl.ClientID);
        return new ScriptDescriptor[] { descriptor };
    }
    
    // Generate the script reference
    protected override IEnumerable<ScriptReference> GetScriptReferences(){
        yield return new ScriptReference("AjaxControlExtenderLibrary.AjaxClock.js", this.GetType().Assembly.FullName);
    }
  4. Make sure in the GetScriptReferences()  you include full path to the js file - This is Assembly name + FolderName(if any) + file name.
  5. Ensure js file's Action property is set to Embedded Resource.
  6. Ensure proper namespace is registered in the line
    Type.registerNamespace("AjaxControlExtenderLibrary");
  7. Ensure proper js inheritance of Sys.UI.Behavior class in the line before last one
    AjaxControlExtenderLibrary.AjaxClock.registerClass('AjaxControlExtenderLibrary.AjaxClock', Sys.UI.Behavior);

Create properties

For creating property we need to do following:

  1. Create normal property in cs class like this one (automatic props in VS 2008)
    public string PrefixText { get; set; }
  2. Register property inside GetScriptDescriptors() method so it's value will be passed  in js initialization code like this:
    descriptor.AddProperty("prefixText", this.PrefixText);

    js initialization code is in near of end of code that is sent to the browser:
    $create(AjaxControlExtenderLibrary.AjaxClock, {"prefixText":"Hello at "}, null, null, $get("lblTime"));
  3. Create appropriate code in js file meaning:
  4. Add variable default value right after the call to initializeBase()
    AjaxControlExtenderLibrary.AjaxClock.initializeBase(this, [element]);
    this._prefixText = '';
  5. Add getter and setter methods (starting with 'get_' and 'set_' and ending with the first parameter passed to AddProperty method in point 3)
  6. get_prefixText: function(){
        return this._prefixText;
    },
    set_prefixText: function(value){
        this._prefixText = value;
    }

This is it. We are ready to test. You can see at the end of the page how to setup test page.

Create events

Creating events is similar to creating properties::

  1. Create normal property in cs class like this one (automatic props in VS 2008)
    public string OnClientClick { get; set; }
  2. Register property inside GetScriptDescriptors() method so it's value will be passed  in js initialization code like this:
    descriptor.AddProperty("clientClick", this.OnClientClick);
  3. Add event add_ and remove_ functions as well as helper method for firing the event in js file:
    add_clientClick : function(handler) {
        this.get_events().addHandler('clientClick', handler);
    },
    remove_clientClick : function(handler) {
        this.get_events().removeHandler('clientClick', handler);
    },
    raiseclientClick : function(eventArgs) {   
        var handler = this.get_events().getHandler('clientClick');
        if (handler) {
            handler(this, eventArgs);
        }
    }
     

So all we have to do now to fire the event is call

this.raiseclientClick(Sys.EventArgs.Empty);

Attaching and handling the events to target element

ASP.NET AJAX extensions provide a good set of helper methods to do common tasks. One of them is attaching event to DOM element. This can be done using $addHandler() method. So we can attach our handler to target element with this code:

$addHandler(this.get_element(), "click", function(){
    //display current time
    alert(this._prefixText + this._currentTime);
    
    //raise attached event
    this.raiseclientClick(Sys.EventArgs.Empty);
});

Unfortunately this code won't work because in anonymous event handler this keyword refer to the window object in IE. so to workaround this issue we can use this code instead:

var t = this;
$addHandler(this.get_element(), "click", function(){
    //display current time
    alert(t._prefixText + t._currentTime);
    
    //raise attached event
    t.raiseclientClick(Sys.EventArgs.Empty);
});

Although this works it is still workaround. To make it in more elegant way Function.createDelegate() method can be used instead.

$addHandler(this.get_element(), "click", Function.createDelegate(this, function(){
    //display current time
    alert(this._prefixText + this.get_element().innerHTML);
    
    //raise attached event
    this.raiseclientClick(Sys.EventArgs.Empty);
}));

Setting test page

In order to create test page to our extender we need to create new Web Application project and reference the assembly that contains the extender - "AjaxControlExtenderLibrary.dll" in our case. Then in ASPX page register controls

<%@ Register Assembly="AjaxControlExtenderLibrary" Namespace="AjaxControlExtenderLibrary" TagPrefix="ext"
                %>

Add control and extender to body of the page. And do not forget to add ScriptManager right after <form> tag.

<asp:Label ID="lblTime" runat="server" Text="Label"></asp:Label>
<ext:AjaxClock ID="clock1" runat="server" TargetControlID="lblTime" PrefixText="Hello at " />

How to use the component on client-side

Sometimes you need to use the code on client-side in your js functions. You can use $find() method to find the component with passed id. There is little trick though -  the id is not passed when component is created. so you have to add this line in GetScriptDescriptors() method:

descriptor.AddProperty("id", this.ClientID);

And this results on following js initialization code:

$create(AjaxControlExtenderLibrary.AjaxClock, {"id":"clock1","prefixText":"Hello at "}, {"clientClick":ClockClicked}, null, $get("lblTime"));

And having id we can find the component and use it's methods like this:

function ClockClicked(){
    var clockExtender = $find('clock1');
    $get('log').innerHTML += "<br/>Clock clicked at " + clockExtender.get_CurrentTime();
}

Sample project can be downloaded here: AjaxControlExtenderLibrary.zip (46 KB)