Building Client-Side ASP.NET AJAX Controls


JavaScript files can be written using several different techniques. A great majority of scripts on the Internet consist of multiple functions thrown into one or more files used to perform client-side functionality. While these types of unstructured scripts certainly get the job done, they don’t encapsulate related functions into containers for better code reuse and organization. Relatively few scripts rely on object-ori-ented techniques where JavaScript classes are defined using the JavaScript function and prototype design pattern.

Although JavaScript is not a true object-oriented language, it is capable of emulating several object-ori-ented characteristics that can be used to build new types, extend existing types, and in general create a reuseable client-side object library. Building custom AJAX controls requires understanding how to leverage encapsulation features built into the JavaScript language that can be used to create classes. It also requires an understanding of how the ASP.NET AJAX script library extends JavaScript’s default set of features.

Terminology between JavaScript and VB.NET or C# objects is surprisingly similar. Functionality can be grouped into classes, although JavaScript doesn’t technically support the concept of a class and doesn’t use the class keyword. However, JavaScript does support organizing related functionality into container objects much like VB.NET and C# do. Both JavaScript and .NET “classes” can expose fields, properties, and methods, although methods are typically referred to as “functions” in JavaScript, as they are defined using the function keyword. To keep things consistent, the remainder of this chapter will use standard .NET terminology and refer to actions a JavaScript object performs as methods rather than functions. It will also refer to variables as fields and characteristics/attributes of an object as properties.

Extending JavaScript

The ASP.NET AJAX script library extends JavaScript in several ways by allowing namespaces, classes, enumerations, and even interfaces to be defined and used on the client side. These extensions provide a familiar framework that resembles the server-side environment most ASP.NET developers are accustomed to working with and using. As a result, ASP.NET and JavaScript developers alike can create custom ASP.NET AJAX controls rather quickly once a few core concepts are understood.

One of the key classes involved in extending JavaScript’s existing functionality is the ASP.NET AJAX Type class. The Type class exposes several different methods that are useful when building custom controls as shown in the following table.

Open table as spreadsheet

Method

Description

callBaseMethod

Returns a reference to a base class’s method for the specified instance. Typically used in a control’s initialize() method and dispose() method when building custom client-side controls.

implementsInterface

Determines if a type implements a specific interface. Returns a Boolean value.

InheritsFrom

Determines if a type inherits from a specific base type. Returns a Boolean value.

initializeBase

Initializes the base type of a derived type. A call to the initializeBase() method is made in a control’s constructor.

registerClass

Registers a specific type with the ASP.NET AJAX client-side framework. A base class and one or more interfaces can optionally be defined when calling registerClass().

registerInterface

Registers an interface with the ASP.NET AJAX client-side framework.

registerNamespace

Registers a namespace with the ASP.NET AJAX client-side framework.

When building a custom AJAX control, you use the Type class within client-side code to define a names-pace, register a class, and possibly even an interface or enumeration with the ASP.NET AJAX framework. You’ll also use the Type class when a custom control needs to inherit functionality from an existing class in the ASP.NET AJAX script library. Every client-side control used in an ASP.NET AJAX application is considered to be a “type,” just as any VB.NET or C# class built using the .NET framework is considered to be a “type.” As a result, methods exposed by the ASP.NET AJAX Type class are available on all AJAX controls that you create.

In addition to the Type class, the ASP.NET AJAX script library provides several different classes that can serve as the foundation for a custom control depending upon the control’s intended purpose. Three main categories of custom controls can be built, including behaviors, visual controls, and nonvisual controls. In cases where you’d like to extend an existing DOM element’s behavior (to add additional style information to a button or textbox, for instance), the Sys.UI.Behavior class can be inherited from to create a custom behavior control. While controls that derive from Sys.UI.Behavior extend the overall behavior of a DOM element, they don’t modify the original purpose or intent of the control. When visual controls need to be created that modify the original intent of a DOM element and extend the element’s functionality, the Sys.UI.Control class can be used as the base class for the control. Nonvisual controls that don’t extend the behavior of a DOM element or extend its overall functionality (a timer for example) should derive from the Sys.Component base class.

The custom control used throughout this chapter derives from the Sys.UI.Control class, as it extends the functionality of a DOM element and adds significant functionality to it. Inheriting from Sys.UI.Control provides a solid starting foundation that can be built upon rather than writing the entire control from scratch. The control that we’ll develop here is named AlbumViewer and allows users to view one or more albums, and to search through them using asynchronous calls made to a .NET web service. Figure 11-1 shows the output generated by the AlbumViewer control.

image from book
Figure 11-1

To create the AlbumViewer control, a prescribed set of steps can be followed. These steps are fairly standard and should be followed when building ASP.NET AJAX client-side controls in general.

  1. Register a namespace - Register the namespace for the control using the Type class’s registerNamespace() method.

  2. Create a constructor - Create the control’s constructor and pass in the DOM element that represents the container for the control as a parameter. Initialize the base class by calling the initializeBase() method and define any fields that will be used by the control to store data.

  3. Prototype properties and methods - Follow the prototype design pattern to define properties and methods exposed by the control. Properties and methods are defined using JSON notation.

  4. Override initialize() - Within the prototype section of the control, initialize the control by overriding the initialize() method. Within initialize you can dynamically assign values to fields defined in the constructor, create delegates, handle events, plus more.

  5. Override dispose() - Within the prototype section of the control, override the dispose() method to allow objects such as delegates used by the control to be cleaned up properly.

  6. Register the control and inherit from a base class - Register the control with the ASP.NET AJAX client-side object model by using the Type class’s registerClass() method. Pass the appropriate base class to derive from as a parameter to registerClass().

Take a look at how a custom AlbumViewer control can be created and used in an ASP.NET AJAX application.

Tip 

All of the code that follows for the AlbumViewer client-side control can be found in the Scripts/ AlbumViewerStandAlone.js file or in the AlbumViewer class library project available in the code download for this book on the WROX website. The individual code listings that follow represent only a portion of the overall control’s code and will not run on their own.

Registering a Control Namespace

When creating custom client-side controls, you’ll want to define a namespace for each control to resolve any possible naming conflicts that may arise in an application. The AlbumViewer control is placed in a Wrox.ASPAJAX.Samples namespace and registered by using the Type class’s registerNamespace() function. Namespaces can contain other nested namespaces, classes, interfaces, enumerations, and even delegates and should be defined at the beginning of a control’s code:

 Type.registerNamespace("Wrox.ASPAJAX.Samples");

Note that no End Namespace keywords (as in VB.NET) or start and end brackets (as in C#) are used to define where an ASP.NET AJAX client-side namespace starts and ends. Instead, the namespace is referenced directly by a given control, as shown next.

Creating a Control Constructor

Once the namespace for a control is registered using the Type class, the control itself can be defined using the JavaScript "function" keyword. This is done by prefixing the control’s name with the names-pace that it should be placed in and defining an anonymous JavaScript function as shown in Listing 11-1.

Listing 11-1

image from book
  Type.registerNamespace("Wrox.ASPJAX.Samples"); Wrox.ASPAJAX.Samples.AlbumViewer = function(element) {     // Initialize base class     Wrox.ASPAJAX.Samples.AlbumViewer.initializeBase(this,[element]);     this._AlbumDivOverCssClass = null;     this._AlbumDivOutCssClass = null;     this._AlbumTitleCssClass = null;     this._AlbumArtistCssClass = null;     this._SongTitleCssClass = null;     this._Albums = [];     this._ServicePath = null;     this._ServiceMethod = null;     this._StatusLabel = null;     this._ShowSearchBox = false;     this._albumContainer = null;     //Delegate definitions     this._overHandler = null;     this._outHandler = null;     this._clickHandler = null;     this._btnClickHandler = null;     this._propertyChanged = null; }    
image from book

The code in Listing 11-1 associates the namespace of Wrox.ASPAJAX.Samples with the AlbumViewer control and creates a constructor for the control by defining a new anonymous JavaScript function. Like VB.NET and C# constructors, JavaScript constructors can accept initialization parameters. The AlbumViewer constructor shown in Listing 11-1 accepts a DOM element object representing the parent container that the control will use. The AlbumViewer control uses a div DOM element as its parent container.

The first line of code inside of the AlbumViewer control’s constructor makes a call to the Type class’s initializeBase() method (recall that all controls are considered types and have access to the Type class’s methods) to initialize the control’s base class. Because the AlbumViewer control is a visual control that adds content to a div element, it inherits from the Sys.UI.Control base class provided by the ASP.NET AJAX script library. The call to initializeBase() instantiates the Sys.UI.Control class. You’ll learn more about how the AlbumViewer class inherits from Sys.UI.Control later in the chapter.

The initializeBase() method accepts two parameters. The first parameter represents the object instance to initialize the base class for (the AlbumViewer control, in this case). You’ll normally use the JavaScript this keyword for the parameter value. The second parameter accepts an array, which is why the [ and ] characters are used, that can be passed to the base constructor as it initializes. Listing 11-1 passes the container element of the AlbumViewer control as the parameter value. Because the control uses a div element as its container, the div is passed directly to the base class’s constructor.

Once the base class has been initialized, fields used by the AlbumViewer control are defined. Each field is prefixed with the this keyword, which represents the current class (the this keyword is much like the Me keyword VB.NET developers use and analogous to the this keyword in C#). Because the fields shown in Listing 11-1 are private, their names are prefixed by the underscore character. This character isn’t required and has no actual significance; it is a standard naming convention used in the ASP.NET AJAX script library for private members of a class. As a general rule, all object and array fields used by a control should be defined in the control’s constructor so that the field instances are unique to all control instances and not shared across instances.

The AlbumViewer class defines several fields in its constructor such as this._AlbumTitleCssClass and this._AlbumArtistCssClass that are used to store the CSS class applied to various child elements within the control. It also defines an array named this._Albums using the shortcut []JSON notation. this._Albums is used to store album information used by the control. Other fields such as this._ServicePath and this._ServiceMethod are used to store information about the web service that provides album, artist, and song information to the control. The handler fields shown (this._overHandler, this._outHandler, and so on) represent delegates used by the control that route data from events to event handlers.

Using the Prototype Design Pattern with JSON

JavaScript has long supported the ability to extend objects by using the prototype property. By using it, JavaScript objects can inherit from base types and add additional properties and methods. The ASP.NET AJAX Library uses the prototype property and a pattern referred to as the “prototype design pattern” to extend existing objects and create new ones. Listing 11-2 shows how the prototype property can be used to define a Person object and expose two property getters and setters, as well as a method.

Listing 11-2

image from book
  function Person() {     this._FirstName = null;     this._LastName = null; } Person.prototype.get_FirstName = function() {       return this._FirstName; } Person.prototype.set_FirstName = function(value) {       this._FirstName = value; } Person.prototype.get_LastName = function() {       return this._LastName; } Person.prototype.set_LastName = function(value) {       this._LastName = value; } Person.prototype.getFullName = function() {       return this._FirstName + " " + this._LastName; } 
image from book

Tip 

JavaScript doesn’t officially support the concept of property getters and setters as in VB.NET or C#. However, getters and setters can be emulated by writing the corresponding methods. For example, get_FirstName represents the get block while set_FirstName represents the set block. Both of these are officially methods in .NET terms and functions in JavaScript terms but are typically treated as properties, as they get and set the value of a field.

Usually, there is a one-to-one relationship between a prototype property and a function definition, as shown in Listing 11-2. If four methods need to be added to a custom control, the prototype property is used four times. An exception to this rule would be for objects that define their properties and methods directly in the constructor, which is not a practice followed by the ASP.NET AJAX script library. Properties and methods added directly into the constructor get duplicated each time a new object instance is created. However, by using the prototype property, properties and methods are shared across multiple object instances resulting in less bloat.

Although the code shown in Listing 11-2 is fairly standard, ASP.NET AJAX client-side controls use the prototype design pattern with JSON notation to define properties and methods rather than using the prototype property for each property or method. Listing 11-3 shows how the Person class shown in Listing 11-2 can be refactored using the prototype design pattern with JSON. As you look through the code, notice that all properties and methods are wrapped within a single prototype statement. Property and method names are following by a colon that separates the name from the actual functionality. Each property and method is then separated by a comma.

Listing 11-3

image from book
  function Person() {     this._FirstName = null;     this._LastName = null; } Person.prototype = {     get_FirstName: function() {        return this._FirstName;     },     set_FirstName: function(value) {        this._FirstName = value;     },     get_LastName: function() {        return this._LastName;     },     set_LastName: function(value) {        this._LastName = value;     },     getFullName: function() {        return this._FirstName + " " + this._LastName;     } } 
image from book

The functionality for a property or method can also be defined in a separate location rather than inline, as shown in Listing 11-4. The ASP.NET AJAX script library uses this technique quite frequently to define client-side classes.

Listing 11-4

image from book
  function Person() {     this._FirstName = null;     this._LastName = null; } Person.prototype = {     get_FirstName: Person$get_FirstName,     set_FirstName: Person$set_FirstName,     get_LastName: Person$get_LastName,     set_LastName: Person$set_LastName,     getFullName: Person$getFullName } function Person$get_FirstName() {     return this._FirstName; } function Person$set_FirstName(value) {     this._FirstName = value; } function Person$get_LastName() {     return this._LastName; } function Person$set_LastName(value) {     this._LastName = value; } function Person$getFullName() {     return this._FirstName + " " + this._LastName; } 
image from book

Now that you’ve seen the fundamentals of building constructors and using the prototype design pattern, let’s take a look at some of the properties defined in the AlbumViewer control.

Defining Control Properties

Properties in client-side controls work in the same way as properties in server-side controls. However, the way properties are defined in client-side controls is quite different because JavaScript doesn’t support the .NET property syntax that formally defines get and set blocks. As an example, to define a getter and setter for a property named Albums that reads and writes from a private field named this._Albums, the following code could be written using the prototype design pattern and JSON:

 get_Albums: function() {     return this._Albums; }, set_Albums: function(value) {     this._Albums = value; }

The get block normally found in VB.NET or C# classes is simulated by adding get to the front of the property name while the set block uses set_. Any keywords can be used, but get_ and set_ are used throughout the ASP.NET AJAX script library and are also used in the AlbumViewer control for the sake of consistency.

The AlbumViewer control defines several different properties used to control CSS styles, searching capabilities, and web service calls. Each property acts as a gateway to private fields defined in the control’s constructor shown in Listing 11-1. A list of properties exposed by the AlbumViewer control is shown in the following table. Keep in mind that although these properties are really JavaScript functions prefixed by get_ and set_ keywords, their only purpose is to get and set data, so we’ll refer to them as properties to stay consistent with VB.NET and C# property definitions. However, the class consumer needs to use a function syntax when calling the getter or setter, as there is no explicit property syntax support in JavaScript.

Open table as spreadsheet

Property Name

Description

Albums

Gets and sets an array of Albums objects.

AlbumDivOverCssClass

Gets and sets the CSS class name to use when a user hovers over an album in the control.

AlbumDivOutCssClass

Gets and sets the CSS class name to use when a user moves the mouse out of an album in the control.

AlbumTitleCssClass

Gets and sets the CSS class name to use for an individual album title.

AlbumArtistCssClass

Gets and sets the CSS class name to use for an album artist.

SongTitleCssClass

Gets and sets the CSS class name to use for album songs.

ShowSearchBox

Gets and sets a Boolean value that is used by the AlbumViewer control to determine if an album search textbox should be shown or not.

ServicePath

Gets and sets the path to the album web service. The service is used to retrieve album data dynamically.

ServiceMethod

Gets and sets the Web Method to call on the album web service.

Listing 11-5 shows how the properties in the previous table are defined and used within the AlbumViewer control’s prototype definition. Each property has an associated getter and setter used to access the associated field variable defined in the control’s constructor.

Listing 11-5

image from book
  Wrox.ASPAJAX.Samples.AlbumViewer.prototype = {     get_Albums: function() {         return this._Albums;     },     set_Albums: function(value) {         this._Albums = value;     },     get_AlbumDivOverCssClass: function() {     return this._AlbumDivOverCssClass; }, set_AlbumDivOverCssClass: function(value) {     this._AlbumDivOverCssClass = value; }, get_AlbumDivOutCssClass: function() {     return this._AlbumDivOutCssClass; }, set_AlbumDivOutCssClass: function(value) {     this._AlbumDivOutCssClass = value; }, get_AlbumTitleCssClass: function() {     return this._AlbumTitleCssClass; }, set_AlbumTitleCssClass: function(value) {     this._AlbumTitleCssClass = value; }, get_AlbumArtistCssClass: function() {     return this._AlbumArtistCssClass; }, set_AlbumArtistCssClass: function(value) {     this._AlbumArtistCssClass = value; }, get_SongTitleCssClass: function() {     return this._SongTitleCssClass; }, set_SongTitleCssClass: function(value) {     this._SongTitleCssClass = value; }, get_ShowSearchBox: function() {     return this._ShowSearchBox; }, set_ShowSearchBox: function(value) {     this._ShowSearchBox = value; }, get_ServicePath : function() {     return this._ServicePath; }, set_ServicePath : function(value) {     if (this._ServicePath != value) {         this._ServicePath = value;             this.raisePropertyChanged('ServicePath');         }     },     get_ServiceMethod : function() {         return this._ServiceMethod;     },     set_ServiceMethod : function(value) {         if (this._ServiceMethod != value) {             this._ServiceMethod = value;             this.raisePropertyChanged('ServiceMethod');         }     }     //More code follows for methods and event handlers } 
image from book

The majority of the properties shown in Listing 11-5 simply read and write from private fields. While the ServicePath and ServiceMethod properties follow this same pattern, the setter for each of these properties adds a call to a method named raisePropertyChanged(). Calling this method is useful in situations where a control needs to be notified when a property changes so that it can act upon the change. This is a simple analog of the .NET event mechanism. The AlbumViewer control requires notification if the ServicePath or ServiceMethod properties are changed by the consumer of the control so that it can retrieve fresh album data.

You may wonder where the raisePropertyChanged() method is defined. Fortunately, it’s not something you have to code manually. The AlbumViewer control derives from the Sys.UI.Control class, which in turn derives from the Sys.Component class. The Sys.Component class defines the raisePropertyChanged() method as one of its members (for a complete listing of the Control class’s properties and methods refer to the ASP.NET AJAX documentation). When raisePropertyChanged() is called, an event is raised that can be handled by an event handler. The AlbumViewer control handles the property changed event as the ServicePath and ServiceMethod properties change and uses the two property values to call the appropriate web service to retrieve new album data and load it into the control. Additional information about handling events such as the one raised by raisePropertyChanged() is covered in the next section.

Initializing a Control and Handling Events

The Sys.Component class mentioned earlier defines an initialize() method that’s useful when a control is first initialized. When initialize() is called, a property named isInitialized is set to true by Sys.Component through a call to set_isInitialized(). This property is referenced by other classes in the ASP.NET AJAX script library such as Sys.UI.Behavior to determine if a control has been initialized or not.

Custom controls such as the AlbumViewer control can override the initialize() method and use it to initialize a base class, initialize properties or field values, define delegates, and hook events to event han-dlers. The AlbumViewer control defines the initialize() method within the prototype section of the code shown in Listing 11-6.

Listing 11-6

image from book
  Wrox.ASPAJAX.Samples.AlbumViewer.prototype = {     //Property definitions go here     initialize : function() {         var e = this.get_element();         e.style.className = this.get_AlbumDivOutCssClass();         //Initialize Delegates         this._overHandler = Function.createDelegate(this, this._onMouseOver);         this._outHandler = Function.createDelegate(this, this._onMouseOut);         this._clickHandler = Function.createDelegate(this, this._onClick);            this._btnClickHandler = Function.createDelegate(this,this._onButtonClick);          this._propertyChanged =            Function.createDelegate(this,this._onPropertyChanged);         this.add_propertyChanged(this._propertyChanged);         //Create search text box         if (this._ShowSearchBox) {             var lblText = document.createElement("SPAN");             lblText.style.cssText = "font-family:arial;font-weight:bold;" +               "font-size:8pt;";             lblText.innerHTML = "Album Name: ";             var txtAlbum = document.createElement("INPUT");             txtAlbum.setAttribute("type","text");             txtAlbum.setAttribute("id",e.id + "_txtAlbumSearch");             txtAlbum.style.cssText = "width:150px;";             var btnAlbumSearch = document.createElement("INPUT");             btnAlbumSearch.setAttribute("type","button");             btnAlbumSearch.id = e.id + "_btnAlbumSearch";             btnAlbumSearch.value = "Search";             $addHandler(btnAlbumSearch,"click",this._btnClickHandler);             e.appendChild(lblText);             e.appendChild(txtAlbum);             e.appendChild(btnAlbumSearch);             e.appendChild(document.createElement("P"));                 }         //Create div that will hold albums         this._albumContainer = document.createElement("DIV");         this._albumContainer.id = e.id + "_AlbumContainer";               e.appendChild(this._albumContainer);          Wrox.ASPAJAX.Samples.AlbumViewer.callBaseMethod(this, 'initialize');          //Bind data if albums have been assigned         if (this._ServicePath && this._ServiceMethod) {                                 // Call the web service             this._invokeWebService(null);         } else if (this._Albums != null) {             this.set_data(this._Albums);         }     },     //Additional methods go here } 
image from book

When the AlbumViewer control’s initialize() method is called, several tasks are performed. First, a reference to the DOM element used by the control is made by calling get_element(). The element property is inherited from Sys.UI.Control and is referenced any time the AlbumViewer control needs to access the DOM element container. Once the DOM element is accessed, the CSS class to apply to the element is assigned by reading from the AlbumDivOutCssClass property. This class defines the overall style applied to the control’s parent container.

Next, several delegates are created that point to various event handler methods. Delegates in ASP.NET AJAX client-side controls perform the same overall function as delegates used in VB.NET or C#: They pass data raised by an event to an event handler. Client-side delegates are created by calling the Function class’s createDelegate() method and passing the object context that the delegate applies to, as well as the name of the event handler method that the delegate should direct data to. The delegates created in the AlbumViewer control are used to route data when mouseover, mouseout, click, and propertychanged events are raised.

The delegate instances returned from calling Function.createDelegate() are assigned to private fields defined in the control’s constructor because several of them are reused throughout the control and need to be globally accessible. For example, the following code creates a delegate that is responsible for routing mouseover event data to an event handler named _onMouseOver. A reference to the delegate is stored in the this._overHandler field.

 this._overHandler = Function.createDelegate(this, this._onMouseOver);

Property changed events raised by the AlbumViewer control are routed to an event handler by calling the add_propertyChanged() method. This method routes data raised when the raisePropertyChanged() method discussed earlier is called to an event handler so that the change can be handled by the control. It accepts the name of the event handler method to call or a delegate instance that points to the method.

Once a delegate is created, it can be used to hook an event directly to an event handler using the ASP.NET AJAX $addHandler method. VB.NET developers will be quite comfortable using the $addHandler() method, as it’s similar in functionality to the VB.NET AddHandler keyword. $addHandler() is a shortcut to the Sys.UI.DomEvent class’s addHandler() method. It accepts several parameters, including the object that raises the event, the name of the event that will be raised, and the delegate that should be used to route event data to an event handler. Listing 11-6 contains an example of using the $addHandler method to hook up a button control’s click event to a delegate named this._btnClickHandler:

 //Define Delegate this._btnClickHandler =      Function.createDelegate(this,this._onButtonClick); //Hook button's click event $addHandler(btnAlbumSearch,"click",this._btnClickHandler);

In addition to $addHandler(), the ASP.NET AJAX script library provides an $addHandlers() method that allows multiple event handlers to be defined for a DOM element with a single statement. For example, if you need to attach the mouseover, mouseout, and click events of a DOM element named albumDiv to event handlers, you could use $addHandlers() in the following way:

 $addHandlers(albumDiv,     {"mouseover" : this._overHandler,     "mouseout" : this._outHandler,     "click" : this._clickHandler },   this);

The second parameter passed to $addHandlers is a JSON object containing the different event names and related delegates that should be attached to the DOM element.

The initialize() method shown in Listing 11-6 is also used to add several controls into the AlbumViewer control’s parent container element. Because album data is being displayed, an end user may want to search for one or more albums based upon title. Searching is controlled by data stored in the this._ShowTextBox field of the control. When the field value is true, a span tag, textbox, and but-ton are added to the AlbumViewer control’s div container. End users can then use the controls to perform a search. Adding controls is accomplished by calling document.createElement() and passing in the name of the element object that should be created. Once element objects are created, they’re added to the appropriate parent container by calling its appendChild() method.

The final two tasks performed by the AlbumViewer control’s initialize() method are calling the base Sys.UI.Control class’s initialize() method and binding album data to the control. The Type class’s callBaseMethod() method calls the initialize() method of the base class. This method accepts the current object instance as well as the name of the method to call on the base class. Although not used by the AlbumViewer control, callBaseMethod() returns a reference to the base class’s ini tialize() method.

Now that you’ve been exposed to the initialize() method, let’s move on to a few other methods used by the AlbumViewer control to bind data, call web services, and perform additional functionality.

Defining Control Methods

The AlbumViewer control provides several ways for an end user to interact with the control to view albums, artists, and songs. Users can search for albums using all or part of the album name, highlight albums as they move their mouse, and click albums to view more data about an album. Each of these interactions requires event handlers to be written to handle the performed action. In addition to supporting user interactions, the AlbumViewer supports calling a web service to retrieve album data as well as data binding. All of this functionality is handled by methods defined within the AlbumViewer control.

Methods used by the control are shown in the following table. Methods that start with an underscore are considered private, although JavaScript doesn’t truly support access modifiers such as public or private.

Open table as spreadsheet

Method Name

Description

initialize

Initializes the control as well as the base class.

dispose

Cleans up resources used by the control such as event handlers.

set_data

Binds album data supplied by the client or by a web service to the control. Performs the bulk of the control’s functionality. This method is similar in purpose to the DataBind() method found on ASP.NET server controls.

_getAlbumNode

Retrieves a specific album object within the AlbumViewer control.

_invokeWebService

Calls a web service based upon values supplied by the ServicePath and ServiceMethod properties.

_onButtonClick

Handles the search button’s click event.

_onClick

Handles the click event for album divs in the control.

_onMouseOut

Handles the mouseout event for album divs in the control.

_onMouseOver

Handles the mouseover event for album divs in the control.

_onMethodComplete

Processes data returned from a web service call and binds the data by calling set_data().

_onMethodError

Handles errors returned from calls made to a web service.

_onPropertyChanged

Handles property changed events that occur as the ServicePath and ServiceMethod property values change.

_setStyle

Used to apply a CSS style to an album div as the user interacts with the AlbumViewer control.

The AlbumViewer control’s set_data() method performs the bulk of the work done by the control. It accepts a single parameter that contains the Album objects to bind to the control and display to the end user. Although Album objects aren’t the focus of this chapter, it’s important to note that many of the same steps followed when building a custom control are also applied to building custom client-side classes. Listing 11-7 shows a representation of the Album and Song classes consumed and bound by the set_data() method. These classes are used when a client script creates objects and passes them to the control’s set_data() method. When the AlbumViewer control calls a web service, the JSON Album objects returned from the service are used instead.

Tip 

The fields defined in the Song and Album class constructors aren’t prefixed with an underscore character (to simulate a private field) so that the objects are more in line with JSON objects returned from web service calls that have properties defined such as Title, Artist, ImageUrl, and Songs for the Album class. By matching up the Album class shown in Listing 11-7 with the objects exposed by the web service, the AlbumViewer control can work in a flexible manner with both types of objects.

Listing 11-7

image from book
  Type.registerNamespace("Wrox.ASPAJAX.Samples"); //#  Song Object # Wrox.ASPAJAX.Samples.Song = function(trackNumber,title) {     // Initialize as a base class.     Wrox.ASPAJAX.Samples.Song.initializeBase(this);     this.Title = title;     this.TrackNumber = trackNumber; }   //Define Album properties Wrox.ASPAJAX.Samples.Song.prototype = {     initialize: function() {        Wrox.ASPAJAX.Samples.Song.callBaseMethod(this,"initialize");     },     get_Title: function() {         /// <value type="String"></value>         if (arguments.length !== 0) throw Error.parameterCount();         return this.Title;     },     set_Title: function(value) {         var e = Function._validateParams(arguments,            [{name: "value", type: String}]);         if (e) throw e;         this.Title = value;     },     get_TrackNumber: function() {         /// <value type="String"></value>         if (arguments.length !== 0) throw Error.parameterCount();         return this.TrackNumber;     },     set_TrackNumber: function(value) {         var e = Function._validateParams(arguments,            [{name: "value", type: String}]);         if (e) throw e;         this.TrackNumber = value;     },     dispose: function() {         this.Title = null;         this.TrackNumber = null;         Wrox.ASPAJAX.Samples.Song.callBaseMethod(this, "dispose");     } } Wrox.ASPAJAX.Samples.Song.registerClass("Wrox.ASPAJAX.Samples.Song",    Sys.Component, Sys.IDisposable); //#  Album Object # Wrox.ASPAJAX.Samples.Album = function() {     // Initialize as a base class.     Wrox.ASPAJAX.Samples.Album.initializeBase(this);     this.Title;     this.Artist;     this.ImageUrl;     this.Songs = []; }   //Define Album properties Wrox.ASPAJAX.Samples.Album.prototype = {     initialize: function() {        Wrox.ASPAJAX.Samples.Album.callBaseMethod(this,"initialize");     },     get_Title: function() {         return this.Title;     },     set_Title: function(value) {         /// <value type="String"></value>         this.Title = value;     },     get_ImageUrl: function() {         return this.ImageUrl;     },     set_ImageUrl: function(value) {         /// <value type="String"></value>         this.ImageUrl = value;     },     get_Artist: function() {         return this.Artist;     },     set_Artist: function(value) {         /// <value type="String"></value>         this.Artist = value;     },     addSong: function(song)     {         /// <value type="Wrox.ASPAJAX.Samples.Song"></value>         if (Object.getTypeName(song) != "Wrox.ASPAJAX.Samples.Song")          {             var e = Error.argumentType("song", Object.getType(song),               Wrox.ASPAJAX.Samples.Song,"Wrox.ASPAJAX.Samples.Song required!");             e.popStackFrame();             throw e;         }         Array.add(this.Songs,song);     },     removeSong: function(song)     {         /// <value type="Wrox.ASPAJAX.Samples.Song"></value>         Array.remove(this.Songs,song);     },     get_Songs: function()     {         return this.Songs;     },     rateSong: function(song,rating)     {         throw Error.notImplemented("rateSong() has not yet been implemented");       },     dispose: function() {         this.Title = null;         this.Artist = null;         this.Songs = null;         this.ImageUrl = null;         Wrox.ASPAJAX.Samples.Album.callBaseMethod(this, "dispose");     } } Wrox.ASPAJAX.Samples.Album.registerClass("Wrox.ASPAJAX.Samples.Album",    Sys.Component, Sys.IDisposable); //Added to satisfy new notifyScriptLoaded() requirement if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded(); 
image from book

Looking through the code in Listing 11-7, you’ll see that the Album class has several properties, including Title, Artist, Songs, and ImageUrl. These properties are used by the AlbumViewer control to create new div element objects that are used to display album data. The AlbumViewer control’s set_data() method is responsible for iterating through Album and Song objects passed to the control by a page or by calls made to a web service and binding those objects to the control. It’s also responsible for clearing existing albums that are displayed when a user performs a new search and assigning styles to newly created DOM element objects.

The Album object binding process creates new div element objects as well as several others using docu- ment.createElement() and adds the div objects to the AlbumViewer control’s parent div container. set_data() also handles hooking up mouseout, mouseover, and click events for each album div object created to event handlers by using the $addHandlers() method discussed earlier. Listing 11-8 shows the complete code for the set_data() method.

Listing 11-8

image from book
  set_data: function(albums) {     var e = this.get_element();     //Clear any albums already showing     while (this._albumContainer.childNodes.length > 0) {         var child = this._albumContainer.childNodes[0];         this._albumContainer.removeChild(child);     }     //Handle case where no albums are available to bind     if (albums == null) {         var noRecords = document.createElement("SPAN");         noRecords.style.cssText = "font-weight:bold;";         noRecords.innerHTML = "No albums found.";         this._albumContainer.appendChild(noRecords);         return;     }     //Loop through albums     for (var i=0; i<albums.length; i++) {         var album = albums[i];         //Create Album Div         var id = this.get_element().id + "_Album" + i.toString();         var albumDiv = document.createElement("DIV");         albumDiv.id = id;         albumDiv.style.clear = "both";         albumDiv.className = this._AlbumDivOutCssClass;         this._albumContainer.appendChild(albumDiv);           //Attach Events         $addHandlers(albumDiv,            {"mouseover" : this._overHandler,            "mouseout" : this._outHandler,            "click" : this._clickHandler },            this);            //Create album title         var albumTitle = document.createElement("SPAN");         albumTitle.className = this._AlbumTitleCssClass;         albumTitle.innerHTML = album.Title;             albumDiv.appendChild(albumTitle);          //Create album artist          var albumArtist = document.createElement("SPAN");         albumArtist.className = this._AlbumArtistCssClass;         albumArtist.innerHTML = "&nbsp;-&nbsp;" + album.Artist;         albumDiv.appendChild(albumArtist);         //Create child content div                      childrenDiv = document.createElement("DIV");         childrenDiv.id = id + "_Children";         childrenDiv.style.display = "none";         albumDiv.appendChild(childrenDiv);         //Create Image div         var albumPicDiv = document.createElement("DIV");         var img = document.createElement("IMG");         img.src = album.ImageUrl;         albumPicDiv.style.paddingTop = "5px";         albumPicDiv.appendChild(img);         childrenDiv.appendChild(albumPicDiv);         //Create songs div         var albumSongsDiv = document.createElement("DIV");         var ul = document.createElement("UL");         albumSongsDiv.appendChild(ul);         //Loop through songs         var songs = album.Songs;         for (var j=0;j<songs.length;j++) {             var li = document.createElement("LI");             li.className = this._SongTitleCssClass;             li.innerHTML = songs[j].Title;             ul.appendChild(li);         }         childrenDiv.appendChild(albumSongsDiv);     } }      
image from book

Listing 11-9 shows how the AlbumViewer control’s private methods are used to handle events, locate album div nodes, assign CSS styles, and call web services. Each method is prefixed with an underscore character to convey that it’s private. Several of the methods such as _onMouseOver() and _onMouseOut handle style changes to album objects as the end user moves his or her mouse or clicks. Others perform more complex functionality such as calling web services and processing results or errors.

Listing 11-9

image from book
  _onMouseOver : function(evt) {     this._setStyle(evt,this._AlbumDivOverCssClass); }, _onMouseOut : function(evt) {    this._setStyle(evt,this._AlbumDivOutCssClass); }, _setStyle: function(evt,className) {    evt.stopPropagation();    this._getAlbumNode(evt.target).className = className;    return true; }, _getAlbumNode: function(node) {    var baseID = this.get_element().id + "_Album";                    var currElem = node;    while (!(currElem.id.startsWith(baseID) && !currElem.id.endsWith("_Children")))     {        if (currElem.parentNode) {            currElem = currElem.parentNode;        } else {            break;        }                    }    return currElem; },   _onClick : function(evt) {      var parent = this._getAlbumNode(evt.target);      var child = $get(parent.id + "_Children");      if (child.style.display == "block") {          child.style.display = "none";      } else {          child.style.display = "block";      }   },   _onButtonClick : function(evt) {      var searchText = $get(this.get_element().id + "_txtAlbumSearch").value;      this._invokeWebService(searchText);   }, _onMethodComplete : function(result, userContext, methodName) {     //Remove status label     this._removeStatusLabel();     // Bind returned data     this.set_data(result); }, _onMethodError : function(webServiceError, userContext, methodName) {     // Call failed     this._removeStatusLabel();     if (webServiceError.get_timedOut()) {         alert("Web Service call timed out.");     } else {         alert("Error calling Web Service: " +            webServiceError.get_statusCode() + " " + webServiceError.get_message());     } }, _removeStatusLabel: function() {     if (this._StatusLabel) {         this.get_element().removeChild(this._StatusLabel);         this._StatusLabel = null;     } }, _invokeWebService : function(searchText) {     //Add status label in case operation takes awhile     this._StatusLabel = document.createElement("DIV");     this._StatusLabel.style.cssText = "font-weight:bold;color:Navy;";     this._StatusLabel.innerHTML = "Calling Albums Web Service....";     this.get_element().appendChild(this._StatusLabel);     Sys.Net.WebServiceProxy.invoke(this._ServicePath, this._ServiceMethod, false,         { prefixText:searchText, count:10 },         Function.createDelegate(this, this._onMethodComplete),          Function.createDelegate(this, this._onMethodError)); }, _onPropertyChanged : function(sender,args) {     var propname = args.get_propertyName();     if (propname == "ServicePath" || propname === "ServiceMethod") {         var searchText = null;         if (this._ShowSearchBox) {             $get(this.get_element().id + "_txtAlbumSearch").value;         }         this._invokeWebService(searchText);     } } 
image from book

The _onPropertyChanged() method, shown in Listing 11-9, handles propertychanged events raised by calling the raisePropertyChanged() method discussed in the “Defining Properties” section of this chapter. Recall that _onPropertyChanged() is defined as the event handler by calling the add_propertyChanged() method in the AlbumViewer control’s initialization phase shown in Listing 11-6.

When the ServicePath or ServiceMethod properties change, _onPropertyChanged() is automatically called and used to invoke an album data web service. This allows fresh data to be loaded into the control in cases where the consumer of the control wants to dynamically change information about the web service. How do you actually call a web service in a custom control when the location of the service hasn’t been assigned to the ScriptManager’s Services property, though? Let’s take a closer look.

Calling Web Services with a Custom Control

The AlbumViewer control’s set_data() method can receive data from a client-side script or from a .NET web service. Calling a web service can be a tricky process, as different browsers having different XmlHttp objects must be taken into account. Fortunately, the ASP.NET AJAX script library has cross-browser functionality built in that makes it straightforward to make web service calls and retrieve data without worrying about the user agent.

The _invokeWebService() method shown in Listing 11-9 calls an album viewer service for the AlbumViewer control (the album web service is available in the download code for this chapter). The method accepts a single parameter that represents the album title the end user would like to search on. When called, _invokeWebService() adds a div tag to the AlbumViewer control to notify the end user that the search has begun. Because the call is made asynchronously, this provides a nice visual clue for the user so that he or she is aware that the request is being processed. The script library’s Sys.Net .WebServiceProxy class is then used to make the web service request by calling its invoke() method:

 Sys.Net.WebServiceProxy.invoke(this._ServicePath, this._ServiceMethod,   false, { prefixText:searchText, count:10 },   Function.createDelegate(this, this._onMethodComplete),   Function.createDelegate(this, this._onMethodError));

The invoke() method works across all major browsers and creates a JavaScript proxy, calls the web service, and processes the results. Several parameters can be passed to invoke(), including the path to the web service, the name of the Web Method to call; whether or not the call should be made using HttpGet; the parameter data to be passed to the service (parameter data is passed using JSON); a delegate that points to the callback method that handles the response that is received (_onMethodComplete()); and a delegate that points to an error handler callback method (_onMethodError()).

Once the web service is called and a response is returned, _onMethodComplete() is used to handle the response data. This method accepts one or more Album objects returned from the web service, any context data associated with the call, and the name of the Web Method that was called:

 _onMethodComplete : function(result, userContext, methodName) {     //Remove status label     this._removeStatusLabel();     // Bind returned data     this.set_data(result); }

The album data returned from the web service is bound to the AlbumViewer control by calling the set_data() method discussed earlier.

Web service calls can fail when the service is unavailable or when invalid data is passed to or from the service. Fortunately, the Sys.Net.WebServiceProxy.invoke() method used to call the service allows a callback method to be specified that handles errors. The _onMethodError() method handles any errors returned by the web service or errors generated if an invalid service path or Web Method name was used.

 _onMethodError : function(webServiceError, userContext, methodName) {     // Call failed     this._removeStatusLabel();     if (webServiceError.get_timedOut()) {         alert("Web Service call timed out.");     } else {         alert("Error calling Web Service: " +            webServiceError.get_statusCode() + " " +             webServiceError.get_message());     } }

If an error occurs and the callback method is invoked, a Sys.Net.WebServiceError object is passed as the first parameter, along with call context data and the name of the Web Method that was initially called. Because the AlbumViewer control can’t display album data when a web service error occurs, it displays an alert to the user that contains the status code of the error as well as the error message. Before displaying error data, a check is made to ensure that the call didn’t time out by calling get_timedOut(). If the call returns true, the service was unavailable. The following table lists the properties of the Sys.Net.WebServiceError class.

Open table as spreadsheet

Property Name

Description

exception

Gets the type of exception that occurred. A string is returned.

message

Gets the error message associated with the current error.

statusCode

Gets the status code of the current HTTP response.

stackTrace

Gets the stack trace returned by the server.

timedOut

Gets a Boolean value that indicates if the web service call timed out.

Disposing of Control Resources

Because JavaScript is a garbage-collected language, many complexities associated with cleaning up variables, arrays, and other objects are greatly simplified and typically don’t require any code to be written by a developer. However, it’s important to note that specific types of resources must be explicitly cleaned up to avoid memory leaks caused by circular references. DOM event handlers used in a control, as well as processes (such as timers) that may be used internally, should be explicitly cleaned up in your custom controls.

By deriving from base classes such as Sys.Component and Sys.UI.Control, access to a method named dispose() is immediately available in custom controls that can be overridden. The AlbumViewer control uses the dispose() method shown in Listing 11-10 to cleanup event handlers and call it’s base class’s dispose() method.

Listing 11-10

image from book
  dispose: function() {     var e = this.get_element();     this.remove_propertyChanged(this._propertyChanged);     if (this._ShowSearchBox) $removeHandler($get(e.id +        "_btnAlbumSearch"),"click",this._btnClickHandler);     //Remove event handlers     for (var i=0;i<e.childNodes.length;i++) {         var albumDiv = e.childNodes[i];         $clearHandlers(albumDiv);     }     this._overHandler = null;     this._outHandler = null;     this._clickHandler = null;     this._btnClickHandler = null;     this._propertyChanged = null;     Wrox.ASPAJAX.Samples.AlbumViewer.callBaseMethod(this, "dispose"); }   
image from book

In cases where all event handlers attached to a given DOM element need to be removed, the ASP.NET AJAX script library’s $clearHandlers() method can be called. When a specific event handler needs to be removed the $removeHandler() method can be called. Property changed event handlers can be removed by calling the remove_propertyChanged() method.

Once dispose() is called and event handlers are cleaned up, a call is made to the base Sys.UI.Control class’s dispose() method via callBaseMethod(), which raises a disposing event and unregisters the control from the application. Removal occurs as a result of the following calls made in the Sys.Component class’s dispose() method:

 Sys.Application.unregisterDisposableObject(this); Sys.Application.removeComponent(this);

While you don’t have to override the dispose() method on every client-side object or control used in an ASP.NET AJAX application, it’s important to override it in cases where event handlers or processes are used by a control and need to be cleaned up to prevent memory issues.

Registering a Custom Control Class

Now that you’ve seen how to create a custom control’s constructor, properties, and methods, let’s examine how the control can inherit from a base class and register itself with the ASP.NET AJAX framework. Although you can’t use the VB.NET Inherits keyword or the C# colon character to handle inheritance, you can call the registerClass() method available to all ASP.NET AJAX types through the Type class.

The registerClass() method accepts three parameters. The first parameter is a string containing the fully qualified name of the class to register with the framework. The parameter value should include the namespace and class name. The second parameter represents the base class that the control should inherit from. The third parameter specifies one or more interfaces that the control implements. This parameter is optional. Following is an example of using registerClass() to register the AlbumViewer control. Notice that the control derives from Sys.UI.Control.

 Wrox.ASPAJAX.Samples.AlbumViewer.registerClass( "Wrox.ASPAJAX.Samples.AlbumViewer", Sys.UI.Control);

As with any client-side script used by the ASP.NET AJAX framework, the AlbumViewer control adds the following line of code at the bottom the script file to notify the framework when the entire script has been loaded and is ready to be used:

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

Creating a Client-Side Control Instance

Once a custom client-side control has been written, it needs to be instantiated by an application. To do this, the control’s script must be referenced by the page either by using the standard script tag or by declaring it in the ScriptManager’s Scripts property. Once the control script is available for use in a page, the control can be instantiated by using the ASP.NET AJAX script library’s $create() method. $create() is an alias for the Sys.Component class’s create() method.

The $create() method accepts several different parameters as shown in the following table:

Open table as spreadsheet

Parameter

Description

type

The type of component or control that should be created.

properties

A JSON object containing properties and values that should be passed to the control. This parameter is optional.

events

A JSON object containing events and associated handlers defined through delegates. This parameter is optional.

references

A JSON object containing references to other control properties.

element

The DOM element that the control uses as its parent container. Output rendered by the control will be placed in this element. This parameter is optional.

Listing 11-11 (AlbumViewerClientBinding.aspx in the sample code) shows how the $create() method can be used to instantiate a new instance of the AlbumViewer control and hook it to a div container. When the pageLoad() event handler is called, two new Album objects are created and added to an array. The $create() method is then used to instantiate the AlbumViewer control and attach it to a div element named divAlbums. Initial CSS property values used by the control, as well as the Album objects to bind, are passed into $create() using a JSON object. Because the control’s initialize() method calls set_data(), the two albums are iterated through and added into the parent container automatically as the control initializes and loads.

Listing 11-11

image from book
  <%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server">     <title>Album Viewer: Client-side Control</title>     <link href="Style/Styles.css" rel="stylesheet" type="text/css" />     <script type="text/javascript">         function pageLoad()          {             //Create albums             var album = new Wrox.ASPAJAX.Samples.Album();             album.set_Artist("Incubus");             album.set_Title("Light Grenades");             album.set_ImageUrl("Images/Incubus_Light_Grenades.jpg");             album.addSong(new Wrox.ASPAJAX.Samples.Song(1,"QuickSand"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(2,              "A Kiss to Send Us Off"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(3,"Dig"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(4,"Anna Molly"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(5,"Love Hurts"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(6,"Light Grenades"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(7,"Earth to Bella Pt 1"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(8,"Oil and Water"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(9,"Diamonds and Coal"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(10,"Rogues"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(11,"Paper Shoes"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(12,"Earth to Bella Pt 1"));             album.addSong(new Wrox.ASPAJAX.Samples.Song(13,"Pendulous Threads"));             var album2 = new Wrox.ASPAJAX.Samples.Album();             album2.set_Artist("The Killers");             album2.set_Title("Sam's Town");             album2.set_ImageUrl("Images/The_Killers_Sams_Town.jpg");             album2.addSong(new Wrox.ASPAJAX.Samples.Song(1,"Sam 's Town "));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(2,"Enterlude"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(3,"When You Were Young"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(4,"Bling"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(5,"For Reasons Unknown"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(6,"Read My Mind"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(7,"Uncle Johnny"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(8,"Bones"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(9,"My List"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(10,"The River is Wild"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(11,              "Why Do I Keep Counting"));             album2.addSong(new Wrox.ASPAJAX.Samples.Song(12,"Exitlude"));             var albums = [];             Array.add(albums,album);             Array.add(albums,album2);             //Create AlbumViewer object and pass in albums             $create(Wrox.ASPAJAX.Samples.AlbumViewer,                {Albums: albums,AlbumDivOverCssClass: "AlbumDivOver",                 AlbumDivOutCssClass: "AlbumDivOut",                 AlbumTitleCssClass: "AlbumTitle",                 AlbumArtistCssClass: "AlbumArtist",                 SongTitleCssClass: "SongTitle"},                 null, //Events and handlers                null, //References to other control properties                $get("divAlbums"));  //parent container DOM element         }     </script> </head> <body>     <form  runat="server">     <asp:ScriptManager  runat="Server">         <Scripts>             <asp:ScriptReference Path="~/Scripts/Album.js" />             <asp:ScriptReference Path="~/Scripts/AlbumViewerStandAlone.js" />         </Scripts>     </asp:ScriptManager>     <h2>Album Viewer</h2>     <div  style="width:500px;">             </div>     </form> </body> </html> 
image from book

A web service can also automatically be called when an AlbumViewer control is created to retrieve album data, rather than creating the album objects in the page as in Listing 11-11. This is done by passing the path to the web service along with the name of the Web Method to call in the JSON object that contains properties and values. An example of using the $create() method to pass ServicePath and ServiceMethod property values to the control is shown in Listing 11-12 (AlbumViewerWSBinding.aspx in the sample code). The ShowSearchBox property is also set to a value of true so that the album search textbox and button controls are created and shown. The output generated by the control is shown in Figure 11-1.

Listing 11-12

image from book
  function pageLoad()  {                 $create(Wrox.ASPAJAX.Samples.AlbumViewer,         {ShowSearchBox: true,AlbumDivOverCssClass: "AlbumDivOver",          AlbumDivOutCssClass: "AlbumDivOut",AlbumTitleCssClass: "AlbumTitle",          AlbumArtistCssClass: "AlbumArtist",SongTitleCssClass: "SongTitle",          ServicePath: "AlbumService.asmx",ServiceMethod: "GetAlbums"},          null,         null,         $get("divAlbums")); } 
image from book

Building a Server-Side ASP.NET AJAX Control

Creating custom ASP.NET AJAX client-side controls is a fairly intensive exercise in JavaScript. Calling control methods, handling events, and even instantiating the control itself requires JavaScript code to be written. While some ASP.NET developers understand how to write JavaScript code, others may not have a complete understanding of the language and may prefer to work with server-side objects that can be manipulated using VB.NET or C#. They may prefer to be shielded from JavaScript code whenever possible.

Because server-side controls can output both HTML and JavaScript, they’re good candidates for use with ASP.NET AJAX client-side controls. Developers familiar with defining and interacting with server-side controls can use an AJAX-enabled server-side control without having to worry about the client-side complexities. A nice "feature" of ASP.NET AJAX server-side controls is that once you have created the client-side script that handles all of the HTML output and event handling (as with the AlbumViewer control shown in this chapter), going a little further to create a server-side control is fairly straightforward and requires very little VB.NET or C# code. After all, most of the control’s functionality is performed by the client-side script.

There are a few different paths to creating an ASP.NET AJAX server control. To choose the correct path, you need to decide what type of control you want to create. Will the control be visual, or is it something that runs in the background like a timer? Will the control extend functionality of another control? The answer to these questions determines how the server-side control is built. For example, if you want to extend an existing control’s functionality by adding AJAX capabilities to it, you’ll want to derive from the System.Web.UI.ExtenderControl class. If you’re writing a visual control from scratch that generates HTML and JavaScript, you’ll want to derive from System.Web.UI.WebControl and implement the IScriptControl interface.

The AlbumViewer server-control discussed in the remainder of this chapter derives from WebControl, implements the IScriptControl interface, and leverages the AlbumViewer client-side control script discussed earlier in this chapter. It lives in an ASP.NET control library project within Visual Studio .NET 2005. Before getting into specifics about the server-side control, let’s examine how to embed a client-side control script into a server-side control assembly.

Embedding Scripts in Server-Side Controls

ASP.NET AJAX server-side controls perform two main purposes. First, they output a client-side control script to the browser that handles the bulk of the functionality required by the control. This includes interaction with the end user, event handling, data binding, calls to remote data source, and more. Second, they define properties that can be set on the server-side and passed to the client-side script. This allows ASP.NET developers to define properties using standard ASP.NET control syntax without worrying about writing custom JavaScript code or even using the ASP.NET AJAX script library’s $create() method to instantiate the control.

Client-side ASP.NET AJAX control scripts can be embedded directly in ASP.NET server control assemblies by marking the scripts as an embedded resource. First, add a script file to an ASP.NET server control project (officially called a Class Library project in Visual Studio .NET 2005) and add all of the client-side functionality to it. Next, right-click the script in the Visual Studio .NET 2005 Solution Explorer, select Properties and set the Build Action property to a value of Embedded Resource. This will cause the compiler to embed the script file directly in the control’s assembly. Figure 11-2 shows the Properties window for the embedded script file.

image from book
Figure 11-2

To make the script accessible, add the following assembly attribute immediately above the server con-trol’s namespace definition in code file (C# is shown below):

 [assembly: System.Web.UI.WebResource("AlbumViewer.AlbumViewer.js",   "text/javascript")]

This makes the script accessible through an HttpHandler built into ASP.NET AJAX called ScriptResource.axd and avoids having to deploy the physical script file along with the server control assembly, which can greatly simplify deployment and maintenance. The WebResource attribute’s constructor accepts the name of the resource followed by the content-type of the resource. The name given to the resource is the assembly name followed by the name of the script (AlbumViewer.AlbumViewer.js, in this case). Although JavaScript is used in this example, images can also be embedded in custom server controls using the same technique. One disadvantage of embedding scripts or images into assemblies is that they can’t be modified on the web server; you would have to rebuild the assembly with the modified objects and redeploy it to the web server.

Creating a Control Class and Implementing IScriptControl

The AlbumViewer ASP.NET AJAX server-side control allows a developer to set properties on the control using familiar ASP.NET server control syntax. After property values are set, the control handles out-putting the client-side control script that’s embedded as a resource and creates a new instance of the control using the $create() method. Property values set on the server side are passed to the $create() method through a JSON object that is passed as a parameter to $create().

Because the AlbumViewer server-side control doesn’t extend an existing control, it inherits from System.Web.UI.WebControl. Had the control extended an existing control, such as a Textbox, it would inherit from System.Web.UI.ExtenderControl located in the System.Web.Extensions.dll assembly that’s installed with the ASP.NET AJAX framework. In addition to inheriting from WebControl, the AlbumViewer control also implements the IScriptControl interface located in the System.Web.UI namespace of the System.Web.Extensions.dll assembly. The IScriptControl interface defines two methods, shown in the following table:

Open table as spreadsheet

Method

Description

GetScriptDescriptors

Gets a collection of script descriptors that represent JavaScript components on the client. This method is used to map server-side control properties to client-side control properties.

GetScriptReferences

Gets a collection of ScriptReference objects that define script resources required by the control. This method is used to identify one or more embedded script resources in a control assembly and output them to the client.

Listing 11-13 shows the shell for the AlbumViewer server-side control. Notice that it derives from WebControl and implements IScriptControl.

Listing 11-13

image from book
  using System; using System.Configuration; using System.Collections.Generic; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; //Client-side script file embedded in assembly [assembly: System.Web.UI.WebResource("AlbumViewer.AlbumViewer.js",    "text/javascript")] namespace Wrox.ASPAJAX.Samples {     public class AlbumViewer : WebControl, IScriptControl     {         //Properties and methods         IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()         {             return GetScriptReferences();         }         IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()         {             return GetScriptDescriptors();         }     } }  
image from book

The two implemented methods shown in Listing 11-13 satisfy the IScriptControl interface and delegate processing to two additional methods included in the class named GetScriptReferences() and GetScriptDescriptors(). These two methods are shown in Listing 11-14.

Listing 11-14

image from book
  protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors() {     ScriptControlDescriptor descriptor =        new ScriptControlDescriptor("Wrox.ASPAJAX.Samples.AlbumViewer",        this.ClientID);     descriptor.AddProperty("AlbumDivOverCssClass", this.AlbumDivOverCssClass);     descriptor.AddProperty("AlbumDivOutCssClass", this.AlbumDivOutCssClass);     descriptor.AddProperty("AlbumTitleCssClass", this.AlbumTitleCssClass);     descriptor.AddProperty("AlbumArtistCssClass", this.AlbumArtistCssClass);     descriptor.AddProperty("SongTitleCssClass", this.SongTitleCssClass);     descriptor.AddProperty("ServicePath", this.ServicePath);     descriptor.AddProperty("ServiceMethod", this.ServiceMethod);     descriptor.AddProperty("ShowSearchBox", this.ShowSearchBox);     //could also use: yield return descriptor;     return new ScriptDescriptor[] { descriptor };   } protected virtual IEnumerable<ScriptReference> GetScriptReferences() {     ScriptReference r = new ScriptReference();     r.Assembly = "AlbumViewer";     r.Name = "AlbumViewer.AlbumViewer.js";     //could also use:  yield return r;     return new ScriptReference[] { r };   } 
image from book

The GetScriptReferences() method shown in Listing 11-14 creates a ScriptReference object and identifies the script resource that the server control needs to send to the client (AlbumViewer.AlbumViewer.js, in this case). The GetScriptDescriptors() method creates a new ScriptControlDescriptor object that maps server-side control properties to client-side control properties. This is necessary so that the property values set on the control by a developer can be passed to the $create() method used to instantiate the client-side control. The properties defined in the AlbumViewer server-side control are the same ones discussed earlier in the chapter for the client-side control (AlbumDivOverCssClass, AlbumDivOutCssClass, and so on).

Overriding Render and OnPreRender

Once the AlbumViewer control’s script resources are identified and server-side properties are mapped to client-side properties, the control overrides the OnPreRender() method of WebControl and retrieves a reference to the ScriptManager object in the page. The ScriptManager object is then used to register the AlbumViewer control as a script control by calling the ScriptManager’s RegisterScriptControl() method.

The AlbumViewer server-side control also overrides the Render() method of WebControl to register the property mappings (referred to as script descriptors) returned from the GetScriptDescriptors() method. This serves the purpose of passing the control’s property values defined on the server-side to the $create() method generated automatically by the ScriptManager control. Listing 11-15 shows the OnPreRender() and Render() methods.

Listing 11-15

image from book
  ScriptManager _ScriptManager; protected override void OnPreRender(EventArgs e) {     if (!this.DesignMode)     {         _ScriptManager = ScriptManager.GetCurrent(Page);         if (_ScriptManager == null)             throw new HttpException("A ScriptManager control is required " +             "in the page.");         _ScriptManager.RegisterScriptControl(this);     }     base.OnPreRender(e); } protected override void Render(HtmlTextWriter writer) {     if (!this.DesignMode)     {         _ScriptManager.RegisterScriptDescriptors(this);     }     base.Render(writer); } 
image from book

By registering itself with the ScriptManager control, the script associated with the custom AlbumViewer control will automatically be referenced in the page where the control is used. A call to the client-side $create() method will also be added automatically into the page as it renders. This allows developers to use the AlbumViewer control without writing a single line of JavaScript yet benefit from all of the client-side functionality provided by the control.

Listing 11-16 shows the complete code for the AlbumViewer server-side control.

Listing 11-16

image from book
  using System; using System.Configuration; using System.Collections.Generic; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; //Client-side script file embedded in assembly [assembly: System.Web.UI.WebResource("AlbumViewer.AlbumViewer.js",   "text/javascript")] namespace Wrox.ASPAJAX.Samples {     public class AlbumViewer : WebControl, IScriptControl     {                            string _AlbumDivOverCssClass;         string _AlbumDivOutCssClass;         string _AlbumTitleCssClass;         string _AlbumArtistCssClass;         string _SongTitleCssClass;         string _ServicePath;         string _ServiceMethod;         bool _ShowSearchBox;         ScriptManager _ScriptManager;         #region Properties         public string AlbumDivOverCssClass {     get { return _AlbumDivOverCssClass; }     set { _AlbumDivOverCssClass = value; } } public string AlbumDivOutCssClass {     get { return _AlbumDivOutCssClass; }     set { _AlbumDivOutCssClass = value; } } public string AlbumTitleCssClass {     get { return _AlbumTitleCssClass; }     set { _AlbumTitleCssClass = value; } } public string AlbumArtistCssClass {     get { return _AlbumArtistCssClass; }     set { _AlbumArtistCssClass = value; } } public string SongTitleCssClass {     get { return _SongTitleCssClass; }     set { _SongTitleCssClass = value; } } public string ServicePath {     get { return _ServicePath; }     set { _ServicePath = value; } } public string ServiceMethod {     get { return _ServiceMethod; }     set { _ServiceMethod = value; } } public bool ShowSearchBox {     get { return _ShowSearchBox; }     set { _ShowSearchBox = value; } } #endregion public AlbumViewer() {} protected override void OnPreRender(EventArgs e) {     if (!this.DesignMode)     {         _ScriptManager = ScriptManager.GetCurrent(Page);         if (_ScriptManager == null)             throw new HttpException("A ScriptManager control is " +             "required in the page.");         _ScriptManager.RegisterScriptControl(this);     }     base.OnPreRender(e); } protected override void Render(HtmlTextWriter writer) {     if (!this.DesignMode)     {         _ScriptManager.RegisterScriptDescriptors(this);     }     base.Render(writer); } protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors() {     ScriptControlDescriptor descriptor =        new ScriptControlDescriptor("Wrox.ASPAJAX.Samples.AlbumViewer",        this.ClientID);     descriptor.AddProperty("AlbumDivOverCssClass",        this.AlbumDivOverCssClass);     descriptor.AddProperty("AlbumDivOutCssClass",        this.AlbumDivOutCssClass);     descriptor.AddProperty("AlbumTitleCssClass", this.AlbumTitleCssClass);     descriptor.AddProperty("AlbumArtistCssClass",       this.AlbumArtistCssClass);     descriptor.AddProperty("SongTitleCssClass", this.SongTitleCssClass);     descriptor.AddProperty("ServicePath", this.ServicePath);     descriptor.AddProperty("ServiceMethod", this.ServiceMethod);     descriptor.AddProperty("ShowSearchBox", this.ShowSearchBox);     //could also use: yield return descriptor;     return new ScriptDescriptor[] { descriptor };   } protected virtual IEnumerable<ScriptReference> GetScriptReferences() {     ScriptReference r = new ScriptReference();     //Use following when client-side script      //embedded in custom control:     //reference.Path = this.ResolveClientUrl("~/Scripts/AlbumViewer.js");     r.Assembly = "AlbumViewer";     r.Name = "AlbumViewer.AlbumViewer.js";     //could also use:  yield return r;     return new ScriptReference[] { r };           }         IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()         {             return GetScriptReferences();         }         IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()         {             return GetScriptDescriptors();         }     } } 
image from book

Now that you’ve seen the pieces required to build a custom ASP.NET AJAX server control, let’s examine how to use one in an ASP.NET page.

Using a Custom ASP.NET AJAX Control in an ASP.NET Page

Once the AlbumViewer server-side control project is compiled and the generated assembly is referenced by an ASP.NET website project, the control can be registered in an ASP.NET page by using the Register directive:

 <%@ Register Assembly="AlbumViewer" Namespace="Wrox.ASPAJAX.Samples"     TagPrefix="ajax" %>

This directive references the assembly name (AlbumViewer), the namespace that the control resides in (Wrox.ASPAJAX.Samples), as well as the tag prefix that should be used to define the control in the ASP.NET page (ajax). Once registered, this control can be used in the page just the same as any other ASP.NET server control. You merely need to define the tag prefix and class name followed by all of the necessary properties in the Register directive. An example of adding the AlbumViewer control into a page is shown as follows:

 <ajax:AlbumViewer  runat="server" ShowSearchBox="true"   AlbumDivOverCss   AlbumDivOutCss AlbumTitleCss   AlbumArtistCss SongTitleCss   ServicePath="AlbumService.asmx" ServiceMethod="GetAlbums" />

Once the page is called by an end user through a browser, the necessary client-side script code used by the AlbumViewer control will automatically be added to the rendered output by the ScriptManager control. A portion of the output generated by the ScriptManager that instantiates the AlbumViewer control is shown in Listing 11-17.

Listing 11-17

image from book
  <script type="text/javascript"> <!-     Sys.Application.initialize();     Sys.Application.add_init(function() {         $create(Wrox.ASPAJAX.Samples.AlbumViewer,               {"AlbumArtistCssClass":"AlbumArtist",            "AlbumDivOutCssClass":"AlbumDivOut",            "AlbumDivOverCssClass":"AlbumDivOver",            "AlbumTitleCssClass":"AlbumTitle",            "ServiceMethod":"GetAlbums",            "ServicePath":"AlbumService.asmx",            "ShowSearchBox":true,            "SongTitleCssClass":"SongTitle"},             null, null,             $get("divAlbums"));         }); // --> </script> 
image from book

The AlbumViewer.js script embedded in the control is automatically referenced through a script tag embedded into the page. The script tag’s src attribute points to a ScriptResource.axd HttpHandler that will extract the script from the server-side assembly where it was embedded and send it down to the browser:

 <script    src="/books/2/538/1/html/2//Chapter11/ScriptResource.axd?d=eyWA0k6p8cJT5ZxERTSqjh74z_7LL        XVOadc8XgiZ-JKa85A2IV0dD9FkMF5UiS40&amp;t=633048513771318000"    type="text/javascript"> </script>




Professional ASP. NET 2.0 AJAX
Professional ASP.NET 2.0 AJAX (Programmer to Programmer)
ISBN: 0470109629
EAN: 2147483647
Year: 2004
Pages: 107

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net