In this chapter:
The power of technologies like ASP.NET lies in the ability to work with server controls and, particularly, web controls. A web control is an object that abstracts and manages a particular portion of the web page, be it a single element (like a text box) or a table (like a grid). A web control covers the tasks from the rendering of the HTML to postback handling and communication with other server controls. All of the web control’s logic is programmed, encapsulated, and executed on the server side as soon as you declare the web control on the page.
Having learned how to use the Microsoft Ajax Library to build client components, you’ll find out in this chapter how to wire them programmatically to ASP.NET server controls to obtain Ajax-enabled controls. By the end of the chapter, you’ll learn how to build ASP.NET server controls with Ajax capabilities.
In chapter 8, you saw how to create instances of client components in the page. Because the instantiation of a client component is a process that involves numerous steps besides creating a new instance, the $create method is a valid ally for successfully accomplishing this task.
All the listings in the previous chapter assumed that the $create statements were manually injected in a JavaScript code block in the page and executed during the init stage of the client page lifecycle. Given the possibilities that the ASP.NET server model offers, here’s an idea: If you can use $create to automate the process of instantiating a client component, why not also automate the process of injecting a $create statement into the page? If a server control can perform this job, it can instantiate the client components it needs. Then, you can proudly say that you’ve created an Ajax-enabled server control.
The first step toward this goal is mastering the concept of script descriptors. A script descriptor is an object that can be used on the server side to generate a $create statement programmatically. To understand how script descriptors work and the reasons behind their usage, let’s introduce some classes provided by the ASP.NET AJAX server framework.
Script descriptors are classes contained in the System.Web.Extensions assembly. They’re part of the System.Web.UI namespace and derive from a base abstract class called ScriptDescriptor. Figure 9.1 shows that the hierarchy of script descriptors reflects how classes are organized in the client component model. For example, the ScriptComponentDescriptor class represents the script descriptor associated with the client Sys.Component class. The same kind of mapping exists between the other classes, as the figure suggests.
Script descriptors behave in an interesting manner. For example, if you create an instance of the ScriptComponentDescriptor class, you can generate the $create statement needed for creating and configuring an instance of a nonvisual component. In a similar manner, you can use instances of the ScriptBehaviorDescriptor and ScriptControlDescriptor classes to generate—on the server side—the $create statements needed for instantiating and configuring a client behavior or a control, on the client side.
Programmatically generating a $create statement offers two main advantages. First, you don’t need to hard-code any strings in the application logic. Instead, you can instruct the script descriptor to generate the $create statement based, for example, on the values of some server-side variables. Second, an external object can receive the script descriptor and use it to generate the $create statement at the right time. As we’ll explain in section 9.2.1, the ScriptManager control can query a server control for a list of script descriptors. All the script descriptors are collected during the Render phase of the server page lifecycle and used to render all the $create statements in the markup code sent to the browser.
Before we go deeper into this process, you need more confidence with script descriptors, because you’ll use them often when programming Ajax-enabled controls. In the following sections, we’ll focus on the ScriptBehaviorDescriptor and ScriptControlDescriptor classes, which inherit almost all their functionality from the ScriptComponentDescriptor class. You’ll gain a comprehensive knowledge of how script descriptors work.
Whenever you need to instantiate a client behavior, you can generate the corresponding $create statement on the server side by using an instance of the ScriptBehaviorDescriptor class. The relevant properties and methods exposed by this class are shown in figure 9.2.
To understand how it works, let’s take a $create statement and see how you can generate the same statement using a script descriptor. The $create statement that you used in section 8.3.2 to create an instance of the FormattingBehavior behavior is perfect:
$create(Samples.FormattingBehavior, {'hoverCssClass':'field_hover', 'focusCssClass':'field_focus'}, {}, {}, $get('Name'));
To generate the same statement on the server side, you can create an instance of the ScriptBehaviorDescriptor class. Then, you pass the client type and the client ID of the associated DOM element as strings to the class constructor:
ScriptBehaviorDescriptor desc = new ScriptBehaviorDescriptor("Samples.FormattingBehavior", "Name");
The client ID of the associated element, passed as the second argument to the class constructor, becomes the argument of the $get method when the $create statement is generated by the script descriptor. The string with the type of the client control becomes the first argument passed to the $create method.
The goal of the script descriptors is to configure the parameters accepted by the $create method. Figure 9.3 will help you understand which methods of the script descriptor classes are used to build the parameters passed to the $create statement generated on the server side.
The first and last arguments passed to $create—the client type and the associated element, respectively—are passed as arguments to the constructor of the script descriptor. An exception is represented by the ScriptComponentDescriptor class, whose constructor accepts only a parameter with the client type (a nonvisual component doesn’t have an associated DOM element).
As you know from chapter 8, the second argument accepted by $create is an object that maps properties of the client component to their values. To add a name/value pair to this object, call the AddProperty method of the script descriptor instance, passing the name of the client property and its value as arguments. Here’s the code:
ScriptBehaviorDescriptor desc = new ScriptBehaviorDescriptor("Samples.FormattingBehavior", "Name"); desc.AddProperty("name", "myFormattingBehavior");
The previous code will generate the following $create statement:
$create(Samples.FormattingBehavior, {"name":"myFormattingBehavior"}, {}, {}, $get("Name"));
Note that the second argument passed to AddProperty is of type object. The script descriptor takes care of serializing the value using the JSON data format and embedding it in the $create statement. The AddElementProperty method behaves in a manner similar to AddProperty, but the value passed as an argument is the client ID of a DOM element and is passed to the $get method in the generated $create statement. The other methods exposed by the ScriptBehaviorDescriptor class—and by the other script descriptors—set the remaining parameters accepted by $create. They are as follows:
These are all the methods you need to use to generate a $create statement. The same methods are found in the ScriptControlDescriptor class, which is the script descriptor associated with a client control. Examining the ScriptControlDescriptor class, as we’ll do in the next section, will help you become even more confident with script descriptors.
If you want to generate a $create statement to instantiate a client control, the ScriptControlDescriptor class is the right choice. Figure 9.4 shows the relevant properties and methods exposed by this class.
Except for the Name property (which is relevant only for behaviors), the ScriptControlDescriptor class exposes the same properties and methods found in the ScriptBehaviorDescriptor class. Again, let’s take a $create statement and explain which methods you have to call to generate it with a script descriptor. What about the $create statement you used to build an instance of the PhotoGallery control in section 8.4.5? The code for this slightly more complex statement is shown in listing 9.1.
$create(Samples.PhotoGallery, { 'imageElement': $get('gal_image'), 'prevElement': $get('gal_prevButton'), 'nextElement': $get('gal_nextButton'), 'progressElement': $get('gal_progress'), 'images': ['Album/photo1.jpg', 'Album/photo2.jpg', 'Album/photo3.jpg'] }, {}, {}, $get('photoGallery'));
How can you instruct a ScriptControlDescriptor object to generate a string with the same statement? The answer is shown in listing 9.2, where you use the AddElementProperty method to assign references to DOM elements to the properties of the client control. Note how the array of strings passed to AddProperty in the last statement is turned into a JSON array in the generated $create statement.
ScriptControlDescriptor desc = new ScriptControlDescriptor("Samples.PhotoGallery", "photoGallery"); desc.AddElementProperty("imageElement", "gal_image"); desc.AddElementProperty("prevElement", "gal_prevButton"); desc.AddElementProperty("nextElement", "gal_nextButton"); desc.AddElementProperty("progressElement", "gal_progress"); desc.AddProperty("images", new string[3] { "Album/photo1.jpg", "Album/photo2.jpg", "Album/photo3.jpg")});
Usually, client components are contained in external JavaScript files that must be loaded in the web page. In ASP.NET AJAX, you can do this by referencing the script files manually in the Scripts section of the ScriptManager control. For this reason, it’s not enough for a server control to generate $create statements using script descriptors. You also need a way to automatically generate script tags with references to external JavaScript files. This is the purpose of the ScriptReference class, as we’ll clarify in the following section.
In an ASP.NET AJAX page, you can use the mighty ScriptManager control to load all the JavaScript files needed by the page. To do this, you declare one or multiple ScriptReference elements in the Scripts section of the ScriptManager control. As you know from previous chapters, a ScriptReference object exposes all the properties needed for locating a script file, whether it’s stored in one of the folders of the website or embedded in an assembly as a web resource.
If a server control wants to provide a list of script files to load in the web page, it can create instances of the ScriptReference class programmatically. For example, the following code uses an instance of the ScriptReference class to reference a script file called MyScriptFile.js, located in the website’s ScriptLibrary folder:
ScriptReference scriptRef = new ScriptReference(); scriptRef.Path = Page.ResolveClientUrl("~/ScriptLibrary/MyScriptFile.js");
If the file is embedded as a web resource in a separate assembly, you have to specify values for the Assembly and Name properties. The Assembly property holds the name of the assembly that embeds the file. The Name property stores a string with the name of the registered web resource.
Web resources are a feature of ASP.NET 2.0 that let you embed files, images, and documents in an assembly and load them in a web page through a HTTP handler. In section 4.3.1 we briefly discussed how to embed web resources in an assembly. To learn more about web resources, browse to http://support.microsoft.com/kb/910442/en-us.
Script descriptors and script references are the objects you need to build Ajax-enabled controls. Now that you have a foundation, let’s examine how ASP.NET server controls can take advantage of these objects to become Ajax-enabled controls.
An ASP.NET AJAX-enabled control (or an Ajax-enabled control, as we’ll call it) is an ASP.NET server control. It’s associated with one or more client components that add client functionality to the markup code it renders. Usually, an Ajax-enabled control renders, in the page, some HTML code and one or more $create statements, depending on the client components it wants to instantiate. The Ajax-enabled control also takes care of loading the necessary script files in the page.
The problem of how to programmatically generate a $create statement without relying on hard-coded strings has been solved with the introduction of script descriptors. The ScriptReference object lets you programmatically reference a script file. You need to understand how and when these objects are used to render the various pieces of information in the page—in the right place, at the right time. The ScriptManager is the control elected to accomplish this delicate task. In this section, we’ll shed some light on the mechanisms that let a server control become an Ajax-enabled control.
When a server control wants to take advantage of client components, it must do two things. First, it must implement an interface that specifies which kind of Ajax-enabled control it’s going to be. We’ll introduce the two kinds of Ajax-enabled controls in the next section, but it’s important to point out that by implementing one of these interfaces, a server control declares that it can provide a list of script references and script descriptors.
Second, it must register itself with the ScriptManager during the PreRender and Render phases of the server page lifecycle. When this happens, the ScriptManager knows that a server control wishes to instantiate client components. The ScriptManager queries the server control for a list of script descriptors and script references. The returned objects are used to render the $create statements and the script tags in the web page. Figure 9.5 uses a sequence diagram to show the sequence of events involved in this rather elaborate process.
The diagram shows that the registration procedure begins during the PreRender stage of the server page lifecycle, where the Ajax-enabled control registers itself with the ScriptManager (event 2). During this step, the ScriptManager gets a list of script references from the server control. The second step happens during the Render phase, when the Ajax-enabled control provides the ScriptManager with a list of script descriptors (event 4). Finally, the ScriptManager renders in the page both the script tags with the references to the script files and the $create statements generated by the script descriptors.
If you’re worried by the complexity of this procedure, have no fear. To make things simpler, ASP.NET AJAX provides base classes for creating Ajax-enabled controls. The advantage of deriving from these classes is that they take care of performing the entire registration procedure automatically. Creating an Ajax-enabled control is a matter of implementing the methods that return the list of script descriptors and script references. These methods are defined in the interfaces implemented by the base classes. As a consequence, you have to override them to get the job done.
In some situations, it’s not possible to derive from a base class, and you need to implement the procedure manually. Later in the chapter, we’ll go under the hood of the registration procedure. Now, it’s time to introduce the base classes and interfaces that you’ll use to create Ajax-enabled controls.
In the previous section, we mentioned that you can choose between two kinds of Ajax-enabled controls: You can create either an extender or a script control. The difference between these two types of controls is mainly conceptual, because the goal of both is to provide a list of script descriptors and script references.
You can think of extenders as providers of client functionality. The goal of an extender is to attach client components to an existing server control at any time, without the need to derive a new class. To understand why you have to bother with extenders, suppose you’ve created a client component that adds auto-complete functionality—like the one provided by Google Suggest—to a text box element. To wire the client component to the text box, you could create an AutoCompleteTextBox class that derives from the TextBox class and provides the necessary script references and script descriptors.
Another approach keeps the TextBox class and lets an external object do the work of wiring a client component to the TextBox control. In this scenario, the external object is the extender, and the TextBox becomes the extended control or target control. An extender can upgrade or extend existing server controls to Ajax-enabled controls without the need to replace them with custom server controls. This concept is represented in figure 9.6.
An ASP.NET AJAX extender is conceptually similar to an extender provider in Windows Forms. It keeps a portion of functionality separated from a server control, and it provides additional properties to the extended control. These properties are used, in turn, to configure the properties of the client component that are associated with the extended control.
To learn more about extender providers in Windows Forms, browse to http://msdn2.microsoft.com/en-us/library/ms171835.aspx.
If you decide that both the server and the client capabilities should be specified in the same place, you need a script control. It’s a server control that is created as an Ajax-enabled control and can provide script references and script descriptors without the need for an external object. Returning to the example of an auto-complete text box, the AutoCompleteTextBox class that derives from TextBox is a good candidate for becoming a script control. This model is illustrated in figure 9.7.
Deciding whether to build an extender or a script control is a design choice you should make based on the requirements of the web application. Typically, an extender is the right choice when you want to plug client functionality into an existing server control, without the need to create a new control. A script control is the right choice if you want complete control over its capabilities both on the server and on the client side.
From a slightly different point of view, the choice between an extender and a script control can be determined by the kind of client component you want to wire to the server control. Creating an extender is typically the right choice if you want to wire a client behavior to a DOM element. Because an element can have multiple behaviors, it makes sense to wire—on the server side—multiple extenders to a server control. Each extender contributes to the client capabilities of the extended control by providing a different client behavior. On the other hand, because a DOM element can be associated with one and only one client control, it makes more sense to associate the client control with a script control and have all the properties needed for configuring the client component embedded in the server control.
We discussed client components and the client component model in great detail in chapter 8. Figure 9.8 shows the base interfaces and classes you can use to create extenders and script controls.
There are two base classes: ExtenderControl and ScriptControl. The ExtenderControl class creates extenders and implements the IExtenderControl interface. The ScriptControl class creates script controls and implements the IScriptControl interface.
The following sections will dive into the details of extenders and script controls. You’ll study the base interfaces and classes and learn how to use them to create Ajax-enabled controls. Let’s start the discussion by introducing extenders.
You already know that an extender’s goal is to wire a client component to an existing server control. You need to know how the client functionality is attached to the extended control.
The easiest way to build an extender is to declare a class that inherits from the base ExtenderControl class. This class implements an interface called IExtenderControl and takes care of registering the extender with the ScriptManager control. A derived class should override the methods defined in the IExtenderControl interface. Let’s look at this interface before you develop your first extender.
The IExtenderControl interface defines the contract to which a class adheres to become an extender. Figure 9.9 shows the methods implemented by the interface, which have the following responsibilities:
Interestingly, both methods defined in the interface return an IEnumerable type. When you implement the method or override it in a derived class, you can return an array or a list or (if you’re using C# 2.0) implement an iterator to return the lists of script descriptors and script references.
Iterators are a feature introduced in C# 2.0 to support foreach iteration in a class or a struct without the need to implement the entire IEnumerable interface. If you want to know more about C# iterators, browse to http://msdn2.microsoft.com/en-us/library/65zzykke.aspx.
Even if your main job is to override the methods defined in the IExtenderControl interface, it’s important to know how the registration procedure is particularized for an extender. In the following section, we’ll look at how an extender is registered with the ScriptManager control.
The process of registering with the ScriptManager lets the extender be recognized as an Ajax-enabled control. It’s a two-step process:
1. During the PreRender stage, you call RegisterExtenderControl method, passing the extender instance and the extended control as arguments.
2. During the Render stage, you call the RegisterScriptDescriptors method to register the script descriptors.
As shown in figure 9.10, the first part of the registration procedure involves calling the RegisterExtenderControl method on the ScriptManager control (event 2). This method receives the current extender instance and the extended control as arguments. The registration procedure is completed during the Render phase, where you call the RegisterScriptDescriptors method on the ScriptManager, passing the current extender instance as an argument (event 4).
Luckily, the ExtenderControl class takes care of performing the registration procedure automatically on your behalf. Because you always create a new extender by deriving from the ExtenderControl class, you don’t need to worry about the implementation details. However, when we discuss script controls, you’ll discover that in some situations you need to manually register the Ajax-enabled control with the ScriptManager. For this reason, we’ll postpone a deeper examination of the registration procedure until section 9.4.
In general, the design of an extender follows three phases:
1. Build a client component—either a behavior or a control—that encapsulates the client functionality you intend to provide to a server control.
2. The real development of the extender starts. Determine which properties of the client component you want to configure on the server side. You can map them to a group of server properties and perform the configuration of the client component through the extender.
3. Build the extender class, which provides the lists of script references and script descriptors to the ScriptManager control.
Let’s apply this design strategy to a concrete example. In the following section, you’ll create an extender for the FormattingBehavior behavior you built in chapter 8. This will let you wire the behavior to an ASP.NET TextBox and configure it on the server side.
In chapter 8, we demonstrated how to enrich a text box element by simulating in-place edit functionality with the help of a client behavior. Now that you’ve implemented this client component, it would be great if you could wire it to TextBox controls in different web applications. If your intention is to not write one more line of JavaScript code or change any web controls declared in a form, building an extender is the right path. If you have the code for the FormattingBehavior class stored in a JavaScript file you’ve completed the first phase of the design strategy and can move to the second phase.
Once the client functionality is encapsulated in a client component, you need to filter the client properties you want to configure on the server side. The goal is to create corresponding properties in the extender class and use them to set the value of the client properties. How is this possible? By using a script descriptor.
Recall from chapter 8 that the FormattingBehavior class exposes two properties called hoverCssClass and focusCssClass. They hold the names of the CSS classes used by the client behavior. To set their values from the server side, you need to expose corresponding properties in the extender. In preparation, it’s useful to draw a table that shows the mapping between properties of the client component and properties of the extender; see table 9.1.
Client property |
Extender property |
---|---|
hoverCssClass | HoverCssClass |
focusCssClass | FocusCssClass |
Once you’ve drawn the table, you’re ready to move to the third and final, where you’ll create the extender class and implement the server-side logic.
An extender is a class that inherits from the base System.Web.UI.ExtenderControl class. Usually, an extender includes a group of server properties and the overrides of the methods defined in the IExtenderControl interface. Other than these, an extender shouldn’t perform any tasks. Because the purpose of an extender is to provide script descriptors and script references, all the other logic added to the extender should relate to the configuration of the associated client component.
Let’s return to the example. The extender class is called FormattingExtender, and its code is shown in listing 9.3.
Above the class declaration is a TargetControlType attribute. Its goal is to put a constraint on the types of server controls that the extender can extend. Because you pass typeof(TextBox) as an argument to the attribute, only TextBox controls can be extended by the FormattingExtender. Associating the extender with a web control other than a TextBox will cause a server exception to be thrown by ASP.NET. If you pass typeof(Control), all the controls can be extended, although it doesn’t make much sense given the kind of client functionality that, in this example, you’ll add to the target control.
The FormattingExtender class exposes a ScriptPath property that isn’t listed in table 9.1. This property specifies the location of the JavaScript file that contains the code of the FormattingBehavior behavior. The property isn’t listed in the table because it’s not exposed by the client component. You’ll need it when you create the ScriptReference instance that you return to the ScriptManager, so it makes sense to have it in the extender control.
The other two properties are those shown in table 9.1. The HoverCssClass property stores the value assigned to the hoverCssClass property of the client behavior. The same is true for the FocusCssClass property. Note that you store and retrieve all the values from the ViewState of the extender control.
For the first time, you can see how the methods defined in the IExtenderControl interface are overridden in the extender control. As expected, the GetScriptDescriptors method returns a script descriptor for the FormattingBehavior behavior. In the override, the script descriptor uses the values of the server HoverCssClass and FocusCssClass properties to build a $create statement that contains values for the client hoverCssClass and focusCssClass properties. Finally, the GetScriptReferences method returns a ScriptReference instance with the information needed to load the right JavaScript file in the page. The location of the file is configured through the ScriptPath property.
Listing 9.3 uses the yield return construct in both the GetScriptReferences and GetScriptDescriptors methods. You use the yield keyword when implementing a C# iterator, to signal the end of an iteration.
Without much effort, you’ve built your first extender. But we’ve left some things unsaid: For example, how do you wire an extender to an ASP.NET control? The next section will teach you how to declare and configure extenders.
An extender is nothing more than a custom ASP.NET server control. The ExtenderControl class derives from the base Control class; an extender is registered and declared in an ASP.NET page like any other server control. Figure 9.11 shows how the files are organized in the sample ASP.NET AJAX-enabled website that you can download at http://www.manning.com/gallo.
As you can see, the extender class is contained in the App_Code directory. The file with the code for the client behavior, FormattingBehavior.js, is located in the ScriptLibrary folder. Another possible configuration has both the server class and the JavaScript file stored in a separate assembly; we’ll cover this scenario in section 9.4, but the same rules apply to extenders.
To use the extender in an ASP.NET page, all you have to do is register the namespace that contains the FormattingExtender class in the page that will use it:
<%@ Register Namespace="Samples" TagPrefix="samples" %>
Now, you have to wire the extender to its target control. The code in listing 9.4 shows a simple ASP.NET TextBox with an associated FormattingExtender control.
<%-- Extended Control --%> <asp:TextBox ID="txtName" runat="server"></asp:TextBox> <%-- Extender --%> <samples:FormattingExtender ID="FormattingExtender1" runat="server" TargetControlID="txtName" HoverCssClass="hover" FocusCssClass="focus" ScriptPath="~/ScriptLibrary/FormattingBehavior.js" />
All the magic of extenders happens when you set the extender control’s TargetControlID property to the ID of the target control. In listing 9.4, you extend the TextBox by assigning its ID to the TargetControlID property of the FormattingExtender control. The remaining properties of the extender are used to configure the CSS classes used by the client behavior. The ScriptPath property contains the path to the FormattingBehavior.js file.
The TargetControlID property is the main property exposed by an extender. You always set this property, because it identifies the server control that’s wired to the extender.
An extender can also be instantiated programmatically, as shown in listing 9.5. The extender must be always added to the same container as the target control; if the target control is declared in an UpdatePanel, the extender must be declared in the panel. If the target control is declared in the form tag, then the extender must be added to the Page.Form.Controls collection.
FormattingExtender ext = new FormattingExtender(); ext.ID = "FormattingExtender1"; ext.TargetControlID = txtName.ID; ext.HoverCssClass = "hover"; ext.FocusCssClass = "focus"; ext.ScriptPath = "~/ScriptLibrary/FormattingBehavior.js"; Page.Form.Controls.Add(ext);
To complete our discussion, let’s run the ASP.NET page and look at the source code sent to the browser. After a bit of scrolling, you can find the script file required by the FormattingExtender control:
<script src="FormattingBehavior.js" type="text/javascript"></script>
After more scrolling, you see the $create statement generated by the script descriptor that the FormattingExtender returned to the ScriptManager control:
Sys.Application.add_init(function() { $create(Samples.FormattingBehavior, {"focusCssClass":"focus","hoverCssClass":"hover"}, null, null, $get("txtLastName")); });
Note how the $create statement is correctly injected into a JavaScript function that handles the init event raised by Sys.Application. So far, so good; everything went as expected.
Keep in mind that an extender is used to wire a client component to an existing server control. The extender provides the necessary script references and script descriptors to the ScriptManager control. It does so by overriding the methods defined in the IScriptControl interface. An extender control can also expose properties to enable the configuration of the properties exposed by the client component. Now, we’re ready to explore the second category of Ajax-enabled controls: script controls.
Extenders are great for providing client functionality to existing server controls in an incremental way. In many cases, though, you don’t want or don’t need an external control to wire client components to a server control. To describe both the server-side and the client-side functionalities in a single place, the server control is a good candidate for becoming a script control. Script controls are server controls that can provide script references and script descriptors without relying on an external object.
Building a script control can be slightly more difficult than building an extender. If you’re writing the control from scratch, you can safely derive from the base ScriptControl class, which takes care of registering the script control with the ScriptManager under the hood. Coding the control is similar to coding an extender. The only difference is that the properties used to configure the client component and the overrides of the methods defined in the IScriptControl interface are embedded in the control rather than in a different object.
In some situations, you’ll want to turn an existing control into a script control. In such a case, you have to derive a class from the existing server control and manually implement the IScriptControl interface. The following sections will introduce the IScriptControl interface and provide some insights as to how you implement the registration procedure.
The IScriptControl interface must be implemented by every script control. It’s similar to the IExtenderControl interface, as shown in figure 9.12. A script control doesn’t have a target control; this is why the RegisterScriptDescriptors method doesn’t receive a reference to the extended control as happened with extenders. The methods defined by the IScriptControl interface have the following responsibilities:
Sometimes you can’t derive from the base ScriptControl class. Therefore, it’s important to be familiar with what happens behind the scenes during the registration of a script control.
Registration with the ScriptManager is necessary in order to recognize a script control as an Ajax-enabled control. It’s a two-step process similar to that used for extenders:
As shown in figure 9.13, the first part of the registration procedure involves calling the RegisterScriptControl method on the ScriptManager control (event 2). This method receives the current script control instance as an argument. The registration procedure is completed during the Render phase, where you call the RegisterScriptDescriptors method, passing the current script control instance as an argument (event 4).
As promised, let’s dive into the implementation details of the registration procedure. Typically, a script control that will register itself with the ScriptManager overrides the OnPreRender and OnRender methods. In the OnPreRender method, you first check that a ScriptManager control is present on the page before calling the RegisterScriptControl method. Listing 9.6 shows a possible override of the OnPreRender method.
The registration procedure is completed in the Render method, where the script control registers its script descriptors by calling the RegisterScriptDescriptors method on the ScriptManager instance; see listing 9.7.
In the OnRender override, you don’t perform the check on the ScriptManager because you already did it during the OnPreRender stage. This registration procedure should be implemented whenever you can’t derive from the base ScriptControl class. But when does it happen? The following section provides some design guidelines for creating script controls.
With ASP.NET, there are many ways to create a server control. For example, you can extend an existing server control by deriving from its class. As an alternative, you can inherit from one of the base classes contained in the System.Web.UI namespace. You can also build a control by compositing existing web controls. In this case, CompositeControl is the base class. If you need data-binding capabilities, the DataBoundControl class lets you easily build such controls. In addition, you can create a custom control with an associated declarative template. This is called a web user control; you’ve probably created many in your web applications. It consists of a .ascx file that contains the declarative markup code and a code-behind file that encapsulates the control’s logic.
All these custom server controls can acquire Ajax capabilities and become script controls. Table 9.2 shows the categories of ASP.NET server controls and the suggested strategy for turning them into Ajax-enabled controls.
I want to... |
How to Ajax-enable it |
---|---|
Extend an existing web control. | Create an extender, or implement the IScriptControl interface. |
Derive from System.Web.UI.WebControl, create a composite control, or create a databound control. | Implement the IScriptControl interface. |
Start with an Ajax-enabled control. | Derive from System.Web.UI.ScriptControl. |
Create a web user control. | Implement the IScriptControl interface. |
Now that you have all the tools you need to start developing script controls, let’s build one. You’ll start by adding Ajax capabilities to the Login control, a server control shipped with ASP.NET 2.0 that, unfortunately, isn’t compatible with the UpdatePanel at the moment. A quick look at table 9.2 reveals that in order to upgrade the Login control to a script control, you should derive from the Login class and implement the IScriptControl interface. That’s what you’ll do in the next section.
Trying to put the ASP.NET Login control in an UpdatePanel reveals a sad truth: The control suddenly stops working, and your dreams of performing user authentication in the background vanish miserably. But you don’t have to wait until the next update of the ASP.NET framework to make your dreams come true. With the help of a script control and a client control that leverages the ASP.NET authentication service, you can perform the desired task.
As we explained in chapter 4, the Microsoft Ajax Library provides a way to access the authentication service asynchronously on the client side. Given this premise, you’ll create a client control that accesses the markup code rendered by the Login control and performs the authentication using an Ajax request. Finally, you’ll create a script control that extends the existing Login control and instantiates the client control in the page.
Let’s start by setting up the project for the new AjaxLogin control. Earlier, we explained how to embed the code files in an ASP.NET AJAX-enabled website. This time, we’ll explain how to embed the files in a separate assembly referenced by the website.
In Visual Studio 2005, let’s create a new class-library project called ScriptControls. The Visual Studio template creates a file called Class1.cs, which you can safely delete. Add a new JavaScript file called AjaxLogin.js. Select it and, in the Properties panel, set the Build Action property to Embedded Resource. This instructs the compiler to embed the file as an assembly resource that can be loaded in a web page. The AjaxLogin.js file—empty at the moment—will contain the client AjaxLogin control that adds Ajax capabilities to the server Login control. To complete the project layout, add a new class file called AjaxLogin.cs to obtain the structure shown in figure 9.14.
The client AjaxLogin control leverages the authentication service proxy to authenticate a user on the client side using an asynchronous HTTP request. We discussed the authentication service and the other application services provided by ASP.NET AJAX in chapter 5. Once the user types her username and password and clicks the login button, you invoke the Sys.Services.AuthenticationService.login method, which performs the authentication procedure asynchronously. Listing 9.8 shows the code for the AjaxLogin client control; add it to the AjaxLogin.js file created in the project.
The objective is to associate the client control with the DOM element that contains the markup code rendered by the Login control. You use some fields to store references to the child DOM elements. For example, the _userName and _password variables hold references to the text boxes rendered by the Login control.
In the prototype object, the initialize and dispose methods are overridden to participate in the client control’s lifecycle. You use the $addHandlers method to attach a handler for the click event of the login button. The event handler , _onLoginButtonClicked, takes into account the ASP.NET validators and invokes the login method of the authentication service proxy.
The last two parameters passed to the login method are callbacks. The first, _onLoginComplete, is invoked if the authentication procedure succeeds (whether the user has supplied right or wrong credentials); it displays the login status in a message box. The second callback, _onLoginFailed, is called if something goes wrong during the call to the authentication service proxy.
The script control you’ll build derives from the Login class and implements the IScriptControl interface. You need to override the methods of the IScriptControl interface as well as implement the registration procedure. You do so in the code for the AjaxLogin class, shown in listing 9.9.
The WebResource attribute that decorates the AjaxLogin class is used to register the AjaxLogin.js file as a web resource. This is necessary in order to be able to reference the script file in a web page.
The first method declared in the AjaxLogin class is a helper method that can find child controls declared in the base Login control. It’s also responsible for assigning references to the child elements to properties of the client control. As you saw in section 9.1.2, script descriptors expose a method called AddElementProperty that passes the client ID of an element to the $get method in the generated $create statement.
The subsequent two methods are the overrides of the OnPreRender and Render methods. You implement the registration procedure in the same way outlined in section 9.4.2. The last two methods are the overrides of the methods defined in the IScriptControl interface. As expected, the GetScriptDescriptors method returns a script descriptor for creating an instance of the AjaxLogin client control. In the GetScriptReferences method, you use the GetWebResourceUrl method to load the AjaxLogin.js file (embedded as a web resource) in the web page.
The first script control is complete, and you can safely build the project. As with extenders, we need to address a final point before we end our discussion of Ajax-enabled controls.
To use a script control, follow the usual steps required for using an ASP.NET server control. Steps include registering the custom control in the page using a @Register directive, like so:
<%@ Register Assembly="ScriptControls" Namespace="Samples" TagPrefix="samples" %>
This code takes into account the fact that the script control is located in a separate assembly. You need to specify the values for the Assembly and the Namespace attributes.
Once the control is registered in the page, you can declare it as in the following example:
<samples:AjaxLogin ID="AjaxLogin1" runat="server" />
Because the AjaxLogin control plays with the authentication service, you need to enable the service in the web.config file through the authenticationService element:
<system.web.extensions> <scripting> <webServices> <authenticationService enabled="true" /> </webService> </scripting> </system.web.extensions>
You must also enable forms authentication to take advantage of the AjaxLogin control.
You can learn about forms authentication by browsing to the following URL: http://msdn2.microsoft.com/en-us/library/aa480476.aspx.
The final touch is seeing what is rendered in a page that hosts the AjaxLogin control; see figure 9.15. The following code was extracted from such a web page:
[...] <script src="/AspNetAjaxInAction_09/WebResource.axd?d=eoxf8D IaviQBVsfEu1YjPF6KBBzaOSU3pQeO_UcQIK309neS2pzazIDzdhCZT9d0& t=633122647281397436" type="text/javascript"></script> [...] Sys.Application.add_init(function() { $create(Samples.AjaxLogin, {"LoginButton":$get("AjaxLogin1_LoginButton"), "Password":$get("AjaxLogin1_Password"), "RememberMe":$get("AjaxLogin1_RememberMe"), "UserName":$get("AjaxLogin1_UserName")}, null, null, $get("AjaxLogin1")); });
As expected, the script tag contains the URL of the AjaxLogin.js file embedded as a web resource in the ScriptControls assembly. The tag was generated thanks to the ScriptReference instance returned by the GetScriptReferences method overridden in the AjaxLogin control. The script descriptor returned by the AjaxLogin control generated the $create statement contained in the anonymous function passed as an argument to the add_init method.
In this chapter, we discussed how to wire client components to server controls to obtain Ajax-enabled controls. First, we introduced script descriptors and script references, which are the main objects used by server controls to instantiate client components and load script files in a web page.
Script descriptors can generate the $create statement used to instantiate a client component in the page. Script references let you specify the location of a script file to reference in a static script tag.
An Ajax-enabled control can return a list of script descriptors and script references to the ScriptManager, which in turn injects the generated $create statements and the script tags into the ASP.NET page sent to the browser. Extenders and script controls are the two kinds of Ajax-enabled controls you can create. Extenders can provide a list of script descriptors and script references to an existing server control, which becomes the extended control. Script controls are server controls that don’t need external objects in order to instantiate the client components they need.
In the next chapter, we’ll take a lap around the Ajax Control Toolkit, which is the biggest collection of Ajax-enabled controls available at the moment.
3.144.224.76