Skip to content

ProConcepts Editing

uma2526 edited this page Nov 10, 2021 · 41 revisions

The editing functionality in ArcGIS Pro is delivered through the ArcGIS.Desktop.Editing assembly. This assembly provides the framework to create and maintain your geographic data.

ArcGIS.Desktop.Editing.dll

Language:      C#
Subject:       Editing
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          9/28/2021
ArcGIS Pro:    2.9
Visual Studio: 2017, 2019

In this topic

Editing in ArcGIS Pro

The editing assembly is the entry point for performing all edits in ArcGIS Pro. It provides coarse grained classes for making edits to layers in the map in addition to directly editing feature classes through the Geodatabase API. When developing editing customizations, you can incorporate concepts and functionality from other assemblies, notably Geometry and Geodatabase.

Editing Controls

The application framework can be customized to add your own commands, tools and modules. The type of customization to use is largely based on how the user will interact with it. The following sections provide an overview of the types of customizations available.

Commands

Commands are usually buttons or menu items on the UI. Their functions typically work with a selected set, a whole layer, or by manipulating the editing environment in some way. Commands do not interact with the display, so if you need to draw a sketch, select features, or define an area, you need to write a Tool instead. The Save, Discard, and Delete buttons on the Edit tab are examples of commands specific to the editing environment.

You can use the ArcGIS Pro Button template in Visual Studio to create a command.

Tools

Tools allow you to interact with the display in some manner. They can be used to define areas, create a sketch, or select features for an edit operation. They can be active or inactive. When a tool is active, it is THE tool that the user is interacting with. Only one tool can be active in the application at a time.

Sketch Tools

Sketch tools are used to create an edit sketch in the display. The returned geometry is then used for further edit operations such as editing existing features.

You can use the ArcGIS Pro MapTool template in Visual Studio to create a sketch tool. A MapTool can sketch feedback on the UI when its this.IsSketchTool property is set to true. Use the SketchType property to specify what type of feedback you require. Common values are SketchGeometryType.Point, SketchGeometryType.Rectangle, SketchGeometryType.Line or SketchGeometryType.Polygon. Other options include SketchGeometryType.Circle, SketchGeometryType.Ellipse, or SketchGeometryType.Lasso amongst others.

At 2.7+, a regular N-sided regular polygon or polyline feedback can be achieved by using SketchGeometryType.RegularPolygon or SketchGeometryType.RegularPolyline, along with setting the SketchNumerofSides property. The higher the number of sides, the more the sketch feedback begins to represent a circle. If no number of sides is specified, the sketch defaults to 6 (hexagon). The maximum number of sides is 100, the minimum that can be specified is 3.

For example; use the following code to sketch octagon feedback.

public CustomEditTool() {
  IsSketchTool = true;
  SketchType = SketchGeometryType.RegularPolygon;
  SketchNumberOfSides = 8;
  ...
}

For more information on MapTools, see the MapTool section in the Map Exploration ProConcepts. The editing section in the community samples contains many examples of sketch tools.

Sketch tools can also make use of the various sketch events within their workflows.

UseSelection

Sketch Tools can make use of a property in their constructors called UseSelection. Generally speaking, UseSelection, when set true, marks a tool as needing a selection in order to operate. If a COTS tool is activated that requires a selection, and no features are selected, it automatically goes into select mode to allow you to select a feature before beginning the sketch.

Custom tools can also toggle between sketch and select modes by using the UseSelection property. If a custom tool sets UseSelection = true it can be placed into select mode by holding down the SHIFT key. Releasing the SHIFT key puts the tool back into sketch mode and restores the previous state of the sketch (and the sketch undo / redo stack). Note your SketchType must be SketchGeometryType.Point, SketchGeometryType.Multipoint, SketchGeometryType.Line or SketchGeometryType.Polygon in order to be able to toggle into select mode. No other SketchTypes support this behavior.

public CustomEditTool() {
  IsSketchTool = true;
  SketchType = SketchGeometryType.Line;
  ...

  //To allow default activation of "selection" mode
  //Hold down the SHIFT key to toggle into 
  //selection mode. The state of the sketch is preserved.
  UseSelection = true;//set to false to "turn off"
}

ActivateSelectAsync

Developers are not restricted to using the default SHIFT key behavior to toggle to select mode. If developers want to "manually" toggle their tools (into select mode) they can use ActivateSelectAsync(bool activate) with activate = true. To go back to sketch mode, the tool must call ActivateSelectAsync with activate = false. With UseSelection = true set in the constructor, the previous state of the sketch will be restored, including the sketch undo/redo stack, when the tool toggle back to sketch mode. If UseSelection=false (the default), the sketch is simply cleared and the tool operator begins sketching again.

Tools that choose to manually toggle between select mode and sketch mode via ActivateSelectAsync are advised to handle the SHIFT key to override its default behavior (if they have UseSelection = true set in their constructors).

Again, only SketchTypes of SketchGeometryType.Point, SketchGeometryType.Multipoint, SketchGeometryType.Line or SketchGeometryType.Polygon support this behavior.

The behavior of a custom tool with regards to UseSelection and ActivateSelectAsync is described in the table below:

UseSelection Select Mode  Notes
True SHIFT key Gets built-in behavior. Sketch preserved.
True Custom key Manual. Use ActivateSelectAsync. Sketch preserved. Handle SHIFT
False N/A Select mode not available
False Custom key Manual. Use ActivateSelectAsync. Sketch cleared.

An example tool based on the code from the DemoUseSelection sample shows an implementation of ActivateSelectAsync. Notice how it handles the SHIFT key to prevent the base tool behavior from interfering.

internal class DifferenceTool_v2 : MapTool {
  private bool _inSelMode = false;

  public CustomEditTool() {
    IsSketchTool = true;
    ...
    //Set UseSelection = false to clear the sketch if we toggle
    //Handle the SHIFT key to prevent it interfering
    UseSelection = true;
  }

  protected override void OnToolKeyDown(MapViewKeyEventArgs k) {
    
    //toggle sketch selection mode with a custom key
    if (k.Key == System.Windows.Input.Key.W) {
      if (!_inSelMode) {
        _inSelMode = true;
        k.Handled = true;
        //toggle the tool to select mode.
        //The sketch is saved if UseSelection = true;
        ActivateSelectAsync(true);
      }
    }
    else if (!_inSelMode) {
      //disable effect of Shift in the base class.
      //Mark the key event as handled to prevent further processing
      k.Handled = Module1.Current.IsShiftKey(k);
    }
  }

  protected override Task HandleKeyDownAsync(MapViewKeyEventArgs k) {
    //Called when we return "k.Handled = true;" from OnToolKeyDown
    //TODO any additional key down handling logic here
    return Task.FromResult(0);
  }

  protected override void OnToolKeyUp(MapViewKeyEventArgs k) {
 
    if (k.Key == System.Windows.Input.Key.W) {
      if (_inSelMode) {
        _inSelMode = false;
        k.Handled = true;//process this one
        //Toggle back to sketch mode. If UseSelection = true
        //the sketch will be restored
        ActivateSelectAsync(false);

      }
    }
    else if (_inSelMode) {
      //disable effect of Shift in the base class.
      //Mark the key event as handled to prevent further processing
      k.Handled = Module1.Current.IsShiftKey(k);
    }
  }

  protected override Task HandleKeyUpAsync(MapViewKeyEventArgs k) {
    //Called when we return "k.Handled = true;" from OnToolKeyUp
    //TODO any additional key up handling logic here
    return Task.FromResult(0);
  }

  protected override Task OnToolActivateAsync(bool active) {
    //clear the flag
    _inSelMode = false;
     return base.OnToolActivateAsync(active);
   }

   protected override Task<bool> OnSketchCompleteAsync(Geometry geometry) {
     var mv = MapView.Active;
      return QueuedTask.Run(() =>  {
        ...
        //TODO - the actual edits
      });
    }
}

internal class Module1 : Module {
  ...
  public bool IsShiftKey(MapViewKeyEventArgs k) {
    return (k.Key == System.Windows.Input.Key.LeftShift ||
           k.Key == System.Windows.Input.Key.RightShift);
  }

Construction Tools

Construction tools-such as the Point, Line, Polygon, and Circle tools—are used to create features within the template environment. Construction tools are sketch tools that define the type of edit template geometry they are associated with.

You can use the ArcGIS Pro Construction Tool template in Visual Studio to create a construction tool. Configure the categoryRefID tag in the config.daml file for your tool to set the type of geometry feature class the tool will be associated with. Configure the SketchType property in the tool constructor to the correct geometry that you wish to sketch (rectangle, polygon, point, line etc).

Here is the default implementation of a construction tool provided by the Visual Studio template.

    <tool id="Sample_ConstructionTool_ConstructionTool1" categoryRefID="esri_editing_construction_point" 
               caption="ConstructionTool 1" 
               className="ConstructionTool1" loadOnClick="true" 
               smallImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonRed16.png" 
               largeImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonRed32.png">
      <!--note: use esri_editing_construction_polyline,  esri_editing_construction_polygon for categoryRefID as needed-->
      <tooltip heading="Tooltip Heading">Tooltip text<disabledText /></tooltip>
      <content guid="ee73d46f-913f-42b6-b951-57e18622f183" />
    </tool>
  internal class ConstructionTool1 : MapTool
  {
    public ConstructionTool1()
    {
      IsSketchTool = true;
      UseSnapping = true;
      // Select the type of construction tool you wish to implement.  
      // Make sure that the tool is correctly registered with the correct component category type in the daml 
      SketchType = SketchGeometryType.Point;
      // SketchType = SketchGeometryType.Line;
      // SketchType = SketchGeometryType.Polygon;
      //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
      UsesCurrentTemplate = true;
      //Gets or sets whether the tool supports firing sketch events when the map sketch changes. 
      //Default value is false.
      FireSketchEvents = true;
    }

    /// <summary>
    /// Called when the sketch finishes. This is where we will create the sketch operation and then execute it.
    /// </summary>
    /// <param name="geometry">The geometry created by the sketch.</param>
    /// <returns>A Task returning a Boolean indicating if the sketch complete event was successfully handled.</returns>
    protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
    {
      if (CurrentTemplate == null || geometry == null)
        return Task.FromResult(false);

      // Create an edit operation
      var createOperation = new EditOperation();
      createOperation.Name = string.Format("Create {0}", CurrentTemplate.Layer.Name);
      createOperation.SelectNewFeatures = true;

      // Queue feature creation
      createOperation.Create(CurrentTemplate, geometry);

      // Execute the operation
      return createOperation.ExecuteAsync();
    }
  }

Table Construction Tools

Table Construction Tools are a special type of construction tool used to create rows in a standalone table. By definition, a table does not have a geometry field, so a table construction tool by default does not sketch feedback on the UI. It does however follow the tool pattern of being active or inactive and the construction tool pattern of being associated with a template type (specified by the categoryRefID attribute in the config.daml file).

If you wish to build a custom tool to create a row in a standalone table, use the ArcGIS Pro Table Construction Tool template in Visual Studio. The categoryRefID tag in the config.daml file defaults to the table category, and the constructor for the tool class has the IsSketchTool property set to false and the SketchType property set to None to reflect the default behavior of not sketching on the UI.

Here is the default implementation of a table construction tool

    <tool id="Sample_ConstructionTool_TableConstructionTool1" categoryRefID="esri_editing_construction_table" 
                caption="TableConstructionTool 1" 
                className="TableConstructionTool1" loadOnClick="true" 
                smallImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonRed16.png" 
                largeImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonRed32.png">
          <!--note: use esri_editing_construction_polyline,  esri_editing_construction_polygon for categoryRefID as needed-->
      <tooltip heading="Tooltip Heading">Tooltip text<disabledText /></tooltip>
      <content guid="8800805c-e704-4c72-97dc-fd54bd5cbd66" />
    </tool>
  class TableConstructionTool1 : MapTool
  {
    public TableConstructionTool1()
    {
      IsSketchTool = false;
      // set the SketchType to None
      SketchType = SketchGeometryType.None;
      //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
      UsesCurrentTemplate = true;
    }

    /// <summary>
    /// Called when the "Create" button is clicked. This is where we will create the edit operation and then execute it.
    /// </summary>
    /// <param name="geometry">The geometry created by the sketch - will be null because SketchType = SketchGeometryType.None</param>
    /// <returns>A Task returning a Boolean indicating if the sketch complete event was successfully handled.</returns>
    protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
    {
      if (CurrentTemplate == null)
        return Task.FromResult(false);

      // geometry will be null

      // Create an edit operation
      var createOperation = new EditOperation();
      createOperation.Name = string.Format("Create {0}", CurrentTemplate.StandaloneTable.Name);
      createOperation.SelectNewFeatures = false;

      // determine the number of rows to add
      var numRows = this.CurrentTemplateRows;
      for (int idx = 0; idx < numRows; idx++)
        // Queue feature creation
        createOperation.Create(CurrentTemplate, null);

      // Execute the operation
      return createOperation.ExecuteAsync();
    }
  }

Within the Create Features UI, a row can be created for a table template by clicking the "Create" button. The active tool receives the OnSketchCompleteAsync callback with a null geometry parameter. Table construction tools use an additional property CurrentTemplateRows to determine how many rows are to be created. The UI control connected to this property is visible on the ActiveTemplate pane for the table template.

Table construction tools can be modified to become sketching construction tools. For example, perhaps you wish to create a construction tool that will obtain certain attributes from features on the map and have them be populated in your table row. In this case you would change the IsSketchTool property to true, modify the SketchType property to the appropriate geometry type and add the UseSnapping property (set to true) if you require snapping.

    public TableConstructionTool1()
    {
      IsSketchTool = true;
      // set the SketchType
      SketchType = SketchGeometryType.Rectangle;
      // sketch will snap to features
      UseSnapping = true;
      //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
      UsesCurrentTemplate = true;
    }

The table construction tool now operates like a feature construction tool (ie it can sketch feedback on the UI and when the sketch is finished, the tool receives the OnSketchCompleteAsync callback with the geometry parameter populated with the sketch), and you can use the sketch geometry to obtain information from the map to populate the table rows.

The TableConstructionTool sample provides examples of table construction tools including two that are sketching construction tools.

Modules

Every add-in creates a module that you can add custom code to. In an editing context, this can be used to set up event listeners for editing or application events when the module loads, assuming AutoLoad is true in the module configuration. See the Events section in this topic for more information on working with events.

Enable and Disable Editing

Editing in the application, starting at 2.6, can be enabled and disabled via the UI. For users this means first checking on the "Enable and disable editing from the Edit tab" in the Session section of the Editing options on the Pro backstage.

EnableAndDisableEditing

If this checkbox is checked, the "Toggle editing on/off" button becomes visible on the Edit UI ribbon and users must enable editing within a project before doing any edits (they can also disable editing or "toggle it off" once they have enabled it or "toggled it on"). If the "Enable and disable editing" checkbox has been checked, then editing must always be enabled for any project after it is opened; the editing state of a project is not preserved on close.

ToggleEditing

Custom editing tools and buttons that want to be enabled and disabled in concert with the Pro editing tools, in response to the editing status of the application, can use either the pre-existing esri_editing_EditingPossibleCondition condition or, for (map) tools, they can use the slightly more robust esri_editing_EditingMapCondition condition which additionally checks that a 2D map has been activated. If a user disables editing on the UI, Edit Operations in core tools and add-ins will not execute and return false (see "EditOperation" sub-section below).

Enable and Disable Editing via the API

By default, unless the user has explicitly checked the "Enable and disable editing" checkbox (or an add-in has disabled editing), editing in a project will always be enabled. Add-ins can check the editing enabled status within a given project via the Project.Current.IsEditingEnabled property. To enable or disable editing via the api, add-ins can call Project.Current.SetIsEditingEnabledAsync with true or false respectively.

If Project.Current.SetIsEditingEnabledAsync is called with "true", editing is enabled and the editing UI will likewise be enabled. If Project.Current.SetIsEditingEnabledAsync is called with "false", any on-going editing is stopped and further editing is disabled. The editing UI is also disabled. Before add-ins can disable editing any pending edits must either be discarded or saved or the call to Project.Current.SetIsEditingEnabledAsync(false) will fail. Enabling and disabling editing via the api is shown in the following example:

Enable

  //Can we edit ? If not, ask the user to enable editing
  if (!Project.Current.IsEditingEnabled) {
    var res = MessageBox.Show(
       "You must enable editing to use editing tools. Would you like to enable editing?",
       "Enable Editing?", System.Windows.MessageBoxButton.YesNoCancel);

    if (res == System.Windows.MessageBoxResult.No ||
    res == System.Windows.MessageBoxResult.Cancel)
      return;//user does not want to enable editing

    Project.Current.SetIsEditingEnabledAsync(true);
 }

Disable

 //We are editing. Does the user want to disable it?
 if (Project.Current.IsEditingEnabled) {
   var res = MessageBox.Show(
      "Do you want to disable editing? Editing tools will be disabled",
      "Disable Editing?", System.Windows.MessageBoxButton.YesNoCancel);

   if (res == System.Windows.MessageBoxResult.No ||
       res == System.Windows.MessageBoxResult.Cancel)
     return;//user does not want to stop

   //Before disabling we must check for any edits
   if (Project.Current.HasEdits) {
     res = MessageBox.Show("Save edits?", "Save Edits?", 
     System.Windows.MessageBoxButton.YesNoCancel);
     if (res == System.Windows.MessageBoxResult.Cancel)
        return;//user has cancelled
     else if (res == System.Windows.MessageBoxResult.No)
        Project.Current.DiscardEditsAsync(); //user does not want to save
     else
        Project.Current.SaveEditsAsync();//save
  }
  //ok to disable editing
  Project.Current.SetIsEditingEnabledAsync(false);
}

EditOperation
If editing has been disabled in the current project, meaning Project.Current.IsEditingEnabled returns false, edit operations will not execute. If editOperation.Execute() is called, it is essentially a no-op and returns false. Once disabled, editing must be (re)enabled either from the UI or via a call to Project.Current.SetIsEditingEnabledAsync(true) to successfully execute an edit operation.

Prior to 2.6
Prior to 2.6 developers can continue to use the esri_editing_EditingPossibleCondition DAML condition. Add-ins can enable or disable the editing UI by deactivating or activating the "esri_editing_editorDisabled" state respectively. Edit operations, however, are not affected and continue to execute. The "esri_editing_editorDisabled" state is intended to be used during an add-in module initialization and not while a project is open.

  //Pre-2.6, Pro editing UI can be enabled/disabled via the 
  //esri_editing_editorDisabled state
  if (!FrameworkApplication.State.Contains("esri_editing_editorDisabled")) {
     FrameworkApplication.State.Activate("esri_editing_editorDisabled");//Disable
  }
  else {
    FrameworkApplication.State.Deactivate("esri_editing_editorDisabled");//re-enable
  }

Performing Edits in Pro

The two preferred mechanisms are the EditOperation and Inspector classes. EditOperations provide a coarse-grained API for creating and manipulating shapes and attributes whereas Inspector is a utility class more geared toward editing attributes of selected features.

Edit Operations

Edit operations perform 3 key functions:

  1. Execute the operations against underlying datastores (edit operations can span multiple datastores)
  2. Consolidate multiple edits into a single operation
  3. Invalidate any layer caches associated with the layers edited by the Edit Operation.

All edits specified for a given edit operation instance are combined into a single "execute" (e.g. creation, modifying geometry and attributes, deleting features, etc.). Combining individual edits into a single execute improves performance over executing each edit individually. Edit operations can combine edits across different datasets in the same datastore or different datastores. Pro establishes and manages the edit sessions on all underlying datastores (assuming long transaction semantics, more on this here). All edits within all active edit sessions are saved or discarded together (when a user "saves" or "discards" edits on the Pro UI or via the API). At the conclusion of a save or discard, all existing edit sessions are closed and the undo/redo operation stack is cleared.

Each edit specified on an edit operation instance must be independent of the other edits also specified. For example, a modify cannot depend on the completion of a create or a clip specified on the same edit operation nor on the completion of a move or rotate, and so on. If edits are dependent on each other, the canonical example being the creation of a feature combined with the creation of an attachment, then those operations must be chained (see chaining edit operations). The ordering of the EditOperation methods in your add-in code has no bearing on the order in which the edits are executed. Edits within an edit operation are executed in an arbitrary order though the edit operation callback is always executed last.

If the underlying datastores on which the edits are being performed support undo/redo then the EditOperation adds an entry to Pro's Undo operation stack. If a single edit specified within the execute fails then all edits in the edit operation will fail. The ExecuteAsync and Execute methods return a Boolean indicating the success of the edit. If the edit fails, returning false, the ErrorMessage property is populated with an error description, if known. The value in the "ErrorMessage" property will be applicable to the first operation that failed. At the conclusion of the execute, the edit operation also ensures that any underlying layer and table caches are invalidated.

Here are 3 examples of editing using edit operations:

The first example shows 3 separate creates along with two feature updates (all independent) combined into a single edit operation:

  var editOp = new EditOperation();
  editOp.Name = "Simple edit operation";
  //Add three points - 1 point to each layer
  editOp.Create(pointsLayer1, start_pt);
  editOp.Create(pointsLayer2, GeometryEngine.Instance.Move(start_pt, distance, 0.0));
  editOp.Create(pointsLayer3, GeometryEngine.Instance.Move(start_pt, distance * 2, 0.0));

  //Modify two polygons - 1 polygon in two layers
  editOp.Modify(polyLayer2, 1, GeometryEngine.Instance.Buffer(boundary, distance));
  editOp.Modify(polyLayer3, 1, GeometryEngine.Instance.Buffer(boundary, distance * 2));

  //Execute the operations
  editOp.ExecuteAsync();

The second example shows Clip, Split, and Planarize edits on a single feature also combined into a single edit operation:

   //Multiple operations can be performed by a single
   //edit operation.
   var op = new EditOperation();
   op.Name = "Clip, Split, and Planarize Features";

   //Combine three operations, one after the other...
   op.Clip(featureLayer, oid, clipPoly);
   op.Split(featureLayer, oid, cutLine);
   op.Planarize(featureLayer, new List<long>() { oid});

   //Execute them all together
   await op.ExecuteAsync();

   if (!op.IsSucceeded) {
     //TODO: get the op.ErrorMessage, inform the user
   }

In the third example, a dictionary is being used to set preliminary values for a new feature to include two of its attributes and geometry:

   var polyLayer = MapView.Active.Map.GetLayersAsFlattenedList().First((l) => l.Name == "Polygons") as FeatureLayer;
   var polygon = .... ;  //New polygon to use

   //Define some default attribute values
   Dictionary<string,object> attributes = new Dictionary<string, object>();

   attributes["SHAPE"] = polygon;//Geometry

   attributes["Name"] = string.Format("Poly {0}", ++count);
   attributes["Notes"] = string.Format("Notes {0}", count);

   //Create the new feature
   var op = new EditOperation();
   op.Name = string.Format("Create {0}", polys.Name);
   op.Create(polyLayer, attributes);
   await op.ExecuteAsync();

   if (!op.IsSucceeded) {
     //TODO: get the op.ErrorMessage, inform the user
   }

Refer to the ProSnippets Editing for other examples of using EditOperation.

Chaining Edit Operations

In certain situations a given edit may be dependent on the results of a previous edit being committed yet you still want all the edits to be grouped as one undo-able entry on the undo/redo stack. For example you may wish to create a feature and add an attachment to that feature. You cannot queue the Create and AddAttachment methods in the same EditOperation because the AddAttachment method signature requires the Object ID of its related feature (meaning it must already have been created). Thus you are restricted to calling these methods in two separate EditOperations. Calling two separate edit operations will add two separate "undo" items to the undo/redo stack (requiring two undo's to undo the feature create and/or two redo's to redo it). This is not your desired solution. Instead, by chaining the second "AddAttachment" edit operation to the first, even though the two operations execute independently, only one undo operation is added to the undo/redo stack. Undo-ing or redo-ing the create feature and its attachment is accomplished with a single operation.

Therefore, a chained operation is simply an EditOperation that is linked to a previous EditOperation allowing all edits from both operations to be part of the same undo/redo item. You use a chained operation when the edit you require is dependent on the results of previous edits being committed and you want the combined group of edits to be linked together and treated as one item on the undo/redo stack. Any number of edit operations can be chained.

To chain an edit operation, create it using the EditOperation.CreateChainedOperation() method on the edit operation instance you want to be chained to (and not via "new"). Call CreateChainedOperation once the preceding edit operation is known to have executed successfully. Configure the chained operation as needed and then call its execute method (so both edit operations are explicitly executed). When the chained operation is executed, its undo/redo is linked with, or chained to, the Undo operation already added by the preceding edit operation.

The code snippet below illustrates this example of a chained edit linking the Create and AddAttachment methods. The first edit operation creates the feature and obtains the Object ID when the operation is executed. The second operation, AddAttachment, is chained to the first operation via var op2 = op1.CreateChainedOperation(); (instead of creating the second edit operation with var op2 = new EditOperation()). It adds the attachment using the Object ID of the newly created feature from the previous operation. Because these operations are chained, they are treated as a single operation on the undo stack (which will either apply or undo the creation and add attachment together).

      //create a feature and add an attachment as one operation.
      return QueuedTask.Run(() =>
      {
        //create an edit operation and name.
        var op1 = new EditOperation();
        op1.Name = string.Format("Create point in '{0}'", CurrentTemplate.Layer.Name);

        //create a new feature and return the oid.
        long newFeatureID = -1;
        op1.Create(CurrentTemplate, geometry, oid => newFeatureID = oid);
        // newFeatureID is populated when the operation executes
        bool result = op1.Execute();

        if (result)
        {
          //create a chained operation from the first
          var op2 = op1.CreateChainedOperation();//we do NOT use "new"!

          // add the attachment
          op2.AddAttachment(CurrentTemplate.Layer, newFeatureID, @"C:\Hydrant.jpg");
          op2.Execute();//The chained edit operation is executed
          return new Tuple<bool, string>(op2.IsSucceeded, op2.ErrorMessage);
        }

        return new Tuple<bool, string>(op1.IsSucceeded, op1.ErrorMessage);
      });

Edit Operations and Utility Network Associations

In the Utility Network topology, features are related to each other through geometric coincidence (features co-located at the same physical x, y, z coordinates) or via association mechanisms. An association is used when features to be "associated" are not necessarily geometrically coincident. One feature can have many associations (e.g., a utility pole and a transformer, a vault and a switch or fuse, and so on). When creating features and associations, it may be desirable to create the features and their associations in a single transaction. Similar to the "create feature and attachment" scenario, the challenge is the features must be created before they can be associated.

Therefore, to allow feature and association creation to be combined, associations between features can be defined using a placeholder for the Global IDs of the features "to be" created that will end up being associated. The special placeholder is an ArcGIS.Desktop.Editing.RowHandle. The RowHandle can either be a real row (in the case of an association made on existing features) or can represent a row that will be made in the future (i.e., at some point within the scope of the edit operation execute). To create a RowHandle for features to be created (and associated), use an ArcGIS.Desktop.Editing.RowToken, when constructing the association RowHandles.

The EditOperation class provides a series of CreateEx overloads that return a RowToken immediately after they are called (before EditOperation.Execute). The RowToken can be passed into the constructor of the RowHandle when creating the appropriate association. When the edit operation is executed, the RowTokens are replaced with the real rows and the association is created in conjunction with the feature creates. The creation of the features and association(s) are treated as a single undo-able operation on the Undo/Redo stack (same as chaining edit operations).

In this example, a utility pole and a transformer are created and associated within a single edit operation:

   var editOp = new EditOperation();
   editOp.Name = "Create pole + transformer bank. Attach bank to pole";

   //Create the transformer and pole
   RowToken transformerToken = editOp.CreateEx(transformerLayer, transformerAttributes);
   RowToken poleToken = editOp.CreateEx(poleLayer, poleAttributes);
   
   //Create association using the row tokens not features
   var poleAttachment = new AssociationDescription(AssociationType.Attachment, 
        new RowHandle(poleToken), new RowHandle(transformerToken));

   editOp.Create(poleAttachment);

   //Execute the EditOperation
   editOp.ExecuteAsync();

Refer to Utility Network Associations for more detail.

Edit Operation Callback

There are certain instances where you need to perform edits that span both GIS and non-GIS data and the edits (GIS and non-GIS) must be applied (or undone) within a single edit operation. For example, you create a new parcel feature in the Geodatabase and need to insert a row into one or more assessor tables, or you modify an asset in the GIS (facility, customer, utility related) and need to propagate the change to related business tables (non-GIS). In these scenarios, you will need to perform your edits within an EditOperation.Callback() method, topic 9494.

When working directly with feature classes or tables in a "callback", you are responsible for providing your own editing logic (e.g. using GeometryEngine to edit or create geometry and Rows and RowCursors for row or feature attribute updates). You cannot use edit operation methods within the context of your call back routine or lambda. The edit operation callback is executed when EditOperation.ExecuteAsync (or EditOperation.Execute) is called (not when the call back lambda is declared). The edit operation will perform the edits specified in the call back Action delegate within its ("edit operation") transactional scope.

The following restrictions apply when implementing editing logic within EditOperation Callbacks:

  • All edits must be done "by hand". No mixing in of other edit operation method calls.
  • Use non-recycling row cursors to update rows
  • You must call Store, CreateRow, etc. as needed to commit changes, create new rows, etc.
  • You are responsible for feature and feature dataset invalidation. Layer and table caches are not updated automatically when using an edit operation callback.

The following example illustrates the general implementation pattern of EditOperation call back:

   var editOp = new EditOperation();
   editOp.Name = "Do callback";
   var featLayer1 = ...
   
   editOp.Callback((context) => {
     //Do all your edits here – use non-recycling cursors
     var qf = ...;
     using (var rc = featLayer1.GetTable().Search(qf, false)) {
       while (rc.MoveNext()) {
           context.Invalidate(rc.Current);//Invalidate the row before
           //Do edits...
           rc.Current.Store(); //call store
           context.Invalidate(rc.Current);//Invalidate the row after
       }
     }
     //Edit the non_versioned_table here, etc.

     //Note: can also invalidate any datasets (instead of rows)
     //context.Invalidate(featLayer1.GetTable()); - simpler but less efficient
   }, featLayer1.GetTable(), non_versioned_table,...); //Pass as parameters the datasets 
                                              //and tables you will be editing
   editOp.Execute();

When deleting features within a callback, call invalidate before the feature delete.

  while (rc.MoveNext()) {
    context.Invalidate(rc.Current);//Invalidate the row ~before~ delete only
    rc.Current.Delete(); //delete the row
  }

Edit Operation Callback Abort

The EditOperation.IEditContext provides an Abort method that can be called within the edit operation callback to cancel the on-going transaction. After calling EditOperation.IEditContext.Abort your callback should exit.

Any undo-able edits will be rolled back, and the edit operation result from Execute will be false. The edit operation error message will have the value specified when the abort was made.

  var editOp = new EditOperation();
  editOp.Callback(Edits, dataset1, dataset2); //Nothing happens at the transaction level when this executes
  await editOperation.ExecuteAsync();   //will return false if the callback is aborted

  //The callback 'Edits'
  private void Edits(EditOperation.IEditContext context) {
     bool somethingBadHappened = false;

     ... Editing logic here ---
     
     if (somethingBadHappened)
        context.Abort("This is the ErrorMessage");
        //TODO, any clean up
        return;//exit
     }
  }

Layer Editability

Edits can be attempted on all editable layers and standalone tables, i.e. MapMembers, determined by the CanEditData property on BasicFeatureLayer, StandAloneTable and IDisplayTable. This property is a combination of the editability of the data source (read/write), and the IsEditable property of those classes which represents the check box state on the List by Editing view of the table of contents.

Feature Inspector and Working with Attributes

For updating attribute values of features, the Editor extension provides a convenient utility class called Inspector class. The basic pattern for using the inspector is:

  1. Instantiate an inspector instance (var inspector = new Inspector();)
  2. Load (the attributes) of one or more features into the inspector (inspector.Load(...) or inspector.LoadAsync(...)). This will load joined data.
  3. Use indexer-style access (via the "[" and "]" brackets) by field index or name to set attribute values.
  4. Call Inspector.ApplyAsync() or use EditOperation.Modify(inspector) to apply the attribute updates to the set of features.

The Inspector.ApplyAsync() method applies and saves the changes in one shot (there is no undo). Whereas EditOperation.Modify(inspector) will apply the changes and add an undo operation to the Pro undo/redo stack on EditOperation.Execute.

Note: All features loaded into the inspector must be from the same feature layer. Only feature values that are modified in the inspector are applied to the loaded feature(s). If tables are joined to the feature layer then the joined attributes are loaded also.

In this example, the attributes for one feature are loaded and a name attribute is modified.

  var inspector = new Inspector();
  await inspector.LoadAsync(roads, oid);
  inspector["NAME"] = "New name";

  var op = new EditOperation();
  op.Name = string.Format("Modify Road {0}",oid) ;
  op.Modify(inspector);
  await op.ExecuteAsync();

  //Or
  //await inspector.ApplyAsync(); //but no undo

Feature geometry can be updated as well as attributes simply by setting the "SHAPE" field to the new geometry value.

  var poly = .... 
  var inspector = new Inspector();
  inspector.Load(...);
  inspector["NAME"] = "New name";

  inspector.Shape = poly;
  //Or...where "SHAPE" is the name of the shape field
  //inspector["SHAPE"] = poly;

  //Elsewhere
  await inspector.ApplyAsync();

In this example the first features selected by a zipcode polygon are loaded. Their "Zipcode" attribute is modified:

var zipcode = 92373;
var zippoly = ...;
var inspector = new Inspector();

//Select all visible features that are within the polygon
//Get the first layer that has features selected...
var kvp = MapView.Active.SelectFeatures(zippoly, isWhollyWithin: true).FirstOrDefault(k => k.Value.Count > 0);
if (kvp.Key != null) {
   // load the features from the layer
   await inspector.LoadAsync(kvp.Key, kvp.Value);
   //If the layer has a field called Zipcode, set its value
   //and update all the selected features
   if (inspector.HasAttributes && inspector.Count(a => a.FieldName == "Zipcode") > 0) {
      inspector["Zipcode"] = zipcode;

      var op = new EditOperation();
      op.Name = string.Format("Updated {0} Zipcode to {1}",
                        kvp.Key.Name, zipcode);
      op.Modify(inspector);
      await op.ExecuteAsync();
    }
}

The inspector can also be used as a convenient short-cut for querying the attributes of a single feature (if a set of features has been loaded then the attribute values for the first feature in the set of object ids are loaded only). The attributes can be accessed using either the indexer-style access with field name or index and via enumeration - foreach(var attrib in inspector) - as the inspector implements a IEnumerator<Attribute> GetEnumerator() method.

Editing Subtypes

Subtype fields should be updated with the Inspector.ChangeSubtype(int code, bool propogate) method. Set the propagation parameter to true to ensure domain values on other fields are given default values to match the new subtype value. The subtype field can be referenced by name, if known, or via the SubtypeAttribute property on the inspector.

In this example a set of features for a specified subtype are all loaded into an inspector. Their subtype is changed to a new subtype:

   var status = await QueuedTask.Run(() => 
   {
        var fcdef = roads.GetFeatureClass().GetDefinition();
        //equivalent to "inspector.SubtypeAttribute.FieldName"
        var subtypeField = fcdef.GetSubtypeField();

        int code = fcdef.GetSubtypes().First(s => s.GetName().Equals("Backroad")).GetCode();
        int new_code = fcdef.GetSubtypes().First(s => s.GetName().Equals("Country road")).GetCode();

        //Select features with the current subtype code "code"
        var qf = new QueryFilter {WhereClause = string.Format("{0} = {1}", subtypeField, code)};
        var select = roads.Select(qf);

        //Load them
        var inspector = new Inspector();
        inspector.Load(roads, select.GetObjectIDs());

        //Change subtype for all features in the set
        //Note: "propogate" param is set to true.
        inspector.ChangeSubtype(new_code, true);

        var op = new EditOperation();
        op.Name = "Change Subtype";
        op.Modify(inspector);
        op.Execute();
        return new Tuple<bool, string>(op.IsSucceeded, op.ErrorMessage);
    });

Feature Templates and Table Templates

Feature templates are a central concept in the editing environment in ArcGIS Pro. Creating features using the editor relies on the use of feature templates.

Map authors create and manage feature templates with the Manage Templates pane, accessed from the Templates popup button in the lower right of the Features group on the Edit tab. The map author can modify the default attribute values and set the default construction tool used to create the new type of feature.

Every feature template is associated with a feature layer. When a layer is persisted, as a layer file or in a project, the feature templates are stored as part of the layer's definition.

Standalone tables are used to create and maintain non-spatial data in a database. Each such table can have a number of templates that facilitate the creation of records in that table. You can modify default attribute values in the table templates so that when you create a new record using those templates, corresponding default attributes are automatically filled in the new row/record. Table templates are stored in the table's definition.

Accessing Templates

The following example shows how to access feature templates and set the current template:

//Get selected layer in toc
var featLayer = MapView.Active.GetSelectedLayers().First() as FeatureLayer;

QueuedTask.Run(() =>
{
  //Get the selected template in create features pane
  var currTemplate = ArcGIS.Desktop.Editing.Templates.EditingTemplate.Current;

  //Get all templates for a layer 
  var layerTemplates = featLayer.GetTemplates();

  //Find a template on a layer by name
  var resTemplate = featLayer.GetTemplate("Residential");

  //Activate the default tool on a template and set the template as current
  resTemplate.ActivateDefaultToolAsync();
});

The following example shows how to access table templates:

//Get the standalone table
var table = MapView.Active.Map.GetStandaloneTablesAsFlattenedList().FirstOrDefault();

QueuedTask.Run(() =>
{
  //Get all templates for a table 
  var tableTemplates = table.GetTemplates();

  //Find a table template by name
  var ownerTemplate = table.GetTemplate("Owners");
});

Creating and Modifying Templates

Feature templates are automatically generated for editable layers when they are added to a map. Templates are not regenerated if the renderer changes, for example, changing from single symbol to unique value. New templates can be created for a layer or copied and altered from an existing template. Creating and modifying templates is done through the CIM classes. Since ArcGIS Pro 2.2, the CreateTemplate extension method facilitates template creation with a populated Inspector object. You can also easily assign the template name, description, tags, default tool and tool filter with the same call. Table templates are also created and modified the same way through CIM classes. Pro 2.9 introduces the CreateTemplate extension method for table templates that helps you create templates with default attribute values assigned to it.

The following example creates a new template for a layer from an existing template using the CIM:

//Get parcels layer
var featLayer = MapView.Active.Map.FindLayers("Parcels").First();

QueuedTask.Run(() =>
{
  //Find a template on a layer by name
  var resTemplate = featLayer.GetTemplate("Residential");

  //Get CIM layer definition
  var layerDef = featLayer.GetDefinition() as CIMFeatureLayer;
  //Get all templates on this layer
  var layerTemplates = layerDef.FeatureTemplates.ToList();
  //Copy template to new temporary one
  var resTempDef = resTemplate.GetDefinition() as CIMFeatureTemplate;
  //Could also create a new one here
  //var newTemplate = new CIMFeatureTemplate();

  //Set template values
  resTempDef.Name = "Residential copy";
  resTempDef.Description = "This is the description for the copied template";
  resTempDef.WriteTags(new[] { "Testertag" });
  resTempDef.DefaultValues = new Dictionary<string, object>();
  resTempDef.DefaultValues.Add("YEARBUILT", "1999");

  //Add the new template to the layer template list
  layerTemplates.Add(resTempDef);
  //Set the layer definition templates from the list
  layerDef.FeatureTemplates = layerTemplates.ToArray();
  //Finally set the layer definition
  featLayer.SetDefinition(layerDef);
});

The following example creates a new template for a table from an existing template using the CIM:

//Get owners table
var authoritiesTable = MapView.Active.Map.FindStandaloneTables("Authorized persons").First();

QueuedTask.Run(() =>
{
  //Find a table template by name
  var ownerTemplate = authoritiesTable.GetTemplate("Owners");

  //Get CIM table definition
  var tableDef = authoritiesTable.GetDefinition();
  //Get all templates on this table
  var tableTemplates = tableDef.RowTemplates.ToList();
  //Copy template to new temporary one
  var tableTempDef = ownerTemplate.GetDefinition();

  //Set template values
  tableTempDef.Name = "Owners template copy";
  tableTempDef.Description = "New table template description";
  tableTempDef.WriteTags(new[] { "Tag1" });
  tableTempDef.DefaultValues = new Dictionary<string, object>();
  tableTempDef.DefaultValues.Add("Authorization Level", "1");

  //Add the new template to the table template list
  tableTemplates.Add(tableTempDef);
  //Set the table definition templates from the list
  tableDef.RowTemplates = tableTemplates.ToArray();
  //Finally set the table definition
  authoritiesTable.SetDefinition(tableDef);
});

This example creates a new feature template using the extension method:

   // Must be executed on the MCT - wrap in a QueuedTask.Run

   // Load the schema
   insp = new Inspector();
   insp.LoadSchema(layer);

   // Set up the fields 
   insp["FieldOne"] = value1;
   insp["FieldTwo"] = value2;

   // Set up tags
   var tags = new[] { "tag1", "tag2" };

   // Set up default tool - use daml-id rather than guid
   string defaultTool = "esri_editing_SketchCircleLineTool";

   // Create a new CIM template  - new extension method
   var newTemplate = layer.CreateTemplate("My new template", "sample description", insp, defaultTool, tags);

   // You can create new table template using the new extension method the same way
   //var newTableTemplate = table.CreateTemplate("New table template", "description", insp, tags: tags);

Removing Templates

Templates can be removed from a layer or table by removing them from the list of templates on the corresponding layer or table definition. The definition is then set back on the layer/table.

The following example removes templates from a layer that matches a pattern:

QueuedTask.Run(() =>
{
  //Get parcels layer
  var featLayer = MapView.Active.Map.FindLayers("Parcels").First();
  //Get CIM layer definition
  var layerDef = featLayer.GetDefinition() as CIMFeatureLayer;
  //Get all templates on this layer
  var layerTemplates = layerDef.FeatureTemplates.ToList();

  //Remove templates matching a pattern
  layerTemplates.RemoveAll(t => t.Description.Contains("Commercial"));

  //Set the templates and layer definition back on the layer
  layerDef.FeatureTemplates = layerTemplates.ToArray();
  featLayer.SetDefinition(layerDef);
});

Active Template Changed Event

Add-ins can subscribe to the ArcGIS.Desktop.Editing.Events.ActiveTemplateChangedEvent to be notified when the active template is activated or deactivated.

The ActiveTemplateChangedEventArgs event argument contains the incomming template and view (the template being activated and which view it is in), and the outgoing template and view (the template being deactivated and the view it is in). Note that the incoming and outgoing templates may be null when changing views that do not support templates such as the catalog, model builder and layout views.

The following example activates a specific tool on a template whenever that template is activated:

//Template Activation Event
ArcGIS.Desktop.Editing.Events.ActiveTemplateChangedEvent.Subscribe(OnActiveTemplateChanged);

private async void OnActiveTemplateChanged(ActiveTemplateChangedEventArgs obj)
{
  //return if incoming template is null
  if (obj.IncomingTemplate == null)
    return;

  //Activate two-point line tool for Freeway template in the Layers map
  if (obj.IncomingTemplate.Name == "Freeway" && obj.IncomingMapView.Map.Name == "Layers")
    await obj.IncomingTemplate.ActivateToolAsync("esri_editing_SketchTwoPointLineTool");
}

Annotation Feature Editing

Annotation features differ from other geodatabase features in a few small but fundamental ways. It is important to keep these in mind when developing custom annotation editing tools. See the Annotation Editing Concepts document for specifics about editing annotation.

Dimension Feature Editing

Dimension features are also a little different from standard geodatabase features. As with annotation editing it is important to keep these differences in mind when developing custom dimension editing tools. See the Dimension Editing Concepts document for specifics about editing dimensions.

Snapping

On the UI, you can toggle snapping, set the application snap modes (point, edge, vertex, and so on) and other environment options such as snapping tolerance through the snapping drop-down menu and settings dialog from the Snapping group on the Edit tab. Additionally, you can control what layers to snap to through the List by Snapping view in the Contents pane. The snapping API reflects these UI options.

The following example enables snapping on the UI and sets some snapping environment options:

//Using ArcGIS.Desktop.Mapping
//enable snapping
Snapping.IsEnabled = true;

//Enable a snap mode, others are not changed.
Snapping.SetSnapMode(SnapMode.Point,true);
      
//Set multiple snap modes exclusively. All others will be disabled.
Snapping.SetSnapModes(SnapMode.Edge, SnapMode.Point);
      
//Set snapping options via get/set options
var snapOptions = Snapping.GetOptions(myMap);
snapOptions.SnapToSketchEnabled = true;
snapOptions.XYTolerance = 100;
snapOptions.ZToleranceEnabled = true;
snapOptions.ZTolerance = 0.6;
Snapping.SetOptions(myMap,snapOptions);

The following example shows setting some general snapping options and snapping availability for all polyline layers in the current map:

 internal static async Task ConfigureSnappingAsync() {
     //General Snapping
     Snapping.IsEnabled = true;
     Snapping.SetSnapMode(SnapMode.Edge, true);
     Snapping.SetSnapMode(SnapMode.End, true);
     Snapping.SetSnapMode(SnapMode.Intersection, true);

     //Snapping on any line Feature Layers that are not currently snappable
     var flayers = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().Where(
                l => l.ShapeType == esriGeometryType.esriGeometryPolyline && !l.IsSnappable).ToList();

     if (flayers.Count() > 0) {
         await QueuedTask.Run(() => {
            foreach (var fl in flayers) {

               // use an extension method
               //  (must be called inside QueuedTask)
               fl.SetSnappable(true);

               // or use GetDefinition, SetDefinition to access the CIM directly
               //   (must be called inside QueuedTask)
               //var layerDef = fl.GetDefinition() as CIMGeoFeatureLayerBase;
               //layerDef.Snappable = true;
               //fl.SetDefinition(layerDef);
            }
         });
     }
  }

Layer Snap Modes

Finer snapping control can be achieved using layer snap modes. For example, to only snap to the vertices of a layer of interest and not the vertices of others, the application vertex Snap Mode and the layer of interest vertex Snap Mode should be set True while the other layer’s vertex Snap Modes would be set False. Note that toggling an individual layer’s snap mode will only have an effect if the corresponding application Snap Mode is True.

Snap modes can be set for each layer via the Snapping.SetLayerSnapModes methods while the current state of a layer’s snap modes is obtained via the Snapping.GetLayerSnapModes method.

There is no UI in ArcGIS Pro for viewing layer snap modes and these settings do not persist.

The following examples illustrates the layer snap modes.

  // configure by layer

  // find the state of the snapModes for the layer
  var lsm = ArcGIS.Desktop.Mapping.Snapping.GetLayerSnapModes(layer);
  bool vertexOn = lsm.Vertex;
  bool edgeOn = lsm.Edge;
  bool endOn = lsm.End;

  // or use 
  vertexOn = lsm.GetSnapMode(SnapMode.Vertex);
  edgeOn = lsm.GetSnapMode(SnapMode.Edge);
  endOn = lsm.GetSnapMode(SnapMode.End);


  // update a few snapModes 
  //   turn Vertex off
  lsm.SetSnapMode(SnapMode.Vertex, false);
  // intersections on
  lsm.SetSnapMode(SnapMode.Intersection, true);

  // and set back to the layer
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(layer, lsm);


  // assign a single snap mode at once
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(layer, SnapMode.Vertex, false);


  // turn ALL snapModes on
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(layer, true);
  // turn ALL snapModes off
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(layer, false);
  // configure for a set of layers

  // set Vertex, edge, end on for a set of layers, other snapModes false
  var vee = new LayerSnapModes(false);
  vee.Vertex = true;
  vee.Edge = true;
  vee.End = true;
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(layerList, vee);


  // turn intersection snapping on without changing any of the other snapModes
  var dictLSM = ArcGIS.Desktop.Mapping.Snapping.GetLayerSnapModes(layerList);
  foreach (var layer in dictLSM.Keys)
  {
    var lsm = dictLSM[layer];
    lsm.Intersection = true;
  }
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(dictLSM);


  // set all snapModes off for a list of layers
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(layerList, false);

Here is a combined example illustrating how to snap to only the vertices of a particular layer. All other layer snapmodes are turned off.

  // snapping must be on
  ArcGIS.Desktop.Mapping.Snapping.IsEnabled = true;

  // turn all application snapModes off
  ArcGIS.Desktop.Mapping.Snapping.SetSnapModes();

  // set application snapMode vertex on 
  ArcGIS.Desktop.Mapping.Snapping.SetSnapMode(SnapMode.Vertex, true);

  // ensure layer snapping is on
  await QueuedTask.Run(() =>
  {
    fLayer.SetSnappable(true);
  });

  // set vertex snapping only
  var vertexOnly = new LayerSnapModes(false);
  vertexOnly.Vertex = true;

  // set vertex only for the specific layer, clearing all others
  var dict = new Dictionary<Layer, LayerSnapModes>();
  dict.Add(fLayer, vertexOnly);
  ArcGIS.Desktop.Mapping.Snapping.SetLayerSnapModes(dict, true);  // true = reset other layers

Snapping in Tools

To use the snapping environment in a custom MapTool, set the tool's UseSnapping property to true (usually in the constructor). The snapping environment can be changed at any time during the sketch as required.

internal class MyCustomTool1 : MapTool {

        public MyCustomTool1() {
            IsSketchTool = true;
            UseSnapping = true; //Use snapping
            SketchType = SketchGeometryType.Line;

Snap Results

You can determine what features the sketch has snapped to in your custom tools through the map tool property SnappingResults. This returns a readonly list of SnapResult for each vertex in the current sketch. For those vertices that have snapped to a feature or the grid, SnapResult contains the layer, feature OID, SnapType (vertex, edge, end etc) and the snap location. For those vertices that have not snapped in the sketch, SnapResult contains null for the layer, -1 for the feature OID and the SnapType type is None. If the sketch has snapped to the editing grid, SnapResult contains null for the layer, the SnapType is Grid, the OID denotes the grid feature: 0 for a grid line, 1 for a grid intersection and 2 for grid inference.

protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  //assume a custom point map tool
  //get the feature the point sketch snapped to if any
  var snapResultPoint = SnappingResults[0];
  if (snapResultPoint.Layer != null)
  {
    var snappedLayer = snapResultPoint.Layer;
    var snappedOID = snapResultPoint.OID;
    var snapType = snapResultPoint.SnapType;

    if (snaptype == SnapType.Grid)
    {
       // we snapped to the grid 
       var gridLine = (snappedOID == 0);
       var gridIntersection = (snappedOID == 1);
       var gridInference = (snappedOID == 2);
    }
    else if (snapType != SnapType.None)
    {
       // we snapped to a feature  (snappedLayer, snappedOID)
    }
  }

Edit Operations and Long and Short Transactions

By way of recap, long transactions support an underlying edit session whereas short transactions do not*. An edit session supports save, discard, undo, and redo. Both long and short transactions do support canceling edits in-progress (though to varying degrees depending on the underlying workspace type). By default, any given edit operation allows datasets with short and long transaction semantics to be mixed within the same execute of an edit operation. The transaction semantics of an EditOperation can be inspected at runtime via its EditOperation.EditOperationType property, topic 9568. It has three possible values:

  • Null. This is the default. Mixing short and long semantics is ok.
  • Long. No mixing. All datasets in the edit operation must support long transaction semantics
  • Short. No mixing. All datasets in the edit operation must support short transaction semantics

Refer to the EditOperationType enum.

To restrict an edit operation to just datasets that are long or short, then ~set~ the EditOperation.EditOperationType to EditOperationType.Long or EditOperationType.Short respectively. If the EditOperation.EditOperationType is set then adding a dataset to the edit operation with the wrong transation semantic will cause the edit operation execute to fail.

Mixing datasets with long and short transaction semantics (the default behavior) can lead to some inconsistencies with the Pro UI experience for Undo/Redo and Save/Discard. Undo/Redo and Save/Discard only apply to workspaces with edit sessions (long transactions). So if edits were mixed, only the long transactions (contained in an edit session) can be saved, discarded, undone, or redone. If a user clicks "Undo" or "Save" on the Pro UI, for example, any "Direct" edits (short transactions) will be unaffected. To undo a direct edit another edit must be performed to reverse it. Similar for a redo. Direct edits do not have an edit session and are immediately committed on completion of the edit operation execute.

If you are mixing versioned and non-versioned data from the same datastore (e.g. some datasets are registered as versioned in the datastore whereas some datasets are not) then all edits against the versioned data (in that datastore) must be saved or discarded before executing any operations against non-versioned data (from that same datastore) unless the non-versioned datasets are edited first. Said another way, when mixing versioned and non-versioned data from the same datastore, the non-versioned data must always be edited first. Attempting to execute edit operations against non-versioned data when there are already pending edits for versioned data in the same datastore will throw an exception.

Note: In ArcGIS Pro 2.5 non-versioned feature services support an edit session with undo and redo capabilities. However, the EditOperationType still requires Short (rather than Long).

EditOperationType

Typically, in most editing scenarios, the application developer already knows upfront the schemas and characteristics of his/her TOC content (that the given application is editing). However, if the EditOperationType does need to be discovered at runtime, it can be determined by analyzing the underlying ArcGIS.Core.Data.GeodatabaseType, topic 6821, and ArcGIS.Core.Data.RegistrationType, topic 6822 of the layer or standalone table's dataset (when dealing with branch versioned data, differentiating between the default and a named version is also required). Consult the DatasetCompatibility sample for an example implementation.

Use the following tables to reference the participating EditOperationType for a given dataset and its characteristics with respect to Cancel Edit, Undo/Redo, Save/Discard:

EditOperationType by dataset at ArcGIS Pro 2.4 and earlier

Dataset GeodatabaseType RegistrationType Version EditOperationType
Shape file FileSystem N/A N/A LONG
File GDB LocalDatabase N/A N/A LONG
Enterprise (Versioned) RemoteDatabase Versioned N/A LONG
Enterprise (Direct) RemoteDatabase NonVersioned N/A SHORT
Feature Service (Hosted) Service NonVersioned N/A SHORT
Feature Service (Standard) Service NonVersioned N/A SHORT
Feature Service (Branch) Service Versioned Default SHORT
Feature Service (Branch) Service Versioned Named LONG

EditOperationType by dataset at ArcGIS Pro 2.5+

Dataset GeodatabaseType RegistrationType Version EditOperationType
Shape file FileSystem N/A N/A LONG
File GDB LocalDatabase N/A N/A LONG
Mobile (.geodatabase) LocalDatabase N/A N/A LONG
Memory GDB Memory N/A N/A LONG
Enterprise (Versioned) RemoteDatabase Versioned N/A LONG
Enterprise (Direct) RemoteDatabase NonVersioned N/A SHORT
Feature Service (Hosted)* Service NonVersioned N/A LONG
Feature Service (Standard)* Service NonVersioned N/A LONG
Feature Service (Branch) Service Versioned Default SHORT
Feature Service (Branch) Service Versioned Named LONG

*In ArcGIS Pro 2.5 non-versioned feature services do support an edit session with undo and redo capabilities. However, the EditOperationType requires SHORT (rather than LONG). This is a known issue.

Characteristics by dataset by participating EditOperationType at ArGIS Pro 2.4 and earlier

Dataset Cancel Edit Undo/Redo Save/Discard EditOperationType
Shape file Yes Yes Yes LONG
File GDB Yes Yes Yes LONG
Enterprise (Versioned) Yes Yes Yes LONG
Enterprise (Direct) Yes No No SHORT
Feature Service (Hosted) Yes* No No SHORT
Feature Service (Standard) Yes* No No SHORT
Feature Service (Branch, Default) Yes No No SHORT
Feature Service (Branch, Named) Yes Yes Yes LONG

*Prior to 2.3, there is no Cancel Edit for feature creates. At 2.3, cancelling a create on a hosted or standard feature service generates another edit event (essentially, the "cancel" performs a delete).

Characteristics by dataset by participating EditOperationType at ArGIS Pro 2.5+

Dataset Cancel Edit Undo/Redo Save/Discard EditOperationType
Shape file Yes Yes Yes LONG
File GDB Yes Yes Yes LONG
Mobile (.geodatabase) Yes Yes Yes LONG
Memory GDB Yes Yes Yes LONG
Enterprise (Versioned) Yes Yes Yes LONG
Enterprise (Direct) Yes No No SHORT
Feature Service (Hosted)* Yes Yes Yes LONG*
Feature Service (Standard)* Yes Yes Yes LONG*
Feature Service (Branch, Default) Yes No No SHORT
Feature Service (Branch, Named) Yes Yes Yes LONG

*In ArcGIS Pro 2.5 non-versioned feature services do support an edit session with undo and redo capabilities. However, the EditOperationType still requires SHORT (rather than LONG). This is a known issue.

Notes
Memory GDB refers to a memory geodatabase created with a MemoryConectionProperties via the Geodatabase class or the keyword "memory" via GP tools.

For SQLite, if the extension is “.sqlite” or “.gpkg” (geopackage), then the SQLite instance will exhibit pure database behavior, i.e., it doesn’t support geodatabase functionality. SQLite connections are created via the SQLiteConnectionPath as the connector to a Database constructor. Because the Database class is not derived from Geodatabase, it does not support a GetGeodatabaseType() method.

PluginDatastore is a new type introduced at Pro 2.3. Plugin data stores are read-only. They do not have a corresponding GeodatabaseType or EditOperationType.

Edit Sessions

Unlike in ArcObjects where an edit session is explicitly started on the underlying datastore, in Pro, the Editor manages all active edit sessions for you. Edit sessions are maintained for all datasets with LONG EditOperationType semantics (see above). The first edit performed (on a given dataset) will start an edit session (on that datastore). The edit session will be maintained until the edits are saved or discarded. Once an edit session is closed, the OperationManager's Undo/Redo stack is cleared.

For datasets with SHORT EditOperationType semantics, each edit operation is treated as an individual session. If the first edit operation is performed on non-versioned or non-undoable data, the edit proceeds and is committed on success. The edit will not be placed on the undo stack and calling Save or Discard is, essentially, a no-op.

Save and Discard Edits

For those datasets participating in an edit session, edits can be saved or discarded using methods available on the Project instance object. These methods are awaitable and do not block the UI:

Edits can be undone or redone via the OperationManager class. To access the operation manager containing your edits, use the Map.OperationManager property on the map (or scene) that contains the data you are editing. For example:

   //Undo all edits on the current map
   var operationMgr = MapView.Active.Map.OperationManager;
   while (operationMgr.CanUndo)
      await operationMgr.UndoAsync();

   //Redo the last edit 
   if (operationMgr.CanRedo)
      await operationMgr.RedoAsync(); 

MapViews with the same underlying map or scene always share the operation manager. Access MapView.Active.Map.OperationManager to get the operation manager instance associated with the currently active view. This is important if your edit code is executing as part of a MapTool or other component that always works on data in whichever map or scene is active.

Checking for Unsaved Edits

Project.Current.HasEdits specifies if the Project has unsaved edits. If the project has more than one datasource, HasEdits will be true if there are edits to data in any of the datasources (participating in an edit session). The datastores that have pending edits can be retrieved from the current project via Project.Current.EditedDatastores and, additionally, any datastore can be queried individually to determine if it has pending edits via its HasEdits property. Datastores with SHORT semantics will always return false (for HasEdits) because their edits are never pending and are saved immediately after each edit has completed. For file geodatabase datastores, within the first edit operation, the HasEdits method will return false until the edit operation is complete.

Edit Events

The editing assembly provides events to listen for changes to objects at row level and for the progress and execution of an edit operation. The specific events in the ArcGIS.Desktop.Editing.Events namespace are as follows:

EditStartedEvent

Add-ins can subscribe to the ArcGIS.Desktop.Editing.Events.EditStartedEvent to be notified when any EditOperation is starting to execute. The EditStartedEventArgs event argument passes the currently executing EditOperation. This event can not be canceled.

Additional EditOperation's should not be created within this event, nor should the passed EditOperation be executed.

EditCompletingEvent

Add-ins can subscribe to the ArcGIS.Desktop.Editing.Events.EditCompletingEvent to be notified when any EditOperation is completing execution. The EditCompletingEventArgs event argument passes the currently executing EditOperation for which additional edits can be queued for execution. This event, and by association the EditOperation, can be canceled via the EditCompletingEventArgs.CancelEdit method.

Additional EditOperation's should not be created within this event, nor should the passed EditOperation be executed.

EditCompletedEvent

Add-ins can subscribe to the ArcGIS.Desktop.Editing.Events.EditCompletedEvent to be notified when:

  1. Any create, modify, and delete has completed (e.g. after an editOperation.Execute)
  2. Any Undo or Redo has completed.
  3. Any Save or Discard has completed (and, hence, closing any current edit sessions)
  4. Any Reconcile or Post has completed.
  5. Any Change Version has completed.

In the case where features are created, modified, or deleted (whether directly or indirectly via an undo or redo), the EditCompletedEventArgs event argument contains lists of all creates, modified, and deletes by object id per MapMember (layer or standalone table). Note: by default the creates, modifies, and deletes are null. There is no cancelation possible via the EditCompletedEvent.

EditCompletedEvent is a global event. It fires after any edit has completed on any dataset in any map within the current project. Note: EditCompletedEvent only fires once after the conclusion of all edits included within the edit operation execution or undo/redo.

  private SubscriptionToken _token;

  //elsewhere, register for the event
  _token = EditCompletedEvent.Subscribe(OnEditComplete);

  //unregister calling unsubscribe
  if (_token != null) {
    EditCompletedEvent.Subscribe(_token);
    _token = null;
  }

  //Event handler
  protected Task OnEditComplete(EditCompletedEventArgs args) {
   
     //Creates, Modifies, Deletes are all 'IReadOnlyDictionary<MapMember, IReadOnlyCollection<long>>'
     //Check for null
     if (args.Creates != null) {
       foreach (var kvp in args.Creates) {
         MapMember layerOrTable = kvp.Key;
         IReadOnlyCollection<long> oids = kvp.Value;

         // TODO, handle creates 
       }
     }
     ... etc ...
     return Task.FromResult(0);
  }
        
  //Elsewhere, perform an edit
  editOp.Create(this.CurrentTemplate, geometry);
  // Execute the operation
  editOp.ExecuteAsync(); //EditCompletedEvent fires when
                         //ExecuteAsync has finished

Row Events

In ArcGIS Pro, the row events are public, but the subscription is on a per-dataset basis. To listen to RowCreatedEvent, RowChangedEvent, and RowDeletedEvent, you subscribe to a public event by calling the static Subscribe on the event and passing in the Table or FeatureClass for which you want to receive row level events. Use the row events to:

  • Make changes to the row being edited (changes become part of the ongoing transaction)
  • Validate row attributes that have been changed
  • Cancel row transactions* (e.g. if they fail validation)
  • Add custom logging/audit trail functionality

*Prior to 2.3, feature creates on Hosted and Standard Feature Services cannot be cancelled.
*Short transaction edits already committed within the ongoing transaction will not be undone.

The row events are published during the execution of the edit operation (whereas the EditCompletedEvent is published after the entire edit operation has completed). For every individual create or edit, for every dataset for which you have registered, a corresponding RowEvent is fired. For example, if you create 3 features in an edit operation and you are registered for RowCreate events for the corresponding dataset(s), then you will receive 3 RowCreatedEvent callbacks - one for each create. Similarly for modifies or deletes. Each individual modify and delete results in a RowEvent being fired per edit per feature.

Note that RowEvent callbacks are always called on the QueuedTask so there is no need to wrap your code within a QueuedTask.Run lambda.

The following snippet subscribes to the RowCreatedEvent, RowChangedEvent for the 'Addresses' layer and the RowCreatedEvent for the 'Buildings' layer

  var map = MapView.Active.Map;
  var addrLayer = map.GetLayersAsFlattenedList().FirstOrDefault(l => l.Name == "Addresses") as FeatureLayer;
  if (addrLayer == null)
    return;
  var bldgLayer = map.GetLayersAsFlattenedList().FirstOrDefault(l => l.Name == "Buildings") as FeatureLayer;
  if (bldgLayer == null)
    return;

  QueuedTask.Run(() =>
  {  
    ArcGIS.Desktop.Editing.Events.RowCreatedEvent.Subscribe(RowCreated, addrLayer.GetTable());
    ArcGIS.Desktop.Editing.Events.RowChangedEvent.Subscribe(RowChanged, addrLayer.GetTable());

    ArcGIS.Desktop.Editing.Events.RowCreatedEvent.Subscribe(RowCreated, bldgLayer.GetTable());
  });

A RowChangedEventArgs is passed in as an argument to your RowEvent handler. There are two important properties on the RowChangedEventArgs object; the Row property that is the row being created, modified, or deleted, and the Operation property which is the EditOperation object that is currently being executed. You can query the Row using Row.HasValueChanged to determine which values have changed.

Because the events are published during the execution of the edit operation, you can cancel the ongoing transaction by calling the RowChangedEventArgs CancelEdit method. Cancelling within a row event will cancel the entire transaction (and edit operation Execute will return false) rolling back all undo-able edits to that point.

In the following example, any changes to the "POLICE_DISTRICT" field in "crimes_fc" are validated. If a "POLICE_DISTRICT" value fails validation, the edit operation is cancelled.

  RowChangedEvent.Subscribe((rc) => {
    //Validate any change to “police district”
    if (rc.Row.HasValueChanged(rc.Row.FindField("POLICE_DISTRICT"))) {
       if (FailsValidation(rc.Row["POLICE_DISTRICT"])) {
          //Cancel edits with invalid “police district” values
          rc.CancelEdit($"Police district {rc.Row["POLICE_DISTRICT"]} is invalid");
       }
     }
   }, crimes_fc);

In addition to validating field values in modified or created features, you may wish to make additional edits by creating new features or modifying other rows in your RowEvent callbacks. Do not use a new edit operation object.

The following snippets show how to make further modifications to an edited row and add a record to an audit table after subscribing to the RowChangedEvent.

The first uses the ArcGIS.Core.Data API to make additional edits and is the required pattern if you are working with ArcGIS prior to version 2.4.

  private Guid _currentRowChangedGuid = Guid.Empty;
  private void RowChanged(RowChangedEventArgs args)
  {
    try
    {
      // prevent re-entrance 
      if (_currentRowChangedGuid == args.Guid)
        return;

      var row = args.Row;
      row["Description"] = description;

      // calling store will also result in a RowChanged event if any
      // attribute columns have changed. 
      // I need to keep track of args.Guid to prevent re-entrance and a 
      //    recursive situation (see check above)
      _currentRowChangedGuid = args.Guid;
      row.Store();

      // now add a record to the history table
      //    use the geodatabase API
      if (history != null)
      {
        using (var table = row.GetTable())
        using (var hist_table = history.GetTable())
        {
          using (var rowBuff = hist_table.CreateRowBuffer())
          {
            rowBuff["TABLENAME"] = table.GetName();
            rowBuff["DESCRIPTION"] = description;
            rowBuff["WHO"] = Environment.UserName;
            rowBuff["WHAT"] = args.EditType.ToString();
            hist_table.CreateRow(rowBuff);
          }
        }
      }

      // clear the cached row guid
      _currentRowChangedGuid = Guid.Empty;
    }
  }

At 2.4 or later, you can make use of the running EditOperation passed via RowChangedEventArgs.Operation. Any additional edits are included in the current operation and appear as one entry in the undo/redo stack.

  private Guid _currentRowChangedGuid = Guid.Empty;
  private void RowChanged(RowChangedEventArgs args)
  {
    try
    {
      // prevent re-entrance 
      if (_currentRowChangedGuid == args.Guid)
        return;

      var row = args.Row;
      row["Description"] = description;

      // calling store will also result in a RowChanged event if any
      // attribute columns have changed. 
      // I need to keep track of args.Guid to prevent re-entrance and a 
      //    recursive situation (see check above)
      _currentRowChangedGuid = args.Guid;
      row.Store();

      // now add a record to the history table
      //   'history' is in my map and has been retrieved separately
      //    use the running EditOperation to add the record
      if (history != null)
      {
         // get the current EditOperation
         var parentEditOp = obj.Operation;

         // build attributes
         using (var table = row.GetTable())
         {
           var atts = new Dictionary<string, object>();         
           atts.Add("TABLENAME", table.GetName());
           atts.Add("DESCRIPTION", description);
           atts.Add("WHO", Environment.UserName);
           atts.Add("WHAT", args.EditType.ToString());

           parentEditOp.Create(history, atts);
         }
      }

      // clear the cached row guid
      _currentRowChangedGuid = Guid.Empty;
    }
  }

Note if the history table does not exist in your current map, then you will need to use a combination of the ArcGIS.Core.Data API and the running EditOperation to accomplish the logging.

  private Guid _currentRowChangedGuid = Guid.Empty;
  private void RowChanged(RowChangedEventArgs args)
  {
    try
    {
      // prevent re-entrance 
      if (_currentRowChangedGuid == args.Guid)
        return;

      var row = args.Row;
      row["Description"] = description;

      // calling store will also result in a RowChanged event if any
      // attribute columns have changed. 
      // I need to keep track of args.Guid to prevent re-entrance and a 
      //    recursive situation (see check above)
      _currentRowChangedGuid = args.Guid;
      row.Store();

      // now add a record to the history table
      //  use the geodatabase API to retrieve the 'historyTable'
      //  use the running EditOperation to add the record
      using (var geodatabase = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(@"D:\Data\MyHistoryGDB.gdb"))))
      {
        using (var historyTable = geodatabase.OpenDataset<Table>("History"))
        {
           // get the current EditOperation
           var parentEditOp = obj.Operation;

           // build attributes
           using (var table = row.GetTable())
           {
             var atts = new Dictionary<string, object>();         
             atts.Add("TABLENAME", table.GetName());
             atts.Add("DESCRIPTION", description);
             atts.Add("WHO", Environment.UserName);
             atts.Add("WHAT", args.EditType.ToString());

             parentEditOp.Create(historyTable, atts);
           }
        }
      }

      // clear the cached row guid
      _currentRowChangedGuid = Guid.Empty;
    }
  }

Here's another example of how to use the current operation within the row events. In this scenario we wish to add an attachment to a newly created feature. Previously we discussed this example as requiring you to chain edit operations together in order to have the two edits participate in a single undo/redo action; because the AddAttachment method requires the feature to already be created. The RowChangedEventArgs.Operation property provides us with an alternative implementation if we use the RowCreated event.

  private void OnRowCreatedEvent(RowChangedEventArgs obj)
  {
    // get the row and the current operation
    var row = args.Row;
    var parentEditOp = obj.Operation;

    // retrieve the row objectid
    var sOid = row["objectid"].ToString();
    if (long.TryParse(sOid, out long oid))
      // add the attachment
      parentEditOp.AddAttachment(layer, oid, @"C:\Temp\Painting.jpg");
  }

There are a couple of important things to keep in mind when using the row events.

Firstly, any modifications performed within the RowEvent handlers can also cause cascaded events to be generated. This can occur if you edit rows or features other than the row participating in the current row event (passed to you via the RowChangedEventArg.Row property). A RowChangedEvent will be raised immediately whenever Row.Store() or Feature.Store() is executed (the same is also true for RowCreatedEvent and RowDeletedEvent in response to a row create or delete). Make sure you have an exit condition to avoid infinite recursion. For example, if you are changing a feature attribute within a RowCreatedEvent or RowChangedEvent, on a different row then consider tracking that row/feature's object id to ignore the corresponding RowChangedEvent your attribute change will generate.

Additionally, do not dispose the row obtained from the RowChangedEventArgs. The same row can be used for calling more than one event handler.

Finally with the new RowChangedEventArgs.Operation property, note that you cannot chain or execute the running operation within the events. Attempting to do so will cause exceptions to be thrown.

Sketch Events

As of 2.7, the SDK supports several events for notifications of edit sketch changes, including:

Note: These events and event argument classes live in the ArcGIS.Desktop.Mapping.Events namespace (within the ArcGIS.Desktop.Editing.dll). The following edit tools fire these sketch events:

  • All construction tools (except for annotation and dimensions)
  • Align Features
  • Reshape and Edit Vertices.
  • 3rd party (custom) tools*

Note that these events always fire on the MCT so running subscription code within a QueuedTask.Run delegate is not necessary. These events are provided to allow custom logic, that is not associated with a (sketch|edit) tool, to receive notifications of changes to the sketch geometry (custom sketch and construction tools should continue to use the built in method overloads to handle sketch changes rather than subscribing to these events - i.e. override OnSketchModifiedAsync, OnSketchCancelledAsync, and OnSketchCompleteAsync in the usual way).

*Custom tools that want to publish sketch events should opt-in by setting their FireSketchEvents property to true. The default is false (no sketch events published). Custom tools can change their FireSketchEvents property at any time during their lifecycle, not just when the tool is instantiated. The FireSketchEvents setting has no effect on the sketch method overrides. Custom tools always receive callbacks on the sketch method overrides when they are the active tool.

SketchModifiedEvent

The most commonly used event is the SketchModifiedEvent. This event fires whenever the geometry of the sketch is changed such as when adding, removing or moving a vertex. You may want to listen to this event when performing validation logic (on the sketch geometry). SketchModifiedEventArgs provides subscribers with the MapView, the previous and current sketch, and the SketchOperationType which can be used to determine the nature of the sketch modification. The Boolean IsUndo property indicates if this event occurred as a result of an undo action. Note that in a sketch undo operation, IsUndo = true, and SketchOperationType will report the type of sketch operation that was undone.

 ArcGIS.Desktop.Mapping.Events.SketchModifiedEvent.Subscribe((args) => {
   if (args.IsUndo)//not interested in undos
     return;
   if (args?.CurrentSketch?.IsEmpty ?? true)//not interested in null or empty sketch
     return;
   //interested in polylines only
   //Examine the current (last) vertex in the line sketch
   if (args.CurrentSketch is Polyline polyLine) {
     var lastSketchPoint = polyLine.Points.Last();
     //TODO use lastSketchPoint
     ...
   }
 });

SketchCancelledEvent

The SketchCancelledEvent fires whenever the sketch is cancelled. The accompanying SketchCancelledEventArgs contains the map view the sketch cancellation originated on.

 ArcGIS.Desktop.Mapping.Events.SketchCancelledEvent.Subscribe((args) => {
    var mapView = args.MapView;
    //TODO, handle sketch cancelled
    ...
 });

BeforeSketchCompletedEvent

The BeforeSketchCompletedEvent fires when a sketch is finished but before the active tool receives the completed sketch via its OnSketchCompleteAsync overload and before the SketchCompletedEvent fires. Subscribers to BeforeSketchCompletedEvent, therefore, have an opportunity to modify the sketch prior to the active tool consuming it (e.g. within an edit or feature creation). A typical workflow might be for logic external to the tool ensuring that the Z or M values on the sketch are correctly set prior to it being passed to (the active tool and subsequent) edit operations. The completed sketch is passed in to subscribers via the BeforeSketchCompletedEventArgs.Sketch property and can be changed by setting the altered geometry back via BeforeSketchCompletedEventArgs.SetSketchGeometry(updated_sketch). Attempts to pass a different sketch geometry type to BeforeSketchCompletedEventArgs.SetSketchGeometry than the current SketchType (eg swap a polygon for a polyline or vice versa) will be ignored*.

If there are multiple subscribers to the event, each subscriber in turn, gets the latest modified sketch geometry consisting of all edits made to the sketch (via SetSketchGeometry calls) thus far. Once the last subscriber is complete, the active tool receives the sketch, comprising any changes made by BeforeSketchCompletedEvent subscribers, via OnSketchCompleteAsync as usual.

    ArcGIS.Desktop.Mapping.Events.BeforeSketchCompletedEvent.Subscribe((args) => {
      //Sketch can be null depending on preceding BeforeSketchCompletedEvent subscribers
      if (args.Sketch?.IsEmpty ?? true)
          return;
      //assign Zs from default surface
      var result = args.MapView.Map.GetZsFromSurfaceAsync(args.Sketch).Result;
      args.SetSketchGeometry(result.Geometry);
      return Task.CompletedTask;
   });

*Null can be passed in as the geometry to SetSketchGeometry. This will "null" the sketch for all downstream listeners and active tool.

SketchCompletedEvent

The SketchCompletedEvent fires after the sketch has been completed ("finished") and after the active tool's OnSketchCompleteAsync callback is complete. The accompanying SketchCompletedEventArgs contains the map view the sketch completion originated on as well as the finished sketch in the SketchCompletedEventArgs.Sketch property. This is the same sketch geometry as was received by the active tool.

 ArcGIS.Desktop.Mapping.Events.SketchCompletedEvent.Subscribe((args) => {
   var mapView = args.MapView;
   //Sketch can be null depending on BeforeSketchCompletedEvent listeners
   if (args.Sketch?.IsEmpty ?? true)
     return;
   var sketch_geom = args.Sketch;
   //TODO, use the completed sketch
   ...
 });

UI controls

Geometry Vertices

The Geometry control provides a UI for viewing vertices.

Use it in GeometryMode.Geometry to display the vertices of a geometry. It can be a geometry that is created on-the-fly or a geometry of an existing feature. If the geometry is Z-aware or M-aware the control will display the Z and/or M values. In Geometry mode the control acts the same as the Geometry tab on the Attributes pane. Bind the geometry to be shown to the Geometry property of the control.

 <UserControl x:Class="Example.GeometryViewDockpane"
    ... xmlns:editControls="clr-namespace:ArcGIS.Desktop.Editing.Controls;assembly=ArcGIS.Desktop.Editing"
    ...>

 <Grid>
   <editControls:GeometryControl GeometryMode="Geometry" Geometry="{Binding Geometry}" />
 </Grid>

The Geometry property in the associated (dockpane) view model is defined as:

    private Geometry _geometry;
    public Geometry Geometry
    {
      get => _geometry;
      set => SetProperty(ref _geometry, value);
    }

geometryControlGeometry

In GeometryMode.Sketch, the control responds to sketching on the map view allowing you to view and modify the vertices of the current sketch. If the SketchLayer property is set; the properties of the layer's feature class determine whether the GeometryControl displays Z and/or M values as you sketch. If no SketchLayer is set, the GeometryControl only displays X,Y coordinates. When the sketch is finished or cancelled, the control is cleared.

<UserControl x:Class="Example.GeometryViewDockpane"
    ... xmlns:editControls="clr-namespace:ArcGIS.Desktop.Editing.Controls;assembly=ArcGIS.Desktop.Editing"
    ...>

<Grid>
  <editControls:GeometryControl GeometryMode="Sketch" SketchLayer="{Binding SketchLayer}"/>
</Grid>

The SketchLayer property in the associated (dockpane) view model is defined as:

    private BasicFeatureLayer _sketchLayer;
    public BasicFeatureLayer SketchLayer
    {
      get => _sketchLayer;
      set => SetProperty(ref _sketchLayer, value);
    }

geometryControlSketch

A sample using the Geometry control can be found at GeometryControl.

Sketch Feedback and Sketch Symbology

The MapTool sketch feedback consists of vertices and segments. A vertex can be fixed or it can be the active or current vertex. The default symbology for a fixed vertex is a hollow green square; whereas the default symbology for the current vertex is a hollow red square. Similarly segments are either fixed or the active / current segment. There is no difference in the symbology between fixed or current segments; all sketch segments have the default symbology of a black dash-dot line; commonly referred to as the ant-trail. All vertices and segments whether they are fixed or current comprise the sketch feedback.

Sketch feedback

Within a custom MapTool, you can customize the sketch symbol using the SketchSymbol property. Typically you would set this in the OnToolActivateAsync method. Alternatively you could use the OnSketchModifiedAsync method if you wanted to make the modifications more dynamic (for example changing the sketch symbol's color based on the number of vertices in the sketch).

Here's a code snippet for setting the SketchSymbol property in OnToolActivateAsync

    private CIMLineSymbol _lineSymbol = null;
    protected override async Task OnToolActivateAsync(bool hasMapViewChanged)
    {
      if (_lineSymbol == null)
      {
        _lineSymbol = await Module1.CreateLineSymbolAsync();
      }
      this.SketchSymbol = _lineSymbol.MakeSymbolReference();
    }

And the result. Note that you can still see the green and red vertex squares along with the dash-dot ant-trail line of the sketch feedback overlaying the sketch symbol.

Modified Sketch feedback

Starting with Pro 2.9, you can also change the symbology of the vertices and segments of the sketch feedback in your custom MapTool. There are 4 different types of vertices - unselected vertex, selected vertex, unselected current vertex, and selected current vertex. For the purposes of sketching all vertices are unselected. Selected vertices are only visible within the Edit Vertices tool. As previously mentioned the default vertex symbology whilst sketching is the green hollow square (unselected vertex) and the red hollow square (unselected current vertex). The default symbology for vertices within the Edit Vertices tool is a green filled square (selected vertex) and a red filled square (selected current vertex). The images below illustrate this; the first shows the unselected vertex symbology whilst sketching; the lower images show the selected vertex symbology within Edit Vertices.

Sketch segment, unselected sketch vertex, & unselected current sketch vertex

Selected sketch vertex Selected current sketch vertex

You can modify the vertex and segment symbology of the sketch feedback using the following new methods:

Symbology set via these methods override the default symbology defined in the application settings. Here's a code snippet showing how to customize the segment symbol of the sketch feedback. In this example, the ant-trail becomes a blue dash-dot line of width 2.5. Note it is not possible to change the segment symbol type. It continues to be constrained to be a dash-dot line.

  protected override Task OnToolActivateAsync(bool active)
  {
    return QueuedTask.Run(() =>
    {
      var options = GetSketchSegmentSymbolOptions();

      var blue = new CIMRGBColor();
      blue.R = 0;
      blue.G = 0;
      blue.B = 255;
      options.PrimaryColor = blue;
      options.Width = 2.5;

      SetSketchSegmentSymbolOptions(options);
    });
  }

Modified Segment Sketch feedback

A sample tool modifying sketch feedback can be found at CustomToolSketchSymbology.

Of course, you can customize both the SketchSegment and the sketch feedback options at the same time. Here's a combined example along with the output. You can see the red SketchSegment overlaid with the blue sketch feedback segment options. The vertices are the cyan circle from the SketchSegment overlaid with the default red and green hollow squares of the sketch feedback vertex options.

  private CIMLineSymbol _lineSymbol = null;
  protected override async Task OnToolActivateAsync(bool hasMapViewChanged)
  {
    if (_lineSymbol == null)
    {
      _lineSymbol = await Module1.CreateLineSymbolAsync();
    }
    this.SketchSymbol = _lineSymbol.MakeSymbolReference();

    await QueuedTask.Run(() =>
    {
      var options = GetSketchSegmentSymbolOptions();

      var blue = new CIMRGBColor();
      blue.R = 0;
      blue.G = 0;
      blue.B = 255;
      options.PrimaryColor = blue;
      options.Width = 2.5;

      SetSketchSegmentSymbolOptions(options);
    });
  }

Combined Segment Sketch feedback

See the Sketch Symbology Options section for details on how to modify the sketch feedback vertex and segment application options for all tools rather than just for a custom tool. Note that any symbol modifications made in a custom tool will take precedence over those in the application options.

Editing Options

Editing options corresponding to the editing options on the Pro options UI are available in the api via the static ApplicationOptions.EditingOptions property - refer to ProConcepts Content - ApplicationOptions for more information. The available options include:

//Gets and sets the application editing options.
public class EditingOptions
{
  //Gets and sets whether the dialog to confirm deletes is displayed.
  public bool ShowDeleteDialog { get; set; }
  //Gets and sets the sketch behavior on a touch pad (click + drag vs click-drag-click)
  public bool DragSketch { get; set; }
  //Gets and sets whether dynamic constraints are displayed in the map
  public bool ShowDynamicConstraints { get; set; }
  //Gets and sets whether Deflection is the default direction constraint. Set to
  //false for Absolute to be the default direction constraint.
  public bool IsDeflectionDefaultDirectionConstraint { get; set; }
  //Gets and sets whether Direction is the default constraint for input mode. Set
  //to false for Distance to be the default constraint for input mode.
  public bool IsDirectionDefaultInputConstraint { get; set; }
  //Gets and sets whether the editing toolbar is visible.
  public bool ShowEditingToolbar { get; set; }
  //Gets and sets the editing toolbar position.
  public ToolbarPosition ToolbarPosition { get; set; }
  //Gets and sets the editing toolbar size.
  public ToolbarSize ToolbarSize { get; set; }
  //Gets and sets whether the Escape key is enabled as a shortcut to cancel the 
  //active tool in Stereo maps.
  public bool EnableStereoEscape { get; set; }
  //Gets and sets whether the editing toolbar is magnified.
  public bool MagnifyToolbar { get; set; }
  //Gets and sets whether edits are to be automatically saved.
  public bool AutomaticallySaveEdits { get; set; }
  //Gets and sets whether edits are to be saved by time or operation when 
  //ArcGIS.Desktop.Core.EditingOptions.AutomaticallySaveEdits is true.
  public bool AutoSaveByTime { get; set; }
  //Gets and sets the time interval (in minutes) after which edits will be saved
  //if ArcGIS.Desktop.Core.EditingOptions.AutomaticallySaveEdits is true and 
  //ArcGIS.Desktop.Core.EditingOptions.AutoSaveByTime is true.
  public int SaveEditsInterval { get; set; }
  //Gets and sets the number of operations after which edits will be saved if 
  //ArcGIS.Desktop.Core.EditingOptions.AutomaticallySaveEdits
  //is true and ArcGIS.Desktop.Core.EditingOptions.AutoSaveByTime is false.
  public int SaveEditsOperations { get; set; }
  //Gets and sets whether edits are saved when saving project.
  public bool SaveEditsOnProjectSave { get; set; }
  //Gets and sets whether the dialog to confirm save edits is displayed.
  public bool ShowSaveEditsDialog { get; set; }
  //Gets and sets whether the dialog to confirm discard edits is displayed.
  public bool ShowDiscardEditsDialog { get; set; }
  //Gets and sets whether editing can be enabled and disabled from the Edit tab.
  public bool EnableEditingFromEditTab { get; set; }
  //Gets and sets whether the active editing tool is deactivated when saving or 
  //discarding edits.
  public bool DeactivateToolOnSaveOrDiscard { get; set; }
  //Gets and sets whether newly added layers are editable by default.
  public bool NewLayersEditable { get; set; }
  //Gets and sets whether double-click is enabled as a shortcut for Finish when 
  //sketching.
  public bool FinishSketchOnDoubleClick { get; set; }
  //Gets and sets whether vertex editing is allowed while sketching.
  public bool AllowVertexEditingWhileSketching { get; set; }
  //Gets and sets whether a warning is displayed when subtypes are changed.
  public bool WarnOnSubtypeChange { get; set; }
  //Gets and sets whether default values are initialized on a subtype change.
  public bool InitializeDefaultValuesOnSubtypeChange { get; set; }
  //Gets and sets the uncommitted attribute edits setting.
  public UncommitedEditMode UncommitedAttributeEdits { get; set; }
  //Gets and sets whether attribute validation is enforced.
  public bool EnforceAttributeValidation { get; set; }
  //Gets and sets whether topology is stretched proportionately when moving a 
  //topology element.
  public bool StretchTopology { get; set; }
  //Gets and sets the uncommitted geometry edits setting.
  public UncommitedEditMode UncommitedGeometryEdits { get; set; }
  //Gets and sets if Move tool is to be activated after all paste operations.
  public bool ActivateMoveAfterPaste { get; set; }
  //Gets and sets whether feature symbology is displayed in the sketch.
  public bool ShowFeatureSketchSymbology { get; set; }
  //Gets and sets whether geometry is stretched proportionately when moving a vertex.
  public bool StretchGeometry { get; set; }
  //Determines if the segment sketch symbol can be set.


  public bool CanSetSegmentSymbolOptions(SegmentSymbolOptions segmentSymbol);
  //Determines if the vertex sketch symbol can be set.
  public bool CanSetVertexSymbolOptions(VertexSymbolType symbolType, 
                         VertexSymbolOptions vertexSymbol);

  //Gets the default segment sketching symbol information. This method must be called
  //on the MCT. Use QueuedTask.Run.
  public SegmentSymbolOptions GetDefaultSegmentSymbolOptions();
  //Gets the default symbol information for a vertex while sketching. This method
  //must be called on the MCT. Use QueuedTask.Run.
  public VertexSymbolOptions GetDefaultVertexSymbolOptions(VertexSymbolType symbolType);

  //Gets the segment sketching symbol information. This method must be called on
  //the MCT. Use QueuedTask.Run.
  public SegmentSymbolOptions GetSegmentSymbolOptions();
  //Gets the symbol for a vertex while sketching. This method must be called on the
  //MCT. Use QueuedTask.Run.
  public VertexSymbolOptions GetVertexSymbolOptions(VertexSymbolType symbolType);
  //Sets the segment sketch symbol. This method must be called on the MCT. 
  //Use QueuedTask.Run.
  public void SetSegmentSymbolOptions(SegmentSymbolOptions segmentSymbol);
  //Sets the symbol for a vertex while sketching. This method must be called on the
  //MCT. Use QueuedTask.Run.
  public void SetVertexSymbolOptions(VertexSymbolType symbolType, 
                       VertexSymbolOptions vertexSymbol);
}

Example:

  //toggle/switch option values
  var options = ApplicationOptions.EditingOptions;

  options.EnforceAttributeValidation = !options.EnforceAttributeValidation;
  options.WarnOnSubtypeChange = !options.WarnOnSubtypeChange;
  options.InitializeDefaultValuesOnSubtypeChange = !options.InitializeDefaultValuesOnSubtypeChange;
  options.UncommitedAttributeEdits = (options.UncommitedAttributeEdits == UncommitedEditMode.AlwaysPrompt) ? 
     UncommitedEditMode.Apply : UncommitedEditMode.AlwaysPrompt;

  options.StretchGeometry = !options.StretchGeometry;
  options.StretchTopology = !options.StretchTopology;
  options.UncommitedGeometryEdits = (options.UncommitedGeometryEdits == UncommitedEditMode.AlwaysPrompt) ? 
      UncommitedEditMode.Apply : UncommitedEditMode.AlwaysPrompt;

  options.ActivateMoveAfterPaste = !options.ActivateMoveAfterPaste;
  options.ShowFeatureSketchSymbology = !options.ShowFeatureSketchSymbology;
  options.FinishSketchOnDoubleClick = !options.FinishSketchOnDoubleClick;
  options.AllowVertexEditingWhileSketching = !options.AllowVertexEditingWhileSketching;
  options.ShowDeleteDialog = !options.ShowDeleteDialog;
  options.EnableStereoEscape = !options.EnableStereoEscape;
  options.DragSketch = !options.DragSketch;
  options.ShowDynamicConstraints = !options.ShowDynamicConstraints;
  options.IsDeflectionDefaultDirectionConstraint = !options.IsDeflectionDefaultDirectionConstraint;
  options.IsDirectionDefaultInputConstraint = !options.IsDirectionDefaultInputConstraint;
  options.ShowEditingToolbar = !options.ShowEditingToolbar;
  options.ToolbarPosition = (options.ToolbarPosition == ToolbarPosition.Bottom) ? 
                                     ToolbarPosition.Right : ToolbarPosition.Bottom;
  options.ToolbarSize = (options.ToolbarSize == ToolbarSize.Medium) ? ToolbarSize.Small : ToolbarSize.Medium;
  options.MagnifyToolbar = !options.MagnifyToolbar;

  options.EnableEditingFromEditTab = !options.EnableEditingFromEditTab;
  options.AutomaticallySaveEdits = !options.AutomaticallySaveEdits;
  options.AutoSaveByTime = !options.AutoSaveByTime;
  options.SaveEditsInterval = (options.AutomaticallySaveEdits) ? 20 : 10;
  options.SaveEditsOperations = (options.AutomaticallySaveEdits) ? 60 : 30;
  options.SaveEditsOnProjectSave = !options.SaveEditsOnProjectSave;
  options.ShowSaveEditsDialog = !options.ShowSaveEditsDialog;
  options.ShowDiscardEditsDialog = !options.ShowDiscardEditsDialog;
  options.DeactivateToolOnSaveOrDiscard = !options.DeactivateToolOnSaveOrDiscard;
  options.NewLayersEditable = !options.NewLayersEditable;

Sketch Symbology Options

EditingOptions contains a number of option settings related to the sketch symbology. These require use of the Pro Main CIM Thread, MCT, and so are accessed and set via Get and Set methods (which must be wrapped in a QueuedTask.Run(...)) rather than Get and Set properties. Symbology settings are also a little different from other settings in that they are constrained by what can represent a valid symbology setting. Not all possible point and line combinations are valid for use on the sketch. Add-ins, therefore, can use the CanSetVertexSymbolOptions(vertexOptions) and CanSetSegmentSymbolOptions(segmentOptions) to determine if their symbology options are valid/can be used with sketch vertex and sketch segment symbology respectively. Additionally, to assist add-ins in returning to the default settings for vertex and segment sketch symbology, a GetDefaultVertexSymbolOptions(vertexType) and GetDefaultSegmentSymbolOptions() methods are provided. Examples follow:

Retrieve the existing sketch symbology settings:

var options = ApplicationOptions.EditingOptions;

//Must use QueuedTask
QueuedTask.Run(() => {

  //There are 4 vertex symbol settings - selected, unselected and the
  //current vertex selected and unselected.
  var reg_select = options.GetVertexSymbolOptions(VertexSymbolType.RegularSelected);
  var reg_unsel = options.GetVertexSymbolOptions(VertexSymbolType.RegularUnselected);
  var curr_sel = options.GetVertexSymbolOptions(VertexSymbolType.CurrentSelected);
  var curr_unsel = options.GetVertexSymbolOptions(VertexSymbolType.CurrentUnselected);

  //to convert the options to a symbol use
  //GetPointSymbol
  var reg_sel_pt_symbol = reg_select.GetPointSymbol();
  //ditto for reg_unsel, curr_sel, curr_unsel

  //Segment symbol options
  var seg_options = options.GetSegmentSymbolOptions();
 
});

Set sketch symbol options:

var options = ApplicationOptions.EditingOptions;

QueuedTask.Run(() => {
  //change the regular unselected vertex symbol:
  //default is a green, hollow, square, 5pts. Change to
  //Blue outline diamond, 10 pts
  var vertexSymbol = new VertexSymbolOptions(VertexSymbolType.RegularUnselected);
  vertexSymbol.OutlineColor = ColorFactory.Instance.BlueRGB;
  vertexSymbol.MarkerType = VertexMarkerType.Diamond;
  vertexSymbol.Size = 10;

  //check are these changes valid?
  if (options.CanSetVertexSymbolOptions(
     VertexSymbolType.RegularUnselected, vertexSymbol)) {
    //apply them - will throw for invalid choices
    options.SetVertexSymbolOptions(VertexSymbolType.RegularUnselected, vertexSymbol);
  }

  //change the segment symbol primary color to green and
  //width to 1 pt
  var segSymbol = options.GetSegmentSymbolOptions();
  segSymbol.PrimaryColor = ColorFactory.Instance.GreenRGB;
  segSymbol.Width = 1;

  //check are these changes valid?
  if (options.CanSetSegmentSymbolOptions(segSymbol)) {
    //apply them - will throw for invalid choices
    options.SetSegmentSymbolOptions(segSymbol);
  }

Set sketch options back to defaults:

var options = ApplicationOptions.EditingOptions;

QueuedTask.Run(() => {
  //set vertex reg unselected back to defaults...
  //ditto for reg selected and current selected, unselected
  var def_reg_unsel = 
    options.GetDefaultVertexSymbolOptions(VertexSymbolType.RegularUnselected);
  //apply default
  options.SetVertexSymbolOptions(
    VertexSymbolType.RegularUnselected, def_reg_unsel);
  
  //set segment sketch back to default
  var def_seg = options.GetDefaultSegmentSymbolOptions();
  options.SetSegmentSymbolOptions(def_seg);
});

Versioning Options

Versioning option settings control the conflict and conflict reconciliation behavior for an edit session involving versioned data. They are available via the static ApplicationOptions.VersioningOptions property. The VersioningOptions include:

//Gets and sets the application versioning options.
public class VersioningOptions
{
  public VersioningOptions();
  //Gets and sets the value defining how conflicts are flagged.
  public ConflictDetectionType DefineConflicts { get; set; }
  //Gets and sets the value defining how conflicts are resolved.
  public ConflictResolutionType ConflictResolution { get; set; }
  //Gets and sets the value indicating if a dialog is displayed during 
  //the reconcile process.
  public bool ShowReconcileDialog { get; set; }
  //Gets and sets the value indicating if a dialog is displayed to review 
  //conflicts.
  public bool ShowConflictsDialog { get; set; }
}

For example, this snippet toggles/changes the current versioning option values:

  var vOptions = ApplicationOptions.VersioningOptions;

  vOptions.DefineConflicts = (vOptions.DefineConflicts == ConflictDetectionType.ByRow) ? 
    ConflictDetectionType.ByColumn : ConflictDetectionType.ByRow;
  vOptions.ConflictResolution = (vOptions.ConflictResolution == ConflictResolutionType.FavorEditVersion) ? 
     ConflictResolutionType.FavorTargetVersion : ConflictResolutionType.FavorEditVersion;
  vOptions.ShowConflictsDialog = !vOptions.ShowConflictsDialog;
  vOptions.ShowReconcileDialog = !vOptions.ShowReconcileDialog;

Developing with ArcGIS Pro

    Migration


Framework

    Add-ins

    Configurations

    Customization

    Styling


Arcade


Content


CoreHost


DataReviewer


Editing


Geodatabase

    3D Analyst Data

    Plugin Datasources

    Topology

    Linear Referencing

    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