Contents |
· 1 Introduction · 2 AJAX Controls · 3 Control Functionality · 4 Script Registration · 5 Thinking Along AJAX Lines · 6 Consuming the Component · 7 Conclusion |
We’ve already discussed many of the new features that ASP.NET AJAX brings to client-side development in parts 1 through 3 of this series. The next subject we’re going to take a look at is developing an example extender using the ASP.NET AJAX approach of developing AJAX components.
I mentioned previously that controls and extenders have both a server and a client API. The server API is responsible for describing the client-side component so both the server and the client side can interoperate (as much as it does). This description process also allows the client-side code to receive values set in the server-side code.
ASP.NET AJAX framework Series
· Part 1 Overview of the client portion of the ASP.NET AJAX framework.
· Part 2 Overview of the client portion of the ASP.NET AJAX framework.
· Part 3 Overview of the client portion of the ASP.NET AJAX framework.
· Part 4 Overview of the server portion of the ASP.NET AJAX framework.
· Part 5 Overview of rendering the UI of a control with ASP.NET AJAX.
It’s first important to understand a little more about the control process. AJAX controls have at least two code files associated with them: a CS or VB code file that contains the server code, and the .JS file (as an embedded resource) that contains the client code. Both portions are married together to form one entity, the AJAX component. At runtime, the server pushes values down to the client through the description process. The server knows about the client because the code has to identify fully the information about the client component in the script description and reference process.
Without getting too theoretical about the process, I’d like to go into the sample that is a functional CheckboxList control, called the ClientCheckboxList. This control inherits from CheckBoxList so it doesn’t lose the existing functionality. To take advantage of AJAX functionality, the control implements from IScriptControl. This component will utilize the existing structure and extend it to expose client side properties and events that can be accessed in an ASPX page.
Listing 1: Structure of Server Component
1.public class ClientCheckboxList : CheckBoxList, IScriptControl { }
The component has a few properties inherited from the base ListControl class; one of which is the SelectedIndex property, an important property for this example. Another index property that works similarly is the ActiveIndex, which stores the index of the last item with activity, whether selected or unselected.
AJAX also allows event definition, which is defined using a property. The property read and writes the name of a JavaScript event handler that will receive notification of that event. This works very close to the way .NET registers event handlers. Let’s take a look at the server properties; I’ll touch upon that process later.
Listing 2: ClientCheckboxList server properties
01./// <summary>
02./// Gets the index for the item in the collection, which is active (not necessarily
03./// selected).
04./// </summary>
05.public int ActiveIndex
06.{
07. get { return (int)(ViewState["ActiveIndex"] ?? -1); }
08. set { ViewState["ActiveIndex"] = value; }
09.}
10./// <summary>
11./// Gets or sets a client method to receive a call to the all items selected event.
12./// </summary>
13.public string OnClientAllItemsSelected
14.{
15. get
16. {
17. object o = ViewState["OnClientAllItemsSelected"];
18. return (o == null) ? null : (string)o;
19. }
20. set { ViewState["OnClientAllItemsSelected"] = value; }
21.}
22./// <summary>
23./// Gets or sets a client method to receive a call to the item selected event.
24./// </summary>
25.public string OnClientItemSelected
26.{
27. get { return (string)(ViewState["OnClientItemSelected"] ?? null); }
28. set { ViewState["OnClientItemSelected"] = value; }
29.}
30./// <summary>
31./// Gets or sets a client method to receive a call to the all item toggled event.
32./// </summary>
33.public string OnClientItemToggled
34.{
35. get { return (string)(ViewState["OnClientItemToggled"] ?? null); }
36. set { ViewState["OnClientItemToggled"] = value; }
37.}
38./// <summary>
39./// Gets or sets a client method to receive a call to the no items selected event.
40./// </summary>
41.public string OnClientNoItemsSelected
42.{
43. get
44. {
45. object o = ViewState["OnClientNoItemsSelected"];
46. return (o == null) ? null : (string)o;
47. }
48. set { ViewState["OnClientNoItemsSelected"] = value; }
49.}
These properties are mostly important for the description process, as the values will be pushed down to the client. The description process uses a ScriptControlDescriptor to describe the properties that gets the server component’s values passed down to the client.
Listing 3: Descriping Properties and Events
01.public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
02.{
03. ScriptBehaviorDescriptor descriptor = new
04. ScriptBehaviorDescriptor("Nucleo.Web.ListControls.ClientCheckboxList",
05. this.ClientID);
06. descriptor.AddProperty("activeIndex", this.ActiveIndex);
07. descriptor.AddProperty("selectedIndex", this.SelectedIndex);
08. descriptor.AddElementProperty("newItemsClientState", this.NewItemClientStateID);
09. descriptor.AddElementProperty("removedItemsClientState",
10. this.RemovedItemClientStateID);
11. descriptor.AddEvent("allItemsSelected", this.OnClientAllItemsSelected);
12. descriptor.AddEvent("noItemsSelected", this.OnClientNoItemsSelected);
13. descriptor.AddEvent("itemSelected", this.OnClientItemSelected);
14. descriptor.AddEvent("itemToggled", this.OnClientItemToggled);
15. return new ScriptDescriptor[] { descriptor };
16.}
Notice all of the described items are properties at the server level (some of which aren’t fully functional yet, as I haven’t completely implemented them). Also notice that the client names of the properties (in AddProperty and AddElementProperty) are lower-camel case and defined on the client this way. This differs from the server, of course, as the server uses upper-camel case. Events are passed using the AddEvent method. If the method name is actually null (which is a good thing if there isn’t an event handler), the event declaration is ignored, so no handler is added.
Two methods are very helpful with the description process, one of which is shown. The AddElementProperty method, on the server at runtime when the description process happens, actually passes down a live reference to that HTML element, which is very useful. The AddComponentProperty method would pass down a reference to a client AJAX component. Elements can be referenced through the $get helper method and components through $find, but this is a shortcut to get access to the object.
On the client, these values are passed down to the following objects:
Listing 4: Client Object’s Properties/Events
001.Nucleo.Web.ListControls.ClientCheckboxList.prototype = {
002. //******************************************************
003. // Properties
004. //******************************************************
005. get_activeIndex : function()
006. {
007. return this._activeIndex;
008. },
009. set_activeIndex : function(value)
010. {
011. if (this._activeIndex != value)
012. {
013. this._activeIndex = value;
014. this.raisePropertyChanged("activeIndex");
015. }
016. },
017. get_checkboxes : function()
018. {
019. if (this._checkboxes == null)
020. this._processCheckboxes();
021. return this._checkboxes;
022. },
023. get_newItemsClientState : function()
024. {
025. return this._newItemsClientState;
026. },
027. set_newItemsClientState : function(value)
028. {
029. if (this._newItemsClientState != value)
030. {
031. this._newItemsClientState = value;
032. this.raisePropertyChanged("newItemsClientState");
033. }
034. },
035. get_removedItemsClientState : function()
036. {
037. return this._removedItemsClientState;
038. },
039. set_removedItemsClientState : function(value)
040. {
041. if (this._removedItemsClientState != value)
042. {
043. this._removedItemsClientState = value;
044. this.raisePropertyChanged("removedItemsClientState");
045. }
046. },
047. get_selectedIndex : function()
048. {
049. return this._selectedIndex;
050. },
051. set_selectedIndex : function(value)
052. {
053. if (this._selectedIndex != value)
054. {
055. this._selectedIndex = value;
056. this.raisePropertyChanged("selectedIndex");
057. }
058. },
059. //******************************************************
060. // Event Methods
061. //******************************************************
062. add_allItemsSelected : function(handler)
063. {
064. this.get_events().addHandler("allItemsSelected", handler);
065. },
066. remove_allItemsSelected : function(handler)
067. {
068. this.get_events().removeHandler("allItemsSelected", handler);
069. },
070. _onAllItemsSelected : function()
071. {
072. var handler = this.get_events().getHandler("allItemsSelected");
073. if (handler != null)
074. handler(this, Sys.EventArgs.Empty);
075. },
076. add_noItemsSelected : function(handler)
077. {
078. this.get_events().addHandler("noItemsSelected", handler);
079. },
080. remove_noItemsSelected : function(handler)
081. {
082. this.get_events().removeHandler("noItemsSelected", handler);
083. },
084. _onNoItemsSelected : function()
085. {
086. var handler = this.get_events().getHandler("noItemsSelected");
087. if (handler != null)
088. handler(this, Sys.EventArgs.Empty);
089. },
090. add_itemSelected : function(handler)
091. {
092. this.get_events().addHandler("itemSelected", handler);
093. },
094. remove_itemSelected : function(handler)
095. {
096. this.get_events().removeHandler("itemSelected", handler);
097. },
098. _onitemSelected : function()
099. {
100. var handler = this.get_events().getHandler("itemSelected");
101. if (handler != null)
102. handler(this, Sys.EventArgs.Empty);
103. },
104. add_itemToggled : function(handler)
105. {
106. this.get_events().addHandler("itemToggled", handler);
107. },
108. remove_itemToggled : function(handler)
109. {
110. this.get_events().removeHandler("itemToggled", handler);
111. },
112. _onitemToggled : function()
113. {
114. var handler = this.get_events().getHandler("itemToggled");
115. if (handler != null)
116. handler(this, Sys.EventArgs.Empty);
117. }
118.}
The description process passes the values to the setters (prefixed with the set_) and the values can be retrieved using the get_ prefix in client code. Notice that what this is doing is not simply processing data on the client-side; it’s creating a complete object model that can be taken advantage of. On the client, these properties can be get and set, which makes it a powerful combination. Notice that these properties and events are defined in the prototype; I’ve explained the purpose of properties and events, as well as the prototype in my previous AJAX articles.
Moving along, AJAX components have two lifecycle methods (outside of being able to tap into the init, load, and dispose events) that they can take advantage of, shown in Figure 5 below.
Listing 5: Initialize and Dispose methods
01.initialize : function()
02.{
03. Nucleo.Web.ListControls.ClientCheckboxList.callBaseMethod(this, "initialize");
04. this._processCheckboxes();
05.},
06.dispose : function()
07.{
08. for (var i = 0; i < this.get_checkboxes().length; i++)
09. $removeHandler(this.get_checkboxes()[i], "click",
10. this._checkboxClickHandler);
11. this._checkboxClickHandler = null;
12. Nucleo.Web.ListControls.ClientCheckboxList.callBaseMethod(this, "dispose");
13.},
14._processCheckboxes : function()
15.{
16. this._checkboxClickHandler = Function.createDelegate(this,
17. this.checkboxClickCallback);
18. this._checkboxes = this.get_element().getElementsByTagName("input");
19. for (var i = 0; i < this.get_checkboxes().length; i++)
20. $addHandler(this.get_checkboxes()[i], "click",
21. this._checkboxClickHandler);
22.},
Initialize processes the checkbox controls by looping through each check and attaching to it an event handler. This method is called in the processCheckboxes private method. Private methods are noted by the underscore, and although they can still be executed, they should be ignored if prefixed with an underscore. Dispose is responsible for removing the handlers for each checkbox, and to remove the instance of the handler.
So what happens when a checkbox is clicked? This is the most important routine of the entire component. This is important because all of the events fire and the properties get set accordingly.
Listing 6: Handling the check clicks
01.checkboxClickCallback : function(domEvent)
02.{
03. var checkbox = domEvent.target;
04. if (checkbox == null) return;
05. if (checkbox.checked)
06. this.raise_itemSelected();
07. var total = this.get_checkboxes().length;
08. var count = 0;
09. var hasSelected = false;
10. for (var i = 0; i < this.get_checkboxes().length; i++)
11. {
12. var checkboxToCompare = this.get_checkboxes()[i];
13. if (checkbox == checkboxToCompare)
14. this.set_activeIndex(i);
15. if (checkboxToCompare.checked)
16. {
17. if (!hasSelected)
18. {
19. this.set_selectedIndex(i);
20. hasSelected = true;
21. }
22. count++;
23. }
24. }
25. if (hasSelected == false)
26. this.set_selectedIndex(-1);
27. if (count == 0)
28. this.raise_noItemsSelected();
29. else if (count == total)
30. this.raise_allItemsSelected();
31. this.raise_itemToggled();
32.}
So what happens? The first part of the process is to get a reference of the checkbox. If the checkbox is checked, fire the itemSelected client event. Next, looping through the checkboxes occur to count the number of checked checkboxes. While this is occurring, the selected index is set to the lowest index of the checkbox list; otherwise, its defaulted to -1. The active index is also set to the currently active item, whether selected or deselected. At the end of the script, if the total number of items counted is zero, the noItemsSelected client event fires. If the count of items matches the total, the allItemsSelected event fires. Otherwise, the itemToggled event fires. While it may seem complicated, it isn’t overly complicated, but AJAX scripting features need properly planned.
The final process that has to occur is to register the script. There are two steps that have to occur in OnPreRender and Render. They are shown below.
Listing 7: Script Registration
01.protected override void OnPreRender(EventArgs e)
02.{
03. ScriptManager manager = ScriptManager.GetCurrent(this.Page);
04. manager.RegisterScriptControl<ClientCheckboxList>(this);
05. base.OnPreRender(e);
06. base.EnsureID();
07. ScriptManager.RegisterHiddenField(this, this.NewItemClientStateID,
08. string.Empty);
09. ScriptManager.RegisterHiddenField(this, this.RemovedItemClientStateID,
10. string.Empty);
11.}
12.protected override void Render(HtmlTextWriter writer)
13.{
14. if (!base.DesignMode)
15. {
16. ScriptManager manager = ScriptManager.GetCurrent(this.Page);
17. manager.RegisterScriptDescriptors(this);
18. }
19. base.Render(writer);
20.}
The two process that must occur are:
· Call RegisterScriptControl or RegisterExtenderControl to register the control with the ScriptManager component on PreRender
· Call the RegisterScriptDescriptors method to register the descriptors with the ScriptManager component on Render
These two methods are key methods to getting the component to work. They are already defined in the ScriptControl base class, so if you implement from ScriptControl or any derivative thereof, you don’t have to worry about this process. Another situation where you will have to worry about it is if you override the Render method, and do not call base.Render(). In this case, script descriptor registration doesn’t occur and you have to add the code manually to your component.
In thinking AJAX, one of the features I was adding was the ability to add and remove items dynamically on the client to the list through a method. This method would then write the values to a hidden field, and I could parse the hidden field on the server to add or remove those items from the list. Turns out there was some trouble rendering the interface correctly, which I’m rethinking that approach, and so the component wasn’t completely finished.
But these kinds of features can be added into components, and as you design AJAX components, think about these kinds of features into your controls and extenders. That’s thinking the AJAX way.
As a test, I created the following user control:
Listing 8: Consuming the Control
01.<%@ Control Language="C#" AutoEventWireup="true"
02. CodeBehind="ClientCheckboxListTest.ascx.cs"
03. Inherits="Nucleo.Web.ListControls.ClientCheckboxListTest" %>
04.<script language="javascript" type="text/javascript">
05. function ClientCheckboxListTest_AllItemsSelected(e)
06. {
07. alert('all items');
08. }
09. function ClientCheckboxListTest_ItemSelected(e)
10. {
11. alert('item selected');
12. }
13. function ClientCheckboxListTest_ItemToggled(e)
14. {
15. alert('item toggled');
16. }
17. function ClientCheckboxListTest_NoItemsSelected(e)
18. {
19. alert('no items');
20. }
21. function clearAll()
22. {
23. var checkbox = $find("<%= extItems.ClientID %>");
24. checkbox.clearAll();
25. }
26. function selectAll()
27. {
28. var checkbox = $find("<%= extItems.ClientID %>");
29. checkbox.selectAll();
30. }
31.</script>
32.<n:ClientCheckboxList ID="extItems" runat="server" RepeatDirection="Vertical"
33. RepeatLayout="Flow"
34. OnClientAllItemsSelected="ClientCheckboxListTest_AllItemsSelected"
35. OnClientNoItemsSelected="ClientCheckboxListTest_NoItemsSelected"
36. OnClientItemSelected="ClientCheckboxListTest_ItemSelected"
37. OnClientItemToggled="ClientCheckboxListTest_ItemToggled">
38. <asp:ListItem>1</asp:ListItem>
39. <asp:ListItem>2</asp:ListItem>
40. <asp:ListItem>3</asp:ListItem>
41. <asp:ListItem>4</asp:ListItem>
42. <asp:ListItem>5</asp:ListItem>
43.</n:ClientCheckboxList>
44.<br />
45.<input type="button" value="Select All" οnclick="selectAll()" />
46.
47.<input type="button" value="Clear All" οnclick="clearAll()" />
48.<br /><br />
49.Text: <asp:TextBox ID="txtNewItemText" runat="server" /><br />
50.Value: <asp:TextBox ID="txtNewItemValue" runat="server" /><br />
51.Selected: <asp:CheckBox ID="chkNewItemSelected" runat="server" /><br />
52.Enabled: <asp:CheckBox ID="chkNewItemEnabled" runat="server" /><br />
53.<br />
54.<input type="button" value="Add New Item" οnclick="addItem()" />
55.<br /><br />
56.<asp:Button ID="btnPostback" runat="server" Text="Postback" />
As each item is checked, the correct events fire an alert to the screen. Also, two buttons allow for selecting all of, and clearing of, the checkbox values through two helpful methods in the component (omitted, but in the code sample).
Notice the use of $find. This method finds the client component by the control’s ID value. As I said before, an AJAX control has a server and client piece, and $find returns a reference to the underlying control.
Hopefully you’ve learned more about the control development process, how it gets described, where the values go, what can be exposed, and how it can be used.