home

Softsteel Solutions

About Us Contact Us Newsletter Training
Tutorials
 

ASP.NET Ajax Tutorial Lesson 8. Control Extenders

printer friendly version

The ASP.NET Ajax Control Toolkit comes with a number of control extenders, which we shalln’t go through here - see http://ajax.asp.net/ajaxtoolkit/ for a list and demonstrations. Instead, we shall consider how to roll our own control extenders.

To explain what we’re doing here: while it is possible to use the ASP.NET Ajax libraries for ad hoc functionality, it is generally desirable to wrap this functionality around server controls, so that you end up with controls that have both server-side and client-side capabilities. There are two main approaches to achieve this end.

The first is simply to extend a server control so that you end up with a more complicated type of entity. For instance, you might want to extend a radio button so that it asynchronously posts back a selection to a web service.

The second is to provide a wrapper class (the ‘Decorator’ design pattern) to add generic functionality to a range of controls. For instance, you might want to provide custom tooltip behaviour for each of a range of different types of control.

Option 1: Extending a Server Control

Using this option, the chosen server control implements the IScriptControl interface. This involves two methods, GetScriptReferences() and GetScriptDescriptors(). The first of these allows you to specify a number of supporting javascript files to get referenced on the client page. The second allows you to specify the javascript object to be used on the client side.

It is important to understand that the script you specify in ‘GetScriptReferences’ has to provide the class specification for the javascript object; this isn’t generated for you by the specification under ‘GetScriptDescriptors’. Rather, this specification is used to create and populate an object of the type you’ve independently defined.

To illustrate a control extender of this type, I shall make a simple object which extends the Lable control so that it pops up a message, defined by its ‘HelloWordString’ property, when it is clicked. Obviously this doesn’t itself display any Ajax behaviour, but it could do so by adding in client-side calls to Page Methods or Web Services.

Here is the server-side code (minus the ‘using’ statements):

1.

namespace FunWithAjax

2.

{

3.

    public class TestExtendedControl : Label, IScriptControl

4.

    {

5.

6.

    private string helloWorldString;

7.

8.

    public string HelloWorldString

9.

    {

10.

        get { return helloWorldString; }

11.

        set { helloWorldString = value; }

12.

    }

13.

14.

    public System.Collections.Generic.IEnumerable<ScriptReference> GetScriptReferences()

15.

    {

16.

        ScriptReference myRef = new ScriptReference(ResolveClientUrl("TestExtendedControlScript.js"));

17.

        return new ScriptReference[] { myRef };

18.

    }

19.

20.

    public System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors()

21.

    {

22.

        ScriptControlDescriptor myDesc = new ScriptControlDescriptor("FunWithAjax.TestExtendedControl", this.ClientID);

23.

        myDesc.AddProperty("helloWorldString", this.HelloWorldString);

24.

        return new ScriptControlDescriptor[] { myDesc };

25.

    }

26.

27.

    // START OF STANDARD BOILERPLATE CODE

28.

    private ScriptManager sm;

29.

    protected override void OnPreRender(EventArgs e)

30.

    {

31.

        if (!this.DesignMode)

32.

        {

33.

            // Test for ScriptManager and register if it exists

34.

            sm = ScriptManager.GetCurrent(Page);

35.

36.

            if (sm == null)

37.

                throw new HttpException("A ScriptManager control must exist on the current page.");

38.

39.

            sm.RegisterScriptControl(this);

40.

        }

41.

42.

        base.OnPreRender(e);

43.

    }

44.

45.

    protected override void Render(HtmlTextWriter writer)

46.

    {

47.

        if (!this.DesignMode)

48.

            sm.RegisterScriptDescriptors(this);

49.

50.

        base.Render(writer);

51.

    }

52.

    // END OF STANDARD BOILERPLATE CODE

53.

54.

    }

55.

}


At the top of this class there is a HelloWorldString property, which will hold the message to be alerted to the user.

Following this there are the implementations of the two IScriptControl methods. The first simply indicates that the javascript file to use is TestExtendedControlScript.js. The second provides some information about the client-side object to create. If you read through the method you’ll see that the ScriptControlDescriptor is set up with the following information:

The namespace and class name to be used for the client-side object (here we’ve used ‘FunWithAjax.TestExtendedControl’,which corresponds to the names used for the server-side code, but the choice of client-side names is actually independent of server-side names).
The clientID of the server control, so that the client-side object knows with which DOM elements it is associated
The fact that the client-side control has a property called ‘helloWorldString’, which should be set from the server-side HelloWorldString property.

These pieces of information are used by the Ajax framework to instantiate a client-side object of the appropriate type and with the appropriate properties. If you look at the source of a page containing our extended label, you’ll see that these definitions lead to the following javascript, in which can be seen all the bits of information described above:

1.

Sys.Application.add_init(function()

2.

{

3.

    $create(FunWithAjax.TestExtendedControl, {"helloWorldString":"hello world"}, null, null, $get("mytestextendedcontrol"));

4.

});


Finally, the server-side script contains some standard boilerplate code to check for the existence of a ScriptManager control, and to do the appropriate registration.

Let’s now move on to look at the client-side script that specifies the ‘FunWithAjax.TestExtendedControl’ class. As you will see, this is written in the manner that Microsoft prescribes in order to make it more obviously object oriented. As with the server-side code, I’ll first give the full code and then discuss various points.

1.

// register 'FunWithAjax' as a namespace

2.

Type.registerNamespace('FunWithAjax');

3.

4.

FunWithAjax.TestExtendedControl = function(element)

5.

{

6.

    FunWithAjax.TestExtendedControl.initializeBase(this, [element]);

7.

    this._helloWorldString = '';

8.

}

9.

10.

FunWithAjax.TestExtendedControl.prototype

11.

=

12.

{

13.

    // called when the object is created

14.

    initialize : function()

15.

    {

16.

        // call initialize on the base class - a standard piece of OO code

17.

        FunWithAjax.TestExtendedControl.callBaseMethod(this, 'initialize');

18.

19.

        // wire up the onclick event

20.

        this._onClickHandler = Function.createDelegate(this, this._onClick);

21.

        $addHandler(this.get_element(), 'click', this._onClickHandler);

22.

    },

23.

24.

    // called when the object is removed

25.

    dispose : function()

26.

    {

27.

        // unwire the event handlers

28.

        $clearHandlers(this.get_element());

29.

30.

        // call dispose on the base class - a standard piece of OO code

31.

        FunWithAjax.TestExtendedControl.callBaseMethod(this, 'dispose');

32.

    },

33.

34.

    // the private onClick handler

35.

    _onClick : function()

36.

    {

37.

        // do the alert

38.

        alert(this._helloWorldString);

39.

    },

40.

41.

    // GET and SET methods for the helloWorldString property

42.

    get_helloWorldString : function()

43.

    {

44.

        return this._helloWorldString;

45.

    },

46.

47.

    set_helloWorldString : function(value)

48.

    {

49.

        if (this._helloWorldString != value)

50.

        {

51.

            this._helloWorldString = value;

52.

            this.raisePropertyChanged('helloWorldString');

53.

        }

54.

    }

55.

}

56.

FunWithAjax.TestExtendedControl.registerClass('FunWithAjax.TestExtendedControl', Sys.UI.Control);

57.

if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();


Notes on the code:

1. At the top of this piece of code there’s a call to register the namespace ‘FunWithAjax’, and at the bottom there’s a call to register the class that’s just been defined. It’s not clear to me why the latter registration doesn’t also involve a call to the Type object, but they both work to register objects into the object oriented hierarchy.

2. The code uses what Microsoft calls the ‘Prototype model’, whereby the methods of a javascript class are defined on the prototype object of a class. Using prototype inheritance in this way minimises the memory used by instances of the class, though this is a topic we needn’t get into here.

3. The code has ‘initialize’ and ‘dispose’ methods which are called by the framework when the object is created and destroyed (respectively). Note that these methods also call corresponding methods on their base classes, which is a good OO practice.

4. Javascript has no support for ‘private’ members of a class, so a convention has been adopted whereby an underscore is prefixed to any member that should be considered private to the class.

5. Javascript also has no support for properties as opposed to fields. A different convention has been adopted here, so that a class has a property X just in case it has the functions get_X and set_X. It is important to note that if MyClass has property X in this sense, then writing code like

var y = MyClass.X;

will fail; it needs to be

var y = MyClass.get_X();

Recall, as an illustration of this point, that in the server-side GetScriptDescriptors() method we identified the javascript object’s property as simply ‘helloWorldString’.

A final point to note about properties is that we should raise ‘property changed’ events in the ‘set’ methods. This allows any object that is interested in the changing of property values to react to this change.

6. Event handlers are added and removed using the browser-independent methods $addHandler[s], $removeHander and $clearHandlers. In the initialize function there is also a method to create a function delegate called ‘_onClickHandler’, and this is passed to the $addHandler method rather than a direct reference to the _onClick function. This is necessary because otherwise the DOM element would try to execute _onClick in its own context, and the method would fail.

7. The notifyScriptLoaded method at the end of the script is necessary to inform the script manager that a particular script has finished loading, which it needs to know in order to handle certain timing issues.

Option 2: Writing a Wrapper Class

We started this section by noting that there are two ways to extend a web server control’s client-side functionality. The way we proceeded above involves inheriting from a control, and implementing the IScriptControl interface. The alternative method is to write the client-side functionality into a completely separate object which gets hooked up to a web server control at runtime. This separate object needs to inherit from the ‘ExtenderControl’ class.

The following script comprises the server-side code for an extender control which provides the same on-click functionality as above.

1.

namespace FunWithAjax

2.

{

3.

    [TargetControlType(typeof(Label))]

4.

    public class TestControlExtender : ExtenderControl

5.

    {

6.

7.

        private string helloWorldString;

8.

        public string HelloWorldString

9.

        {

10.

            get { return helloWorldString; }

11.

            set { helloWorldString = value; }

12.

        }

13.

14.

        protected override System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors(System.Web.UI.Control control)

15.

        {

16.

            ScriptControlDescriptor myDesc = new ScriptControlDescriptor("FunWithAjax.TestControlExtender", control.ClientID);

17.

            myDesc.AddProperty("helloWorldString", this.HelloWorldString);

18.

            return new ScriptControlDescriptor[] { myDesc };

19.

        }

20.

21.

        protected override System.Collections.Generic.IEnumerable<ScriptReference> GetScriptReferences()

22.

        {

23.

            ScriptReference myRef = new ScriptReference(ResolveClientUrl("TestControlExtenderScript.js"));

24.

            return new ScriptReference[] { myRef };

25.

        }

26.

    }

27.

}


Note the following differences from the IScriptControl object:

- The render and prerender methods are gone
- The GetScriptDescriptors method now takes a reference to a control, and it is this control’s ClientID that needs to be passed to the ScriptControlDescriptor.
- The class is decorated with a TargetControlType attribute which limits the type of web server control that the extender can be hooked up to. Each ExtenderControl object requires at least on such attribute.

The client-side script referenced in GetScriptReferences is essentially the same as that for IScriptControl objects, except that the class is registered not as a Sys.UI.Control but as a Sys.UI.Behavior, ie.

FunWithAjax.TestControlExtender.registerClass('FunWithAjax.TestControlExtender', Sys.UI.Behavior);

This concludes our overview of the ExtenderControl object, except to say that if you install the Ajax Toolkit then you can use its ‘ASP.NET Ajax Control Extender’ template to simplify some of the coding.

The Toolkit Template

When you instantiate an object using the Toolkit’s template, you are given three files: a class code file, a behavior javascript file, and a control designer file. We can ignore the control designer, but the javascript file gives you some useful skeleton javascript code which you need to fill out as described above. And the class code file changes the model a little, because it makes your control extender inherit not from ExtenderControl but ExtenderControlBase. This object works more off attributes than ExtenderControl; the following is the equivalent implementation of our ‘hello world’ class:

1.

namespace FunWithAjax

2.

{

3.

    [Designer(typeof(MyControl1Designer))]

4.

    [ClientScriptResource("FunWithAjax.MyControl1Behavior", "FunWithAjax.MyControl1Behavior.js")]

5.

    [TargetControlType(typeof(Label))]

6.

    public class MyControl1Extender : ExtenderControlBase

7.

    {

8.

9.

        private string helloWorldString;

10.

        [ExtenderControlProperty]

11.

        public string HelloWorldString

12.

        {

13.

            get { return helloWorldString; }

14.

            set { helloWorldString = value; }

15.

        }

16.

    }

17.

}


Here we can see that the javascript file is hooked up using the ‘ClientScriptResource’ attribute rather than the GetScriptReferences method. And instead of using the GetScriptResources method, the HelloWorldString property is hooked up via the ‘ExtenderControlProperty’ attribute.

Use of the Toolkit class does simply creation of control extenders somewhat, plus there are extra features such as the ability to specify a name for the client-side object. However, we shall not go further into this here.

 

ASP.NET Ajax Tutorial