The Ajax Library Control Class

Controls can be granular and represent a single user interface component such as a text box or a button, or they can be built as application controls and be based on container elements such as the HTML DIV or SPAN elements. For example, I might define a control to provide functionality for an input element (as you’ll see in the next code sample) that will provide a discrete control implementation. However, when implementing major functionality, such as usage reports, user management, or the main collaborative user interface, I create a composite control much like the case study we’ll examine at the end of the chapter.

Controls can contain multiple child controls, and child controls can bubble up events to parent controls. Typically, you should develop a library of generic controls that can be bound to DOM elements and back-end data sources.

Before we look at control examples, let’s examine the Sys.UI.Control class as implemented in the AJAX library. Properties of the Control class are listed in Table 8-5, where properties are defined by get_ and set_ methods. Control adds properties and methods to the Component base class that manipulate the DOM element and include event-bubbling methods that allow child controls to bubble up events to parent controls. Methods of the Control class are defined in Table 8-6. Finally, the Control class contains the events defined in Sys.Component, and these are listed in Table 8-7. Events are handled by using the add_ and remove_ methods for each event to add or remove event handlers.

Table 8-5. Control Class Properties

Name

Description

element

Gets the DOM element that the Control instance is based on (read-only).

id

Gets the ID of the element (read-only).

parent

Gets or sets the parent control. If no parent control is explicitly set, get_ parent searches up the DOM hierarchy for the next control.

visibilityMode

Gets or sets the visibility mode of the control, either VisibilityMode.hide or VisibilityMode.collapse. VisibilityMode.hide specifies that the hidden element takes up space on the page and uses the style visibility=’hidden’ for hidden elements. VisibilityMode.collapse uses the style display=’none’, and the hidden element uses no space on the page.

visible

Indicates whether the control’s DOM element is visible or not. Setting the control to hidden uses the visibilityMode property to determine the style to use.

events

Returns an instance of the Sys.EventHandlerList for the component (read-only).

isInitialized

Returns true if the control is initialized, otherwise false (read-only).

isUpdating

Returns true if the control is currently updating itself, otherwise false (read-only).

Table 8-6. Control Class Methods

Name

Description

beginUpdate

Called to set isUpdating to true. Called from the beginning of the $create method (Sys.Component.create).

endUpdate

Called to set isUpdating to false. Called from the end of the $create method (Sys.Component.create). Calls initialize if isInitialized is false, and then calls the updated method.

updated

Called when the component is updated. Used to implement any custom post-update or post-initialization logic.

initialize

Used to initialize the control.

addCssClass

Adds a CSS class to the DOM element.

removeCssClass

Removes a CSS class from the DOM element.

toggleCssClass

Toggles a CSS class on the DOM element.

onBubbleEvent

Handles events bubbled from child controls.

raiseBubbleEvent

Raises an event to parent controls.

raisePropertyChanged

Raises a propertyChanged event for a specific property.

Table 8-7. Control Class Events

Name

Description

propertyChanged

Raised when a property is changed.

disposing

Raised when the control is disposing.

As I discussed in the section "Implementing Custom Behaviors with the AJAX Library," the main distinction between the Control and the Behavior classes is that an instance of Control can be defined only once per DOM element. To understand this concept, it’s best to look at the source code for the Control class’s constructor.

Sys.UI.Control = function Sys$UI$Control(element) {
    if (typeof (element.control) != 'undefined')
        throw Error.invalidOperation(Sys.Res.controlAlreadyDefined);
    Sys.UI.Control.initializeBase(this);
    this._element = element;
    element.control = this;
}

First the control saves a reference to the DOM element as this._element (exposed through the element property methods) and then adds a reference to itself to the DOM element’s control expando property. This expando property is used by other methods that search the DOM for controls, and it’s also used to ensure that only one Control instance is defined per DOM element. Only one Control instance can be defined per DOM element because the instance represents a one-to-one relationship between the script control and the DOM control. If you try to create an additional control based on the same DOM element, an error is thrown.

Because the control can be accessed through the DOM element’s .control expando property, the control can easily be accessed by code through a reference to the DOM element. For example, the following code demonstrates how a control can be accessed through script from a reference to its DOM element, given that an HTML INPUT element ("passwordInput") exists as the DOM element of a Sys.UI.Control:

var inputElement = $get('passwordInput'),
var inputControl = inputElement.control;

Note

Note

An expando property describes an arbitrary property that is applied to a DOM element through script.

The Control class contains the read-only properties element and id that are used with the DOM element. You’ll use the element property method frequently when writing custom controls. Following are the element and id property methods, which simply return the element passed into the constructor (or its ID).

// Methods from Control defined in MicrosoftAjax.js:
function Sys$UI$Control$get_element() {
    return this._element;
}
function Sys$UI$Control$get_id() {
    if (!this._element) return '';
    return this._element.id;
}

If you create a control that is added to a page as part of a composite control, you might want access to the control’s parent. The parent is the next Sys.UI.Control instance up in the DOM hierarchy. For example, you might want to get the parent control when working with a custom gridview control that contains row controls. The Control class provides support for a parent relationship through the parent property. If the parent control is explicitly set, it returns with the get_parent method. If the parent has not been set, the code walks the DOM hierarchy until it locates a DOM element with a control expando property. The following code sample shows get_parent and set_parent:

// Methods from Control defined in MicrosoftAjax.js:
function Sys$UI$Control$get_parent() {
    if (this._parent) return this._parent;
    if (!this._element) return null;
    var parentElement = this._element.parentNode;
    while (parentElement) {
        if (parentElement.control) {
            return parentElement.control;
        }
        parentElement = parentElement.parentNode;
    }
    return null;
}
function Sys$UI$Control$set_parent(value) {
    var parents = [this];
    var current = value;
    while (current) {
        if (Array.contains(parents, current)) throw
            Error.invalidOperation(Sys.Res.circularParentChain);
        parents[parents.length] = current;
        current = current.get_parent();
    }
    this._parent = value;
}

Finally, the Control class contains methods for working with the DOM element. The following methods are defined in Sys.UI.Control as wrapper methods for Sys.UI.DomElement methods. These methods can be used to set visibility and to add, remove, or toggle CSS classes.

function Sys$UI$Control$get_visibilityMode() {
    return Sys.UI.DomElement.getVisibilityMode(this._element);
}
function Sys$UI$Control$set_visibilityMode(value) {
    Sys.UI.DomElement.setVisibilityMode(this._element, value);
}
function Sys$UI$Control$get_visible() {
    return Sys.UI.DomElement.getVisible(this._element);
}
function Sys$UI$Control$set_visible(value) {
    Sys.UI.DomElement.setVisible(this._element, value)
}
function Sys$UI$Control$addCssClass(className) {
    Sys.UI.DomElement.addCssClass(this._element, className);
}
function Sys$UI$Control$removeCssClass(className) {
    Sys.UI.DomElement.removeCssClass(this._element, className);
}
function Sys$UI$Control$toggleCssClass(className) {
    Sys.UI.DomElement.toggleCssClass(this._element, className);
}

Creating a Custom Control with the AJAX Library

Like the Behavior class, the Control class is an abstract class that provides functionality for working with a DOM element. To use controls in your application, you must develop your own control implementations that inherit from Sys.UI.Control. In this section we’ll start with the Visual Studio template for the AJAX Client Control and develop a fully functional control based on the INPUT DOM element with properties, events, and custom functionality.

A common requirement for input elements in Web applications is that they respond to the Enter button on the keyboard so that a user can perform an action without having to click a button with the mouse. This type of functionality can improve the usability of the application by requiring fewer clicks. In the following code samples, we’ll create an input control that extends the capability of the HTML input text box by adding support for the Enter button and simple validation. We can also provide a tooltip for these controls by using the tooltip behavior created earlier in the chapter. This control can be used throughout an application and in multiple contexts, wherever you have a need for simple validation and support for the Enter button.

We’ll start with a simple Web page containing the ScriptManager control and an empty JavaScript library. The test Web page can be created using the AJAX Web Form template in Visual Studio. To create the JavaScript library, you can use the AJAX Client Control template. The template creates the JavaScript namespace by using the name of the Web application project it’s created in, but a simple search and replace can be used to get a good starting point. Example 8-5 shows a JavaScript class generated by the AJAX Client Control template. As I did with the AJAX Client Behavior template, I replaced the default JavaScript namespace after the initial code was generated to use SOAjax.Controls.

Tip

Tip

You can use the code-generation tool CodeSmith to create JavaScript control instances with little effort. The tool generates a control class with properties, events, and delegates based on your input. This is my preferred method for generating complex JavaScript classes. CodeSmith 5.0 includes a rich set of templates for the Microsoft AJAX Library. For more information on CodeSmith, visit www.codesmithtools.com.

Example 8-5. The Ajax Client Control template in Visual Studio can be used to create a new JavaScript control class (Web/Script/ClientControlTemplate.js).

/// <reference name="MicrosoftAjax.js"/>

Type.registerNamespace("SOAjax.Controls");

SOAjax.Controls.SmartInputControl = function(element) {
    SOAjax.Controls.SmartInputControl.initializeBase(this, [element]);
}

SOAjax.Controls.SmartInputControl.prototype = {
    initialize: function() {
        SOAjax.Controls.SmartInputControl.callBaseMethod(this, 'initialize'),

        // Add custom initialization here
    },
    dispose: function() {
        //Add custom dispose actions here
        SOAjax.Controls.SmartInputControl.callBaseMethod(this, 'dispose'),
    }
}
SOAjax.Controls.SmartInputControl.registerClass(
    'SOAjax.Controls.SmartInputControl', Sys.UI.Control);

Sys.Application.notifyScriptLoaded();

By examining the generated code in Example 8-5, you can see that the Control class is ready for custom implementation—the base class already contains the core control functionality, and our class is set up to inherit from Sys.UI.Control. If your control implementation depends on a specific DOM element type, you might want to add basic validation in the constructor, as in the following sample:

SOAjax.Controls.SmartInputControl = function(element) {
    if (element.tagName != 'INPUT') {
        Sys.Debug.fail('Expected an input control!'),
    }
    SOAjax.Controls.SmartInputControl.initializeBase(this, [element]);
}

Another common step you’ll almost always want to take is to add disposal code for any handlers you add, using the $clearHandlers method to clear any handlers added to the DOM element. The code in the following dispose method is useful in almost all control classes. Notice that if the element contains child nodes that you added handlers to, you might need to iterate recursively through its children. In the Behavior class example, we were particular about removing only the handlers used by the behavior so that behaviors could be added and removed during the page execution without disabling other functionality. However, it is assumed that by design controls are used for the lifetime of the DOM element. The following dispose method demonstrates the typical disposal logic for the control implementation:

dispose: function() {
    var element = this.get_element();
    if (element)
        $clearHandlers(element);
    SOAjax.Controls.SmartInputControl.callBaseMethod(this, 'dispose'),
}

As with component and behavior implementations, the dispose method here must contain conditional logic—the dispose method might be called multiple times, so it’s important to check whether a reference such as the element exists before using the method because the element reference might have been removed in an earlier disposal pass.

Tip

Tip

As a rule, in JavaScript you should always check that objects are not null before using them.

For our text box control implementation, we’ll implement the custom event submit, which is fired from the control when a user presses the Enter key. Client code is able to handle the submit event in external code through an event handler, which allows the control to be used in multiple contexts without tying the control implementation to any given page or use.

The following code is used in the SmartInputControl’s prototype to define the submit event.

// Bind and unbind to custom submit event.
add_submit: function(handler) {
    this.get_events().addHandler('submit', handler);
},
remove_submit: function(handler) {
    this.get_events().removeHandler('submit', handler);
}

You’ll notice that the event implementation uses the get_events method of the base Component class. (Remember that Sys.UI.Control inherits from Sys.Component.) Chapter 7 described the Sys.Component infrastructure in depth, including the Sys.EventHandlerList class that is used to support events.

To add a handler for the Enter button, use the $addHandler method to add a handler to the keydown DOM event. The handler is created by using a delegate—using Function.createDelegate (discussed in Chapter 7) to create a delegate that refers to the class instance. The following code demonstrates a handler method for the keydown event that fires the submit event on the enter key event. To provide support for the TooltipBehavior behavior class, we can call Sys.UI.Behavior.getBehaviorByName to see whether an instance of the behavior is active on the control. If it is, we can hide the tooltip.

initialize: function() {
    var element = this.get_element();
    $addHandler(element, 'keydown',
        Function.createDelegate(this, this._keyDownHandler));
    SOAjax.Controls.SmartInputControl.callBaseMethod(this, 'initialize'),
},

_keyDownHandler: function(event) {
    if (event == null) {
        event = new Sys.UI.DomEvent(null); throw Error.argumentNull();
    }

    // Check for the "submit" action (enter key)
    if (!event.keyCode) return;
    if (event.keyCode != Sys.UI.Key.enter) return;

    var handler = this.get_events().getHandler('submit'),
    if (!handler) { Sys.Debug.trace('Handler is not defined for the enter key!'), }
    if (handler) handler(this, Sys.EventArgs.Empty);
    // If a tooltip behavior is active, hide it
    var element = this.get_element();
    var toolTipBehavior = Sys.UI.Behavior.getBehaviorByName(element, 'TooltipBehavior'),
    if (toolTipBehavior)
        toolTipBehavior.hide();
}

To handle the submit event from external code, declare an event handler method. The event handler uses the signature handler(sender, eventArgs), where sender is the instance that fired the event and eventArgs is an instance of type Sys.EventArgs, which is used to pass extra information in certain cases. In this case, we don’t need to pass any information other than the control instance, so we pass Sys.EventArgs.Empty from our control class. The following code can be placed in an HTML page to instantiate the control and test the Enter button functionality:

function onSubmit(control, eventArgs) {
    Sys.Debug.traceDump(control);
    var textValue = control.get_text();
    alert(textValue);
}

function pageLoad(){
    var events = { submit: onSubmit };
    var domControl = $get('testInput'),
    $create(SOAjax.Controls.SmartInputControl, null, events, null, domControl);
}

Simple Validation and the propertyChanged Event

Sys.Component provides the propertyChanged event, which can be used to notify external clients of property changes. (I introduced this event in Chapter 7.) The propertyChanged event is a general purpose event that client code can subscribe to.

In our sample input control, simple input validation can be useful in developing a control library. For example, you might want to raise a propertyChanged event for isValid when an input passes simple validation and the validation status changes. To implement simple validation logic in the control, such as a minimum password length, you can add the properties minLength and isValid and raise the propertyChanged event when the status changes. You can also provide a validationFailure event that fires only when the user attempts a submission that includes invalid data.

Tip

Tip

As an alternative, you could use a delegate property that implements validation, letting the external implementation code provide the validation. To keep the example simple, I used simple validation in the control itself.

The code in Example 8-6 extends SmartInputControl with validation logic and raises the propertyChanged event when the isValid property changes. The validationFailure event is also defined in this listing. This event is fired from _keyDownHandler when validation failure occurs. (You can see the code for _keyDownHandler in Example 8-7.)

Example 8-6. Properties and events can be used for simple validation logic (from Web/Script/SmartInputControl.js).

// Simple validation logic for SmartInputControl,
// integrated with the raisePropertyChanged event:
minLength: null,
get_minLength: function() {
    ///<value>The minimum valid length of the user's input</value>
    return this.minLength;
},
set_minLength: function(length) {
    if (length) { // Only validate non-null values.
        if (typeof (length) != 'number') {
            throw Error.argumentType('length', typeof (length), typeof (1));
        }
    }
    this.minLength = length;
},
_lastKnownIsValid : false,
get_isValid: function() {
    ///<value>Returns true if this control passes minimal validation</value>
    var isValid;
    if (this.minLength && this.get_text().length < this.minLength)
        isValid = false;
    else
        isValid = true;
    if (this._lastKnownIsValid != isValid)
        this.raisePropertyChanged('isValid'),
    return (isValid);
}
add_validationFailure: function(handler) {
    ///<value>The validationFailure event is fired when the user
    /// enters the enter key with invalid data.</value>
    this.get_events().addHandler('validationFailure', handler);
},
remove_validationFailure: function(handler) {
    this.get_events().removeHandler('validationFailure', handler);
}

To further integrate validation into the control, we can check whether the control is valid on each key press, raise the propertyChanged event when the validation state changes, and conditionally fire the submit event on the Enter key press so that invalid submissions aren’t fired. If the user tries to submit the control value and the validation fails, the validationFailure event is raised.

Example 8-7 shows the modified _keyDownHandler with the integrated validation logic. On every key press the validation code is called, which raises the propertyChanged event in turn if the validation status has changed. Additionally, the validationFailure event is raised on failure to let the implementation code handle the event.

Example 8-7. The validation logic is applied to the key press event handler (from Web/Script/SmartInputControl.js).

_keyDownHandler: function(event) {
    if (event == null) {
        event = new Sys.UI.DomEvent(null); throw Error.argumentNull();
    }

    // Check for the "submit" action (enter key)
    if (!event.keyCode || event.keyCode != Sys.UI.Key.enter) return;
    if (!this.get_isValid()) {
        var validationHandler = this.get_events().getHandler('validationFailure'),
        if (validationHandler)
            validationHandler(Sys.EventArgs.Empty);
        else
            alert('This input isn't valid!'),
        return;
    }
    var handler = this.get_events().getHandler('submit'),
    if (!handler) { Sys.Debug.trace('Handler is not defined for the enter key!'), }
    if (handler) handler(this, Sys.EventArgs.Empty);
    this._hideTooltipHandler(null);
    // If a tooltip behavior is active, hide it
    var element = this.get_element();
    var toolTipBehavior = Sys.UI.Behavior.getBehaviorByName(
        element, 'TooltipBehavior'),
    if (toolTipBehavior)
        toolTipBehavior.hide();
}

With this simple validation logic in place, we can add the property minLength to the call to the $create method for SmartInputControl. The following code sample sets the minLength property through the $create method:

function pageLoad() {
    var element = $get('testInput'),
    var events = { submit: onSubmit };
    var props = { minLength : 3};
    $create(SOAjax.Controls.SmartInputControl, props, events, null, element);
}

The test page for the control is shown in Example 8-8, which assigns an event handler for the input control’s submit event and assigns TooltipBehavior to the control. The complete code for the final SmartInputControl implementation is included in Example 8-9.

Example 8-8. The SmartInputControl control class can be tested in an isolated page instance or integrated into more complex code (Web/SmartInputControl.aspx).

<%@ Page Language="C#" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>SOAjax Controls Sample Page</title>
    <script type="text/javascript">

        function pageLoad() {
          var element = $get('testInput'),
          var events = { submit: onSubmit };
          var props = {minLength : 3};
          $create(SOAjax.Controls.SmartInputControl, props, events, null, element);

          var toolTipProps = { toolTip: 'Enter a value here!'};
          $create(SOAjax.Behaviors.TooltipBehavior, toolTipProps, null, null,
              element);
        }

      function onSubmit(control, eventArgs) {
          Sys.Debug.traceDump(control);
          var textValue = control.get_text();
          alert(textValue);
      }

    </script>
</head>

<body>
    <form id="form1" runat="server">
      <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server" >
            <Scripts>
                <asp:ScriptReference Path="~/Script/SmartInputControl.js" />
                <asp:ScriptReference Path="~/Script/Tooltip.js" />
            </Scripts>
        </asp:ScriptManager>

        <input type="text" id="testInput" maxlength="255" />

      </div>
    </form>
</body>
</html>

Example 8-9. The SmartInputControl control class adds functionality to the HTML input text box (Web/Script/SmartInputControl.js).

/// <reference name="MicrosoftAjax.js"/>
// A common control library for SOAjax Sample Code

Type.registerNamespace('SOAjax.Controls'),


SOAjax.Controls.SmartInputControl = function(element) {
    if (element.tagName != 'INPUT') {
        Sys.Debug.fail('Expected an input control!'),
    }
    SOAjax.Controls.SmartInputControl.initializeBase(this, [element]);
}
SOAjax.Controls.SmartInputControl.prototype = {

    // Simple validation logic:
    minLength: null,
    get_minLength: function() {
        /// <value>The minimum valid length of the user's input</value>
        return this.minLength;
    },
    set_minLength: function(length) {
        if (length) { // Only validate non-null values.
            if (typeof (length) != 'number') {
                throw Error.argumentType('length', typeof (length), typeof (1));
            }
        }
        this.minLength = length;
    },
    _lastKnownIsValid: false,
    get_isValid: function() {
        /// <value>Returns true if this control passes minimal validation</value>
        var isValid;
        if (this.minLength && this.get_text().length < this.minLength)
            isValid = false;
        else
            isValid = true;

        if (this._lastKnownIsValid != isValid)
            this.raisePropertyChanged('isValid'),

        return (isValid);
    },

    add_validationFailure: function(handler) {
        /// <value>The validationFailure event is fired when the user
        /// enters the enter key with invalid data.</value>
        this.get_events().addHandler('validationFailure', handler);
    },
    remove_validationFailure: function(handler) {
        this.get_events().removeHandler('validationFailure', handler);
    },

    add_submit: function(handler) {
        /// <value>The submit event is fired when the user presses
        /// the enter key.</value>
        this.get_events().addHandler('submit', handler);
    },
    remove_submit: function(handler) {
        this.get_events().removeHandler('submit', handler);
    },

    get_text: function() {
        /// <value>Gets the text value of the control</value>
        return this.get_element().value;
    },
    set_text: function(text) { this.get_element().value = text; },

    initialize: function() {
        var element = this.get_element();
        if (!element.tabIndex) element.tabIndex = 0;

        $addHandler(element, 'keydown',
            Function.createDelegate(this, this._keyDownHandler));

        SOAjax.Controls.SmartInputControl.callBaseMethod(this, 'initialize'),
    },

    _keyDownHandler: function(event) {
        if (event == null) {
            event = new Sys.UI.DomEvent(null); throw Error.argumentNull();
        }

        // Check for the "submit" action (enter key)
        if (!event.keyCode || event.keyCode != Sys.UI.Key.enter) return;
        if (!this.get_isValid()) {
            var validationHandler = this.get_events().getHandler('validationFailure'),
            if (validationHandler)
                validationHandler(Sys.EventArgs.Empty);
            else
                alert('This input isn't valid!'),
            return;
        }
        var handler = this.get_events().getHandler('submit'),
        if (!handler) { Sys.Debug.trace('submit handler is not defined!'), }
        if (handler) handler(this, Sys.EventArgs.Empty);
        var element = this.get_element();
        var toolTipBehavior = Sys.UI.Behavior.getBehaviorByName(
            element, 'TooltipBehavior'),
        if (toolTipBehavior)
            toolTipBehavior.hide();
    },

    dispose: function() {
        var element = this.get_element();
        if (element)
            $clearHandlers(element);
        if (this.toolTip)
            this.toolTip.parentNode.removeChild(this.toolTip);
        this.toolTip = null;
        SOAjax.Controls.SmartInputControl.callBaseMethod(this, 'dispose'),
    }
}
SOAjax.Controls.SmartInputControl.registerClass(
    'SOAjax.Controls.SmartInputControl', Sys.UI.Control);

Sys.Application.notifyScriptLoaded();

After testing SmartInputControl on a sample page, it is easy to integrate the class into real code. For example, we can extend the login input elements from the application runtime component example in Chapter 7 simply by adding a script reference through the ScriptManager control and creating an instance of SmartInputControl. The code in Example 8-10 demonstrates SmartInputControl as used by the application runtime component’s passwordInput property. The set_passwordInput property method in the component’s prototype is used to create an instance of SmartInputControl for the password input element. This allows the user to press Enter rather than click the login button. The full code sample integrating the SmartInputControl control class in the JavaScript component SOAjax.Application is included in this chapter’s code sample in the file ApplicationRuntime.js.

Example 8-10. SmartInputControl can easily be utilized in multiple contexts (Web/Scripts/ApplicationRuntime.js).

// Sample code excerpt from SOAjax.Application's prototype

set_passwordInput: function(element) {
    // Create a new SmartInputControl from the DOM element
    if (!element.control) {
        var properties = { minLength: 3 };
        var events = { submit: Function.createDelegate(this, this.login) };
        var passwordControl = $create(
            SOAjax.Controls.SmartInputControl, properties, events, null, element);

        $create(SOAjax.Behaviors.TooltipBehavior,
        { toolTip: 'Enter your password' }, null, null, element);
    }
    this.passwordInput = element;
}

Now that we’ve seen simple examples of creating and integrating discrete controls, let’s look at a more complex example using the knowledge base wiki application and create a large-scale application control.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset