Common Task results
Common_TaskResults_CSharp\OnDemandTaskResults\OnDemandTaskResults.cs
// 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 the use restrictions.
// 


namespace OnDemandTaskResults_CSharp
{
    [System.Web.UI.ToolboxData(@"<{0}:OnDemandTaskResults runat=""server"" Width=""200px"" Height=""200px"" 
        BackColor=""#ffffff"" Font-Names=""Verdana"" Font-Size=""8pt"" ForeColor=""#000000""
        ShowAttributesOnDemand=""True"" ActivityIndicatorText=""Retrieving Data""> </{0}:OnDemandTaskResults>")]
    // Specify the OnDemandTaskResults JavaScript file as a client script resource.  This will register the script
    // on the client.  LoadOrder is specified to ensure that this custom script is loaded after the Web ADF's 
    // TaskResults control's script is registered, as the custom script requires the availability of the client
    // tier TaskResults control.
    [AjaxControlToolkit.ClientScriptResource("OnDemandTaskResults_CSharp.OnDemandTaskResults",
        "OnDemandTaskResults_CSharp.JavaScript.OnDemandTaskResults.js", LoadOrder = 3)]
    public class OnDemandTaskResults : ESRI.ArcGIS.ADF.Web.UI.WebControls.TaskResults
    {
        #region Public Properties - ShowAttributesOnDemand, ActivityIndicatorText

        /// <summary>
        /// Determines whether to retrieve feature attributes individually when a feature node is expanded (true) 
        /// or all at once when the task results are first generated (false)
        /// </summary>
        public bool ShowAttributesOnDemand
        {
            get
            { 
                return (this.StateManager.GetProperty("ShowAttributesOnDemand") == null) ? true :
                (bool)this.StateManager.GetProperty("ShowAttributesOnDemand"); 
            }
            set { this.StateManager.SetProperty("ShowAttributesOnDemand", value); }
        }

        /// <summary>
        /// Determines the text shown while data is being retrieved for task results
        /// </summary>
        public string ActivityIndicatorText
        {
            get
            {
                return (this.StateManager.GetProperty("ActivityIndicatorText") == null) ? "Retrieving Data" :
                (string)this.StateManager.GetProperty("ActivityIndicatorText");
            }
            set { this.StateManager.SetProperty("ActivityIndicatorText", value); }
        }
        #endregion

        #region Private Properties - NodeDataCache, GraphicsAttributeCache, ProcessedNodes

        /// <summary>
        /// Stores the text (markup) for nodes containing feature data.  The text for a node is retrieved
        /// from the cache when that node is first expanded.  Node ID is used as each entry's key.
        /// </summary>
        private System.Collections.Generic.Dictionary<string, string> NodeDataCache
        {
            get
            {
                // Attempt to retrieve the node data from state
                System.Collections.Generic.Dictionary<string, string> nodeDataCache =
                    this.StateManager.GetProperty("NodeCache") as System.Collections.Generic.Dictionary<string, string>;

                // If the node data is not yet stored, create a new dictionary for the data and store it
                if (nodeDataCache == null)
                {
                    nodeDataCache = new System.Collections.Generic.Dictionary<string, string>();
                    this.StateManager.SetProperty("NodeCache", nodeDataCache);
                }

                return nodeDataCache;
            }
        }

        /// <summary>
        /// Stores the attribute data for the Graphics associated with GraphicsLayerNodes.  The attributes for 
        /// a graphic feature is retrieved from the cache when that feature's MapTips is first shown or when
        /// its task results node is first expanded.
        /// </summary>
        private System.Collections.Generic.Dictionary<string, string> GraphicsAttributeCache
        {
            get
            {
                // Attempt to retrieve the node data from state
                System.Collections.Generic.Dictionary<string, string> graphicsAttributeCache =
                    this.StateManager.GetProperty("GraphicsAttributeCache") as 
                    System.Collections.Generic.Dictionary<string, string>;

                // If the node data is not yet stored, create a new dictionary for the data and store it
                if (graphicsAttributeCache == null)
                {
                    graphicsAttributeCache = new System.Collections.Generic.Dictionary<string, string>();
                    this.StateManager.SetProperty("GraphicsAttributeCache", graphicsAttributeCache);
                }

                return graphicsAttributeCache;
            }
        }

        /// <summary>
        /// Stores the IDs of nodes for which attribute data has been retrieved
        /// </summary>
        private System.Collections.Generic.List<string> ProcessedNodes
        {
            get
            {
                // Attempt to retrieve the node data from state
                System.Collections.Generic.List<string> processedNodes =
                    this.StateManager.GetProperty("ProcessedNodes") as System.Collections.Generic.List<string>;

                // If the node data is not yet stored, create a new dictionary for the data and store it
                if (processedNodes == null)
                {
                    processedNodes = new System.Collections.Generic.List<string>();
                    this.StateManager.SetProperty("ProcessedNodes", processedNodes);
                }

                return processedNodes;
            }
        }

        #endregion

        #region ASP.NET WebControl Life Cycle Event Handlers - CreateChildControls

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.ContextMenuItem menuItem in this.FeatureContextMenu.Items)
            {
                if (menuItem.Text.ToLower() == "remove")
                {
                    this.FeatureContextMenu.Items.Remove(menuItem);
                    break;
                }
            }

            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.ContextMenuItem menuItem in this.GraphicsLayerContextMenu.Items)
            {
                if (menuItem.Text.ToLower() == "remove")
                {
                    this.GraphicsLayerContextMenu.Items.Remove(menuItem);
                    break;
                }
            }

            // Wire event handlers to fire when a node is added, removed, or expanded
            this.NodeAdded += 
                new ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNodeAddedEventHandler(OnDemandTaskResults_NodeAdded);
            this.NodeExpanded += 
                new ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNodeExpandedEventHandler(OnDemandTaskResults_NodeExpanded);
            this.NodeRemoved += 
                new ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNodeRemovedEventHandler(OnDemandTaskResults_NodeRemoved);
        }

        #endregion

        #region Web ADF Control Event Handlers - NodeAdded, NodeExpanded, NodeRemoved

        // Fires when a node is added.  Replaces any feature data with an activity indicator and 
        // stores that data for on-demand retrieval.
        void OnDemandTaskResults_NodeAdded(object sender, 
            ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNodeEventArgs args)
        {
            // If attributes are not to be retrieved on-demand, exit the method
            if (!this.ShowAttributesOnDemand || !(args.Node is ESRI.ArcGIS.ADF.Web.UI.WebControls.TaskResultNode))
                return;

            // Attempt to get GraphicsLayerNodes that are descendants of the added node
            System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode> 
                graphicsLayerNodes = this.FindChildGraphicsLayerNodes(args.Node);

            // Make sure a GraphicsLayerNode was found
            if (graphicsLayerNodes.Count == 0)
                return;

            // Get the control that is adding the result node
            System.Web.UI.Control callingControl = this.GetCallingControl(this.Page);

            // Cast the control to IAttributesOnDemandProvider
            OnDemandTaskResults_CSharp.IAttributesOnDemandProvider attributesOnDemandProvider =
                 callingControl as OnDemandTaskResults_CSharp.IAttributesOnDemandProvider;

            // Set a boolean indicating whether the task results control needs to cache attributes.  This will
            // be the case if the control that added the results does not implement IAttributesOnDemandProvider.
            bool cacheAttributesInWebTier = (attributesOnDemandProvider == null);

            // If the control that has added results implements IAttributesOnDemandProvider, add the ID of 
            // the control as an attribute on the task result node.
            if (attributesOnDemandProvider != null)
                args.Node.Attributes.Add("AttributesProviderID", callingControl.ID);

            // Stores the properties that will be used to initialize client tier graphics layer nodes
            System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> graphicsLayerNodeList =
                new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();

            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode
                in graphicsLayerNodes)
            {
                // Get the feature nodes contained by the current graphics layer node
                System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode> featureNodes =
                    this.FindChildFeatureNodes(graphicsLayerNode);

                // Get the markup for the activity indicator that will be shown when retrieving attributes for
                // a MapTip or feature node
                string activityIndicatorMarkup = this.CreateActivityIndicatorMarkup();

                // Replace the markup for any feature nodes contained in the passed-in node with the
                // activity indicator markup.  This method will store the replaced feature data markup 
                // for retrieval the first time a node is expanded.
                this.CreateOnDemandNodes(args.Node, activityIndicatorMarkup, cacheAttributesInWebTier);

                // Update the task results MapTips so that attributes are retrieved on-demand (i.e. 
                // when a MapTip is first expanded)
                this.CreateOnDemandMapTips(graphicsLayerNode, activityIndicatorMarkup, cacheAttributesInWebTier);

                // Create a dictionary to hold the graphics layer node's properties and add the node ID and the
                // IDs of child feature nodes
                System.Collections.Generic.Dictionary<string, object> graphicsLayerNodeProperties =
                    new System.Collections.Generic.Dictionary<string, object>();
                graphicsLayerNodeProperties.Add("nodeID", graphicsLayerNode.NodeID);
                graphicsLayerNodeProperties.Add("featureNodes", this.GetCurrentPageNodeIDs(graphicsLayerNode));

                // If the node has more than one page of child nodes, add properties storing page information
                if (graphicsLayerNode.NumberOfPages > 1)
                {
                    graphicsLayerNodeProperties.Add("pagingTextFormatString", graphicsLayerNode.PagingTextFormatString);
                    graphicsLayerNodeProperties.Add("pageSize", graphicsLayerNode.PageSize);
                    graphicsLayerNodeProperties.Add("pageCount", graphicsLayerNode.NumberOfPages);
                    graphicsLayerNodeProperties.Add("nodeCount", graphicsLayerNode.Nodes.Count);
                }  

                // Add the properties to the list of graphics layer node properties
                graphicsLayerNodeList.Add(graphicsLayerNodeProperties);
            }

            // Create a dictionary to hold the node ID of the task results node and the client tier properties of child
            // graphics layer nodes.  This information is used to initialize the task results node and its child nodes
            // on the client.
            System.Collections.Generic.Dictionary<string, object> taskResultNodeProperties =
                new System.Collections.Generic.Dictionary<string, object>();
            taskResultNodeProperties.Add("nodeID", args.Node.NodeID);
            taskResultNodeProperties.Add("graphicsLayerNodes", graphicsLayerNodeList);

            System.Web.Script.Serialization.JavaScriptSerializer jsSerializer =
                new System.Web.Script.Serialization.JavaScriptSerializer();
            // Construct script to call the client-side task result node initialization routine, passing
            // it the task results JSON constructed above.  Wrap the script in a callback result for
            // processing on the client
            string nodeInitScript = @"
                var taskResults = $find('{0}');
                taskResults.initClientNodes({1});";
            nodeInitScript = string.Format(nodeInitScript, this.ClientID, jsSerializer.Serialize(taskResultNodeProperties));
            this.CallbackResults.Add(
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(nodeInitScript));            
        }

        // Fires when a node is expanded.  Updates the node text and associated graphic feature with
        // stored attribute data.
        void OnDemandTaskResults_NodeExpanded(object sender, 
            ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNodeEventArgs args)
        {
            // Make sure the expanded node is a feature node
            if (!(args.Node is ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode))
                return;

            // If the node data cache contains data for the graphic feature, then update the feature
            // and its node from the web tier cache.  Otherwise, the control that created the result
            // must implement IAttributesOnDemandProvider, so we update the result with information
            // retrieved from that control.
            if (this.NodeDataCache.ContainsKey(args.Node.Attributes["GraphicID"]))
                this.UpdateResultFromCache(args.Node as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode, null);
            else if (!this.ProcessedNodes.Contains(args.Node.NodeID))
                this.UpdateResultFromProvider(args.Node as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode, null);

            // === USED ONLY FOR DEMONSTRATION - REMOVE FOR PRODUCTION PURPOSES ==
            //  Suspend the current thread to allow the retrieving data activity indicator to display
            System.Threading.Thread.Sleep(1000);
        }

        // Fires when a node is removed.  Removes any feature data associated with the passed-in node
        // or child nodes from the graphic feature and node data caches.
        void OnDemandTaskResults_NodeRemoved(object sender, ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNodeRemovedEventArgs args)
        {
            // Remove any feature data associated with the node or any of its descendants from the web tier cache
            this.RemoveDataFromCache(args.Node);

            // Add a callback result to dispose the client tier node object
            string disposeClientNodeScript = string.Format("$find('{0}').dispose()",
                args.Node.NodeID);
            this.CallbackResults.Add(
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(disposeClientNodeScript));
        }

        #endregion

        #region ICallbackEventHandler Overrides - RaiseCallbackEvent

        // Retrieves node and graphic feature attributes when a task result MapTip is expanded
        public override void RaiseCallbackEvent(string eventArgument)
        {
            // Get the callback arguments
            System.Collections.Specialized.NameValueCollection callbackArgs =
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackUtility.ParseStringIntoNameValueCollection(eventArgument);

            // Check whether the argument specifies retrieval of a graphic feature's attributes or a new node page
            switch (callbackArgs["EventArg"])
            {
                case "getAttributes" :
                    // Get the graphics layer and feature nodes associated with the graphic feature
                    ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode graphicsLayerNode =
                        this.Nodes.FindByNodeID(callbackArgs["LayerNodeID"]);
                    ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode featureNode =
                        this.FindNodeByAttribute(graphicsLayerNode, "GraphicID", callbackArgs["GraphicID"])
                        as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode;

                    if (featureNode == null)
                        return;

                    // If the node data cache contains data for the graphic feature, then update the feature
                    // and its node from the web tier cache.  Otherwise, the control that created the result
                    // must implement IAttributesOnDemandProvider, so we update the result with information
                    // retrieved from that control.
                    if (this.NodeDataCache.ContainsKey(featureNode.Attributes["GraphicID"]))
                        this.UpdateResultFromCache(featureNode, callbackArgs["MapTipsID"]);
                    else if (!this.ProcessedNodes.Contains(featureNode.NodeID))
                        this.UpdateResultFromProvider(featureNode, callbackArgs["MapTipsID"]);

                    // === USED ONLY FOR DEMONSTRATION - REMOVE FOR PRODUCTION PURPOSES ==
                    //  Suspend the current thread to allow the retrieving attributes indicator to display
                    System.Threading.Thread.Sleep(1000);
                    break;
                case "getPage" :
                    // Get the parent node for which a new page of child nodes needs to be retrieved
                    ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode node =
                        this.Nodes.FindByNodeID(callbackArgs["NodeID"]);
                    // Update the node's page number property
                    node.PageNumber = int.Parse(callbackArgs["PageNumber"]);

                    // Get JSON used to initialize the new page of nodes on the client
                    string nodesJson = this.GetNodesJson(node);
                    
                    // Construct JavaScript to initialize the new page on the client using client-side initialization
                    // methods and the nodes JSON.  Wrap the script in a callback result for processing on the client.
                    string changeNodePageScript = @"
                        var node = $find('{0}');
                        node._newNodes[{1}] = {2};
                        node.set_page({1});";
                    changeNodePageScript = string.Format(changeNodePageScript, callbackArgs["NodeID"],
                        node.PageNumber, nodesJson);
                    this.CallbackResults.Add(
                        ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(changeNodePageScript));
                    break;
                case "nodeChecked":
                    // Get the node that was checked
                    ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode checkedNode =
                        this.Nodes.FindByNodeID(callbackArgs["NodeID"]) as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode;
                    if (checkedNode == null)
                        break;

                    // Update the web tier Checked property of the node
                    checkedNode.Checked = bool.Parse(callbackArgs["Checked"]);

                    // Get the parent node of the checked node and make sure it's a GraphicsLayerNode
                    ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode parentNode = checkedNode.Parent as
                        ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode;
                    if (parentNode == null)
                        break;

                    // Find the row in the graphics layer corresponding to the checked node and update its selected
                    // value.  This is necessary so Web ADF operations relying on this value, such as zooming to 
                    // selected features, still function properly.
                    foreach (System.Data.DataRow row in parentNode.Layer.Rows)
                    {
                        string graphicFeatureID = this.GetGraphicClientID(row[parentNode.Layer.GraphicsIDColumn].ToString(), parentNode);
                        if (graphicFeatureID == checkedNode.Attributes["GraphicID"])
                        {
                            row[parentNode.Layer.IsSelectedColumn] = bool.Parse(callbackArgs["Checked"]);
                            break;
                        }
                    }
                    break;                    
            }

            base.RaiseCallbackEvent(eventArgument);
        }

        #endregion

        #region Private Methods

        #region On-Demand Initialization - CreateOnDemandNodes, CreateOnDemandMapTips, CreateMapTipsInitScript

        // Replaces any feature data displayed by the passed-in node or child nodes with a "retrieving
        // data" activity indicator and stores that feature data for on-demand retrieval
        private void CreateOnDemandNodes(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode parentNode, 
            string nodeMarkup, bool cacheInWebTier)
        {
            // Attempt to get a reference to the passed-in node as a FeatureNode
            ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode featureNode = parentNode
                as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode;

            // Check whether the node is a FeatureNode and has a child node.  The child node will contain
            // the feature's attribute data
            if (featureNode != null && featureNode.Nodes.Count > 0 && 
                featureNode.Nodes[0].Text.ToLower().Contains("<table>"))
            {
                // Add the node's feature data to the node data cache
                if (cacheInWebTier)
                    this.NodeDataCache.Add(featureNode.Attributes["GraphicID"], featureNode.Nodes[0].Text);

    
                // Update the node's text with the passed-in markup
                featureNode.Nodes[0].Text = nodeMarkup;
            }

            // Recursively call this method for any nodes that are children of the passed-in node
            if (parentNode.Nodes.Count > 0)
            {
                foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in parentNode.Nodes)
                    this.CreateOnDemandNodes(childNode, nodeMarkup, cacheInWebTier);
            }

            return;
        }

        // Removes the attribute data of features contained in the graphics layer referenced by the passed-in node
        // and stores this data in the web tier.  Generates script to initialize the graphics layer's MapTips to
        // display an activity indicator and retrieve stored data when a MapTip is expanded.
        private void CreateOnDemandMapTips(ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode,
            string activityIndicatorMarkup, bool cacheAttributes)
        {
            // Get the layer referenced by the node
            ESRI.ArcGIS.ADF.Web.Display.Graphics.GraphicsLayer graphicsLayer = graphicsLayerNode.Layer;

            if (cacheAttributes)
            {
                // Store a JSON string containing the MapTips attributes for each feature (row) in the
                // graphics layer.  
                foreach (System.Data.DataRow row in graphicsLayer.Rows)
                {
                    string jsonAttributes = this.GetAttributesJson(row, graphicsLayer);

                    // Add the attributes for the current row to the graphics attribute cache, specifying the 
                    // corresponding client-side GraphicFeature ID as the key
                    string graphicID = this.GetGraphicClientID(row[graphicsLayer.GraphicsIDColumn].ToString(), graphicsLayerNode);
                    this.GraphicsAttributeCache.Add(graphicID, jsonAttributes);
                }

                // Get the graphics layer's title template.  This is the template used to format the title
                // of the layer's MapTips
                string titleTemplate = graphicsLayer.GetTitleTemplate(true);

                // Remove data from the graphics layer for all fields except IsSelected, graphics ID, geometry, and any column 
                // that is included in the MapTips title.
                int removeIndex = 0;
                int columnCount = graphicsLayer.Columns.Count;
                System.Data.DataColumn currentColumn;
                for (int i = 0; i < columnCount; i++)
                {
                    currentColumn = graphicsLayer.Columns[removeIndex];
                    if (!titleTemplate.Contains("{" + currentColumn.ColumnName + "}") &&
                    (currentColumn.ColumnName != graphicsLayer.GraphicsIDColumn.ColumnName) &&
                    (currentColumn.ColumnName != graphicsLayer.IsSelectedColumn.ColumnName) &&
                    !currentColumn.DataType.IsAssignableFrom(typeof(ESRI.ArcGIS.ADF.Web.Geometry.Geometry)))
                        graphicsLayer.Columns.RemoveAt(removeIndex);
                    else
                        removeIndex++;
                }
            }

            // Generate JavaScript to initialize on-demand functionality for the graphics layer's MapTips
            // on the client, and add this script to the TaskResults control's callback results
            string initializeMapTipsJavaScript = this.CreateMapTipsInitScript(graphicsLayerNode, 
                activityIndicatorMarkup);
            this.CallbackResults.Add(
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(initializeMapTipsJavaScript));
        }

        // Generates the JavaScript necessary to initialize attributes-on-demand functionality for the MapTips
        // of the graphics layer referenced by the passed-in node.
        private string CreateMapTipsInitScript(ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode,
            string activityIndicatorMarkup)
        {
            // Get the AJAX client ID of the GraphicFeatureGroup corresponding to the passed-in graphics layer
            string graphicsLayerClientID = this.GetGraphicsLayerClientID(graphicsLayerNode.Layer);

            // Add an attribute on the node to allow convenient retrieval of the GraphicFeatureGroup on the client
            graphicsLayerNode.Attributes.Add("GraphicFeatureGroupID", graphicsLayerClientID);

            // Create a dictionary storing the on-demand mapTips initialization properties
            System.Collections.Generic.Dictionary<string, object> mapTipsInitProperties =
                new System.Collections.Generic.Dictionary<string, object>();
            mapTipsInitProperties.Add("graphicFeatureGroupID", graphicsLayerClientID);
            mapTipsInitProperties.Add("graphicsLayerNodeID", graphicsLayerNode.NodeID);
            mapTipsInitProperties.Add("activityIndicatorTemplate", activityIndicatorMarkup);
            mapTipsInitProperties.Add("callbackFunctionString", this.CallbackFunctionString);
            System.Web.Script.Serialization.JavaScriptSerializer jsSerializer =
                new System.Web.Script.Serialization.JavaScriptSerializer();

            // Construct script to call the client tier on-demand MapTips initialization method.  This logic is
            // embedded in a timeout so it executes after the results graphics layer has been created.  The 
            // initialization properties, stored in a dictionary above, are serialized to JSON and temporarily 
            // stored on the client tier task results object so they can be accessed from within the timeout.
            string mapTipsInitJavaScript = @"var taskResults = $find('{0}');
                if (!taskResults._initializationProps)
                    taskResults._initializationProps = new Array();
                taskResults._initializationProps.push({1});
                window.setTimeout(""var mapTips = $find('{2}').get_mapTips();"" +
                    ""mapTips.setupOnDemandMapTips($find('{0}')._initializationProps.splice(0,1)[0]);"", 0);";
            return string.Format(mapTipsInitJavaScript, this.ClientID, 
                jsSerializer.Serialize(mapTipsInitProperties), graphicsLayerClientID);
        }

        #endregion

        #region On-Demand Data Retrieval - UpdateResultFromProvider, UpdateResultFromCache, UpdateNodeData, UpdateGraphicFeatureData

        // Creates and adds the callback results necessary to update the graphic feature and feature node
        // referred to by the passed-in node.  Retrieves the feature data from the control that created
        // the result.
        private void UpdateResultFromProvider(ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode featureNode,
            string mapTipsClientID)
        {
            // Get the graphics layer and task result nodes that contain the passed-in feature node
            ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode =
                featureNode.Parent as ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode;
            if (graphicsLayerNode == null)
                return;

            ESRI.ArcGIS.ADF.Web.UI.WebControls.TaskResultNode taskResultNode =
                graphicsLayerNode.Parent as ESRI.ArcGIS.ADF.Web.UI.WebControls.TaskResultNode;
            if (taskResultNode == null)
                return;

            // Get the control that added the result and cast it to the IAttributesOnDemandProvider interface
            OnDemandTaskResults_CSharp.IAttributesOnDemandProvider attributesProvider =
                ESRI.ArcGIS.ADF.Web.UI.WebControls.Utility.FindControl(taskResultNode.Attributes["AttributesProviderID"],
                this.Page) as OnDemandTaskResults_CSharp.IAttributesOnDemandProvider;

            // Extract the feature ID from the graphic feature ID
            string graphicFeatureID = featureNode.Attributes["GraphicID"];
            string featureID = graphicFeatureID.Substring(graphicFeatureID.LastIndexOf('_') + 1);

            // Get the feature's attributes
            System.Data.DataRow attributesRow = attributesProvider.GetAttributeData(featureID);

            // Update the feature node and graphic feature with the attributes
            string nodeMarkup = this.GetDataRowHtmlTable(attributesRow);
            this.UpdateNodeData(featureNode, nodeMarkup);
            string attributesJson = this.GetAttributesJson(attributesRow, graphicsLayerNode.Layer);
            this.UpdateGraphicFeatureData(graphicFeatureID, attributesJson, mapTipsClientID);

            // Flag the passed-in node as processed so the control won't attempt to retrieve attributes for the same
            // node again.
            this.ProcessedNodes.Add(featureNode.NodeID);
        }

        // Creates and adds the callback results necessary to update the graphic feature and feature node
        // referred to by the passed-in node.  Retrieves the feature data from the web tier cache created
        // by this instance.
        private void UpdateResultFromCache(ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode featureNode,
            string mapTipsClientID)
        {
            // Get the graphic feature ID
            string graphicID = featureNode.Attributes["GraphicID"];

            // Update the server-side representation of the node with the stored data
            string nodeMarkup = this.NodeDataCache[graphicID];
            this.UpdateNodeData(featureNode, nodeMarkup);

            // Remove the node's data from web tier storage
            this.NodeDataCache.Remove(graphicID);

            // Get the JSON string containing the feature data from storage
            string attributesJson = this.GraphicsAttributeCache[graphicID];

            // Update the graphic feature's data
            this.UpdateGraphicFeatureData(graphicID, attributesJson, mapTipsClientID);

            // Remove the attribute data for the specified feature from storage
            this.GraphicsAttributeCache.Remove(graphicID);

            // Flag the passed-in node as processed so the control won't attempt to retrieve attributes for the same
            // node again.
            this.ProcessedNodes.Add(featureNode.NodeID);
        }

        // Updates the text of the passed-in node with the feature data corresponding to the node
        private void UpdateNodeData(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode node, string nodeMarkup)
        {
            // Update the node's text
            node.Nodes[0].Text = nodeMarkup;

            // Get the node's markup and serialize it to JSON
            System.Web.Script.Serialization.JavaScriptSerializer jsSerializer =
                new System.Web.Script.Serialization.JavaScriptSerializer();
            nodeMarkup = jsSerializer.Serialize(this.GetNodeHtml(node));

            // Construct JavaScript to update the node's content on the client.  Package the script in a callback
            // result for client-side processing.
            string nodeUpdateScript = @"
                var node = $find('{0}');
                if (node)
                    node.set_content({1});";
            nodeUpdateScript = string.Format(nodeUpdateScript, node.NodeID, nodeMarkup);
            this.CallbackResults.Add(
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(nodeUpdateScript));
        }

        // Updates the attributes of the client tier GraphicFeature specified by the passed-in ID with
        // the feature data stored in the web tier
        private void UpdateGraphicFeatureData(string graphicFeatureID, string attributesJson, string mapTipsClientID)
        {
            string updateAttributesScript;

            // Check whether a client-side MapTips control was specified
            if (mapTipsClientID != null)
            {
                // Create script to update the attributes from the specified client tier MapTips control. This 
                // will not only update the GraphicFeature, but also immediately update the MapTip to show 
                // those attributes, if that MapTip is still open when the data is returned to the client.
                updateAttributesScript = @"
                    var mapTips = $find('{0}');
                    mapTips.updateAttributes('{1}', {2});";
                updateAttributesScript = string.Format(updateAttributesScript, mapTipsClientID,
                    graphicFeatureID, attributesJson);
            }
            else
            {
                // Create script to update the attributes of the specified GraphicFeature directly
                updateAttributesScript = @"
                    var graphicFeature = $find('{0}');
                    graphicFeature.set_attributes({1});
                    graphicFeature.set_hasAttributes(true);";
                updateAttributesScript = string.Format(updateAttributesScript, graphicFeatureID, attributesJson);
            }

            // Add the script to the TaskResults control's callback results
            this.CallbackResults.Add(
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(updateAttributesScript));
        }

        #endregion

        #region Activity Indicator Creation - CreateActivityIndicatorTable, GetControlHtml

        // Generates a Table WebControl containing an activity indicator with the text specified via the
        // ActivityIndicatorText property
        private string CreateActivityIndicatorMarkup()
        {
            // Get the web resource URL for the activity indicator gif
            string activityIndicatorUrl = this.Page.ClientScript.GetWebResourceUrl(
                typeof(OnDemandTaskResults_CSharp.OnDemandTaskResults),
                "OnDemandTaskResults_CSharp.Images.callbackActivityIndicator.gif");

            // Create an Image control and initialize it to reference the activity indicator
            System.Web.UI.WebControls.Image activityIndicator = new System.Web.UI.WebControls.Image();
            activityIndicator.ImageUrl = activityIndicatorUrl;

            // Create a cell for the indicator
            System.Web.UI.WebControls.TableCell tableCell = new System.Web.UI.WebControls.TableCell();
            tableCell.Controls.Add(activityIndicator);

            // Create a row and add the indicator cell to it
            System.Web.UI.WebControls.TableRow tableRow = new System.Web.UI.WebControls.TableRow();
            tableRow.VerticalAlign = System.Web.UI.WebControls.VerticalAlign.Middle;
            tableRow.Cells.Add(tableCell);

            // Create the indicator label
            System.Web.UI.WebControls.Label activityLabel = new System.Web.UI.WebControls.Label();
            activityLabel.Text = this.ActivityIndicatorText;
            activityLabel.Font.Italic = true;
            activityLabel.ForeColor = System.Drawing.Color.Gray;

            // Add the indicator label to a new cell
            tableCell = new System.Web.UI.WebControls.TableCell();
            tableCell.Controls.Add(activityLabel);
            tableRow.Cells.Add(tableCell);

            // Add the indicator label cell to the indicator row
            System.Web.UI.WebControls.Table activityIndicatorTable = new System.Web.UI.WebControls.Table();
            activityIndicatorTable.Rows.Add(tableRow);

            // Return the table's markup
            return this.GetControlHtml(activityIndicatorTable);
        }

        // Used to retrieve the markup for the activity indicator that will be shown when task results feature 
        // nodes or MapTips are first expanded
        private string GetControlHtml(System.Web.UI.WebControls.WebControl control)
        {
            // Use RenderControl to retrieve the markup for the passed-in control
            System.IO.StringWriter stringWriter = new System.IO.StringWriter();
            System.Web.UI.HtmlTextWriter htmlWriter = new System.Web.UI.HtmlTextWriter(stringWriter);
            control.RenderControl(htmlWriter);

            // Return the markup
            return stringWriter.ToString();
        }

        #endregion

        #region Node Retrieval - FindNodeByAttribute, FindChildGraphicsLayerNode, FindFeatureNodes

        // Searches the passed-in node and its descendants for a node with the specified attribute
        private ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode FindNodeByAttribute(
            ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode parentNode, string attribute, string value)
        {
            // Retrieve the specified attribute from the passed-in node
            string currentValue = parentNode.Attributes[attribute];

            // Return the passed-in node if it has an attribute matching the one sought
            if (currentValue == value)
                return parentNode;

            // Recursively call this function to search for the specified attribute on child nodes
            ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode matchingNode = null;
            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in parentNode.Nodes)
            {
                matchingNode = this.FindNodeByAttribute(childNode, attribute, value);
                if (matchingNode != null)
                    break;
            }

            return matchingNode;
        }

        // Retrieves all GraphicsLayerNodes that are descendants of the passed-in node, if available
        private System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode> 
            FindChildGraphicsLayerNodes(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode node)
        {
            System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode> nodes =
                new System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode>();

            ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode = node
                as ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode;
            if (graphicsLayerNode != null)
                nodes.Add(graphicsLayerNode);

            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in node.Nodes)
                nodes.AddRange(this.FindChildGraphicsLayerNodes(childNode));

            return nodes;
        }
        
        // Retrieves all FeatureNodes that are descendants of the passed-in node, if available
        private System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode> 
            FindChildFeatureNodes(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode node)
        {
            System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode> nodes =
                new System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode>();

            ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode featureNode = node
                as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode;
            if (featureNode != null)
                nodes.Add(featureNode);

            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in node.Nodes)
                nodes.AddRange(this.FindChildFeatureNodes(childNode));

            return nodes;
        }

        #endregion

        #region Node Information - GetNodesJson, GetNodeHtml, GetCurrentPageNodeIDs

        // Retrieves the IDs and html for child nodes of the passed-in node on the node's current page, 
        // formatted as a JSON string
        private string GetNodesJson(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode parentNode)
        {
            // Get the index of the first and last nodes on the curernt page
            int startIndex = (parentNode.PageNumber - 1) * parentNode.PageSize;
            int stopIndex = parentNode.PageNumber * parentNode.PageSize;
            if (stopIndex > parentNode.Nodes.Count)
                stopIndex = parentNode.Nodes.Count;

            // Create a list to store all the node properties
            System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>> nodeList =
                new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();
            // Create a dictionary to store each node's contents
            System.Collections.Generic.Dictionary<string, object> nodeContents;

            // Loop through the nodes on the current page and add the ID and markup of each to the property list
            for (int i = startIndex; i < stopIndex; i++)
            {
                nodeContents = new System.Collections.Generic.Dictionary<string, object>();
                nodeContents.Add("nodeID", parentNode.Nodes[i].NodeID);
                nodeContents.Add("_content", this.GetNodeHtml(parentNode.Nodes[i]));
                nodeList.Add(nodeContents);
            }

            // Serialize the node properties to JSON and return
            System.Web.Script.Serialization.JavaScriptSerializer jsSerializer =
                new System.Web.Script.Serialization.JavaScriptSerializer();
            return jsSerializer.Serialize(nodeList);
        }

        // Retrieves the markup for the passed-in node
        private string GetNodeHtml(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode node)
        {
            System.IO.StringWriter stringWriter = new System.IO.StringWriter();
            System.Web.UI.HtmlTextWriter htmlWriter = new System.Web.UI.HtmlTextWriter(stringWriter);

            // Output the node markup to the html text writer and return its string representation
            node.Render(htmlWriter);
            return stringWriter.ToString();
        }

        // Retrieves the Node IDs of any feature nodes on the passed-in graphics layer node's current page and returns 
        // them as a comma-delimited string.
        private System.Collections.Generic.List<string>
            GetCurrentPageNodeIDs(ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode)
        {
            // Retrieve feature nodes that are descendants of the passed-in node
            System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode> featureNodes =
                this.FindChildFeatureNodes(graphicsLayerNode);

            // Get the indexes of the first and last nodes on the current page
            int startIndex = (graphicsLayerNode.PageNumber - 1) * graphicsLayerNode.PageSize;
            int stopIndex = graphicsLayerNode.PageNumber * graphicsLayerNode.PageSize;
            if (stopIndex > graphicsLayerNode.Nodes.Count)
                stopIndex = graphicsLayerNode.Nodes.Count;

            // Loop through the nodes on the page and add the ID of each to a string
            ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode featureNode = null;

            System.Collections.Generic.List<string> nodeIDs = new System.Collections.Generic.List<string>();
            for (int i = startIndex; i < stopIndex; i++)
            {
                featureNode = graphicsLayerNode.Nodes[i] as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode;
                if (featureNode != null)
                    nodeIDs.Add(featureNode.NodeID);
            }

            return nodeIDs;
        }

        #endregion

        #region Client Graphic ID Retrieval - GetGraphicsLayerClientID, GetGraphicClientID

        // Retrieves the client ID of the GraphicFeatureGroup corresponding to the passed-in GraphicsLayer.
        // Assumes this GraphicsLayer is associated with a GraphicsLayerNode - the ID will be different otherwise.
        private string GetGraphicsLayerClientID(ESRI.ArcGIS.ADF.Web.Display.Graphics.GraphicsLayer graphicsLayer)
        {
            ESRI.ArcGIS.ADF.Web.Display.Graphics.FeatureGraphicsLayer featureGraphicsLayer =
                graphicsLayer as ESRI.ArcGIS.ADF.Web.Display.Graphics.FeatureGraphicsLayer;
            if (featureGraphicsLayer == null)
                return null;

            string graphicsLayerID = string.Format("{0}_{1} {2} Results_{3}", this.MapInstance.ClientID,
                this.ClientID, featureGraphicsLayer.FeatureType.ToString(), graphicsLayer.TableName);
            return graphicsLayerID;
        }

        // Retrieves the AJAX component ID of a GraphicFeature referenced by the passed-in node or its descendants, 
        // given the unique ID specified in the feature's attribute data.  The unique ID is part of the component
        // ID, but the two are not the same.
        private string GetGraphicClientID(string attributeUniqueID,
            ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode parentNode)
        {
            // Attempt to get a reference to the passed-in node as a FeatureNode
            ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode featureNode =
                parentNode as ESRI.ArcGIS.ADF.Web.UI.WebControls.FeatureNode;

            string clientGraphicID = null;
            if (featureNode != null && !string.IsNullOrEmpty(featureNode.Attributes["GraphicID"]))
            {
                // Get the AJAX componenent ID of the GraphicFeature corresponding to the current FeatureNode
                string graphicID = featureNode.Attributes["graphicID"];

                // Extract the GraphicFeature's unique ID from the component ID
                string nodeUniqueID = graphicID.Substring(graphicID.LastIndexOf("_") + 1);

                // If the extracted unique ID matches that passed-in, use the passed-in node's GraphicFeature ID
                // to initialize the return value
                if (attributeUniqueID == nodeUniqueID)
                    clientGraphicID = graphicID;
            }
            else if (parentNode.Nodes.Count > 0)
            {
                // Try retrieving the GraphicFeature ID from the passed-in node's child nodes
                foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in parentNode.Nodes)
                {
                    clientGraphicID = this.GetGraphicClientID(attributeUniqueID, childNode);
                    if (!string.IsNullOrEmpty(clientGraphicID))
                        break;
                }
            }

            return clientGraphicID;
        }

        #endregion

        // Removes feature data contained by the passed-in node or any of its descendants from the node and
        // graphic feature attributes data caches
        private void RemoveDataFromCache(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode parentNode)
        {
            // If the passed-in node's graphic ID has entries in the node and graphic attribute caches,
            // remove those entries
            string graphicID = parentNode.Attributes["GraphicID"];
            if (!string.IsNullOrEmpty(graphicID) && this.NodeDataCache.ContainsKey(graphicID))
            {
                this.NodeDataCache.Remove(graphicID);
                this.GraphicsAttributeCache.Remove(graphicID);
            }

            // Remove node and graphic attribute data corresponding to nodes that are children of the
            // passed-in node
            if (parentNode.Nodes.Count > 0)
            {
                foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in parentNode.Nodes)
                    this.RemoveDataFromCache(childNode);
            }

            return;
        }

        // Retrieves the control that initiated the asynchronous request
        private System.Web.UI.Control GetCallingControl(System.Web.UI.Page page)
        {
            if (page == null) return null;
            if (page.IsCallback)
            {
                string controlID = page.Request.Params["__CALLBACKID"];
                System.Web.UI.Control control = page.FindControl(controlID);
                return control;
            }
            // For 9.3 we could be using a partial postback instead
            else if (page.IsPostBack && System.Web.UI.ScriptManager.GetCurrent(page) != null &&
                System.Web.UI.ScriptManager.GetCurrent(page).IsInAsyncPostBack)
            {
                string controlID = System.Web.UI.ScriptManager.GetCurrent(page).AsyncPostBackSourceElementID;
                System.Web.UI.Control control = page.FindControl(controlID);
                return control;
            }
            else return null; //Not an asyncronous request
        }

        // Converts a DataRow to a JSON string.  Uses the passed-in graphics layer to determine which fields
        // in the row should be included.
        private string GetAttributesJson(System.Data.DataRow attributesRow, 
            ESRI.ArcGIS.ADF.Web.Display.Graphics.GraphicsLayer graphicsLayer)
        {
            string visibilityString;
            bool visibility;
            System.Collections.Generic.Dictionary<string, object> attributesDictionary =
                new System.Collections.Generic.Dictionary<string, object>();

            // Add the value of each column that is visible, not the is selected column, and not a geometry column
            // to the string dictionary.
            foreach (System.Data.DataColumn column in attributesRow.Table.Columns)
            {
                visibilityString = column.ExtendedProperties[ESRI.ArcGIS.ADF.Web.Constants.ADFVisibility] as string;
                visibility = (visibilityString == null) ? false : bool.Parse(visibilityString);
                if ((column.ColumnName != graphicsLayer.IsSelectedColumn.ColumnName) && visibility
                && !column.DataType.IsAssignableFrom(typeof(ESRI.ArcGIS.ADF.Web.Geometry.Geometry)))
                    attributesDictionary.Add(column.ColumnName, attributesRow[column.ColumnName]);
            }

            // Convert the string dictionary to JSON
            System.Web.Script.Serialization.JavaScriptSerializer jsSerialzer =
                new System.Web.Script.Serialization.JavaScriptSerializer();
            return jsSerialzer.Serialize(attributesDictionary);
        }

        #endregion
    }
}