Common Task results
Common_TaskResults_VBNet\ZoomToResults\ZoomToResults.vb
' 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.
' 

Imports Microsoft.VisualBasic
Imports System
Namespace ZoomToResults_VBNet
  ' 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"" " & ControlChars.CrLf & "        BackColor=""#ffffff"" Font-Names=""Verdana"" Font-Size=""8pt"" ForeColor=""#000000""> </{0}:ZoomToResults>")> _
  Public Class ZoomToResults
    Inherits ESRI.ArcGIS.ADF.Web.UI.WebControls.TaskResults
    Implements 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 Property MaxResultsForMapZoom() As Integer
      Get
        Dim o As Object = StateManager.GetProperty("MaxResultsForMapZoom")
        If (o Is Nothing) Then
          Return 10
        Else
          Return System.Convert.ToInt32(o)
        End If
      End Get
      Set
        If Value >= 0 Then
          StateManager.SetProperty("MaxResultsForMapZoom", Value)
        Else
          Throw New System.Exception()
        End If
      End Set
    End Property

    ''' <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 Property MaxResultsForAutoSelect() As Integer
      Get
        Dim o As Object = StateManager.GetProperty("MaxResultsForAutoSelect")
        If (o Is Nothing) Then
          Return 10
        Else
          Return System.Convert.ToInt32(o)
        End If
      End Get
      Set
        If Value >= 0 Then
          StateManager.SetProperty("MaxResultsForAutoSelect", Value)
        Else
          Throw New System.Exception()
        End If
      End Set
    End Property

    ''' <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 Property MinWidthOfZoom() As Double
      Get
        Dim o As Object = StateManager.GetProperty("MinWidthOfZoom")
        If (o Is Nothing) Then
          Return 10.0
        Else
          Return System.Convert.ToDouble(o)
        End If
      End Get
      Set
        If Value >= 0 Then
          StateManager.SetProperty("MinWidthOfZoom", Value)
        Else
          Throw New System.Exception()
        End If

      End Set
    End Property

    ''' <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 Property ZoomExtentExpansionPercent() As Double
      Get
        Dim o As Object = StateManager.GetProperty("ZoomExtentExpansionPercent")
        If (o Is Nothing) Then
          Return 10.0
        Else
          Return System.Convert.ToDouble(o)
        End If
      End Get
      Set
        If Value >= 0 Then
          StateManager.SetProperty("ZoomExtentExpansionPercent", Value)
        Else
          Throw New System.Exception()
        End If
      End Set
    End Property

    ''' <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 Property ShowTaskActivityIndicator() As Boolean
      Get
        Dim o As Object = StateManager.GetProperty("ShowTaskActivityIndicator")
        If (o Is Nothing) Then
          Return False
        Else
          Return System.Convert.ToBoolean(o)
        End If
      End Get
      Set
        StateManager.SetProperty("ShowTaskActivityIndicator", Value)
      End Set
    End Property

    ''' <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 Property DisplayTaskResults() As Boolean
      Get
        Dim o As Object = StateManager.GetProperty("DisplayTaskResults")
        If (o Is Nothing) Then
          Return False
        Else
          Return System.Convert.ToBoolean(o)
        End If
      End Get
      Set
        StateManager.SetProperty("DisplayTaskResults", Value)
      End Set
    End Property

    #End Region

    #Region "ASP.NET WebControl Overrides - RenderContents"

    ' Overriden to add a design-time warning if the control has not been buddied
    ' with a map
    Protected Overrides Overloads Sub RenderContents(ByVal writer As System.Web.UI.HtmlTextWriter)
      If Me.DesignMode Then
        Me.RenderDesignTimeHtml(writer)
      Else
        MyBase.RenderContents(writer)
      End If
    End Sub

    #End Region

    #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).
        Private Overloads Sub StartTaskActivityIndicator(ByVal task As ESRI.ArcGIS.ADF.Web.UI.WebControls.ITask, ByVal taskJobID As String) Implements ESRI.ArcGIS.ADF.Web.UI.WebControls.ITaskResultsContainer.StartTaskActivityIndicator
            If ShowTaskActivityIndicator Then
                MyBase.StartTaskActivityIndicator(task, taskJobID)
            End If
        End Sub

    ' 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.
        Private Overloads Sub DisplayResults(ByVal task As ESRI.ArcGIS.ADF.Web.UI.WebControls.ITask, ByVal taskJobID As String, ByVal taskInputs As Object, ByVal taskResults As Object) Implements ESRI.ArcGIS.ADF.Web.UI.WebControls.ITaskResultsContainer.DisplayResults
            ' Throw an exception if the Map property is not set to a valid map.
            If Me.MapInstance Is Nothing Then
                Throw New System.Exception("You must set the Map property to a valid map")
            End If

            ' Return if the auto select and map zoom capabilities are disabled 
            If Me.MaxResultsForMapZoom = 0 AndAlso Me.MaxResultsForAutoSelect = 0 Then
                Return
            End If

            ' Create the envlope to hold the extent that the map will be set to
            Dim adfEnvelope As ESRI.ArcGIS.ADF.Web.Geometry.Envelope = Nothing

            ' Declare / initialize variables to use below
            Dim intTotalResultsCount As Integer = 0
            Dim doZoom As Boolean = False
            Dim doSelect As Boolean = False
            Dim resultsDataSet As System.Data.DataSet = Nothing

            ' Test to see if the results are contained in a TreeViePlusNode
            Dim treeViewPlusNode As ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode = TryCast(taskResults, ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode)
            If Not treeViewPlusNode Is Nothing Then
                ' If it is a task results node count all the data 
                ' table rows in each GraphicsLayerNode
                intTotalResultsCount = Me.CountAllResultsInNode(treeViewPlusNode)

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

                ' If we are going to zoom or select then process the results
                If doZoom OrElse doSelect Then
                    Me.ProcessNode(treeViewPlusNode, doSelect, doZoom, adfEnvelope)
                End If
            Else
                ' Cast the results set to a DataSet
                resultsDataSet = TryCast(taskResults, System.Data.DataSet)

                ' Make sure we have a valid dataset with tables before iterating
                If Not resultsDataSet Is Nothing AndAlso resultsDataSet.Tables.Count > 0 Then
                    ' Count the number of results
                    For Each dataTable As System.Data.DataTable In resultsDataSet.Tables
                        intTotalResultsCount += dataTable.Rows.Count
                    Next dataTable

                    If intTotalResultsCount > 0 Then
                        ' Set flags indicating whether we should zoom or select based 
                        ' on the size of the result set
                        doZoom = (intTotalResultsCount <= Me.MaxResultsForMapZoom)
                        doSelect = (intTotalResultsCount <= Me.MaxResultsForAutoSelect)

                        ' If we are going to zoom or select then process the results
                        If doZoom OrElse doSelect Then
                            For Each dataTable As System.Data.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.
                                Dim graphicsLayer As ESRI.ArcGIS.ADF.Web.Display.Graphics.GraphicsLayer = ESRI.ArcGIS.ADF.Web.Converter.ToGraphicsLayer(dataTable.Copy())
                                If Not graphicsLayer Is Nothing Then
                                    Me.ProcessResultsLayer(doSelect, doZoom, graphicsLayer, adfEnvelope)
                                End If
                            Next dataTable
                        End If
                    End If
                End If
            End If

            ' Zoom the map if the results set is equal to or less then the MaxResultsForZoom
            If doZoom AndAlso Not adfEnvelope Is Nothing Then
                ' Resize the envelope if it is smaller than the minimum extent size
                If adfEnvelope.Width < Me.MinWidthOfZoom AndAlso adfEnvelope.Height < Me.MinWidthOfZoom Then
                    Dim dblExpandX As Double = (Me.MinWidthOfZoom - adfEnvelope.Width) / 2
                    Dim dblExpandY As Double = (Me.MinWidthOfZoom - adfEnvelope.Height) / 2
                    adfEnvelope = New ESRI.ArcGIS.ADF.Web.Geometry.Envelope(adfEnvelope.XMin - dblExpandX, adfEnvelope.YMin - dblExpandY, adfEnvelope.XMax + dblExpandX, adfEnvelope.YMax + dblExpandY)
                ElseIf adfEnvelope.Width < Me.MinWidthOfZoom Then
                    Dim dblExpandX As Double = (Me.MinWidthOfZoom - adfEnvelope.Width) / 2
                    adfEnvelope = New ESRI.ArcGIS.ADF.Web.Geometry.Envelope(adfEnvelope.XMin - dblExpandX, adfEnvelope.YMin, adfEnvelope.XMax + dblExpandX, adfEnvelope.YMax)
                ElseIf adfEnvelope.Height < Me.MinWidthOfZoom Then
                    Dim dblExpandY As Double = (Me.MinWidthOfZoom - adfEnvelope.Height) / 2
                    adfEnvelope = New ESRI.ArcGIS.ADF.Web.Geometry.Envelope(adfEnvelope.XMin, adfEnvelope.YMin - dblExpandY, adfEnvelope.XMax, adfEnvelope.YMax + dblExpandY)
                End If

                ' Zoom out a little when you set the extent so you can see some area around the features
                If Me.ZoomExtentExpansionPercent > 0 Then
                    adfEnvelope = adfEnvelope.Expand(ZoomExtentExpansionPercent)
                End If

                ' Hold onto the old extent to test with later
                Dim adfOriginalEnvelope As ESRI.ArcGIS.ADF.Web.Geometry.Envelope = TryCast(MapInstance.Extent.Clone(), ESRI.ArcGIS.ADF.Web.Geometry.Envelope)

                ' Find the scale that the map will display at if the new envelope is applied
                Dim dblEnvelopeScale As Double = 0
                If MapInstance.Extent.Width / adfEnvelope.Width < MapInstance.Extent.Height / adfEnvelope.Height Then
                    dblEnvelopeScale = MapInstance.Scale * (adfEnvelope.Width / MapInstance.Extent.Width)
                Else
                    dblEnvelopeScale = MapInstance.Scale * (adfEnvelope.Height / MapInstance.Extent.Height)
                End If

                ' Remove callbacks from the map to ensure the extent change is processed
                Me.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
                Dim tileCacheInfo As ESRI.ArcGIS.ADF.Web.DataSources.TileCacheInfo = Me.MapInstance.PrimaryMapResourceInstance.MapInformation.TileCacheInfo
                If Not tileCacheInfo Is Nothing Then
                    ' Iterate through the levels of the cache, starting with the lowest (largest
                    ' map scale)
                    Dim lodInfo As ESRI.ArcGIS.ADF.Web.DataSources.LodInfo() = tileCacheInfo.LodInfos
                    For i As Integer = tileCacheInfo.LodInfos.GetUpperBound(0) To 0 Step -1
                        ' 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 Then
                            ' Center the map at the center of the target envelope
                            Me.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 Me.MapInstance.Level <> i Then
                                Me.MapInstance.Level = i
                            End If
                            Exit For
                        End If
                    Next i
                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) Then
                        Me.MapInstance.CenterAt(New ESRI.ArcGIS.ADF.Web.Geometry.Point(adfEnvelope.XMin + (adfEnvelope.Width / 2), adfEnvelope.YMin + (adfEnvelope.Height / 2)))
                    Else
                        Me.MapInstance.Extent = adfEnvelope
                    End If
                End If
            End If

            ' 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 Me.DisplayTaskResults Then
                MyBase.DisplayResults(task, taskJobID, taskInputs, taskResults)
            End If
        End Sub

    #End Region

    #Region "Private Instance Methods - CountAllResultsInNode, ProcessNode, ProcessResultsLayer, RenderDesignTimeHtml"

    ' Gets the number of descendant result features contained by the passed-in node
    Private Function CountAllResultsInNode(ByVal node As ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode) As Integer
      Dim resultsCount As Integer = 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
      Dim graphicsLayerNode As ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode = TryCast(node, ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode)
      If Not graphicsLayerNode Is Nothing Then
        resultsCount += graphicsLayerNode.Layer.Rows.Count
      End If

      ' Get the number of results contained by child nodes
      For Each childNode As ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode In node.Nodes
        resultsCount += Me.CountAllResultsInNode(childNode)
      Next childNode

      Return resultsCount
    End Function

    ' Calculates the containing envelope for and selects any results contained by the passed-in node
    Private Sub ProcessNode(ByVal node As ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode, ByVal doSelect As Boolean, ByVal doZoom As Boolean, ByRef adfEnvelope As ESRI.ArcGIS.ADF.Web.Geometry.Envelope)
      ' If the node is a graphics layer node, pass its graphics layer to ProcessDataTable
      Dim graphicsLayerNode As ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode = TryCast(node, ESRI.ArcGIS.ADF.Web.UI.WebControls.GraphicsLayerNode)
      If Not graphicsLayerNode Is Nothing AndAlso graphicsLayerNode.Value <> "Input Features" Then
        Me.ProcessResultsLayer(doSelect, doZoom, graphicsLayerNode.Layer, adfEnvelope)
      End If

      ' Process any child nodes
      For Each childNode As ESRI.ArcGIS.ADF.Web.UI.WebControls.TreeViewPlusNode In node.Nodes
        Me.ProcessNode(childNode, doSelect, doZoom, adfEnvelope)
      Next childNode
    End Sub

    ' Selects all of the passed-in layer's features and unions the layer's extent with the 
    ' passed-in envelope
    Private Sub ProcessResultsLayer(ByVal doSelect As Boolean, ByVal doZoom As Boolean, ByVal graphicsLayer As ESRI.ArcGIS.ADF.Web.Display.Graphics.GraphicsLayer, ByRef adfEnvelope As ESRI.ArcGIS.ADF.Web.Geometry.Envelope)
      If graphicsLayer.Rows.Count > 0 Then
        ' Expand the new extent to include the extent of the graphics layer
        If doZoom Then
          ' Create a new envelope if the one passed-in isn't initialized
          If adfEnvelope Is Nothing Then
            adfEnvelope = New ESRI.ArcGIS.ADF.Web.Geometry.Envelope()
          End If

          ' Union the layer's extent with the envelope
          adfEnvelope.Union(graphicsLayer.FullExtent)
        End If

        ' Select the layer's features
        If doSelect Then
          For Each dataRow As System.Data.DataRow In graphicsLayer.Rows
            dataRow(graphicsLayer.IsSelectedColumn) = True
          Next dataRow
        End If
      End If
    End Sub

    ' Renders the control at design-tiem.  Displays a warning if the control has
    ' not been buddied with a map
    Private Sub RenderDesignTimeHtml(ByVal writer As System.Web.UI.HtmlTextWriter)
      ' Create a table to format the design-time display
      Dim table As System.Web.UI.WebControls.Table = New System.Web.UI.WebControls.Table()
      Dim row As System.Web.UI.WebControls.TableRow = New System.Web.UI.WebControls.TableRow()
      table.CellPadding = 4
      table.Rows.Add(row)

      ' Add the control's ID
      Dim cell As System.Web.UI.WebControls.TableCell = New System.Web.UI.WebControls.TableCell()
      row.Cells.Add(cell)
      cell.Style(System.Web.UI.HtmlTextWriterStyle.WhiteSpace) = "nowrap"
      cell.Text = String.Format("{0}<br>", Me.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 Me.Map.Equals("(none)") OrElse String.IsNullOrEmpty(Me.Map) Then
        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."
      End If

      table.RenderControl(writer)
    End Sub

    #End Region
  End Class
End Namespace