Common Task results
Common_TaskResults_CSharp\ZoomToResults\ZoomToResults.cs
// Copyright 2011 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 ZoomToResults_CSharp
{
    // To override ITaskResultsContainer methods implemented in TaskResults but not marked virtual, implement 
    // ITaskResultsContainer methods directly.  For example, the DisplayResults and StartTaskActivityIndicator 
    // methods are implemented within this class.
    [System.Web.UI.ToolboxData(@"<{0}:ZoomToResults runat=""server"" Width=""200px"" Height=""200px"" 
        BackColor=""#ffffff"" Font-Names=""Verdana"" Font-Size=""8pt"" ForeColor=""#000000""> </{0}:ZoomToResults>")]
    public class ZoomToResults : ESRI.ArcGIS.ADF.Web.UI.WebControls.TaskResults, 
        ESRI.ArcGIS.ADF.Web.UI.WebControls.ITaskResultsContainer
    {
        #region Public Properties

        /// <summary>
        /// The maximum number of features the result set can contain for zooming to results to occur
        /// </summary>
        [System.ComponentModel.Category("TaskResults"),
         System.ComponentModel.DefaultValue(10),
         System.ComponentModel.Description(
             "The maximum number of features the result set can contain for zooming to results to occur"),
         System.Web.UI.PersistenceMode(System.Web.UI.PersistenceMode.Attribute),
         System.ComponentModel.NotifyParentProperty(true)]
        public int MaxResultsForMapZoom
        {
            get
            {
                object o = StateManager.GetProperty("MaxResultsForMapZoom");
                return (o == null) ? 10 : System.Convert.ToInt32(o);
            }
            set
            {
                if (value >= 0)
                { StateManager.SetProperty("MaxResultsForMapZoom", value); }
                else
                { throw new System.Exception(); }
            }
        }

        /// <summary>
        /// The maximum number of features the result set can contain for automatic result selection to occur
        /// </summary>
        [System.ComponentModel.Category("TaskResults"),
         System.ComponentModel.DefaultValue(10),
         System.ComponentModel.Description(
             "The maximum number of features the result set can contain for automatic result selection to occur"),
         System.Web.UI.PersistenceMode(System.Web.UI.PersistenceMode.Attribute),
         System.ComponentModel.NotifyParentProperty(true)]
        public int MaxResultsForAutoSelect
        {
            get
            {
                object o = StateManager.GetProperty("MaxResultsForAutoSelect");
                return (o == null) ? 10 : System.Convert.ToInt32(o);
            }
            set
            {
                if (value >= 0)
                { StateManager.SetProperty("MaxResultsForAutoSelect", value); }
                else
                { throw new System.Exception(); }
            }
        }

        /// <summary>
        /// Minimum width in map units that will be zoomed to
        /// </summary>
        [System.ComponentModel.Category("TaskResults"),
         System.ComponentModel.DefaultValue(10.0),
         System.ComponentModel.Description("Minimum width in map units that will be zoomed to"),
         System.Web.UI.PersistenceMode(System.Web.UI.PersistenceMode.Attribute),
         System.ComponentModel.NotifyParentProperty(true)]
        public double MinWidthOfZoom
        {
            get
            {
                object o = StateManager.GetProperty("MinWidthOfZoom");
                return (o == null) ? 10.0 : System.Convert.ToDouble(o);
            }
            set
            {
                if (value >= 0)
                { StateManager.SetProperty("MinWidthOfZoom", value); }
                else
                { throw new System.Exception(); }

            }
        }

        /// <summary>
        /// The percentage to expand the result set's extent by when zooming to results
        /// </summary>
        [System.ComponentModel.Category("TaskResults"),
        System.ComponentModel.DefaultValue(10.0),
        System.ComponentModel.Description("The percentage to expand the result set's extent by when zooming to results"),
        System.Web.UI.PersistenceMode(System.Web.UI.PersistenceMode.Attribute),
        System.ComponentModel.NotifyParentProperty(true)]
        public double ZoomExtentExpansionPercent
        {
            get
            {
                object o = StateManager.GetProperty("ZoomExtentExpansionPercent");
                return (o == null) ? 10.0 : System.Convert.ToDouble(o);
            }
            set
            {
                if (value >= 0)
                { StateManager.SetProperty("ZoomExtentExpansionPercent", value); }
                else
                { throw new System.Exception(); }
            }
        }

        /// <summary>
        /// Whether to show an activity indicator during task execution
        /// </summary>
        [System.ComponentModel.Category("TaskResults"),
        System.ComponentModel.DefaultValue(false),
        System.ComponentModel.Description("Whether to show an activity indicator during task execution"),
        System.Web.UI.PersistenceMode(System.Web.UI.PersistenceMode.Attribute),
        System.ComponentModel.NotifyParentProperty(true)]
        public bool ShowTaskActivityIndicator
        {
            get
            {
                object o = StateManager.GetProperty("ShowTaskActivityIndicator");
                return (o == null) ? false : System.Convert.ToBoolean(o);
            }
            set
            {
                StateManager.SetProperty("ShowTaskActivityIndicator", value);
            }
        }

        /// <summary>
        /// Whether to display task results in the control.  Results can be zoomed to whether or not they are displayed.
        /// </summary>
        [System.ComponentModel.Category("TaskResults"),
        System.ComponentModel.DefaultValue(false),
        System.ComponentModel.Description(
            "Whether to display task results in the control.  Results can be zoomed to whether or not they are displayed."),
        System.Web.UI.PersistenceMode(System.Web.UI.PersistenceMode.Attribute),
        System.ComponentModel.NotifyParentProperty(true)]
        public bool DisplayTaskResults
        {
            get
            {
                object o = StateManager.GetProperty("DisplayTaskResults");
                return (o == null) ? false : System.Convert.ToBoolean(o);
            }
            set
            {
                StateManager.SetProperty("DisplayTaskResults", value);
            }
        }

        #endregion

        #region ASP.NET WebControl Overrides - RenderContents

        // Overriden to add a design-time warning if the control has not been buddied
        // with a map
        protected override void RenderContents(System.Web.UI.HtmlTextWriter writer)
        {
            if (this.DesignMode)
                this.RenderDesignTimeHtml(writer);
            else
                base.RenderContents(writer);
        }

        #endregion

        #region ITaskResultsContainer Members - StartTaskActivityIndicator, DisplayResults

        // Overriding the StartTaskActivityIndicator method in TaskResults is not possible since the method is 
        // not marked virtual.  Instead implement the method via ITaskResultsContainer and if necessary call the 
        // method in the base class (TaskResults).
        void ESRI.ArcGIS.ADF.Web.UI.WebControls.ITaskResultsContainer.StartTaskActivityIndicator(
            ESRI.ArcGIS.ADF.Web.UI.WebControls.ITask task, string taskJobID)
        {
            if (ShowTaskActivityIndicator)
            {
                base.StartTaskActivityIndicator(task, taskJobID);
            }
        }

        // Overriding the DisplayResults method in TaskResults is not possible since the method is 
        // not marked virtual.  Instead implement the method via ITaskResultsContainer to zoom to
        // and/or select task results.  Then call the base class's DisplayResults method if results
        // are to be displayed in this control.
        void ESRI.ArcGIS.ADF.Web.UI.WebControls.ITaskResultsContainer.DisplayResults(
            ESRI.ArcGIS.ADF.Web.UI.WebControls.ITask task, string taskJobID, object taskInputs, 
            object taskResults)
        {
            // Throw an exception if the Map property is not set to a valid map.
            if (this.MapInstance == null)
                throw new System.Exception("You must set the Map property to a valid map"); 

            // Return if the auto select and map zoom capabilities are disabled 
            if (this.MaxResultsForMapZoom == 0 && this.MaxResultsForAutoSelect == 0)
                return;

            // Create the envlope to hold the extent that the map will be set to
            ESRI.ArcGIS.ADF.Web.Geometry.Envelope adfEnvelope = null;

            // Declare / initialize variables to use below
            int intTotalResultsCount = 0;
            bool doZoom = false;
            bool doSelect = false;
            System.Data.DataSet resultsDataSet = null;

            // Test to see if the results are contained in a TreeViePlusNode
            ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode treeViewPlusNode = 
                taskResults as ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode;
            if (treeViewPlusNode != null)
            {
                // If it is a task results node count all the data 
                // table rows in each GraphicsLayerNode
                intTotalResultsCount = this.CountAllResultsInNode(treeViewPlusNode);

                // Set flags indicating whether we should zoom or select based 
                // on the size of the result set
                doZoom = (intTotalResultsCount <= this.MaxResultsForMapZoom);
                doSelect = (intTotalResultsCount <= this.MaxResultsForAutoSelect);

                // If we are going to zoom or select then process the results
                if (doZoom || doSelect)
                    this.ProcessNode(treeViewPlusNode, doSelect, doZoom, ref adfEnvelope); 
            }
            else
            {
                // Cast the results set to a DataSet
                resultsDataSet = taskResults as System.Data.DataSet;

                // Make sure we have a valid dataset with tables before iterating
                if (resultsDataSet != null && resultsDataSet.Tables.Count > 0)
                {
                    // Count the number of results
                    foreach (System.Data.DataTable dataTable in resultsDataSet.Tables)
                        intTotalResultsCount += dataTable.Rows.Count;

                    if (intTotalResultsCount > 0)
                    {
                        // Set flags indicating whether we should zoom or select based 
                        // on the size of the result set
                        doZoom = (intTotalResultsCount <= this.MaxResultsForMapZoom);
                        doSelect = (intTotalResultsCount <= this.MaxResultsForAutoSelect);
                        
                        // If we are going to zoom or select then process the results
                        if (doZoom || doSelect)
                        {
                            foreach (System.Data.DataTable dataTable in resultsDataSet.Tables)
                            {
                                // Create a GraphicsLayer to pass to ProcessResultsLayer.  This will be used to
                                // get the extent of its features.  Note that we create the layer from a copy
                                // of the current data table because ToGraphicsLayer alters the passed-in table.
                                ESRI.ArcGIS.ADF.Web.Display.Graphics.GraphicsLayer graphicsLayer =
                                    ESRI.ArcGIS.ADF.Web.Converter.ToGraphicsLayer(dataTable.Copy());
                                if (graphicsLayer != null)
                                    this.ProcessResultsLayer(doSelect, doZoom, graphicsLayer, ref adfEnvelope);
                            }
                        }
                    }
                }
            }

            // Zoom the map if the results set is equal to or less then the MaxResultsForZoom
            if (doZoom && adfEnvelope != null)
            {
                // Resize the envelope if it is smaller than the minimum extent size
                if (adfEnvelope.Width < this.MinWidthOfZoom && adfEnvelope.Height < this.MinWidthOfZoom)
                {
                    double dblExpandX = (this.MinWidthOfZoom - adfEnvelope.Width) / 2;
                    double dblExpandY = (this.MinWidthOfZoom - adfEnvelope.Height) / 2;
                    adfEnvelope = new ESRI.ArcGIS.ADF.Web.Geometry.Envelope(adfEnvelope.XMin - dblExpandX, 
                        adfEnvelope.YMin - dblExpandY, adfEnvelope.XMax + dblExpandX, adfEnvelope.YMax + dblExpandY);
                }
                else if (adfEnvelope.Width < this.MinWidthOfZoom)
                {
                    double dblExpandX = (this.MinWidthOfZoom - adfEnvelope.Width) / 2;
                    adfEnvelope = new ESRI.ArcGIS.ADF.Web.Geometry.Envelope(adfEnvelope.XMin - dblExpandX, 
                        adfEnvelope.YMin, adfEnvelope.XMax + dblExpandX, adfEnvelope.YMax);
                }
                else if (adfEnvelope.Height < this.MinWidthOfZoom)
                {
                    double dblExpandY = (this.MinWidthOfZoom - adfEnvelope.Height) / 2;
                    adfEnvelope = new ESRI.ArcGIS.ADF.Web.Geometry.Envelope(adfEnvelope.XMin, 
                        adfEnvelope.YMin - dblExpandY, adfEnvelope.XMax, adfEnvelope.YMax + dblExpandY);
                }

                // Zoom out a little when you set the extent so you can see some area around the features
                if (this.ZoomExtentExpansionPercent > 0)
                    adfEnvelope = adfEnvelope.Expand(ZoomExtentExpansionPercent);

                // Hold onto the old extent to test with later
                ESRI.ArcGIS.ADF.Web.Geometry.Envelope adfOriginalEnvelope =
                    MapInstance.Extent.Clone() as ESRI.ArcGIS.ADF.Web.Geometry.Envelope;

                // Find the scale that the map will display at if the new envelope is applied
                double dblEnvelopeScale = 0;
                if (MapInstance.Extent.Width / adfEnvelope.Width < MapInstance.Extent.Height / adfEnvelope.Height)
                    dblEnvelopeScale = MapInstance.Scale * (adfEnvelope.Width / MapInstance.Extent.Width); 
                else
                    dblEnvelopeScale = MapInstance.Scale * (adfEnvelope.Height / MapInstance.Extent.Height);

                // Remove callbacks from the map to ensure the extent change is processed
                this.MapInstance.CallbackResults.Clear();
                //RemoveAllMapCallbacks(MapInstance);

                // If the data source is cached, zoom to the lowest cache level (largest map scale) that 
                // encompasses the target extent
                ESRI.ArcGIS.ADF.Web.DataSources.TileCacheInfo tileCacheInfo = 
                    this.MapInstance.PrimaryMapResourceInstance.MapInformation.TileCacheInfo;
                if (tileCacheInfo != null)
                {
                    // Iterate through the levels of the cache, starting with the lowest (largest
                    // map scale)
                    ESRI.ArcGIS.ADF.Web.DataSources.LodInfo[] lodInfo = tileCacheInfo.LodInfos;
                    for (int i = tileCacheInfo.LodInfos.GetUpperBound(0); i > -1; i--)
                    {
                        // Check whether the current level displays at a scale greater than or equal
                        // to that required for the target extent
                        if (lodInfo[i].Scale >= dblEnvelopeScale)
                        {
                            // Center the map at the center of the target envelope
                            this.MapInstance.CenterAt(new ESRI.ArcGIS.ADF.Web.Geometry.Point(
                                adfEnvelope.XMin + (adfEnvelope.Width / 2),
                                adfEnvelope.YMin + (adfEnvelope.Height / 2)));

                            // Set the map level to the current one if it isn't already
                            if (this.MapInstance.Level != i)
                                this.MapInstance.Level = i;
                            break;
                        }
                    }
                }
                else  // This data is not cached so we don't need to worry about tile level.
                {                    
                    // Check whether the current map scale matches the target scale.  If so, we just need to pan, 
                    // so we use the CenterAt method.  If we set the extent when the scale / map dimensions are 
                    // the same we can get artifacts in the graphics layer in IE6
                    if (System.Math.Round(MapInstance.Scale, 0) == System.Math.Round(dblEnvelopeScale, 0))
                        this.MapInstance.CenterAt(new ESRI.ArcGIS.ADF.Web.Geometry.Point(
                                    adfEnvelope.XMin + (adfEnvelope.Width / 2),
                                    adfEnvelope.YMin + (adfEnvelope.Height / 2)));
                    else
                        this.MapInstance.Extent = adfEnvelope;
                }
            }

            // If results are to be displayed in this control, invoke the base class's DisplayResults method.  
            // This will handle adding the results to the control and the map.
            if (this.DisplayTaskResults)
                base.DisplayResults(task, taskJobID, taskInputs, taskResults);
        }

        #endregion

        #region Private Instance Methods - CountAllResultsInNode, ProcessNode, ProcessResultsLayer, RenderDesignTimeHtml

        // Gets the number of descendant result features contained by the passed-in node
        private int CountAllResultsInNode(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode node)
        {
            int resultsCount = 0;
            
            // If the passed-in node is a graphics layer node, add the number of rows in its graphics layer
            // to the number of results
            ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode = 
                node as ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode;
            if (graphicsLayerNode != null)
                resultsCount += graphicsLayerNode.Layer.Rows.Count;

            // Get the number of results contained by child nodes
            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in node.Nodes)
                resultsCount += this.CountAllResultsInNode(childNode);

            return resultsCount;
        }

        // Calculates the containing envelope for and selects any results contained by the passed-in node
        private void ProcessNode(ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode node, bool doSelect, 
            bool doZoom, ref ESRI.ArcGIS.ADF.Web.Geometry.Envelope adfEnvelope)
        {
            // If the node is a graphics layer node, pass its graphics layer to ProcessDataTable
            ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode graphicsLayerNode = 
                node as ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode;
            if (graphicsLayerNode != null && graphicsLayerNode.Value != "Input Features")
                this.ProcessResultsLayer(doSelect, doZoom, graphicsLayerNode.Layer, ref adfEnvelope);

            // Process any child nodes
            foreach (ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode childNode in node.Nodes)
                this.ProcessNode(childNode, doSelect, doZoom, ref adfEnvelope);
        }

        // Selects all of the passed-in layer's features and unions the layer's extent with the 
        // passed-in envelope
        private void ProcessResultsLayer(bool doSelect, bool doZoom, 
            ESRI.ArcGIS.ADF.Web.Display.Graphics.GraphicsLayer graphicsLayer, 
            ref ESRI.ArcGIS.ADF.Web.Geometry.Envelope adfEnvelope)
        {
            if (graphicsLayer.Rows.Count > 0)
            {
                // Expand the new extent to include the extent of the graphics layer
                if (doZoom)
                {
                    // Create a new envelope if the one passed-in isn't initialized
                    if (adfEnvelope == null)
                        adfEnvelope = new ESRI.ArcGIS.ADF.Web.Geometry.Envelope(); 

                    // Union the layer's extent with the envelope
                    adfEnvelope.Union(graphicsLayer.FullExtent);
                }

                // Select the layer's features
                if (doSelect)
                {
                    foreach (System.Data.DataRow dataRow in graphicsLayer.Rows)
                        dataRow[graphicsLayer.IsSelectedColumn] = true;
                }
            }
        }

        // Renders the control at design-tiem.  Displays a warning if the control has
        // not been buddied with a map
        private void RenderDesignTimeHtml(System.Web.UI.HtmlTextWriter writer)
        {
            // Create a table to format the design-time display
            System.Web.UI.WebControls.Table table = new System.Web.UI.WebControls.Table();
            System.Web.UI.WebControls.TableRow row = new System.Web.UI.WebControls.TableRow();
            table.CellPadding = 4;
            table.Rows.Add(row);

            // Add the control's ID
            System.Web.UI.WebControls.TableCell cell = new System.Web.UI.WebControls.TableCell();
            row.Cells.Add(cell);
            cell.Style[System.Web.UI.HtmlTextWriterStyle.WhiteSpace] = "nowrap";
            cell.Text = string.Format("{0}<br>", this.ClientID);
            cell.ColumnSpan = 2;

            row = new System.Web.UI.WebControls.TableRow();
            table.Rows.Add(row);
            
            // Add the control type
            cell = new System.Web.UI.WebControls.TableCell();
            row.Cells.Add(cell);
            cell.Text = "ZoomToResults WebControl";
            cell.ColumnSpan = 2;

            // If necessary, add a warning that the control needs to be buddied to a map
            if (this.Map.Equals("(none)") || string.IsNullOrEmpty(this.Map))
            {
                row = new System.Web.UI.WebControls.TableRow();
                table.Rows.Add(row);

                cell = new System.Web.UI.WebControls.TableCell();
                row.Cells.Add(cell);
                cell.ForeColor = System.Drawing.Color.Red;
                cell.Text = "Warning:";

                cell = new System.Web.UI.WebControls.TableCell();
                row.Cells.Add(cell);
                cell.Style[System.Web.UI.HtmlTextWriterStyle.WhiteSpace] = "nowrap";
                cell.Text = "You must set the Map property.";
            }

            table.RenderControl(writer);
        }

        #endregion
    }
}