ASP.NET AJAX Control Development
An in depth guide to developing controls with the ASP.NET AJAX
Introduction
In my previous article, I have presented the AJAX Grid and a Generic Pager, which mimics the built-in GridView control on the client side. In this part, I will provide details about developing Ajax Controls with the ASP.NET AJAX Framework.
Although the main goal of this article is to show you how to develop ASP.NET AJAX Controls with the ASP.NET AJAX Framework, it's really worth to read this, even if you are only interested in ASP.NET AJAX Framework instead of developing controls.
Developing ASP.NET AJAX Control requires both client and server side knowledge of the ASP.NET AJAX Framework. Certainly, the client side requires more knowledge and coding compared to the server side. In this part I will cover both; let’s start with the client side.
Client Side
The real beauty of the Microsoft Ajax Library is that it extends JavaScript’s object oriented model and provides an enhanced type system, which contains namespace, class, interface, enum, exception, reflection and other constructs that .NET developers are familiar with. If you are new to this enhanced type system or do not know how to create namespaces, classes, interfaces in ASP.NET AJAX following the prototype model, I recommend you to read Extending JavaScript with ASP.NET AJAX first before moving forward with this article.
Although the Ajax controls are inherited from Sys.UI.Control, the heart of Ajax controls is the Sys.Component class. This class mostly resembles the .NET Framework’s System.ComponentModel.Component class. The following diagram will give you a good understanding of the inheritance chain related to Ajax control development.
Figure 1: Classes involved in Ajax control development
Sys.IDisposable
This interface is similar to the .NET Framework’s IDisposable interface. The resources that the component utilizes should be released in the dispose method. These include event handlers, large arrays and so on.
Sys.INotifyDisposing
This interface allows the consumer of the component to detect its disposing through the disposing event.
Sys.INotifyPropertyChange
This interface allows the consumer of the component to detect the property changes through the PropertyChanged event. It will be covered in more depth in the Properties section.
Sys.Component
The Sys.Component class implements all the above interfaces. Component that encapsulate complex logic or contain child DOM elements require a central place to initialize and cleanup an instance. This is usually done by overriding the initialize and dispose method of the Sys.Component class. The following diagram shows the signatures of Sys.Component:
Figure 2: Signatures of the Sys.Component class
There are two kinds of components:
· Nonvisual
· Visual
A nonvisual component does not have any user interface. Examples are the built-in timer control of the ASP.NET AJAX Framework or a custom component that queues web service calls. Nonvisual components are similar to the ASP.NET ObjectDataSource, TableAdapter controls, which don’t have any user interface.
On the other hand, visual components provide a user interface. Examples are the controls of the Ajax Control Toolkit, the UpdateProgress, the Ajax Grid and so on. Visual components are divided into:
· Behavior or Sys.UI.Behavior
· Control or Sys.UI.Control
Sys.UI.Behavior
The purpose of a Behavior is to extend a DOM element without changing its core functionality. Most of the controls of the Ajax Control Toolkit are behaviors, such as the AutoComplete TextBox, MaskEdit, DragPanel etc. A behavior must have an associated DOM element. A single DOM element can have more than one associated behavior. Since this article is about control development, I’m going to skip behaviors. If you are interested to learn more on behaviors, I recommend you visit the Ajax Control Toolkit site.
Sys.UI.Control
On the contrary, a control is itself the DOM element. The main purpose of a control is to provide new functionality by wrapping an existing control. Examples are the UpdatePanel, UpdateProgress or the new Tab Control of the Ajax Control Toolkit. The following listing will show you the minimum code required to create a control.
Listing 1: A Control Skeleton
01.Type.registerNamespace('DummyNamespace');
02.
03.DummyNamespace.DummyControl = function(element)
04.{
05. DummyNamespace.DummyControl.initializeBase(this, [element]);
06.}
07.
08.DummyNamespace.DummyControl.prototype =
09.{
10. initialize : function()
11. {
12. DummyNamespace.DummyControl.callBaseMethod(this, 'initialize');
13. },
14.
15. dispose : function()
16. {
17. DummyNamespace.DummyControl.callBaseMethod(this, 'dispose');
18. }
19.}
20.
21.DummyNamespace.DummyControl.registerClass('DummyNamespace.DummyControl',
22.Sys.UI.Control);
23.
24.if (typeof (Sys) != 'undefined')
25.{
26. Sys.Application.notifyScriptLoaded();
27.}
The above is just a skeleton of a control. As you can see, the associated DOM element is passed in the control constructor. We are also overriding the initialize and dispose methods in order to perform the initializing and cleaning up. Since this is just a dummy control, we are not doing anything except calling the base class methods. The following diagram shows the signatures of Sys.UI.Control class.
Figure 3: Signatures of the Sys.UI.Control class
Another difference between a control and a Behavior is that a Behavior allows you to set an id while a control does not allow it. In fact, the id of a control is the same as the associated DOM element. The following is a quick reference for the above signatures:
· get_element(): Returns the DOM Element that the control represents.
· get_id(): Returns the id of the control, which is used by the $find statement to refer it.
· set_id(): if you try to set id, will you get an Error.invalidOperation exception, as control does not allow to set the id.
· get_parent(): Returns the Parent control.
· set_parent(): Sets the Parent control.
· get_visibilityMode(): visibilityMode is an enum which contains the value hide and collapse.
· set_visibilityMode(): Sets the visibilityMode. Basically the visibilityMode works with the control DOM element style's display property.
· get_visibile() : Returns whether the control is visible or not based upon the DOM element style visibility.
· set_visibile() : Sets the DOM element style visibility to hidden or visible.
· addCssClass(): Adds the specified CSS class in DOM Element className.
· dispose(): Inherited from Sys.Component.
· initialize(): Inherited from Sys.Component.
· onBubbleEvent(): Handles the event which is raised by the raiseBubbleEvent. if this method returns true the event will not be bubbled to its parent, it is recommended to return falseif you do not handle the event.
· raiseBubbleEvent(): Bubbles an event to parent control, basically both onBubbleEvent and this method is used when you are creating complex controls such control contains one or more child controls and you want to bubble an event from the child to its parent control.
· removeCssClass(): Removes the specified CSS class from the DOM element className.
· toggleCssClass(): The specified CSS class will be added in the classNameif it has not been set previously, if the specified class is already set, then the class is removed from the className
In the next example, we will create an enhanced ImageButtoncontrol, which will show a different image on mouse hover.
Listing 2: ImageButton.js
001.Type.registerNamespace('Simple.Controls');
002.
003.Simple.Controls.ImageButton = function(element)
004.{
005. //Module level variables
006. this._hoverImageUrl = '';
007. this._originalImageUrl = '';
008.
009. //Required Event Handlers
010. this._mouseOverHandler = null;
011. this._mouseOutHandler = null;
012. this._clickHandler = null;
013.
014. //Calling the base class constructor
015. Simple.Controls.ImageButton.initializeBase(this, [element]);
016.}
017.
018.Simple.Controls.ImageButton.prototype =
019.{
020. //Getter for hoverImage Property
021. get_hoverImageUrl : function()
022. {
023. return this._hoverImageUrl;
024. },
025.
026. //Setter for hoverImage Property
027. set_hoverImageUrl : function(value)
028. {
029. this._hoverImageUrl = value;
030. },
031.
032. initialize : function()
033. {
034. //Call the base class method
035. Simple.Controls.ImageButton.callBaseMethod(this, 'initialize');
036.
037. var target = this.get_element();
038.
039. //Store the actual image url
040. this._originalImageUrl = target.src;
041.
042. this._mouseOverHandler = Function.createDelegate(this, this._onMouseOver);
043. this._mouseOutHandler = Function.createDelegate(this, this._onMouseOut);
044. this._clickHandler = Function.createDelegate(this, this._onClick)
045.
046. //Attach the required event handlers
047.
048. $addHandlers(target, {'mouseover':this._mouseOverHandler,
049. 'mouseout':this._mouseOutHandler, 'click': this._clickHandler}, this);
050. },
051.
052. dispose : function()
053. {
054. $clearHandlers(this.get_element()); //Detach all event handlers
055.
056. delete this._mouseOverHandler;
057. delete this._mouseOutHandler;
058. delete this._clickHandler;
059.
060. //Call the base class method
061. Simple.Controls.ImageButton.callBaseMethod(this, 'dispose');
062. },
063.
064. add_click : function(handler)
065. {
066. this.get_events().addHandler('click', handler);
067. },
068.
069. remove_click : function(handler)
070. {
071. this.get_events().removeHandler('click', handler);
072. },
073.
074. _onMouseOver : function(e)
075. {
076. e.target.src = this._hoverImageUrl;
077. },
078.
079. _onMouseOut : function(e)
080. {
081. e.target.src = this._originalImageUrl;
082. },
083.
084. _onClick : function(e)
085. {
086. var handler = this.get_events().getHandler('click');
087.
088. // Check if there is any subscriber of this event
089. if (handler != null)
090. {
091. handler(this, Sys.EventArgs.Empty);
092. }
093. }
094.}
095.
096.Simple.Controls.ImageButton.registerClass('Simple.Controls.ImageButton', Sys.UI.Control);
097.
098.if (typeof(Sys) != 'undefined')
099.{
100. Sys.Application.notifyScriptLoaded();
101.}
102.
103.//Creating an instance of ImageButton
104.$create(Simple.Controls.ImageButton, {'hoverImageUrl':'Images/update-h.gif'},
105.{'click':onButtonClick}, null, $get('btnClient'));
106.
107.function onButtonClick(sender, object)
108.{
109. alert('I am clicked');
110.}
As you can see, we are overriding the initialize method to hook up the mouseover, mouseout and clickevents of the DOM element, so that we can set the proper image in those events and raise the clickevent. We are also overriding the dispose method to detach the event handlers that we have set in the initialize method.
Properties
Adding Properties to a control is very simple. The recommended way is to declare a privatevariable in the control constructor and add the getter and setter in the prototype section. The ASP.NET AJAX Framework always adds the get_ and set_ prefix when accessing the properties. So when adding the getter/setter we should follow the get_propertyName and set_propertyName convention. As done in the above example for the hoverImageUrl property, we are using get_hoverImage() as the getter and set_hoverImage() as the setter. But when creating the control with a $create() statement, we are only passing the hoverImageUrlbecause the ASP.NET AJAX Framework adds the set_prefix. Another important concern with properties is to notify the client of the control when its property has changed. The following snippet shows how to raise the propertyChangedevent. Since it only applies to the setter, I am excluding the getter part:
01.set_hoverImageUrl : function(value)
02.{
03. if (this._hoverImageUrl != value)
04. {
05. // Only sets the value if it differs from the current
06. this._hoverImageUrl = value;
07.
08. // Raise the propertyChanged event
09. // This is a base class method which resides in Sys.Component
10. this.raisePropertyChanged('hoverImageUrl');
11. }
12.}
Methods
There is no special consideration except that it is recommended to add the methods in the prototype object of the control.
Events
Adding Events is similar to adding Properties. First, declare the handler as a private variable in the control constructor and create a pair of add/remove methods for that event in the prototype object. Although it is not required to have event handler variable in module level, but there are certainly some advantages of having it, which I will discuss in the Function.createDelegate() section of the Ajax Framework. The Ajax Frameworks always adds the add_ and _removeprefix when adding/removing the events. So, when adding the event subscription code, the method should be named as add_eventNameand remove_eventName. As in the above example for click event we are adding the event subscription methods as add_click and remove_click, but in the $create statement we are naming it as click. The Ajax Framework takes care of adding the add_ prefix. As you can see in the above example, both add_click and remove_click refer to this.get_event() (inherited from Sys.Component). The Sys.Component class maintains an internal list of subscribed events which is stored in an instance of Sys.EventHandlerList class together with their respective handlers. Apart from this method pair, it is recommended to have a raiseEventName method to raise the event. Since the above is a very basic example, I have excluded it. But you should add it when you have to raise the same event from many places. When raising an event, we can pass some contextual data called the event arguments. If you recall my previous article, the Ajax Grid control needs to pass the data item and html table row in the rowDataBound event. The recommended way to pass the argument(s) with the event is to create a new class which inherits from the Sys.EventArgs and that takes required argument(s) in the constructor. For each argument, it will expose the getter and setter in the prototype object. The following code shows how the Ajax Grid Control rowDataBound of my previous article works.
01.//The RowDataBound Event Argument Class
02.Ajax.Controls.GridRowDataBoundEventArgs = function(tableRow, dataItem)
03.{
04. if (arguments.length !== 2) throw Error.parameterCount();
05.
06. //Calling the base class constructor
07. Ajax.Controls.GridRowDataBoundEventArgs.initializeBase(this);
08. this._tableRow = tableRow;
09. this._dataItem = dataItem;
10.}
11.
12.Ajax.Controls.GridRowDataBoundEventArgs.prototype =
13.{
14. get_tableRow : function()
15. {
16. if (arguments.length !== 0) throw Error.parameterCount();
17.
18. return this._tableRow;
19. },
20.
21. get_dataItem : function()
22. {
23. if (arguments.length !== 0) throw Error.parameterCount();
24.
25. return this._dataItem;
26. }
27.}
28.
29.Ajax.Controls.GridRowDataBoundEventArgs.
30.registerClass('Ajax.Controls.GridRowDataBoundEventArgs', Sys.EventArgs);
31.
32.
33.Ajax.Controls.Grid.prototype =
34.{
35. dataBind : function()
36. {
37. // more codes
38. ....
39. ....
40. // Now raise the rowDataBound event, so that the client of this control
41. // has a chance to do some extra work. For example putting the
42. // calculated, lookup column value etc.
43. this._raiseRowDatabound(tr, dataRow);
44. ....
45. ....
46. // more codes
47. }
48. ,
49. // more codes
50. ....
51. ....
52. _raiseRowDatabound : function(tableRow, dataItem)
53. {
54. // gets the handler form the event list
55. var handler = this.get_events().getHandler('rowDataBound');
56.
57. //Only Invoke the event if any handler is registered beofre
58. if (handler)
59. {
60. // Invoke the handler with a new instance of Event Argument class
61. handler(this, new Ajax.Controls.GridRowDataBoundEventArgs(tableRow, dataItem));
62. }
63. },
64. ....
65. ....
66. // more codes
67.}
Currently there are two built-in classes for the event arguments in the Microsoft Ajax Library, Sys.EventArgs and Sys.CancelEventArgs. Even if you have an event that does not need any event arguments, you should pass Sys.EventArgs.Empty - like the ImageButtoncontrol in the above example - so that the subscriber can follow the same pattern to handle all the events. For example, the Ajax Grid Control and Pager’s events (in my previous article) are handled in following way:
01.var _grid;
02.var _pager;
03.
04.function pageLoad()
05.{
06. // Getting the reference of the Client Components and attaching the event handlers
07. _grid = $find('grid');
08. _grid.add_columnDragStart(columnDragStart);
09. _grid.add_columnDropped(columnDropped);
10. _grid.add_sort(sorted);
11. _grid.add_rowDataBound(rowDataBound);
12. _grid.add_selectedIndexChange(selected);
13. _grid.add_rowDelete(deleted);
14.
15. _pager = $find('pager');
16. _pager.add_pageChange(pageChanged);
17.}
18.
19.function pageUnload()
20.{
21. _grid.remove_columnDragStart(columnDragStart);
22. _grid.remove_columnDropped(columnDropped);
23. _grid.remove_sort(sorted);
24. _grid.remove_rowDataBound(rowDataBound);
25. _grid.remove_selectedIndexChange(selected);
26. _grid.remove_rowDelete(deleted);
27.
28. _pager.remove_pageChange(pageChanged);
29.}
30.
31.function columnDragStart(sender, e)
32.{
33. //alert(e.get_column().headerText);
34.}
35.
36.function columnDropped(sender, e)
37.{
38. /*
39. alert(e.get_column().headerText);
40. alert(e.get_oldIndex());
41. alert(e.get_newIndex());
42. */
43.}
44.
45.function sorted(sender, e)
46.{
47. _grid.set_sortColumn(e.get_sortColumn());
48. _grid.set_sortOrder(e.get_sortOrder());
49.
50. // need to reset the current page as sorting has been changed
51. _pager.set_currentPage(1);
52.
53. loadProducts();
54.}
55.
56.function rowDataBound(sender, e)
57.{
58. var product = e.get_dataItem();
59.
60. //Since the grid supports Dnd the column index may vary
61. //Calling the helper function getColumnIndex() will return the proper index
62. var categoryColumnIndex = _grid.getColumnIndex('Category');
63.
64. var tdCategory = e.get_tableRow().childNodes[categoryColumnIndex];
65. var categoryName = getCategoryName(product.CategoryID);
66. tdCategory.innerHTML = categoryName;
67.
68. var unitInStockColumnIndex = _grid.getColumnIndex('Units In Stock');
69. var tdUnitsInStock = e.get_tableRow().childNodes[unitInStockColumnIndex];
70.
71. if (product.UnitsInStock < 10)
72. {
73. tdUnitsInStock.style.backgroundColor = '#ff0000';
74. }
75.}
76.
77.function selected(sender, e)
78.{
79. alert(String.format('commandName: {0}\ncommandArgument: {1}',
80. e.get_commandName(), e.get_commandArgument()));
81.
82. //Here we can do further processing such showing
83. //an Edit UI for the selected record, etc.
84.}
85.
86.function deleted(sender, e)
87.{
88. alert(String.format('commandName: {0}\ncommandArgument: {1}',
89. e.get_commandName(), e.get_commandArgument()));
90.}
91.
92.function pageChanged(sender, e)
93.{
94. _pager.set_currentPage(e.get_newPage());
95. loadProducts();
96.}
As you can see, both the Ajax Grid and the Pager follow the same pattern - HandleEvent(sender, e) - to handle all the events. This is also the same pattern implemented by the .NET framework to handle events. The Ajax Framework Team did a wonderful job to make the whole Ajax framework similar to the .NET framework.
Few Important Ajax Framework Methods
The followings are some of the important methods which you need to know in order to develop client controls.
$get()
It is an alias – or shortcut - for document.getElementById. It takes an optional parent DOM element. If the parent is not specified, the search starts from the document object.
$create()
This is a shortcut for Sys.Component.create, used to instantiate a client Component, Behavior or control. The complete signature of this method is:
1.$create(type, properties, events, references, element);
Example:
1.$create(Simple.Controls.ImageButton, {'hoverImageUrl':'Images/update-h.gif'},
2.{'click':onButtonClick}, null, $get('btnClient'));
The first argument takes the full type name of the object. For example, when creating the ImageButtonControl, we are passing Simple.Controls.ImageButton. This argument is mandatory.
The second argument takes an array of key/value pair for properties. For example, when creating the ImageButtoncontrol, we are passing the hoverImageUrland its value as {'hoverImageUrl':'Images/update-h.gif'}. If we want to set more properties, we will add a comma separated pair. For example:
1.{'aProperty':value1, 'bProperty':value2}.
The third argument is almost the same as the second. This argument is for events, and takes an array of key/value pairs. In the previous example, we are subscribing to the click event with the onButtonClick handler.
The fourth argument takes an array of key/value pairs with references to components that are required by the component that is being created. Usually, the components references are exposed through properties in the component that is being created. Since the Image Button does not require any reference to external components, we are simply passing nullas an argument value.
The fifth and the last argument is a DOM Element. It is a mandatory argument for Behaviors and Controls, but optional for a nonvisual component.
$find()
It is similar to $get, but it returns component or descendents instead of DOM elements. It expects an optional parent of type IContainer from which to start the search. If parent is not specified, it starts from Sys._Application.
Function._validateParams
JavaScript is not a strongly typed language. Ensuring that method arguments are of the expected types requires quite a lot of code, like typeOf, parseInt, parseFloat, (arguments[0] != null) etc statements. And maybe it’s for this reason that the Ajax Team has included this powerful function which can be used to validate all the method parameters with a single statement. Let’s take a look at a quick example:
01.set_hoverImageUrl : function(value)
02.{
03. var e = Function._validateParams(arguments, [{name: 'value',
04. type: String, mayBeNull: false}]);
05. if (e) throw e;
06. //more codes
07. ....
08. ....
09.},
In the above example, we are ensuring that argument value is a string data type and not null. The signature of this function is:
1.Function._validateParams(params, expectedParams)
The first parameter is an array of arguments, usually it is the arguments variable. The second parameter is an array of definition of each argument. Each definition can contain:
· name: name of the argument.
· type: datatype of the argument, such as String, Number, Boolean, Array or any other custom type that you have created.
· mayBeNull: trueor false, the default is false.
· optional: trueor false, the default is false.
· parameterArray: trueor false, the default is false.
· integer: trueor false, the default is false.
· elementType: If the argument is array then the type that the array holds.
· elementInteger: If the argument is array then it ensures that the array holds integers.
· elementDomElement: If the argument is array then it ensures that the array holds DOM elements.
· elementMayBeNull: trueor false. If the argument is an array then it ensures whether the array can hold any null items.
If the validation fails, Function._validateParams returns an exception that should be thrown by the caller of the function. Here is another example from my previous article, where I am ensuring that columns is an array and each item it holds is an Ajax.Controls.GridColumn instance. Also, the array cannot contain any null items.
01.set_columns : function(value)
02.{
03. var e = Function._validateParams(arguments, [{name: 'value',
04. type: Array, elementType:Ajax.Data.Controls.GridColumn,
05. elementMayBeNull: false}]);
06.
07. if (e) throw e;
08. //more codes
09. ....
10. ....
11.}
$addHandler
It is a shortcut for Sys.UI.DomEvent.addHandler. It provides a consistent way to hook up DOM elements’ events and hides the complexity of hooking up events in different browsers. The method signature is the following:
1.$addHandler(element, eventName, handler);
The first argument is a reference to the DOM element. The second one is the name of the event without the 'on' prefix, which is automatically added by the Microsoft Ajax. The third and last argument is the event handler. For example, in the following code I am binding the click event of a button:
01.$addHandler($get('btnSelect'), 'click', selectRow);
02.//more code
03.....
04.....
05.function selectRow()
06.{
07. //Code to handle the click event
08.}
09.....
10.....
11.//more code
$removeHandler
Removes an event handler that was previously added with $addHandler. The signature of this method is the same as $addHandler. It is a good practice to remove all the event handlers in the pageUnload event.
$addHandlers
This is similar to $addHandler, but it allows subscribing to multiple events of the same DOM element with a single statement. For example
1.$addHandlers(this.get_element(), {'mouseover':this._mouseOverHandler,
2.'mouseout':this._mouseOutHandler, 'click': this._clickHandler}, this);
In the above code, we are hooking up the mouseover, mouseout and click events with a single statement. The first parameter is the reference to the DOM element; the second is an array of event name/handler pairs. The last parameter is an optional parameter which usually points to this.
$clearHandlers
Removes all the event handlers which were previously added by $addHandler or $addHandlers. It takes a single parameter with a reference to a DOM element. For example, the following statement clears all the event handlers added to a DOM element:
1.$clearHandlers(this.get_element()); // Detach all event handlers
Function.createDelegate
This is a special function which wraps an existing function and returns a new function. The main purpose of this function is to resolve the this keyword. In the handler for an event raised by a DOM element, the thiskeyword always refers to the DOM element instead of the class. Consider the following example; if we code the Image Button control like so, it will always raise an exception:
01.//More codes
02.initialize : function()
03.{
04. //Call the base class method
05. Simple.Controls.ImageButton.callBaseMethod(this, 'initialize');
06.
07. var target = this.get_element();
08.
09. //Attach the required event handlers
10. $addHandler(target, 'mouseover', this._onMouseOver);
11.},
12.
13.//More codes
14._onMouseOver : function()
15.{
16. //Here this is not the ImageButton Control instead it is the DOM Element
17. //and thus it will always raise exception
18. this.get_element().src = this._hoverImageUrl;
19.},
20.//more codes
21....
22....
Here comes the power of Function.createDelegate. Let’s rewrite the previous code as follows:
01.//more codes
02.....
03.....
04.initialize : function()
05.{
06. //Call the base class method
07. Simple.Controls.ImageButton.callBaseMethod(this, 'initialize');
08.
09. var target = this.get_element();
10.
11. this._mouseOverHandler = Function.createDelegate(this, this._onMouseOver);
12.
13. //Attach the required event handlers
14. $addHandler(target, 'mouseover', this._mouseOverHandler);
15.},
16.//more codes
17.....
18.....
19._onMouseOver : function(e)
20.{
21. //Here this is the ImageButton control instead of the DOM element.
22. //and e.target is the DOM element
23. e.target.src = this._hoverImageUrl;
24.},
25.//more codes
26.....
27.....
The Function.createDelegatemethod requires two parameters. The first one is the object that will be pointed by this inside the event handler. The second parameter is the function that will be executed as the event handler.
Function.createCallback
This is also a special function which wraps an existing function and returns a new function. But the purpose of this function is to provide contextual data for the callback that is being created. Let’s consider the Pagercontrol of my previous article, where I have to pass the page number when the link is created.
01.//more codes
02.....
03.....
04._raisePageChange : function(e, context)
05.{
06. var handler = context.sender.get_events().getHandler('pageChange');
07.
08. if (handler)
09. {
10. handler(context.sender, new Ajax.Controls.PagerPageChangeEventArgs(context.page));
11. }
12.},
13.
14.//more codes
15.....
16.....
17._createLink : function(page, text, tip)
18.{
19. var a = document.createElement('a');
20. a.appendChild(document.createTextNode(text));
21. a.href = 'javascript:void(0)';
22.
23. if (tip.length > 0)
24. {
25. a.title = tip;
26. }
27.
28. var clickCallback = Function.createCallback(this._raisePageChange,
29. {sender:this, page:page});
30.
31. $addHandler(a, 'click', clickCallback);
32.
33. //More Codes
34.},
35.//more codes
36.....
37.....
The Function.createCallback method requires two parameters. The first one is the function that will be executed and second one is the context data. In the previous code, we are passing the current instance and the page number as arguments. Unlike Function.createDelegete, it does not resolve the this keyword and that’s why we are passing the current instance along with the page number as a JSON object in the context data.
I also recommend you to check the Base type extension Sys.UI.DomElement, Sys.UI.DomEvent, Sys.UI.MouseButton and Sys.UI.Key in the ASP.NET AJAX Documentation.
Server Side
So far, we have discussed the Client Part of Developing Ajax Controls. Lets now discuss the Server Side. Apart from the usual tasks, it is also responsible for registering the required JavaScript files in the ScriptManager and injecting the $create statements automatically. So far in this article we have manually added the $create statements. Let’s see how we can perform these things automatically. But before that, let’s explore the Server side classes involved in Ajax Controls development.
Figure 4: Server Side class hierarchy
The above diagram shows the inheritance chain for Component, control and Extender. As you can see, to develop a control (Since Components and Extenders are beyond the scope of this article, I am skipping them) there are two options: creating a class that inherits from ScriptControl or creating a class that implements the IScriptControl interface. If you want to start writing your control from WebControl then certainly ScriptControl is a better option as it is derived from WebControl. But if you want to start your control from scratch and do not want the built-in features of WebControl, then implementing the IScriptControl is the right option. The IScriptControl is also the right option when you want to add Ajax Framework features in an existing control such as the ImageButtonControl in this article. But both these approaches require to override the following methods:
· GetScriptDescriptors
· GetScriptReferences
GetScriptDescriptors
This is the method responsible for generating the $create statements on the client side. It uses a special class, ScriptDescriptor, to generate it. Before moving forward, let’s take a quick look at the ScriptDescriptor class hierarchy:
Figure 5: ScriptDescriptor class hierarchy
I hope you have already guessed that there are separate descriptors for each type of Component. If you are developing a regular Component then this method should return ScriptComponentDescriptor instances. In case of an Extender, the method should return ScriptBehaviorDescriptor instances and for a control; instances of the ScriptControlDescriptor class. The descriptor has some special methods which glue the server side with the client side. Let’s see a short example of how a $create statement is injected from the server side code:
1.//Client Side
2.$create(Simple.Controls.ImageButton, {'hoverImageUrl':'Images/updateh.gif'},
3.{'click':buttonClicked}, null, $get('btnClient'));
01.//Server Side
02.ScriptControlDescriptor desc = new ScriptControlDescriptor("Simple.Controls.ImageButton",
03.ClientID);
04.if (!string.IsNullOrEmpty(HoverImageUrl))
05.{
06. desc.AddProperty("hoverImageUrl", HoverImageUrl);
07.}
08.
09.if (!string.IsNullOrEmpty(ClientClickFunction))
10.{
11. desc.AddEvent("click", ClientClickFunction);
12.}
13.
14.yield return desc;
As you can see, we are passing the Client Side type (Although both Client and Server has the same type name) and the DOM element ID in the constructor. This fills the first and last argument of the $create statement. After that, we are setting the hoverImageUrl property that fills the property section of the $create() statement. Finally, we are filling the Event part by setting the click event handler. Here are the few important methods exposed by script descriptors:
AddProperty()
This adds a Property in the client side. The First argument is the name of the property and the second one is the value.
AddEvent()
This adds an event in the client side, the first parameter is the event name and second one is the name of the function that you want to bind.
AddScriptProperty()
This method assigns JavaScript code as a value for the given property. It is usually required for complex property assignment, such as the Columns collection of the Ajax Grid of my previous article.
AddElementProperty()
This method adds a property on the client side, but the difference with the AddProperty is that the value is passed as an argument to the $get method.
AddComponentProperty()
It adds a component reference in the component section of $create statement. The value will be used with the $find statement for assigning the property. The first parameter is the name of the property and the second one is the id of the component.
GetScriptReferences
This method is responsible for registering the JavaScript files in ScriptManager. In this method we will need to create an instance of ScriptReference for each required JavaScript file. For example, in the Image Button we are registering the JavaScript file as follows:
1.IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
2.{
3. yield return new ScriptReference(Page.ResolveUrl("~/ImageButton.js"));
4.}
We can also register the JavaScript files as embedded resources. To do this, we first have to mark a JavaScript file as an embedded resource in the Solution Explorer in Visual Studio. Then, we have to add the WebResource attribute to utilize the Asp.net 2.0 Web Resource Handler. The following code shows how to register a JavaScript file as an embedded resource:
01.[assembly: WebResource("Ajax.Controls.Pager.Pager.js", "text/javascript")]
02.
03.namespace Ajax.Controls
04.{
05. public class Pager : Control, IScriptControl
06. {
07. .......
08. .......
09. .......
10. //more codes
11.
12. IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
13. {
14. yield return new ScriptReference(this.Page.ClientScript.GetWebResourceUrl(
15. this.GetType(), "Ajax.Controls.Pager.Pager.js"));
16. }
17. }
18.}
We also have to override the OnPreRender and Render methods if we are implementing the IScriptControl instead of inheriting from ScriptControl. This will make sure that the ScriptManger recognizes the server control as an Ajax enabled control. The following listing shows the minimum code required on the server side:
01.using System;
02.using System.Collections.Generic;
03.using System.Web;
04.using System.Web.UI;
05.using System.Web.UI.WebControls;
06.
07.
08.namespace DummyNamespace
09.{
10. public class DummyControl : Control, IScriptControl
11. {
12. public DummyControl(): base()
13. {
14. }
15.
16. protected override void OnPreRender(EventArgs e)
17. {
18. base.OnPreRender(e);
19.
20. ScriptManager scriptManager = ScriptManager.GetCurrent(Page);
21.
22. if (scriptManager == null)
23. {
24. throw new InvalidOperationException(
25. "ScriptManager required on the page.");
26. }
27.
28. scriptManager.RegisterScriptControl(this);
29. }
30.
31. protected override void Render(HtmlTextWriter writer)
32. {
33. base.Render(writer);
34.
35. writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
36. writer.RenderBeginTag(HtmlTextWriterTag.Div);
37. writer.Write("I am a dummy control");
38. writer.RenderEndTag();
39.
40. if (!DesignMode)
41. {
42. ScriptManager.GetCurrent(this.Page).RegisterScriptDescriptors(this);
43. }
44. }
45.
46. IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
47. {
48. //Since it is a Dummy control we simply throwing an new instance
49. yield return new ScriptControlDescriptor("DummyNamespace.DummyControl", ClientID);
50. }
51.
52. IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
53. {
54. yield return new ScriptReference(Page.ResolveUrl("~/DummyControl.js"));
55. }
56. }
57.}
As you can see we are registering the control in the OnPreRender method; and script descriptors in the Render method. We are also throwing an error if there is no ScriptManager in the page. But you can certainly exclude that if you are planning to develop controls that work differently based upon the presence of the ASP.NET AJAX Framework.