Skip to content

ProGuide Editing tool

UmaHarano edited this page May 6, 2024 · 24 revisions
Language:      C#
Subject:       Editing
Contributor:   ArcGIS Pro SDK Team <arcgisprosdk@esri.com>
Organization:  Esri, http://www.esri.com
Date:          04/04/2024
ArcGIS Pro:    3.3
Visual Studio: 2022

This ProGuide shows how to build an editing tool to interact with and modify existing features. The code used to illustrate this add-in can be found at Sketch Tool Demo Sample.

Prerequisite

Create a new add-in, and add a map tool to the add-in called CutTool.cs.

Step 1

In the constructor, change the SketchType of the map tool to a line geometry type. This is the geometry of the map tool feedback used to symbolize the user interaction. For this example, you're using a single solid line that adds a linear segment on each mouse click.

// select the type of tool feedback you wish to implement.  
SketchType = SketchGeometryType.Line;
// a sketch feedback is needed
IsSketchTool = true;
// the geometry is needed in map coordinates
SketchOutputMode = ArcGIS.Desktop.Mapping.SketchOutputMode.Map;

Step 2

Replace the code in the OnSketchCompleteAsync method. Assign a method (ExecuteCut) to modify features in the editor, and, since the API calls need to happen on the CIM thread, use a lambda function to handle the operation using the sketch geometry. For now, simply declare the method. The code will be specified in step 4.

protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  return QueuedTask.Run(() => ExecuteCut(geometry));
}

protected Task<bool> ExecuteCut(Geometry geometry)
{
  return Task.FromResult(true);
}

Step 3

Open the Config.daml file and go to the entry for the tool. Add a categoryRefID attribute setting it to categoryRefID="esri_editing_CommandList". This specifies the tool is to appear in the Modify Features pane. Also add a content xml node to the tool specification. The attribute L_group specifies the group that the tool will live under in the Modify Features pane. Use your own group name to create a new group entry in the pane. The tool daml should be as below

<tool id="ProAppModule1_CutTool" categoryRefID="esri_editing_CommandList" 
      caption="CutTool" className="CutTool" loadOnClick="true" 
      smallImage="Images\GenericButtonRed16.png" 
      largeImage="Images\GenericButtonRed32.png">
  <tooltip heading="Tooltip Heading">Tooltip text<disabledText /></tooltip>
  <content L_group="Pro SDK Samples" />
</tool>

ProGuide: Editing - SketchTool L_group

The attributes gallery2d and gallery3d also live within the content tag and specify whether the tool is shown in the favorites group in the gallery for 2D or 3D editing tools on the Edit ribbon. In this guide, you'll add the tool to the favorites group in the 2D gallery so set the gallery2d to true and the gallery3d to false.

ProGuide: Editing - SketchTool gallery2d

The modified xml configuration for the editing tool should look similar to the following definition:

<tool id="ProAppModule1_CutTool" categoryRefID="esri_editing_CommandList" 
      caption="CutTool" className="CutTool" loadOnClick="true" 
      smallImage="Images\GenericButtonRed16.png" 
      largeImage="Images\GenericButtonRed32.png">
  <tooltip heading="Tooltip Heading">Tooltip text<disabledText /></tooltip>
  <content L_group="Pro SDK Samples" gallery2d="true" gallery3d="false" />
</tool>

Step 4

Go back to the CutTool.cs file and the ExecuteCut method. We'll be using the sketch geometry to perform a cut against all editable polygon features in the active map. Add the following code to the method to first obtain the editable polygon layers.

  if (geometry == null)
    return Task.FromResult(false);

  // create a collection of feature layers that can be edited
  var editableLayers = ActiveMapView.Map.GetLayersAsFlattenedList()
          .OfType<FeatureLayer>()
          .Where(lyr => lyr.CanEditData() == true).Where(lyr =>
          lyr.ShapeType == esriGeometryType.esriGeometryPolygon);

  // ensure that there are target layers
  if (editableLayers.Count() == 0)
    return Task.FromResult(false);

Since an edit is going to be performed, create a new instance of an EditOperation to scope the modifies. See EditOperation in the ProConcept documentation to learn about its functionality.

  // create an edit operation
  EditOperation cutOperation = new EditOperation()
  {
    Name = "Cut Elements",
    ProgressMessage = "Working...",
    CancelMessage = "Operation canceled.",
    ErrorMessage = "Error cutting polygons",
    SelectModifiedFeatures = false,
    SelectNewFeatures = false
  };

Step 5

For each identified editable polygon layer, do the following:

  1. Get the underlying feature class and determine the field index for the "Description" attribute field.
  2. Search for the features that are crossing the sketch geometry. Use a SpatialQueryFilter.
  // initialize a list of ObjectIDs that need to be cut
  var cutOIDs = new List<long>();

  // for each of the layers 
  foreach (FeatureLayer editableFeatureLayer in editableLayers)
  {
    // get the feature class associated with the layer
    var fc = editableFeatureLayer.GetTable();

    // find the field index for the 'Description' attribute
    int descriptionIndex = -1;
    descriptionIndex = fc.GetDefinition().FindField("Description");

    // find the features crossed by the sketch geometry
    //   use the featureClass to search. We need to be able to search with a recycling cursor
    //     seeing we want to Modify the row results
    // define a spatial query filter
    var spatialQueryFilter = new SpatialQueryFilter
    {
      // passing the search geometry to the spatial filter
      FilterGeometry = geometry,
      // define the spatial relationship between search geometry and feature class
      SpatialRelationship = SpatialRelationship.Crosses
    };

    using (var rowCursor = fc.Search(spatialQueryFilter, false))
    {
    }
  }

For each returned feature, test whether the feature is completely intersected by the sketch geometry by using the GeometryEngine.Instance.Relate method. The string argument is detailed in the Dimensionally Extended Nine-Intersection Model. If the feature's geometry is completely intersected, store its ObjectID in a list. Here's the code for the search results:

    using (var feature = rowCursor.Current as Feature)
    {
      var geomTest = feature.GetShape();
      if (geomTest != null)
      {

        // make sure we have the same projection for geomProjected and geomTest
        var geomProjected = GeometryEngine.Instance.Project(geometry, geomTest.SpatialReference);

        // we are looking for polygons are completely intersected by the cut line
        if (GeometryEngine.Instance.Relate(geomProjected, geomTest, "TT*F*****"))
        {
          var oid = feature.GetObjectID();

          // add the current feature to the overall list of features to cut
          cutOIDs.Add(oid);
        }
      }
    }

Step 6

Once we have the list of objectIDs for each polygon layer, modify the Description field for each record using the EditOperation.Modify routine. And perform the Cut operation using the EditOperation.Split method.

          // adjust the attribute before the cut
          if (descriptionIndex != -1)
          {
            var atts = new Dictionary<string, object>();
            atts.Add("Description", "Pro Sample");
            foreach (var oid in cutOIDs)
              cutOperation.Modify(editableFeatureLayer, oid, atts);
          }

          // add the elements to cut into the edit operation
          cutOperation.Split(editableFeatureLayer, cutOIDs, geometry);

Once the code enumerates all the layers, execute the edit operation. Regardless of the number of modified features across the polygon layers, there will be only one edit operation listed in the Undo/Redo stack for the active view.

  //execute the operation
  var operationResult = cutOperation.Execute();
  return Task.FromResult(operationResult);

The complete ExecuteCut function looks like this.

  protected Task<bool> ExecuteCut(Geometry geometry)
  {
    if (geometry == null)
      return Task.FromResult(false);

    // create a collection of feature layers that can be edited
    var editableLayers = ActiveMapView.Map.GetLayersAsFlattenedList()
          .OfType<FeatureLayer>()
          .Where(lyr => lyr.CanEditData() == true).Where(lyr =>
          lyr.ShapeType == esriGeometryType.esriGeometryPolygon);

    // ensure that there are target layers
    if (editableLayers.Count() == 0)
      return Task.FromResult(false);

    // create an edit operation
    EditOperation cutOperation = new EditOperation()
    {
      Name = "Cut Elements",
      ProgressMessage = "Working...",
      CancelMessage = "Operation canceled.",
      ErrorMessage = "Error cutting polygons",
      SelectModifiedFeatures = false,
      SelectNewFeatures = false
    };

    // initialize a list of ObjectIDs that need to be cut
    var cutOIDs = new List<long>();

    // for each of the layers 
    foreach (FeatureLayer editableFeatureLayer in editableLayers)
    {
      // get the feature class associated with the layer
      var fc = editableFeatureLayer.GetTable();

      // find the field index for the 'Description' attribute
      int descriptionIndex = -1;
      descriptionIndex = fc.GetDefinition().FindField("Description");

      // find the features crossed by the sketch geometry
      //   use the featureClass to search. We need to be able to search with a recycling cursor
      //     seeing we want to Modify the row results
      // define a spatial query filter
      var spatialQueryFilter = new SpatialQueryFilter
      {
        // passing the search geometry to the spatial filter
        FilterGeometry = geometry,
        // define the spatial relationship between search geometry and feature class
        SpatialRelationship = SpatialRelationship.Crosses
      };

      using (var rowCursor = fc.Search(spatialQueryFilter, false))
      {
        // add the feature IDs into our prepared list
        while (rowCursor.MoveNext())
        {
          using (var feature = rowCursor.Current as Feature)
          {
            var geomTest = feature.GetShape();
            if (geomTest != null)
            {

              // make sure we have the same projection for geomProjected and geomTest
              var geomProjected = GeometryEngine.Instance.Project(geometry, geomTest.SpatialReference);

              // we are looking for polygons are completely intersected by the cut line
              if (GeometryEngine.Instance.Relate(geomProjected, geomTest, "TT*F*****"))
              {
                var oid = feature.GetObjectID();

                // add the current feature to the overall list of features to cut
                cutOIDs.Add(oid);
              }
            }
          }
        }

        // adjust the attribute before the cut
        if (descriptionIndex != -1)
        {
          var atts = new Dictionary<string, object>();
          atts.Add("Description", "Pro Sample");
          foreach (var oid in cutOIDs)
            cutOperation.Modify(editableFeatureLayer, oid, atts);
        }

        // add the elements to cut into the edit operation
        cutOperation.Split(editableFeatureLayer, cutOIDs, geometry);
      }

    }
    // execute the operation
    var operationResult = cutOperation.Execute();
    return Task.FromResult(operationResult);

  }

Step 7

To emphasize the interactive operation, you may want to change the sketch symbol. The appearance of the sketch can be changed by overriding the OnSketchModifiedAsync method. In this example, you'll change the style, width, and color of the line sketch after the second vertex.

  protected override async Task<bool> OnSketchModifiedAsync()
  {
    // retrieve the current sketch geometry
    Polyline cutGeometry = await base.GetCurrentSketchAsync() as Polyline;

    await QueuedTask.Run(() =>
    {
      // if there are more than 2 vertices in the geometry
      if (cutGeometry.PointCount > 2)
      {
        // adjust the sketch symbol
        var symbolReference = base.SketchSymbol;
        if (symbolReference == null)
        {
          var cimLineSymbol = SymbolFactory.Instance.ConstructLineSymbol(ColorFactory.Instance.RedRGB, 
                    3, SimpleLineStyle.DashDotDot);
          base.SketchSymbol = cimLineSymbol.MakeSymbolReference();
        }
        else
        {
          symbolReference.Symbol.SetColor(ColorFactory.Instance.RedRGB);
          base.SketchSymbol = symbolReference;
        }
      }
    });

    return true;
  }

Step 8

For the last step, set up a custom action after the edit operation has successfully completed to show how many features have been modified. Switch to the Module1.cs file and override the Initialize method for the add-in. Subscribe to the EditCompletedEvent and define a callback method called ReportNumberOfRowsChanged.

protected override bool Initialize()
{
  // subscribe to the completed edit operation event
  EditCompletedEvent.Subscribe(ReportNumberOfRowsChanged);

  return true;
}

The callback method finds the modifies that happened during the edit operation and reports the number of modifies back to the user.

  /// <summary>
  /// Method containing actions as the result of the EditCompleted (the operation) event.
  /// This method reports the total number of changed rows/features.
  /// </summary>
  /// <param name="editArgs">Argument containing the layers where edits occurred and what 
  /// types of changes.</param>
  /// <returns></returns>
  private Task<bool> ReportNumberOfRowsChanged(EditCompletedEventArgs editArgs)
  { 
     if (editArgs.CompletedType != EditCompletedType.Operation)
      return Task.FromResult(true); 

    // get the dictionary containing the modifies on the current feature 
    // operation 
    var editChanges = editArgs.Modifies;

    // use this variable to store the total number of modifies
    int countOfModifies = editChanges.Count;

    if (countOfModifies > 0)
      MessageBox.Show($"{countOfModifies.ToString()} features changed");
    else
      MessageBox.Show("The current edit operation did not contain any row/feature 
                         modification.");

    return Task.FromResult(true);
}

Step 9

Compile the solution and fix any compile errors. Run ArcGIS Pro, open a project containing some polygon data. Open the Modify Features pane and verify that your tool exists in the named group. Verify your tool can also be found on the Edit Tools gallery. Activate the CutTool and sketch a line across one or more polygons. The features will be cut (split) and a message box should appear indicating the number of features modified.

For additional information on edit sketch tools consult ProConcepts Editing, sketch tools.

Developing with ArcGIS Pro

    Migration


Framework

    Add-ins

    Configurations

    Customization

    Styling


Arcade


Content


CoreHost


DataReviewer


Editing


Geodatabase

    3D Analyst Data

    Plugin Datasources

    Topology

    Object Model Diagram


Geometry

    Relational Operations


Geoprocessing


Knowledge Graph


Layouts

    Reports


Map Authoring

    3D Analyst

    CIM

    Graphics

    Scene

    Stream

    Voxel


Map Exploration

    Map Tools


Networks

    Network Diagrams


Parcel Fabric


Raster


Sharing


Tasks


Workflow Manager Classic


Workflow Manager


Reference

Clone this wiki locally