Common Task results
Common_TaskResults_CSharp\OnDemandTaskResults\JavaScript\OnDemandTaskResults.js
 Copyright 2010 ESRI
 
 All rights reserved under the copyright laws of the United States
 and applicable international laws, treaties, and conventions.
 
 You may freely redistribute and use this sample code, with or
 without modification, provided you include the original copyright
 notice and use restrictions.
 
 See <a href="http://help.arcgis.com/en/sdk/10.0/usageRestrictions.htm">the use restrictions</a>.
 

// ***************************
//     MapTips Extensions
// ***************************

// === Public Methods ===

// Initializes MapTips so that attributes for a feature are retrieved only when a MapTip
// is expanded
ESRI.ADF.UI.MapTips.prototype.setupOnDemandMapTips = function(properties) {
    // Retrieve the GraphicFeatureGroup (i.e. layer) corresponding to the passed-in ID
    var graphicFeatureGroup = $find(properties.graphicFeatureGroupID);

    // Loop through the features in the GraphicFeatureGroup, setting the hasAttributes
    // property for each to false and adding a handler to each's mouseOver event.  
    var graphicFeature;
    for (var i = 0; i < graphicFeatureGroup.getFeatureCount(); i++) {
        graphicFeature = graphicFeatureGroup.get(i);
        graphicFeature.set_hasAttributes(false);

        // Add a handler to the feature's mouseOver event to update the MapTip's currentFeature
        // property with the moused-over feature.  Note that createDelegate is used to specify 
        // the handler so that the MapTips control can be referenced in the handler via the 
        // "this" keyword.
        graphicFeature.add_mouseOver(Function.createDelegate(this, this._updateCurrentFeature));
    }

    // Set the NodeID of the TreeViewPlusNode corresponding to the GraphicFeatureGroup
    this.set_graphicsLayerNodeID(properties.graphicsLayerNodeID);

    // Initialize the activity indicator and attributes templates
    this.set_activityIndicatorTemplate(properties.activityIndicatorTemplate);
    this.set_attributesTemplate(this.get_contentTemplate());
    this.set_contentTemplate(properties.activityIndicatorTemplate);

    // Set the syntax to issue a callback to the server tier TaskResults control
    this.set_onDemandCallbackFunctionString(properties.callbackFunctionString);

    // Wire a handler for the MapTips expanded event that will apply either the activity
    // indicator or attributes content and, if necessary, issue a callback to retrieve
    // attributes.  Note we use createDelegate to specify the handler so the "this" 
    // keyword can be used within the handler to reference the MapTips control.
    this.add_expanded(Function.createDelegate(this, this._setOnDemandContent));
}

// Updates the GraphicFeature specified by the passed-in ID with the passed-in attributes and
// also updates the MapTips display, if a MapTip for the GraphicFeature is currently shown
ESRI.ADF.UI.MapTips.prototype.updateAttributes = function(graphicFeatureID, attributes) {
    // Get the GraphicFeature with the specified ID and update it with the passed-in attributes
    var graphicFeature = $find(graphicFeatureID);
    graphicFeature.set_attributes(attributes);
    // Set the flag indicating that attributes have been retrieved for the GraphicFeature
    graphicFeature.set_hasAttributes(true);

    // Check whether a MapTip is currently shown and, if so, whether it is for the graphic feature
    // with the passed-in ID.  If so, call the method to update the MapTip's content.
    if (this.get_callout().get_isOpen() && (graphicFeatureID == this.get_currentFeature().get_id()))
        this._setOnDemandContent(graphicFeature);
}


// === Private Methods ===

ESRI.ADF.UI.MapTips.prototype._updateCurrentFeature = function(graphicFeature) {
    this.set_currentFeature(graphicFeature);
}

// Updates the MapTips content based on whether or not attributes have been retrieved from the
// server.  Fires when a MapTip is expanded and when attributes are returned from the server.
ESRI.ADF.UI.MapTips.prototype._setOnDemandContent = function(graphicFeature) {
    // If the graphic feature was not passed in, use the MapTips' current feature
    if (graphicFeature && !ESRI.ADF.Graphics.GraphicFeature.isInstanceOfType(graphicFeature))
        graphicFeature = this.get_currentFeature();

    // Get the attributes of the passed-in feature
    var attributes = graphicFeature.get_attributes();

    // Create the HTML for the mapTips title by binding the attributes to the title template
    var titleMarkup = ESRI.ADF.System.templateBinder(attributes, this.get_hoverTemplate());

    // If attributes have been retrieved for the feature, create the mapTips content HTML by binding
    // those attributes to the attributes template.  Otherwise, bind to the activity indicator template.
    var contentMarkup;
    if (graphicFeature.get_hasAttributes())
        contentMarkup = ESRI.ADF.System.templateBinder(attributes, this.get_attributesTemplate());
    else
        contentMarkup = ESRI.ADF.System.templateBinder(attributes, this.get_activityIndicatorTemplate());

    // Apply the markup to the MapTips control
    this.setContent(contentMarkup, titleMarkup);

    // Check whether attributes have been retrieved for the current feature.  If not, issue a callback to
    // retrieve them.
    if (!graphicFeature.get_hasAttributes()) {
        // Format the information to be sent in the callback
        var context;
        var argument = 'EventArg=getAttributes&GraphicID={0}&MapTipsID={1}&LayerNodeID={2}';
        argument = String.format(argument, graphicFeature.get_id(), this.get_id(), this.get_graphicsLayerNodeID());

        // Issue the callback
        eval(this.get_onDemandCallbackFunctionString());
    }
}

// === Public Properties ===

// NodeID of the TreeViewPlusNode corresponding to the MapTip's associated GraphicFeatureGroup
ESRI.ADF.UI.MapTips.prototype.get_graphicsLayerNodeID = function() {
    return this._graphicsLayerNodeID;
}

ESRI.ADF.UI.MapTips.prototype.set_graphicsLayerNodeID = function(value) {
    this._graphicsLayerNodeID = value;
}

// HTML markup to use when displaying a MapTip on a feature for which attributes have been
// retrieved
ESRI.ADF.UI.MapTips.prototype.get_attributesTemplate = function() {
    return this._attributesTemplate;
}

ESRI.ADF.UI.MapTips.prototype.set_attributesTemplate = function(value) {
    this._attributesTemplate = value;
}

// HTML markup to use when displaying a MapTip on a feature for which attributes have been
// retrieved
ESRI.ADF.UI.MapTips.prototype.get_currentFeature = function() {
    return this._currentFeature;
}

ESRI.ADF.UI.MapTips.prototype.set_currentFeature = function(value) {
    this._currentFeature = value;
}

// HTML markup to use when displaying a MapTip on a feature for which attributes have not
// yet been retrieved
ESRI.ADF.UI.MapTips.prototype.get_activityIndicatorTemplate = function() {
    return this._activityIndicatorTemplate;
}

ESRI.ADF.UI.MapTips.prototype.set_activityIndicatorTemplate = function(value) {
    this._activityIndicatorTemplate = value;
}

// Syntax to issue a callback to the TaskResults control referencing the GraphicsLayer on
// which MapTips are displayed
ESRI.ADF.UI.MapTips.prototype.get_onDemandCallbackFunctionString = function() {
    return this._onDemandCallbackFunctionString;
}

ESRI.ADF.UI.MapTips.prototype.set_onDemandCallbackFunctionString = function(value) {
    this._onDemandCallbackFunctionString = value;
}

// *********************************
//     GraphicFeature Extensions
// *********************************

// === Public Properties ===

// Indicates whether attributes for the feature have been retrieved from the server
ESRI.ADF.Graphics.GraphicFeature.prototype.get_hasAttributes = function() {
    return this._hasAttributes;
}

ESRI.ADF.Graphics.GraphicFeature.prototype.set_hasAttributes = function(value) {
    this._hasAttributes = value;
}


// *********************************
//     TaskResults Extensions
// *********************************

// === Public Methods ===

// Creates client tier TreeViewPlusNode objects given node data in JSON format
ESRI.ADF.UI.TaskResults.prototype.initClientNodes = function(taskResultNodeData) {
    // Get the DOM element for the task result node
    var taskResultNodeElement = $get(taskResultNodeData.nodeID);
    
    // Create a client tier TaskResultNode AJAX component
    var taskResultNode = $create(ESRI.ADF.Samples.TaskResultNode,
        { 'id': taskResultNodeElement.id }, null, null, taskResultNodeElement);

    // Iterate through the data passed-in for graphics layer nodes that are children of the task
    // results node, creating a client tier node object for each.
    for (var i = 0; i < taskResultNodeData.graphicsLayerNodes.length; i++) {
        // Get the DOM element for the graphics layer node
        var graphicFeatureGroupNodeElement = $get(taskResultNodeData.graphicsLayerNodes[i].nodeID);

        // Declare a variable to store the node's initialization properties and initialize it with
        // the node's id.
        var properties = { 'id': graphicFeatureGroupNodeElement.id };

        // If the node data includes a pagingTextFormatString, then the node contains more than one page
        // of child (feature) nodes.  Add properties to manage paging accordingly.
        if (taskResultNodeData.graphicsLayerNodes[i].pagingTextFormatString) {
            properties.pagingTextFormatString = taskResultNodeData.graphicsLayerNodes[i].pagingTextFormatString;
            properties.pageCount = taskResultNodeData.graphicsLayerNodes[i].pageCount;
            properties.pageSize = taskResultNodeData.graphicsLayerNodes[i].pageSize;
            properties.nodeCount = taskResultNodeData.graphicsLayerNodes[i].nodeCount;
        }
        // Create the graphics layer node object.  Note that, on the client, this is a GraphicFeatureGroup
        // node, following the correspondence between web tier GraphicsLayers and client tier GraphicFeatureGroups
        var graphicFeatureGroupNode = $create(ESRI.ADF.Samples.GraphicFeatureGroupNode, properties,
            null, null, graphicFeatureGroupNodeElement);
            
        // Add the graphic feature group node to the task result node
        taskResultNode.addNode(graphicFeatureGroupNode);

        // Iterate through data for feature nodes that are children of the current graphic feature group node
        for (var j = 0; j < taskResultNodeData.graphicsLayerNodes[i].featureNodes.length; j++) {
            // Get the DOM element for the feature node
            var featureNodeElement = $get(taskResultNodeData.graphicsLayerNodes[i].featureNodes[j]);
            
            // Create the client tier feature node object and add it to the current graphic feature group node
            var featureNode = $create(ESRI.ADF.Samples.GraphicFeatureNode,
                { 'id': featureNodeElement.id }, null, null, featureNodeElement);
            graphicFeatureGroupNode.addNode(featureNode);
        }
    }

    // Add the task result node to the client tier task results control
    this.addNode(taskResultNode);
}

// Makes the passed-in node a child node of the task results control
ESRI.ADF.UI.TaskResults.prototype.addNode = function(node) {
    // Create the private array of child nodes if necessary
    if (!this._nodes)
        this._nodes = new Array();

    // Make sure the argument is a TreeViewPlusNode instance
    if (ESRI.ADF.Samples.TreeViewPlusNode.isInstanceOfType(node)) {
        // Add the node to the control's child nodes array and set the
        // nodes parent property to point to the control.
        this._nodes.push(node);
        node.set_parent(this);
    }
}

// Disassociates the passed-in node with the task results control.  Note that
// this does not alter the node itself, just the association.
ESRI.ADF.UI.TaskResults.prototype.removeNode = function(node) {
    // Make sure the control has child nodes
    if (!this._nodes)
        return;

    // Iterate through the control's child nodes array.  Remove the passed-in
    // node from the array if found.
    for (var i = 0; i < this._nodes.length; i++) {
        if (this._nodes[i] === node) {
            this._nodes.splice(i, 1);
            break;
        }
    }
}

// Adds an array of nodes to the control's child nodes collection
ESRI.ADF.UI.TaskResults.prototype.addNodes = function(nodes) {
    for (var i = 0; i < nodes.length; i++)
        this.addNode(nodes[i]);
}

// Retrieves a descendant node by node ID
ESRI.ADF.UI.TaskResults.prototype.findNodeByID = function(nodeID) {
    var node = null;

    // Iterate through the control's child nodes, checking the id of each to
    // see whether it matches the passed-in value
    for (var i = 0; i < this._nodes.length; i++) {
        // If the current node's ID matches that passed-in, initialize the return
        // value with the node.  Otherwise, recursively call findNodeByID on the
        // child node.
        if (this._nodes[i].get_id() == nodeID)
            node = this._nodes[i];
        else
            node = this._nodes[i].findNodeByID(nodeID);

        // Exit the loop if a matching node was found
        if (node)
            break;
    }

    return node;
}



// *****************************************************
//     TreeViewPlusNode AJAX control implementation
// *****************************************************

Type.registerNamespace('ESRI.ADF.Samples');

ESRI.ADF.Samples.TreeViewPlusNode = function(element) {
    ESRI.ADF.Samples.TreeViewPlusNode.initializeBase(this, [element]);

    // Parent TreeViewPlusNode or TaskResults control
    this._parent = null;
    // Child nodes
    this._nodes = new Array();
    // DOM checkbox element associated with the node
    this._checkBox = null;
    // State of the node's checkbox
    this._isChecked = null;
    // Div that contains the node's children
    this._childNodeDiv = null;
    // Number of child nodes per page
    this._pageSize = null;
    // Current child node page shown
    this._pageNumber = 1;
    // Number of child node pages
    this._pageCount = null;
    // Child nodes pending initialization
    this._newNodes = new Object();
    // HTML of the page display for each child node page
    this._pagingTableMarkup = new Object();
    // Used to format the page display.  Equivalent to the web tier TaskResults 
    // property of the same name.
    this._pagingTextFormatString = null;
    // HTML for indenting the page display so it is aligned with the child nodes
    this._pagingTablePadding = new Array();
    // Total number of child nodes
    this._nodeCount = null;
    // Node page of the parent TreeViewPlusNode on which this node is displayed
    this._containingPage = 1;
    // This node's HTML representation
    this._content = null;
};
ESRI.ADF.Samples.TreeViewPlusNode.prototype = {
    // Fires when the object is instantiated via the AJAX $create function
    initialize: function() {
        ESRI.ADF.Samples.TreeViewPlusNode.callBaseMethod(this, 'initialize');

        // Get the DOM checkbox associated with the node
        this._checkBox = $get(String.format('{0}_CheckBox', this.get_id()));
        if (this._checkBox) {
            // Attach the _checked function to the checkbox's click event.  We use createDelegate
            // for this so that the "this" keyword in the event handler refers to the 
            // TreeViewPlusNode instance.
            this._checkBox.onclick = Function.createDelegate(this, this._checked);
            // Initialize the isChecked property with the checkbox's checked state
            this._isChecked = this._checkBox.checked;
        }

        // Get the div that holds the node's child nodes
        this._childNodeDiv = $get(String.format('{0}_childrenContainer', this.get_id()));

        // Try to get the DOM element containing the link for the next node page.  This will only exist
        // if the number of child nodes exceeds the node's page size
        var nextPageElement = $get(String.format('{0}_NextPage', this.get_id()));

        // If the element was found, then the node has more than one page.  So we initialize paging properties.
        if (nextPageElement) {
            // Change the next page link's href to invoke the paging functionality of the client tier
            // TreeViewPlusNode.  When this element is initially created by the web tier TaskResults control,
            // it invokes web tier functionality when clicked.
            nextPageElement.href = String.format(
                "javascript:$find('{0}').set_page($find('{0}').get_pageNumber() + 1)", this.get_element().id);
            
            // Find the DOM table containing the link.  This table contains the paging display.
            var pagingTable = nextPageElement.parentNode;
            while (pagingTable.parentNode && pagingTable.tagName != 'TABLE')
                pagingTable = pagingTable.parentNode;

            // The paging display is formatted within one row.  Interrogate the cells of this row to extract the 
            // HTML that is used to pad (indent) the table.  This padding aligns the display with the child nodes.
            for (var i = 0; i < pagingTable.rows[0].cells.length; i++) {
                // Get the number of child elements in the current cell
                var childNodeCount = pagingTable.rows[0].cells[i].childNodes.length;
                // Get the inner text of the cell's first child element
                var innerText = pagingTable.rows[0].cells[i].childNodes[0].innerText;
                // If the cell only contains one element that has no inner text, then the cell is used for padding
                if (childNodeCount == 1 && (innerText == '' || innerText == 'undefined' || innerText == null)) {
                    // Store the padding markup for use in updating the paging display
                    var paddingHtml = pagingTable.rows[0].cells[i].innerHTML;
                    this._pagingTablePadding.push(paddingHtml);
                }
            }
        }
    },
    
    // Destroys the node instance and those of any child nodes
    dispose: function() {
        // Invoke the base control's dispose method to destroy the current instance
        ESRI.ADF.Samples.TreeViewPlusNode.callBaseMethod(this, 'dispose');
    
        // Call dispose on child nodes
        for (var i = 0; i < this._nodes.length; i++)
            this._nodes[i].dispose();
    },
    
    // AJAX component ID  
    get_id: function() {
        return this._id;
    },
    set_id: function(value) {
        this._id = value;
    },
    
    // TreeViewPlusNode or TaskResults control that contains the node
    set_parent: function(value) {
        // Make sure the argument is a TreeViewPlusNode or TaskResults control
        if (ESRI.ADF.Samples.TreeViewPlusNode.isInstanceOfType(value) ||
        ESRI.ADF.UI.TaskResults.isInstanceOfType(value))
            this._parent = value;
    },
    get_parent: function() {
        return this._parent;
    },
    
    // Child nodes
    set_nodes: function(value) {
        this._nodes = new Array();
        this.addNodes(value);
    },
    get_nodes: function() {
        return this._nodes;
    },

    // Checked state of the node's checkbox
    get_isChecked: function() {
        return this._isChecked;
    },
    get_checkBox: function() {
        return this._checkBox;
    },
    
    // Makes the passed-in node a child node
    addNode: function(node) {        
        // Make sure the argument is a TreeViewPlusNode instance
        if (ESRI.ADF.Samples.TreeViewPlusNode.isInstanceOfType(node)) {
            // Add the node to the control's child nodes array and set the
            // nodes parent property to point to the control.
            this._nodes.push(node);
            node.set_parent(this);
        }
    },

    // Adds an array of nodes to the child nodes collection
    addNodes: function(nodes) {
        for (var i = 0; i < nodes.length; i++)
            this.addNode(nodes[i]);
    },

    // Retrieves a descendant node by node ID
    findNodeByID: function(nodeID) {
        var node = null;

        // Iterate through the node's child nodes, checking the id of each to see whether 
        // it matches the passed-in value
        for (var i = 0; i < this._nodes.length; i++) {
            // If the current node's ID matches that passed-in, initialize the return
            // value with the node.  Otherwise, recursively call findNodeByID on the
            // child node.
            if (this._nodes[i].get_id() == nodeID)
                node = this._nodes[i];
            else
                node = this._nodes[i].findNodeByID(nodeID);

            // Exit the loop if a matching node was found
            if (node)
                break;
        }

        return node;
    },
    
    // Removes the passed-in node from the child nodes collection
    removeNode: function(node) {
        for (var i = 0; i < this._nodes.length; i++) {
            if (this._nodes[i] === node) {
                this._nodes.splice(i, 1);
                break;
            }
        }
    },

    // Removes all child nodes
    clearNodes: function() {
        this._nodes = new Array();
    },
    
    // Adds a handler for the node checkbox's click event
    add_checked: function(handler) {
        this.get_events().addHandler('checked', handler);
    },

    // Removes the passed-in handler from the node checkbox's click event
    remove_checked: function(handler) {
        this.get_events().removeHandler('checked', handler);
    },
    
    // Fires when the node's checkbox is clicked
    _checked: function() {
        // Update the node's isChecked property
        this._isChecked = this._checkBox.checked;

        // Add or remove the checked attribute from the checkbox's HTML.  Needed because
        // the node's html representation is used for paging.  For the HTML to be correct
        // in FireFox, this attribute must be explicitly added or removed.
        if (this._checkBox.checked)
            this._checkBox.setAttribute('checked', 'checked');
        else
            this._checkBox.removeAttribute('checked');
        
        // Raise the node's checked event to fire any event handlers
        this._raiseEvent('checked');

        // Issue an asynchronous request to the server to update the node's status in the web tier
        var argument = String.format('EventArg=nodeChecked&NodeID={0}&Checked={1}',
            this.get_id(), this._isChecked);
        var context;
        eval(this.get_callbackFunctionString());
    },

    // The current page of child nodes
    get_pageNumber: function() {
        return this._pageNumber;
    },
    
    // Page number of the parent node that contains this node
    get_containingPage: function() {
        return this._containingPage;
    },
    
    set_containingPage: function(value) {
        this._containingPage = value;
    },
    
    // Retrieves all the nodes on the passed-in page
    getChildNodesByPage: function(pageNumber) {
        var nodes = new Array();
        for (var i = 0; i < this._nodes.length; i++) {
            if (this._nodes[i].get_containingPage() == pageNumber)
                nodes.push(this._nodes[i]);
        }

        return nodes;
    },

    // HTML representation of the node.  Setting this property updates the node's appearance
    // with the passed-in markup.
    get_content: function() {
        return this._content;
    },

    set_content: function(value) {
        // Update the content property
        this._content = value;

        // Get the node's DOM element
        var nodeElement = $get(this.get_id());
        if (nodeElement) {
            // Apply the markup to the node
            nodeElement.outerHTML = value;
            
            // Get the node's checkbox
            var checkBox = $get(String.format('{0}_CheckBox', this.get_id()));
            if (checkBox) {
                // Re-wire the _checked function to the checkbox's clicked event.  Necessary because
                // updating the node's markup disconnects this handler.
                this._checkBox = checkBox;
                this._checkBox.onclick = Function.createDelegate(this, this._checked);
                this._checkBox.checked = this._isChecked;
            }
        }
    },
    
    // Changes the page shown
    set_page: function(pageNumber, createNodeObjects) {
        // Try getting the child nodes for the passed-in page from the node's collection
        var nodes = this.getChildNodesByPage(pageNumber);
        // If the node doesn't have any child nodes defined for the page, check if there are 
        // nodes pending initialization.  These are stored in the _newNodes private member
        if (nodes.length == 0)
            nodes = this._newNodes[pageNumber];
            
        // Check whether nodes were found for the requested page
        if (nodes && nodes.length > 0) {
            // Save the markup for the current page's nodes            
            var currentNodes = this.getChildNodesByPage(this._pageNumber);
            for (var i = 0; i < currentNodes.length; i++) {
                // Store the markup for each node in the content property and remove the node element
                // from the document
                var nodeElement = $get(currentNodes[i].get_id());
                currentNodes[i]._content = nodeElement.outerHTML;
                nodeElement.parentNode.removeChild(nodeElement);
            }

            // Add the paging table to the node's content div
            if (!this._pagingTableMarkup[pageNumber])
                this._pagingTableMarkup[pageNumber] = this._createPagingMarkup(pageNumber);
            this._childNodeDiv.innerHTML = this._pagingTableMarkup[pageNumber];
            
            // Add the page's nodes to the content div
            for (var i = 0; i < nodes.length; i++) {
                // Get the current node and update the div's markup with it
                var node = nodes[i];
                this._childNodeDiv.innerHTML += node._content;

                // If the nodes have yet to be initialized (i.e. instantiated as TreeViewPlusNodes), do
                // so here
                if (this._newNodes[pageNumber] && createNodeObjects) {
                    var nodeElement = $get(node.nodeID);
                    node = $create(ESRI.ADF.Samples.TreeViewPlusNode,
                       { 'id': nodeElement.id, 'containingPage': pageNumber }, null, null, nodeElement);
                    this.addNode(node);
                }
            }

            // If nodes for the requested page have already been created, re-wire the _checked function 
            // to the node checkbox's checked event
            if (!this._newNodes[pageNumber]) {                
                nodes = this.getChildNodesByPage(pageNumber);
                for (var i = 0; i < nodes.length; i++) {
                    node = nodes[i];
                    var checkBox = $get(String.format('{0}_CheckBox', node.get_id()));
                    if (checkBox) {
                        node._checkBox = checkBox;
                        node._checkBox.onclick = Function.createDelegate(node, node._checked);
                    }
                }
            }

            // Update the current page number
            this._pageNumber = pageNumber;
            
            // Clear the array of nodes pending initialization
            this._newNodes[pageNumber] = null;
        } else {
            // Data for the requested page was not found on the client, so issue an asynchronous
            // request to the server to retrieve it
            var argument = 'EventArg=getPage&PageNumber={0}&NodeID={1}';
            argument = String.format(argument, pageNumber, this.get_id());
            var context;

            eval(this.get_callbackFunctionString());
        }
    },

    // Specifies the format of the paging display.  The values substituted into the
    // string are as follows:
    // {0} - Page starting record
    // {1} - Page ending record
    // {2} - Total record count
    // {3} - Current page number
    // {4} - Number of pages
    // So setting this property to "Features {0} to {1} of {2}" could, for instance, 
    // evaluate at run-time to "Features 1 to 25 of 352"
    get_pagingTextFormatString: function() {
        return this._pagingTextFormatString;
    },

    set_pagingTextFormatString: function(value) {
        this._pagingTextFormatString = value;
    },

    // Number of nodes displayed on a page.  Used solely for display in the paging table.
    get_pageSize: function() {
        return this._pageSize;
    },
    set_pageSize: function(value) {
        this._pageSize = value;
    },
    
    // Number of pages of child nodes
    get_pageCount: function() {
        return this._pageCount;
    },
    
    set_pageCount: function(value) {
        this._pageCount = value;
    },
    
    // Number of child nodes.  Used solely for the paging display.
    get_nodeCount: function() {
        return this._nodeCount;
    },
    
    set_nodeCount: function(value) {
        this._nodeCount = value;
    },

    // Fires handlers for the specified event
    _raiseEvent: function(name, e) {
        // Get the handlers for the event.  ASP.NET AJAX automatically consolidates all
        // handlers into one function.
        var handler = this.get_events().getHandler(name);
    
        // Make sure a function was returned
        if (handler) {
            // Initialize the event arguments if none were passed in
            if (!e) { e = Sys.EventArgs.Empty; } 
            // Fire the handlers
            handler(this, e); 
        }
    },
    
    // Generates paging display HTML for the specified page
    _createPagingMarkup: function(pageNumber) {
        // Create a DOM table element
        var table = document.createElement('table');
        table.cellPadding = '0px';
        table.cellSpacing = '0px';

        // Create a row and add padding to it to align the paging display with the child nodes
        var row = table.insertRow(0);
        for (var i = 0; i < this._pagingTablePadding.length; i++) {
            var cell = row.insertCell(row.cells.length);
            cell.innerHTML = this._pagingTablePadding[i];
        }

        cell = row.insertCell(row.cells.length);

        // If the page requested is 2 or above, create a previous page button
        if (pageNumber > 1) {
            var previousPageLink = document.createElement('a');
            previousPageLink.href = String.format("javascript:$find('{0}').set_page($find('{0}').get_pageNumber() - 1)",
                this.get_element().id);
            previousPageLink.innerHTML = "&lt;";
            cell.appendChild(previousPageLink);

            cell = row.insertCell(row.cells.length);
        }

        // Create a div for the paging text
        var div = document.createElement('div');
        
        // Calculate the indexes of the first and last nodes on the page
        var startRecord = ((pageNumber - 1) * this._pageSize) + 1;
        var endRecord = pageNumber * this._pageSize;
        if (endRecord > this._nodeCount)
            endRecord = this._nodeCount;
            
        // Create the paging text and add it to the table
        div.innerHTML = String.format(this._pagingTextFormatString, startRecord, endRecord,
            this._nodeCount, pageNumber, this._pageCount);
        cell.appendChild(div);

        // If the requested page is not the last page, add a next page button
        if (pageNumber < this._pageCount) {
            cell = row.insertCell(row.cells.length);

            var nextPageLink = document.createElement('a');
            nextPageLink.href = String.format("javascript:$find('{0}').set_page($find('{0}').get_pageNumber() + 1)",
                this.get_element().id);
            nextPageLink.innerHTML = "&gt;";
            cell.appendChild(nextPageLink);
        }

        // Return the table's markup
        return table.outerHTML;
    },

    // Returns the syntax for invoking an asynchronous request to the server.  Retrieved from the
    // containing TreeViewPlus control
    get_callbackFunctionString: function() {
        // Make sure the node has a parent
        var parent = this.get_parent();
        if (!parent)
            return;

        var callbackFunctionString;

        // If the node's parent is a TreeViewPlus control, return its callbackFunctionString.
        // Otherwise, recursively call the method on the parent node.
        if (ESRI.ADF.UI.TreeViewPlus.isInstanceOfType(parent))
            callbackFunctionString = parent.get_callbackFunctionString();
        else if (ESRI.ADF.Samples.TreeViewPlusNode.isInstanceOfType(parent))
            callbackFunctionString = parent.get_callbackFunctionString();

        return callbackFunctionString;
    }
}

// Register the TreeViewPlusNode class as an ASP.NET AJAX client control
ESRI.ADF.Samples.TreeViewPlusNode.registerClass('ESRI.ADF.Samples.TreeViewPlusNode', Sys.UI.Control);




// **********************************************************
//       TaskResultNode client control implementation
// **********************************************************

ESRI.ADF.Samples.TaskResultNode = function(element) {
    ESRI.ADF.Samples.TaskResultNode.initializeBase(this, [element]);
};
ESRI.ADF.Samples.TaskResultNode.prototype = {
    // Fires when a TaskResultNode is instantiated via $create
    initialize: function() {
        ESRI.ADF.Samples.TaskResultNode.callBaseMethod(this, 'initialize');

        // Wire updateChildVisibility to the node's checked event.  We do this using createDelegate so
        // that the "this" keyword in the handler refers to the TaskResultNode instance.
        this.add_checked(Function.createDelegate(this, this.updateChildVisibility));
    },
    
    // Overrides TreeViewPlusNode's addNode method to ensure the passed-in node is a GraphicFeatureGroupNode
    addNode: function(node) {
        if (ESRI.ADF.Samples.GraphicFeatureGroupNode.isInstanceOfType(node))
            ESRI.ADF.Samples.TaskResultNode.callBaseMethod(this, 'addNode', [node]);
    },

    // Fires when the node checkbox is checked.  Updates the visibility of graphics associated with 
    // child nodes
    updateChildVisibility: function() {
        // Check whether the checkbox is checked
        if (this.get_isChecked()) {
            // Synchronize the visibility of child node graphics with their checked states
            for (var i = 0; i < this._nodes.length; i++) {
                this._nodes[i].updateChildVisibility();
            }
        } else {
            // Hide graphic feature groups associated with child nodes
            for (var i = 0; i < this._nodes.length; i++) {
                var node = this._nodes[i];
                var graphicFeatureGroup = $find(node.get_graphicFeatureGroupID());
                graphicFeatureGroup.set_visible(false);
            }
        }
    }
}

// Register the TaskResultNode class as an ASP.NET AJAX client control
ESRI.ADF.Samples.TaskResultNode.registerClass('ESRI.ADF.Samples.TaskResultNode', ESRI.ADF.Samples.TreeViewPlusNode);




// ******************************************************************
//       GraphicFeatureGroupNode client control implementation
// ******************************************************************

ESRI.ADF.Samples.GraphicFeatureGroupNode = function(element) {
    // AJAX component ID of the graphic feature group associated with the node
    this._graphicFeatureGroupID = null;
    // AJAX component ID of the maptips control associated with the node's graphic feature group
    this._mapTipsID = null;
    // Child feature nodes that were checked while this node was unchecked
    this._childNodesPendingUpdate = new Array();
    ESRI.ADF.Samples.GraphicFeatureGroupNode.initializeBase(this, [element]);
};
ESRI.ADF.Samples.GraphicFeatureGroupNode.prototype = {
    // Fires when a GraphicFeatureGroupNode is instantiated via $create
    initialize: function() {        
        ESRI.ADF.Samples.GraphicFeatureGroupNode.callBaseMethod(this, 'initialize');

        // Wire updateChildVisibility to the node's checked event
        this.add_checked(Function.createDelegate(this, this.updateChildVisibility));

        // Get the component ID of the graphic feature group associated with the node
        this._graphicFeatureGroupID = this.get_element().attributes['graphicfeaturegroupid'].value;

        // Put script to retrieve the component ID of the maptips control associated with the node
        // in a timeout.  We do this because this method may execute before the graphic feature group 
        // and maptips have been instantiated. 
        var mapTipsIDInitScript = "var node = $find('{0}');" +
            "node._mapTipsID = $find(node._graphicFeatureGroupID).get_mapTips().get_id();";
        mapTipsIDInitScript = String.format(mapTipsIDInitScript, this.get_id());
        window.setTimeout(mapTipsIDInitScript, 0);
    },

    // Destroys the node instance and those of any child nodes
    dispose: function() {       
        // Retrieve and destroy the associated MapTips control
        var mapTips = $find(this._mapTipsID);
        if (mapTips)
            mapTips.dispose();

        ESRI.ADF.Samples.GraphicFeatureGroupNode.callBaseMethod(this, 'dispose');
    },
    
    // Component ID of the graphic feature group associated with the node
    get_graphicFeatureGroupID: function() {
        return this._graphicFeatureGroupID;
    },
    
    // Overrides TreeViewPlusNode's addNode method to ensure the passed-in node is a GraphicFeatureNode
    addNode: function(node) {
        if (ESRI.ADF.Samples.GraphicFeatureNode.isInstanceOfType(node))
            ESRI.ADF.Samples.GraphicFeatureGroupNode.callBaseMethod(this, 'addNode', [node]);
    },

    // Child nodes that were checked when this node was unchecked and therefore have graphics needing to be 
    // synced with their checkbox's checked states.  The synchronization happens when this node is checked.
    getNodesPendingUpdate: function() {
        return this._childNodesPendingUpdate;
    },
    
    // Add a node to the collection of those pending visibility synchronization
    addNodePendingUpdate: function(nodeID) {
        this._childNodesPendingUpdate.push(nodeID);
    },
    
    // Overrides TreeViewPlusNode's set_page method to instantiate any new nodes as GraphicFeatureNodes
    set_page: function(pageNumber) {
        // Check whether there are any nodes to initialize
        if (this._newNodes[pageNumber]) {
            // Store the new nodes in a local variable before invoking the base class's set_page method.
            // This is because the base class will clear this array.
            var newNodes = this._newNodes[pageNumber];
            // Invoke the base method to render the requested page
            ESRI.ADF.Samples.GraphicFeatureGroupNode.callBaseMethod(this, 'set_page', [pageNumber]);

            // Instantiate GraphicFeatureNodes for any newly displayed child nodes
            for (var i = 0; i < newNodes.length; i++) {
                // Create a new GraphicFeatureNode and add it to the child node collection
                var nodeElement = $get(newNodes[i].nodeID);
                var node = $create(ESRI.ADF.Samples.GraphicFeatureNode, { 'id': nodeElement.id,
                    'containingPage': pageNumber }, null, null, nodeElement);
                this.addNode(node);
            }
        } else {
            ESRI.ADF.Samples.GraphicFeatureGroupNode.callBaseMethod(this, 'set_page', [pageNumber, true]);
        }
    },

    // Synchronizes the visibility of graphics associated with this node and its child nodes with the checked
    // states of the nodes' checkboxes
    updateChildVisibility: function() {
        // If the parent node is unchecked, the graphics are already not visible
        if (!this.get_parent().get_isChecked())
            return;

        // Get the node's graphic feature group 
        var graphicFeatureGroup = $find(this._graphicFeatureGroupID);

        if (this.get_isChecked()) {
            // Turn on the node's graphic feature group
            graphicFeatureGroup.set_visible(true);
            
            // Synchronize the visibility of child nodes that were checked while this node was unchecked
            var nodeCount = this._childNodesPendingUpdate.length;
            for (var i = 0; i < nodeCount; i++) {
                $find(this._childNodesPendingUpdate[0]).updateVisibility();
                this._childNodesPendingUpdate.splice(0, 1);
            }
        } else {
            graphicFeatureGroup.set_visible(false);
        }
    }
}

// Register the GraphicFeatureGroupNode class as an ASP.NET AJAX client control
ESRI.ADF.Samples.GraphicFeatureGroupNode.registerClass('ESRI.ADF.Samples.GraphicFeatureGroupNode', ESRI.ADF.Samples.TreeViewPlusNode);




// ***************************************************************
//       GraphicFeatureNode client control implementation
// ***************************************************************

ESRI.ADF.Samples.GraphicFeatureNode = function(element) {
    ESRI.ADF.Samples.GraphicFeatureNode.initializeBase(this, [element]);
};
ESRI.ADF.Samples.GraphicFeatureNode.prototype = {
    // Fires when a GraphicFeatureNode is instantiated via $create
    initialize: function() {
        ESRI.ADF.Samples.GraphicFeatureNode.callBaseMethod(this, 'initialize');

        // Wire the updateVisibility method to the node checkbox's checked event
        this.add_checked(Function.createDelegate(this, this.updateVisibility));

        // Get the component ID of the graphic feature associated with the node
        this._graphicFeatureID = this.get_element().attributes['graphicid'].value;
    },
    
    // Component ID of the graphic feature associated with the node
    get_graphicFeatureID: function() {
        return this._graphicFeatureID;
    },
    
    // Synchronizes the associated graphic feature's visibility with the node checkbox's checked state
    updateVisibility: function() {
        if (!this.get_parent() || 
        !ESRI.ADF.Samples.GraphicFeatureGroupNode.isInstanceOfType(this.get_parent()))
            return;

        // Get the graphic feature group containing the node's graphic feature
        var graphicFeatureGroup = $find(this.get_parent().get_graphicFeatureGroupID());
        
        // Check whether the graphic feature group is visible
        if (graphicFeatureGroup && graphicFeatureGroup.get_visible()) {
            // If the node checkbox is checked, remove the node's graphic feature from the graphic feature
            // group.  Otherwise, add it.
            var graphicFeature = $find(this._graphicFeatureID);
            if (this.get_isChecked()) 
                graphicFeatureGroup.add(graphicFeature);                
            else 
                graphicFeatureGroup.remove(graphicFeature);
        } else {
            // Since the graphic feature group is not visible, the graphic feature's visibility can't be
            // updated.  So we use the GraphicFeatureGroupNode's array of nodes pending visiblity updates
            // to manage graphics that need to be updated the next time the GraphicFeatureGroupNode is 
            // checked.
            var featureNodesPendingUpdate = this.get_parent().getNodesPendingUpdate();

            var addNodeToPending = true;
            for (var i = 0; i < featureNodesPendingUpdate.length; i++) {
                // If the node is already in the parent node's pending array, then the current node's checked
                // state matches what it was when the GraphicFeatureGroupNode was unchecked, meaning that the
                // visibility of the associated graphic feature does not need to be updated.  So remove the 
                // node from the pending array.
                if (featureNodesPendingUpdate[i] == this.get_id()) {
                    featureNodesPendingUpdate.splice(i, 1);
                    addNodeToPending = false;
                    break;
                }
            }

            // Since the node was not already in the pending array, its checked state is the opposite of what
            // it was when the parent node was unchecked.  So add the node to the pending array so the 
            // associated graphic feature is updated the next time the parent node is checked.
            if (addNodeToPending)
                this.get_parent().addNodePendingUpdate(this.get_id());
        }
    }
}

// Register the GraphicFeatureNode class as an ASP.NET AJAX client control
ESRI.ADF.Samples.GraphicFeatureNode.registerClass('ESRI.ADF.Samples.GraphicFeatureNode', ESRI.ADF.Samples.TreeViewPlusNode);