ProGuide TableControl - Esri/arcgis-pro-sdk GitHub Wiki

Language:      C#
Subject:       Map Exploration
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          10/06/2024
ArcGIS Pro:    3.4
Visual Studio: 2022

This ProGuide demonstrates how to use the TableControl to display content from a Catalog view. The code used to illustrate this guide can be found at the Table Control Sample.

Prerequisites

Step 1

Add a new ArcGIS Pro Add-Ins | ArcGIS Pro Dockpane to the add-in project, and name the item TableControlDockpane. Open the config.daml file.

Scroll to the bottom of the file and notice a new button and new dockpane elements have been added. 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="..."

Modify the config.daml file items as follows:

  • Change the button caption to "Show Preview"
  • Change the button tooltip heading to "Show Preview" and the tooltip text to "Show the preview dockpane"
  • Change the dockPane caption to "Preview"
  • Change the dockPane dock property to "bottom" and remove the dockWith property
<controls>
  <!-- add your controls here -->
  <button id="TableControl_Preview_ShowButton" caption="Show Preview" 
          keytip="BP" className="Preview_ShowButton" loadOnClick="true" 
          smallImage="GenericButtonPurple16" 
          largeImage="GenericButtonPurple32">
    <tooltip heading="Show Dockpane">Show the preview dockpane
      <disabledText />
    </tooltip>
  </button>
</controls>
<dockPanes>
  <dockPane id="TableControl_Preview" caption="Preview" 
            className="PreviewViewModel" dock="group" 
            dockWith="esri_core_projectDockPane">
    <content className="PreviewView" />
  </dockPane>
</dockPanes>

Step 2

Open the TableControlDockpane.xaml file. We will modify this file to add a TableControl to the dockpane. Add a reference to the Editing.Controls namespace by including the following at the top of the file following the extensions namespace declaration.

xmlns:editing="clr-namespace:ArcGIS.Desktop.Editing.Controls;assembly=ArcGIS.Desktop.Editing"

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

<Grid.RowDefinitions>
  <RowDefinition Height="Auto"/>
  <RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DockPanel LastChildFill="False" Grid.Row="0">
</DockPanel>
<editing:TableControl AutomationProperties.AutomationId="_tableControl" x:Name="tableControl" 
                   Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                   TableContent="{Binding Path=TableContent}">
</editing:TableControl>

Our dockpane has a DockPanel at the top followed by the TableControl. We will add items to the DockPanel in a later step.

The entire xaml file should look as follows:

<UserControl x:Class="TableControl.TableControlDockpaneView"
             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:TableControl"
             xmlns:extensions="clr-namespace:ArcGIS.Desktop.Extensions;assembly=ArcGIS.Desktop.Extensions"
             xmlns:editing="clr-namespace:ArcGIS.Desktop.Editing.Controls;assembly=ArcGIS.Desktop.Editing"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             d:DataContext="{Binding Path=ui.TableControlDockpaneViewModel}">
    <UserControl.Resources>
      <ResourceDictionary>
          <ResourceDictionary.MergedDictionaries>
              <extensions:DesignOnlyResourceDictionary Source="pack://application:,,,/ArcGIS.Desktop.Framework;component\Themes\Default.xaml"/>
          </ResourceDictionary.MergedDictionaries>
      </ResourceDictionary>
  </UserControl.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <DockPanel LastChildFill="False" Grid.Row="0">
    </DockPanel>
    <editing:TableControl AutomationProperties.AutomationId="_tableControl" x:Name="tableControl" 
                       Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                       TableContent="{Binding Path=TableContent}">
    </editing:TableControl>
  </Grid>
</UserControl>

Step 3

The TableControl can be used to display content from layers or tables within a map or from data within the Catalog view. It's content is determined by the TableContent property. This is created using the static TableControlContentFactory class with the Create method. When creating table content it is also recommended to use the IsMapMemberSupported or IsItemSupported functions prior to the Create method to determine if the mapMember or item can be displayed in the TableControl.

Open the TableControlDockpaneViewModel.cs file to begin customization of the ViewModel. Remove the Heading property and the private variable _heading.

Add a new private variable and public property to perform the binding of the table control's content between the View and ViewModel.

private TableControlContent _tableContent;
public TableControlContent TableContent
{
  get => _tableContent;
  set => SetProperty(ref _tableContent, value);
}

Next, we will create and assign content to the table. In this example we will display data from the selected item in the Catalog pane, so will use the ProjectWindowSelectedItemsChangedEvent to determine when a selection occurs within the Catalog window.

In the TableControlDockpaneViewModel constructor subscribe to the ProjectWindowSelectedItemsChangedEvent.

protected TableControlDockpaneViewModel()
{
  ProjectWindowSelectedItemsChangedEvent.Subscribe(OnProjectWindowSelectedItem);
}

Within the OnProjectWindowSelectedItem method we will create the TableControlContent using the TableControlContentFactory.IsItemSupported and TableControlContentFactory.Create methods and then assign the result to the TableContent property.

private Item _selectedItem;
private void OnProjectWindowSelectedItem(ProjectWindowSelectedItemsChangedEventArgs args)
{
  if (args.IProjectWindow.SelectionCount > 0)
  {
    // get the first selected item
    _selectedItem = args.IProjectWindow.SelectedItems.First();

    // check if it's supported by the TableControl
    if (!TableControlContentFactory.IsItemSupported(_selectedItem))
      return;

    // create the content
    var tableContent = TableControlContentFactory.Create(_selectedItem);
    // assign it
    if (tableContent != null)
      this.TableContent = tableContent;
  }
}

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Open a new Map project. Validate the UI by activating the Add-In tab.

showPreview

Click the Show Preview button and see the Preview dockpane appear at the bottom of the ArcGIS Pro window. Navigate to the Catalog pane. Add a new folder connection to the Community Samples Data you previously downloaded by right clicking on Folders and choose Add Folder Connection. Save your project after the connection is made. Navigate to one of the geodatabases in the folder and highlight one of the feature classes. You should see the data populated in the TableControl.

tableControl

Explore the default capabilities of the TableControl including the Row and Column context menus. Explore other datasets in the Community Samples Data by loading them into the TableControl. Stop debugging and return to Visual Studio when you are ready to continue.

Step 4

Return to the TableControlDockpane.xaml file. We will add some buttons to the DockPanel to provide additional functionality. Copy the following so that the DockPanel tag looks like the following:

<DockPanel LastChildFill="False" Grid.Row="0">
  <Button Content="Toggle Sel" Command="editing:TableControlCommands.SwitchSelection"
          CommandTarget="{Binding ElementName=tableControl}"
          Style="{DynamicResource Esri_Button}"/>

  <Button Content="Select All" Command="editing:TableControlCommands.SelectAll"
          CommandTarget="{Binding ElementName=tableControl}"
          Style="{DynamicResource Esri_Button}"/>

  <Button Content="Clear Sel" Command="editing:TableControlCommands.ClearSelection"
          CommandTarget="{Binding ElementName=tableControl}"
          Style="{DynamicResource Esri_Button}"/>

  <Button Content="Add To Map" Command="{Binding AddToMapCommand}"
          Style="{DynamicResource Esri_Button}"/>
</DockPanel>

There are a predefined set of TableControlCommands which are available for use with the TableControl. They can be referenced in xaml. In the above code snippet you can see we are referencing the SwitchSelection, SelectAll and ClearSelection commands. Take particular note of the CommandTarget property. When referencing these TableControlCommands in xaml it is important to also use the CommandTarget property to reference the TableControl's Name property, otherwise the command does not have the correct context and will not execute.

Step 5

Our final button on the toolbar requires an implementation of the AddToMapCommand. Open the Module1.cs file and add the following variable.

internal static MapMember SelectedMapMember = null;

Open the TableControlDockpaneViewModel.cs file and copy the following code.

private ICommand _addToMapCommand = null;
public ICommand AddToMapCommand
{
  get
  {
    if (_addToMapCommand == null)
    {
      _addToMapCommand = new RelayCommand(() =>
      {
        var map = MapView.Active?.Map;
        if (map == null)
          return;
        QueuedTask.Run(() =>
        {
          // test if the selected Catalog item can create a layer
          if (LayerFactory.Instance.CanCreateLayerFrom(_selectedItem))
          {
            var creationParams = new LayerCreationParams(_selectedItem);
            Module1.SelectedMapMember = LayerFactory.Instance.CreateLayer<FeatureLayer>(creationParams, map);
          }
          // test if the selected Catalog item can create a table
          else if (StandaloneTableFactory.Instance.CanCreateStandaloneTableFrom(_selectedItem))
          {
            var creationParams = new StandaloneTableCreationParams(_selectedItem);
            Module1.SelectedMapMember = StandaloneTableFactory.Instance.CreateStandaloneTable(creationParams, map);
          }
          else
            Module1.SelectedMapMember = null;
        });

      });
    }
    return _addToMapCommand;
  }
}

This command uses the LayerFactory and StandaloneTableFactory static classes to check the selected Catalog item before adding it to the active map. Use the LayerCreationParams and StandaloneTableCreationParams classes to configure the item by defining properties such as Name and position on the TOC when it is added to the Map. The resulting mapMember is stored in the Module1.SelectedMapMember variable that we will use in a future step.

Debug the add-in and start ArcGIS Pro. Open your previous project. Return to the Catalog window and your data connection folder to populate the table with content.

Now test the buttons added to the toolbar. Note that because the table control has been loaded from the Catalog item, any table selection will be independent of the map selection even if you add the loaded data to the map.

TableControlButtons

When finished, close Pro and return to Visual Studio.

Step 6

The next step is to customize the row context menu. The default row context menu for the TableControl provides one menu item to toggle the selection of the active row. We will also add the ability to zoom to the active row in the map.

First, define a resource for the context menu in the xaml file. Open TableControlDockpane.xaml and add the following so the entire UserControl.Resources at the top of the file looks like this:

<UserControl.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
        <extensions:DesignOnlyResourceDictionary Source="pack://application:,,,/ArcGIS.Desktop.Framework;component\Themes\Default.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    <ContextMenu x:Key="MyRowContextMenu">
      <MenuItem Header="Select/Unselect" 
                Command="editing:TableControlCommands.ToggleRowSelection">
        <MenuItem.Icon>
          <Image Source="{StaticResource TableSelectUnselect16}" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
        </MenuItem.Icon>
      </MenuItem>
    </ContextMenu>
  </ResourceDictionary>
</UserControl.Resources>

A new ContextMenu resource called MyRowContextMenu now exists. It contains a single menu item that executes the ToggleRowSelection TableControl command. The second command, ZoomToRow will be added in code to illustrate an alternative approach.

Bind the RowContextMenu and SelectedRowContextMenu properties in the TableControl declaration to the MyRowContextMenu resource as follows:

<editing:TableControl AutomationProperties.AutomationId="_tableControl" x:Name="tableControl" 
                   Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                   TableContent="{Binding Path=TableContent}" 
                   RowContextMenu="{StaticResource MyRowContextMenu}"
                   SelectedRowContextMenu="{StaticResource MyRowContextMenu}">
</editing:TableControl>

The RowContextMenu property is the context menu for rows when in All Record view mode. The SelectedRowContextMenu is the context menu for rows when in Selected Record view mode.

Next open the TableControlDockpane.xaml.cs file. In the TableControlDockpaneView constructor, add a hook to the Loaded event. We will use this event to pass a reference to the TableControl object to our ViewModel.

public TableControlDockpaneView()
{
  InitializeComponent();
  Loaded += TableControlDockpaneView_Loaded;
}

private void TableControlDockpaneView_Loaded(object sender, RoutedEventArgs e)
{
  var dockpane = this.DataContext as TableControlDockpaneViewModel;
  dockpane.SetTable(this.tableControl);
}

In the TableControlDockpaneViewModel.cs file, add the following methods which appends our custom 'Zoom to Row' menu item to the context menus when the TableControl is assigned.

private ArcGIS.Desktop.Editing.Controls.TableControl _tableControl = null;
internal void SetTable(ArcGIS.Desktop.Editing.Controls.TableControl tableControl)
{
  _tableControl = tableControl;
  UpdateContextMenu();
}

private bool _contextmenu_item_added = false;
private void UpdateContextMenu()
{
  if (_contextmenu_item_added)
        return;
  if (_tableControl.RowContextMenu == null)
        return;
  var mnuItem_Zoom = new System.Windows.Controls.MenuItem() { Header = "Zoom to Row", Command = this.ZoomToRowCommand };
  _tableControl.RowContextMenu.Items.Add(mnuItem_Zoom);
  _contextmenu_item_added = true;
}

Finally add the ZoomToRowCommand method.

private ICommand _zoomToRowCommand = null;
public ICommand ZoomToRowCommand
{
  get
  {
    if (_zoomToRowCommand == null)
    {
      _zoomToRowCommand = new RelayCommand(() =>
      {
        // if we have some content, a map and our data is added to the map
        if (_tableControl.TableContent != null && MapView.Active != null && Module1.SelectedMapMember is Layer)
        {
          // get the oid of the active row
          var oid = _tableControl.GetObjectIdAsync(_tableControl.ActiveRowIndex).Result;
          // load into an inspector to obtain the Shape
          var insp = new Inspector();
          insp.LoadAsync(Module1.SelectedMapMember, oid).ContinueWith((t) =>
          {
            // zoom
            MapView.Active.ZoomToAsync(insp.Shape.Extent, new TimeSpan(0, 0, 0, 1));
          });
        }
      });
    }
    return _zoomToRowCommand;
  }
}

This block of code uses the GetObjectIdAsync method passing the ActiveRowIndex to retrieve the objectID of the active row. Load an Inspector object with this objectID and the stored MapMember to obtain the Shape. Use the ZoomToAsync method on the active mapView to zoom to that extent.

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Open the previous project. Load data from the Community Samples Data. Click the 'Add to Map' button for your data to be added to the active map. When you right click on a row in the TableControl you should see the custom context menu. Choose the 'Zoom to Row' option and the map will zoom to that record.

TableControlContextMenu

Stop debugging and return to Visual Studio.

Step 7

Return to the TableControlDockpane.xaml file. We will add one more button to the DockPanel to illustrate the Find functionality. Add the following Button to the end of the DockPanel tag.

<Button Content="Find" Command="editing:TableControlCommands.Find"
        CommandTarget="{Binding ElementName=tableControl}"
        Style="{DynamicResource Esri_Button}"/>

The entire DockPanel tag should look like the following.

    <DockPanel LastChildFill="False" Grid.Row="0">
      <Button Content="Toggle Sel" Command="editing:TableControlCommands.SwitchSelection"
              CommandTarget="{Binding ElementName=tableControl}"
              Style="{DynamicResource Esri_Button}"/>

      <Button Content="Select All" Command="editing:TableControlCommands.SelectAll"
              CommandTarget="{Binding ElementName=tableControl}"
              Style="{DynamicResource Esri_Button}"/>

      <Button Content="Clear Sel" Command="editing:TableControlCommands.ClearSelection"
              CommandTarget="{Binding ElementName=tableControl}"
              Style="{DynamicResource Esri_Button}"/>

      <Button Content="Add To Map" Command="{Binding AddToMapCommand}"
              Style="{DynamicResource Esri_Button}"/>

      <Button Content="Find" Command="editing:TableControlCommands.Find"
              CommandTarget="{Binding ElementName=tableControl}"
              Style="{DynamicResource Esri_Button}"/>
    </DockPanel>

Compile and debug the add-in. Open the previous project and load data from the Community Samples Data. Click the Find button and see the Find UI display. Highlight a field header, type in the Find text field and click Enter to execute the Find functionality.

tableControlFind

Step 8

Finally, we will add a MapTool to illustrate the GetRowIndex and BringIntoView functionalities. Right click on the solution and choose Add | New Item. Choose the ArcGIS Pro Add-Ins | ArcGIS Pro Map Tool and name the file IdentifyTool.cs.

Open the config.daml file and update the tool definition. Change the following

  • Change the tool caption to "Identify Tool""
  • Change the tool tooltip heading to "Identify Tool"" and the tooltip text to "Identify a feature"
<tool id="TableControl_IdentifyTool" caption="Identify Tool" 
      className="IdentifyTool" loadOnClick="true" keytip="TI"
      smallImage="GenericButtonRed16" largeImage="GenericButtonRed32" 
      condition="esri_mapping_mapPane">
  <tooltip heading="Identify Tool">Identify a feature<disabledText /></tooltip>
</tool>

Open the IdentifyTool.cs file. Add the following code to the OnSketchCompleteAsync method.

protected override async Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  if (Module1.SelectedMapMember is BasicFeatureLayer layer)
  {
    // create a new spatial filter
    //  to find features intersecting the sketch geometry
    var filter = new SpatialQueryFilter();
    filter.FilterGeometry = geometry;
    filter.SpatialRelationship = SpatialRelationship.Intersects;

    long oid = -1;
    await QueuedTask.Run(() =>
    {
      // search the layer using the filter
      //   finding the first objectId
      using (var cursor = layer.Search(filter))
      {
        if (cursor.MoveNext())
        {
          using (var row = cursor.Current)
          {
            oid = row.GetObjectID();
          }
        }
      }
    });

    // if an objectID was found
    if (oid != -1)
    {
      // find the dockpane viewmodel
      var vm = FrameworkApplication.DockPaneManager.Find("TableControl_TableControlDockpane") as TableControlDockpaneViewModel;
      // call MoveTo
      if (vm != null)
        vm.MoveTo(oid);
    }
  }
  return true;
}

When the sketch is completed, a new SpatialQueryFilter is created and defined to filter for geometries that Intersect with the sketch geometry. This spatial filter is used to Search the layer that exists in the Module1.SelectedMapMember variable. The objectID of the first row returned from the query is passed to the MoveTo method of the TableControlDockpaneViewModel. The TableControlDockpaneViewModel is retrieved using the FrameworkApplication.DockPaneManager.Find method with the daml identifier of the dock pane and then casting to the view model class.

Open the TableControlDockpaneViewModel.cs file and insert the following code to add the MoveTo method.

internal Task MoveTo(long oid)  
{
    if (oid == -1)
      return Task.CompletedTask;
    if (_tableControl == null)
      return Task.CompletedTask;
    // return task
    return QueuedTask.Run(async () =>
    {
      // get the row index from the oid
      // use false to search entire table
      var rowIndex = _tableControl.GetRowIndex(oid, false);
      if (rowIndex != -1)
      {
        // scroll to it
        await _tableControl.BringIntoView(rowIndex);
         // clear any existing selection
        _tableControl.ClearSelection();
        // toggle selection state of the row (ie select it)
        _tableControl.ToggleRowSelection();
      }
    });
}

In this method we translate the objectID into a row index using the GetRowIndex method. We pass a value of false in the second parameter to ensure that the entire table is searched to find the objectID. The BringIntoView method is then called to scroll to that row and make it the active row. We also select the row in the table. The feature will not automatically be selected in the map because the table control was loaded originally from the Catalog item, so any table selection will be independent of the map selection.

Compile the solution and fix any errors that you find. Debug the solution and open your previous project. Load data from the Community Samples Data into the table control and click the 'Add to Map' button to also have the data loaded in the map. Zoom into an area of the data. Navigate to the add-in tab and click the Identify tool. Sketch a rectangle onto the map around one of the features and see the table control scroll to and activate the row containing that feature. The row will also be selected in the table. Experiment with different features to verify that the add-in is working as expected.

Stop debugging and return to Visual Studio.

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