ProGuide Geocoding - kataya/arcgis-pro-sdk GitHub Wiki

Language:      C#
Subject:       Map Authoring
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          11/24/2020
ArcGIS Pro:    2.7
Visual Studio: 2017, 2019

This ProGuide demonstrates how to develop an add-in using the geocoding functionality. There are 3 parts in this guide illustrating different levels of geocoding customization:

  • Simple geocoding using an API method with no custom UI.
  • Using the new LocatorControl on a dockpane to emulate the Locate dockpane and its geocoding functionality.
  • Geocoding using API methods with a custom UI.

The code used to illustrate this add-in can be found at Geocoding Tools Sample.

Prerequisites

Step 1

Add a new ArcGIS Pro Add-ins | ArcGIS Pro Button to the project and name the item SimpleGeocode. Modify the Config.daml file button item as follows:

  • Change the caption to "Simple Geocode"
  • Change the tool heading to "Simple Geocode" and the ToolTip text to "Geocode a particular address."
<button id="GeocodingTools_SimpleGeocode" caption="Simple Geocode" 
                className="SimpleGeocode" loadOnClick="true" 
                smallImage="Images\GenericButtonBlue16.png" largeImage="Images\GenericButtonBlue32.png">
  <tooltip heading="Simple Geocode">Geocode a particular address.<disabledText /></tooltip>
</button>

Build the sample. Debug the add-in and start ArcGIS Pro. Open any project from the sample data. Validate the UI on the ArcGIS Pro ribbon:

simpleGeocode

Step 2

The LocatorManager class encompasses all the geocoding functionalities added to ArcGIS Pro. In addition to geocoding you can also add, remove, reorder, enable and disable geocoding locator providers. The LocatorManager can be accessed from the MapView class.

In this simple geocode example we are going to call the GeocodeAsync method with a hard coded address. Results are returned as an IEnumerable<ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult>. The GeocodeResult class has properties providing the Label, Score, Display Location, Extent. The ToString method concatenates the Label and Score properties.

Modify the SimpleGeocode.cs file; ensuring the OnClick method looks like below

protected override async void OnClick()
{
  string text = "380 New York Street Redlands";

  // geocode
  IEnumerable<ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult> results = 
                    await MapView.Active.LocatorManager.GeocodeAsync(text, false, false);

  // show results
  string msg = "results : " + results.Count().ToString() + "\r\n";
  foreach (var result in results)
    msg = msg + result.ToString() + "\r\n";

  ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show(msg, "Geocode Results");
}

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Click the Simple Geocode button and view the results.

simpleGeocodeResults

Step 3

Another simple way of providing geocoding functionality to users is by using the new LocatorControl. This control looks and acts the same as the Locate dockpane. It allows you to add, remove, reorder, enable and disable locator providers via the Settings tab. It also provides functionality to search and display geocode results. Results are also displayed in the map with preset symbology.

Follow these steps to add a LocatorControl to a new dockpane.

Add a new ArcGIS Pro Add-ins | ArcGIS Pro Dockpane to the project and name it GeocodeDockpane.xaml. Modify the config.daml file as below

<controls>
  <button id="GeocodingTools_SimpleGeocode" caption="Simple Geocode" 
                 className="SimpleGeocode" loadOnClick="true" 
                 smallImage="Images\GenericButtonBlue16.png" largeImage="Images\GenericButtonBlue32.png">
    <tooltip heading="Simple Geocode">Geocode a particular address.<disabledText /></tooltip>
  </button>
  <button id="GeocodingTools_GeocodeDockpane_ShowButton" caption="Show Geocode Dockpane" 
                 className="GeocodeDockpane_ShowButton" loadOnClick="true" 
                 smallImage="Images\GenericButtonPurple16.png" largeImage="Images\GenericButtonPurple32.png">
    <tooltip heading="Show Geocode Dockpane">Show Geocode Dockpane<disabledText /></tooltip>
  </button>
</controls>
<dockPanes>
  <dockPane id="GeocodingTools_GeocodeDockpane" caption="Geocode" 
                 className="GeocodeDockpaneViewModel" 
                 dock="group" dockWith="esri_core_projectDockPane">
    <content className="GeocodeDockpaneView" />
  </dockPane>
</dockPanes>

This daml shows we have a new button and a new dockpane. The dockpane follows the standard View-ViewModel pattern used in ArcGIS Pro with the ViewModel defined in the <dockPane className="..." attribute and the View defined in the child <content className="..."

Step 4

Open the GeocodeDockpane.xaml file. We are going to add the LocatorControl to the dockpane. Add the mapping namespace to the file by including the following at the top of the file following the extensions namespace declaration

xmlns:mappingControls="clr-namespace:ArcGIS.Desktop.Mapping.Controls;assembly=ArcGIS.Desktop.Mapping"

Then replace the entire contents of the <grid> tag with the following

<mappingControls:LocatorControl x:Name="locator" />

The entire file should look as follows:

<UserControl x:Class="GeocodingTools.GeocodeDockpaneView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"   
             xmlns:ui="clr-namespace:GeocodingTools"
             xmlns:extensions="clr-namespace:ArcGIS.Desktop.Extensions;assembly=ArcGIS.Desktop.Extensions"
             xmlns:mappingControls="clr-namespace:ArcGIS.Desktop.Mapping.Controls;assembly=ArcGIS.Desktop.Mapping"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             d:DataContext="{Binding Path=ui.GeocodeDockpaneViewModel}">
  <UserControl.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
          <extensions:DesignOnlyResourceDictionary Source="pack://application:,,,/ArcGIS.Desktop.Framework;component\Themes\Default.xaml"/>
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </UserControl.Resources>
  <Grid>
    <mappingControls:LocatorControl x:Name="locator" />
  </Grid>
</UserControl>

Step 5

Open the GeocodeDockpaneViewModel.cs file to begin customization of the ViewModel. Remove the Heading property and the private variable _heading. Then add the following override method.

/// <summary>
/// when the dockpane is activated, throw the LocatorActivateEvent 
/// </summary>
/// <param name="isActive"></param>
protected override void OnActivate(bool isActive)
{
  ArcGIS.Desktop.Mapping.Controls.LocatorActivateEvent.Publish(new ArcGIS.Desktop.Mapping.Controls.LocatorActivateEventArgs(isActive));
}

Publishing this event when the parent of the LocatorControl (in this case the dockpane) is made active or inactive tells the LocatorControl that it should mark itself as the active (or inactive) LocatorControl.

Build and run the add-in. Verify the UI.

LocatorControl

Enter a location and view the results.

LocatorControlResults

Step 6

The final geocoding example in this ProGuide provides a custom UI and uses the API methods from the LocatorManager. We will add a new dockpane which contains a SearchTextBox to allow searching, display the results in a ListBox and symbolize a result on the map via the overlay when that result is selected in the ListBox.

Start by adding a new ArcGIS Pro Add-ins | ArcGIS Pro Dockpane to the project and name it CustomGeocodeDockpane.xaml. Modify the config.daml file so it looks like below

<controls>
  <button id="GeocodingTools_SimpleGeocode" caption="Simple Geocode" 
                    className="SimpleGeocode" loadOnClick="true" 
                    smallImage="Images\GenericButtonBlue16.png" largeImage="Images\GenericButtonBlue32.png">
    <tooltip heading="Simple Geocode">Geocode a particular address.<disabledText /></tooltip>
  </button>
  <button id="GeocodingTools_GeocodeDockpane_ShowButton" caption="Show Geocode Dockpane" 
                    className="GeocodeDockpane_ShowButton" loadOnClick="true"
                    smallImage="Images\GenericButtonPurple16.png" largeImage="Images\GenericButtonPurple32.png">
    <tooltip heading="Show Geocode Dockpane">Show Geocode Dockpane<disabledText /></tooltip>
  </button>
  <button id="GeocodingTools_CustomGeocodeDockpane_ShowButton" caption="Show Custom Geocode Dockpane"
                    className="CustomGeocodeDockpane_ShowButton" loadOnClick="true"
                    smallImage="Images\GenericButtonPurple16.png" largeImage="Images\GenericButtonPurple32.png">
    <tooltip heading="Show Custom Geocode Dockpane">Show Custom Geocode Dockpane<disabledText /></tooltip>
  </button>
</controls>
<dockPanes>
  <dockPane id="GeocodingTools_GeocodeDockpane" caption="Geocode" 
                    className="GeocodeDockpaneViewModel" 
                    dock="group" dockWith="esri_core_projectDockPane">
    <content className="GeocodeDockpaneView" />
  </dockPane>
  <dockPane id="GeocodingTools_CustomGeocodeDockpane" caption="Custom Geocode" 
                    className="CustomGeocodeDockpaneViewModel" 
                    dock="group" dockWith="esri_core_projectDockPane">
    <content className="CustomGeocodeDockpaneView" />
  </dockPane>
</dockPanes>

Step 7

Customize the UI in the CustomGeocodeDockpane.xaml file to add the SearchTextBox and the ListBox. The ListBox is bound to a CollectionViewSource bound to the search results. The CollectionViewSource allows us to group results according to the locator providers.

<UserControl x:Class="GeocodingTools.CustomGeocodeDockpaneView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"   
             xmlns:ui="clr-namespace:GeocodingTools"
             xmlns:extensions="clr-namespace:ArcGIS.Desktop.Extensions;assembly=ArcGIS.Desktop.Extensions"
             xmlns:frameworkControls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             d:DataContext="{Binding Path=ui.CustomGeocodeDockpaneViewModel}">
  <UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
           <extensions:DesignOnlyResourceDictionary Source="pack://application:,,,/ArcGIS.Desktop.Framework;component\Themes\Default.xaml"/>
        </ResourceDictionary.MergedDictionaries>

      <CollectionViewSource Source="{Binding Path=Results}" x:Key="_results" >
        <CollectionViewSource.SortDescriptions>
        </CollectionViewSource.SortDescriptions>
        <CollectionViewSource.GroupDescriptions>
          <PropertyGroupDescription PropertyName="ProviderName" />
        </CollectionViewSource.GroupDescriptions>
      </CollectionViewSource>

      <Style x:Key="ContainerStyle" TargetType="{x:Type GroupItem}">
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate>
              <Expander Header="{Binding Name}" IsExpanded="True" Background="{DynamicResource Esri_White}" Foreground="{DynamicResource Esri_Gray160}">
                <ItemsPresenter />
              </Expander>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </ResourceDictionary>
  </UserControl.Resources>

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <frameworkControls:SearchTextBox x:Name="edtSearch" Margin="6,6,6,0" Grid.Row="0" AcceptsReturn="false" ShowMagnifier="True" ShowHistory="True" SearchMode="Manual"/>

    <ListBox Grid.Row="1"  ItemsSource="{Binding Source={StaticResource _results}}" SelectedItem="{Binding SelectedResult}">
      <ListBox.GroupStyle>
        <GroupStyle ContainerStyle="{StaticResource ContainerStyle}"/>
      </ListBox.GroupStyle>
    </ListBox>
  </Grid>
</UserControl>

Step 8

The SearchTextBox control has a Search event which we will trap to provide our searching capabilities. Open the CustomGeocodeDockpane.xaml.cs file and add the Search event handler.

    public CustomGeocodeDockpaneView()
    {
      InitializeComponent();
      edtSearch.Search += EdtSearch_Search;
    }

    private CustomGeocodeDockpaneViewModel ViewModel => (DataContext as CustomGeocodeDockpaneViewModel);

    private async void EdtSearch_Search(object sender, RoutedEventArgs e)
    {
      await ViewModel?.Search(edtSearch.Text);
    }

The Search function will be added to the CustomGeocodeDockpaneViewModel.cs file. Open it and add the following methods. The GeocodeAsync results are placed in a ReadOnlyList of GeocodeResult which are bound to the View by the Results property.

    /// <summary>
    /// Gets the ReadOnlyList of geocode results from a GeocodeAsync method
    /// </summary>
    private List<ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult> _results;
    public IReadOnlyList<ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult> Results
    {
      get { return _results; }
    }

    /// <summary>
    /// Performs a geocode operation using the static GeocodeAsync method.
    /// </summary>
    /// <param name="text"></param>
    /// <returns></returns>
    internal async Task Search(string text)
    {
      // check the text
      if (string.IsNullOrEmpty(text))
        return;

      // do the geocode - pass false and false because we want to control the display and zoom ourselves
      IEnumerable<ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult> results = await MapView.Active.LocatorManager.GeocodeAsync(text, false, false);

      // check result count - if no results, most likely no locators
      if ((results == null) || (results.Count() == 0))
        _results = new List<ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult>();
      else
        _results = results.ToList();

      // update UI
      NotifyPropertyChanged(() => Results);
    }

Build the add-in. Fix any compile errors. Run the add-in and test what we have so far. You should be able to enter text into the search box and have the results displayed in the ListBox.

Geocode_CustomUI

Step 9

The final step is to highlight the selected geocode result on the map and zoom to it's extent. We can do this by adding a symbol to the map view's overlay. We already have the ListBox in the View binding to a SelectedResult property, we just need to add this property to our ViewModel.

In CustomGeocodeDockpaneViewModel.cs add the following code

    private IDisposable disposable;

    /// <summary>
    /// The selected geocode result in the ListBox.
    /// </summary>
    private ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult _selectedResult;
    public ArcGIS.Desktop.Mapping.Geocoding.GeocodeResult SelectedResult
    {
      get { return _selectedResult; }
      set
      {
        SetProperty(ref _selectedResult, value, () => SelectedResult);

        // remove any existing item from the overlay
        if (disposable != null)
        {
          disposable.Dispose();
          disposable = null;
        }

        if (_selectedResult == null)
          return;

        if (_selectedResult.DisplayLocation != null)
        {
          QueuedTask.Run(() =>
          {
            // zoom to the extent
            if (_selectedResult.Extent != null)
              MapView.Active.ZoomTo(_selectedResult.Extent);

            // add to the overlay
            disposable = MapView.Active.AddOverlay(_selectedResult.DisplayLocation,
                SymbolFactory.Instance.ConstructPointSymbol(
                        ColorFactory.Instance.RedRGB, 15.0, SimpleMarkerStyle.Star).MakeSymbolReference());
          });
        }
      }
    }

We also need to ensure that we properly clean up the IDisposable object when the dockpane is hidden.

    protected override void OnHidden()
    {
      base.OnHidden();
      if (disposable != null)
      {
        disposable.Dispose();
        disposable = null;
      }
    }

Finally modify the Search method, clearing the overlay before executing another search.

    internal async Task Search(string text)
    {
      // clean the overlay
      if (disposable != null)
      {
        disposable.Dispose();
        disposable = null;
      }

      // check the text
      if (string.IsNullOrEmpty(text))
        return;
...

Build the add-in and fix any compile errors. Debug the add-in. Now when you select one of the search results you should see a symbol displayed on the map and be zoomed to the extent of the result.

⚠️ **GitHub.com Fallback** ⚠️