ProConcepts Editing - Esri/arcgis-pro-sdk GitHub Wiki

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.

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

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 Customizations

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. An overview of each customization type is discussed below.

Commands and Tools

Commands are usually buttons or menu items on the UI. They typically work with a set of rows, 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. 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 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. 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 a sketch in the display. The features intersecting with this sketch can then be used as a basic for an editing operation.

You can use the ArcGIS Pro MapTool template in Visual Studio to create a sketch tool. A MapTool can sketch feedback on the map when its 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 polygon or polyline feedback can be achieved using the 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.

Key Properties

In addition to the IsSketchTool and SketchType properties, there are a few additional key properties on the MapTool class.

Set the UseSnapping property to true to ensure the MapTool snaps according to the current snapping environment. See the snapping section for more information.

Use the SketchMode property in conjunction with the SketchType property when you wish to toggle between sketch modes for a particular sketch geometry type. For example when your SketchType is SketchGeometryType.Line you can toggle between SketchMode.Line, SketchMode.Trace and SketchMode.Stream to move between normal sketching to trace and streaming modes. Other sketch modes can allow beziers and arc curve segments to be constructed within the context of the entire line geometry.

Customize the sketch symbol using the SketchSymbol property. See Sketch Feedback and Sketch Symbology for more discussion and additional options for finer control with the individual sketch vertex and segment symbology.

Customize the context menus and toolbars displayed when sketching via the ContextMenuID, SegmentContextMenuID and ContextToolbarID properties. These ids reference menus specified in the config.daml file. If these are not specified then the default Editing context menus and toolbars are displayed.

If you wish your custom sketch tool to fire the sketch events, set the FireSketchEvents property to true. Typically this is set for construction tools only, however there may be special cases where this is required for your normal sketch tool. This property is available from 2.7 onwards. See sketch events for more information.

To add a custom sketch tip, use the SketchTip property. Or if a more complex tip is required, use an embeddable control and reference it's daml ID using the SketchTipID property. The embeddable control can be referenced using the SketchTipEmbeddableControl property. These properties are available at 3.0 onwards.

public CustomEditTool() {
  IsSketchTool = true;
  SketchType = SketchGeometryType.Line;
  SketchTip = "hello world";
  ...
}

SketchTip

If you are using an embeddable control for a sketch tip, then from 3.4 onwards you can customize some additional properties. By default a sketch tip is located to the lower-right of the cursor and has a grey background. Change the position of the sketch tip by specifying a value for the SketchTipControlPosition property. The background of the sketch tip can also be set to transparent by setting the IsSketchTipControlTransparent to true. Note that these two properties can only be used if a SketchTipID is set.

public SketchToolHalo()
{
  IsSketchTool = true;
  SketchType = SketchGeometryType.Line;
  SketchOutputMode = SketchOutputMode.Map;

  SketchTipID = "SketchToolWithHalos_HaloEmbeddableControl";
  SketchTipControlPosition = SketchTipControlPosition.Center;
  IsSketchTipControlTransparent = true;
}

See the ProGuide Sketch Tip With Halos and the associated Sketch Tool Halo Sample for an example of using the SketchTipID, SketchTipControlPosition and IsSketchTipControlTransparent properties.

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. This requirement allows the tool to have two different modes; the normal sketch mode and an additional selection mode. The user is able to toggle the tool between sketch and select modes by holding down the SHIFT key. When this occurs the sketch is suspended. Releasing the SHIFT key puts the tool back into sketch mode, restores the previous state of the sketch (and the sketch undo / redo stack) and allows you to resume sketching.

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.

Here's the constructor of a custom tool using the UseSelection property. This custom tool will be able to toggle between sketch and select modes when the user presses and holds the SHIFT key.

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

Sketch tools can also toggle between sketch and selection modes programmatically by using the ActivateSelectAsync(bool activate) method. Calling ActivateSelectAsync with activate = true will toggle the tool into select mode. To go back to sketch mode, the tool must call ActivateSelectAsync with activate = false. When the tool is toggled back to sketch mode, the previous state of the sketch will be restored, including the sketch undo/redo stack if UseSelection = true is set in the constructor. If UseSelection=false (the default), the sketch is simply cleared and the tool operator begins sketching again.

Here's one example that illustrates the usage of ActivateSelectAsync: When a COTS tool is activated that requires a selection (that is it has UseSelection = true), and no features are selected, it automatically starts in select mode to ensure features are selected. Once a valid selection has been made by the user, the tool changes into sketch mode allowing a sketch to be made as per normal. The user is able to add additional features to the selection (if necessary) by holding down the SHIFT key which toggles the tool between sketch and select modes. 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).

Custom tools can duplicate this behavior using a combination of the UseSelection property and ActivateSelectAsync. The following code snippet shows an example of such a tool - it requires features from a line layer to be selected. If no line features are selected the tool will start in select mode. Examining the code we can see that the UseSelection property is set to true in the constructor. The tool also overrides the OnSelectionChangedAsync method to check the selection. If no line features are selected, ActivateSelectAsync(true) is called to toggle the tool into select mode. if line features exist in the selection, ActivateSelectAsync(false) is called to toggle the tool back into sketch mode. The code uses an internal property to track the mode when the tool programmatically toggles between sketch and selection mode. Additionally because UseSelection = true, the user can toggle between sketch and select modes to change the selection by holding down the SHIFT key.

  internal class MapTool_RequiresSelection : MapTool
  {
    private bool _inSelMode = false;
    public MapTool_UseSelection_HotSelect()
    {
      IsSketchTool = true;
      SketchType = SketchGeometryType.Line;
      SketchOutputMode = SketchOutputMode.Map;

      UseSelection = true;
    }

    protected override Task OnToolActivateAsync(bool active)
    {
      _inSelMode = false;

      return base.OnToolActivateAsync(active);
    }

    protected override Task OnSelectionChangedAsync(MapSelectionChangedEventArgs args)
    {
      var sel = args.Selection;
      var selDict = sel.ToDictionary<FeatureLayer>();
      var lineLayers = selDict.Keys.Where(fl => fl.ShapeType == esriGeometryType.esriGeometryPolyline);

      // if there's line layers in the selection, toggle back to sketch mode
      if (lineLayers.Any())
      {
        if (_inSelMode)
        {
          this.ActivateSelectAsync(false);
          _inSelMode = false;
        }
      }
      else
      {
        if (!_inSelMode)
        {
          this.ActivateSelectAsync(true);
          _inSelMode = true;
        }
      }

      return base.OnSelectionChangedAsync(args);
    }

    protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
    {
      return base.OnSketchCompleteAsync(geometry);
    }
  }

A second example that illustrates the usage of ActivateSelectAsync is to assign a different key to toggle between sketch and select modes rather than the default SHIFT key behavior. If developers want to toggle their tools into select mode with another key they can use a combination of the OnToolKeyDown, OnToolKeyUp callbacks and the ActivateSelectAsync method. As before, use the ActivateSelectAsync method with activate = true to go to select mode. To go back to sketch mode, the tool must call ActivateSelectAsync with activate = false. When the tool is toggled back to sketch mode, the previous state of the sketch will be restored, including the sketch undo/redo stack if UseSelection = true is set in the constructor. If UseSelection = false (the default), the sketch is simply cleared and the tool operator begins sketching again.

Tools that choose to assign a different key to toggle between select mode and sketch mode via ActivateSelectAsync are also advised to handle the SHIFT key to override its default 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.

The following snippet shows an implementation of a custom tool that assigns a different key to toggle between select and sketch modes. Notice how it also handles the SHIFT key to prevent the base tool behavior from interfering. This tool is based on the code from the DemoUseSelection sample in the community samples.

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 sketch tools used to create features. They have additional properties 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 SketchType property in the tool constructor to the correct geometry that you wish to sketch (rectangle, circle, polygon, point, line etc). 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. Valid values for the categoryRefID tag are esri_editing_construction_point, esri_editing_construction_polyline, esri_editing_construction_polygon, esri_editing_construction_multipoint.

See Table Construction Tools for specifics about creating construction tools for tables.

See Knowledge Graph Construction Tools for specifics about creating construction tools for Knowledge Graph entity and relates.

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);

      if (!createOperation.IsEmpty)
      {
        // Execute the operation
        return createOperation.ExecuteAsync();
      }
    }
  }

Knowledge Graph Construction Tools

When the active view is a link chart, construction tools can be used to create Knowledge Graph entities and relates. On a link chart, entities are displayed as points and relates are displayed as lines. Use the ArcGIS Pro Construction Tool template in Visual Studio to create a construction tool for entities or relates. Configure the SketchType property in the tool constructor to the correct geometry that you wish to sketch (rectangle, circle, polygon, point, line etc) and set the categoryRefID tag in the config.daml file for your tool to esri_editing_construction_knowledge_graph_entity or esri_editing_construction_knowledge_graph_relationship depending upon the type the tool is to be associated with.

See the ProGuide Knowledge Graph Construction Tools for samples of entity and relate tools for use in a link chart.

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 map. 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 esri_editing_construction_table category. 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 map.

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);

      if (!createOperation.IsEmpty)
      {
        // 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.

If desired, a table construction tool can be modified to become a sketching construction tool. For example, perhaps you wish to create a construction tool that will obtain certain attributes from features on the map and have these attributes 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 for sketching 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 will now operate like a feature construction tool (ie it can sketch feedback on the map 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

The "Enable and disable editing from the Edit tab" checkbox in the UI corresponds to the ApplicationOptions.EditingOptions.EnableEditingFromEditTab setting within the application options. Toggling the value of EnableEditingFromEditTab true and/or false via code is equivalent to toggling the checkbox on|off via the UI. The complete list of available editing options in the api are listed in the Editing options section.

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 via 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 canceled
     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 for performing edits 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 suited 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.

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. Before you execute an edit operation, always check the IsEmpty property. If the edit operation will not make any changes to the database, resulting in the operation not being put on the operation stack, the IsEmpty property will return true. If IsEmpty returns true, the edit operation will fail if you try to execute it.

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.

Example:

  var op = new EditOperation();
  op.Name = "Attribute edit";
  var insp = new Inspector();
  insp.Load(layer, oid);
  insp["Name"] = "Attribute Edit";
  assignIDEditOp.Modify(insp);
  if (!op.IsEmpty){ 
    var opSuccess = op.Execute(); 
    if (!opSuccess)
      return op.ErrorMessage;
    else
      return "Operation completed successfully.";
  }
  /*The 1st time you change the "Name" attribute's value to "Attribute Edit",
   IsEmpty will return false. The 2nd time you try, the attribute value is already
   "Attribute Edit" and there is no more change to be made and so IsEmpty will be true.
   op.Execute() returns a boolean value to indicate if the operation succeeded(true) or not(false).*/

By default, 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. For this reason, each edit specified in 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 in the same edit operation nor on the completion of a move or rotate, and so on. If edits are dependent on each other, then those operations must be chained (see chaining edit operations).

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
  if (!editOp.IsEmpty)
  {
    var result = editOp.ExecuteAsync();
    if (!result)
      var errorMessage = editOp.ErrorMessage;
  }
  

The second example shows Clip, Split, and Planarize edits on a single feature also combined into a single edit operation. The order of execution of the Clip, Split and Planarize functions is arbitrary and determined by the ArcGIS Pro Editing code:

   //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
  if (!op.IsEmpty)
  {
    var result = await op.ExecuteAsync();
    if (!result) //Can also use if (!op.IsSucceeded)
     //TODO: get the op.ErrorMessage, inform the user
  }

In the third example, a dictionary is being used to set attributes values for a new feature on creation:

   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);
   if (!op.IsEmpty)
   {
     var result = await op.ExecuteAsync();
     if (!result) //Can also use 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 update the attributes of a feature before splitting it (so that both resulting split features have the same attribute value). You cannot queue the Modify and Split methods in the same EditOperation as the execution order of the operation is controlled internally and cannot be relied upon to be sequential. 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 "Split" 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 Modify and Split methods. The first edit operation modifies the attributes of the feature, the second operation, Split, is chained to the first operation via var op2 = op1.CreateChainedOperation(); (instead of creating the second edit operation with var op2 = new EditOperation()). Because these operations are chained, they are treated as a single operation on the undo stack (which will either apply or undo the modify and split together).

  // perform an edit and then a split as one operation.
  return QueuedTask.Run(() =>
  {
    var queryFilter = new QueryFilter();
    queryFilter.WhereClause = "OBJECTID = " + oid.ToString();
    
    // create an edit operation and name.
    var op1 = new EditOperation();
    op1.Name = "modify followed by split";

    using (var rowCursor = fc.Search(queryFilter, false))
    {
      while (rowCursor.MoveNext())
      {
        using (var feature = rowCursor.Current as Feature)
        {
          op1.Modify(feature, fldIndex, newZoning);
        }
      }
    }
    
    if (!op1.IsEmpty)
    {
      bool result = op1.Execute();
      if (result)
      {
        // create a chained operation from the first
        var op2 = op1.CreateChainedOperation();   //we do NOT use "new"!

        op2.Split(layer, oid, splitLine);
        if(!op2.IsEmpty)
        {
          bool result2 = 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);
  });

RowTokens

In some situations, a placeholder is required to represent a row that will be made in the future (i.e., at some point within the scope of the edit operation execute). This special placeholder is an ArcGIS.Desktop.Editing.RowToken. The RowToken class encapsulates an ObjectID and a GlobalID property.

The EditOperation class provides a series of Create overloads that return a RowToken immediately after they are called (before EditOperation.Execute). When the edit operation is executed, the RowToken properties are replaced with the real row identifiers, allowing the operations to be treated as a single undo-able operation on the Undo/Redo stack.

At 3.0, the number of Create overloads that return a RowToken was expanded. In addition, an AddAttachment method overload was added that takes a RowToken as a parameter.

Here's a code snippet showing the creation of a feature and adding an attachment using the RowToken paradigm. This executes as a single undo-able operation on the Undo/Redo stack. Prior to 3.0, this workflow required the use of a chained EditOperation in order to execute as a single undo-able operation.

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

     var attachRowToken = editOpAttach.Create(this.CurrentTemplate, polygon);
     editOpAttach.AddAttachment(attachRowToken, @"c:\temp\image.jpg");
     
     if (!op.IsEmpty)
     {
       bool result = editOpAttach.Execute();

       // attachRowToken.ObjectID is populated after the operation executes
     }

   });

A ArcGIS.Desktop.Editing.RowHandle represents either a real row (i.e. an existing feature) or can also 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, use a RowToken returned from a Create operation. Utility Network Associations specifically use the RowHandle paradigm.

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; the challenge here is the features must be created before they can be associated.

Therefore, associations between features can be defined using a RowHandle - a placeholder for the Global IDs of the features "to be" created that will end up being associated. 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).

When the edit operation is executed, the future rows 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.Create(transformerLayer, transformerAttributes);
   RowToken poleToken = editOp.Create(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);


   if (!editOp.IsEmpty)
   {
     //Execute the EditOperation
     var result = editOp.ExecuteAsync();
     if (!result)
       var errorMessage = editOp.ErrorMessage;
   }

Refer to Utility Network Associations for more detail.

Edit Operation ExecuteMode

As previously mentioned, 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 determined by the ArcGIS Pro internal code. At 3.0, the ExecuteMode property was added to the EditOperation class to give additional flexibility. This property has two possible values; "Default" or "Sequential". The first allows the execution of the EditOperation methods to occur in the default order (determined by the ArcGIS Pro editing code), the second ensures the methods are executed in sequential order; that is the order they are specified in your code.

Here is an example showing Clip, Split, and Planarize edits on a single feature combined into a single edit operation. In this snippet the order of execution of the Clip, Split and Planarize functions is sequential:

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

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

   if(!op.IsEmpty)
   {
     //Execute them all together
     var result = await op.ExecuteAsync();

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

Here is the Modify and Split example (used previously in the Chaining Edit Operations section) using the ExecuteMode property to ensure the Modify occurs before the Split.

  // perform an edit and then a split as one operation.
  return QueuedTask.Run(() =>
  {
    var queryFilter = new QueryFilter();
    queryFilter.WhereClause = "OBJECTID = " + oid.ToString();
    
    // create an edit operation and name.
    var op = new EditOperation();
    op.Name = "modify followed by split";
    op.ExecuteMode = ExecuteModeType.Sequential;

    using (var rowCursor = fc.Search(queryFilter, false))
    {
      while (rowCursor.MoveNext())
      {
        using (var feature = rowCursor.Current as Feature)
        {
          op.Modify(feature, fldIndex, newZoning);
        }
      }
    }
    
    op.Split(layer, oid, splitLine);
    if(!op.IsEmpty)
    {
      bool result = op.Execute();
      return new Tuple<bool, string>(op.IsSucceeded, op.ErrorMessage);
    }
    else
      return new Tuple<bool, string>(false, "The operation doesn't make any changes to the database so if executed it will fail.");
  });

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
   if (!editOp.IsEmpty)
   {
     var result = editOp.Execute();
     if (!result)
       var errorMessage = editOp.ErrorMessage;
   }

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
  if (!editOp.IsEmpty)
  {
    var result = await editOp.ExecuteAsync();   //will return false if the callback is aborted
    if (!result)
      var errorMessage = editOp.ErrorMessage;
  }
  


  //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, a convenient utility class called Inspector class is provided. 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) and EditOperation.Execute to apply the attribute updates to the set of features.

The Inspector.ApplyAsync() method applies and saves the changes in one shot. Both the Inspector.ApplyAsync and EditOperation.Execute will add an item to the undo stack if the operation is successful.

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.

Here's a simple example where a single feature is loaded into an Inspector and the 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);
  if (!op.IsEmpty)
  {
    var result = await op.ExecuteAsync();
    if (!result)
      var errorMessage = op.ErrorMessage;
  }

  //Or instead of EditOperation
  //var result = await inspector.ApplyAsync(); 

Feature geometry can be updated as well as attributes simply by using the Inspector.Shape property and setting it to the new geometry value.

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

  inspector.Shape = poly;

  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 selectionSet = MapView.Active.SelectFeatures(zippoly, isWhollyWithin: true);
var first = selectionSet.ToDictionary().FirstOrDefault(k => k.Value.Count > 0);
if (first.Key != null) 
{
   // load the features from the layer
   await inspector.LoadAsync(first.Key, first.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);
      if(!op.IsEmpty)
      {
        var result = await op.ExecuteAsync();
        if(!result)
          var errorMessage = op.ErrorMessage;
      }
    }
}

The inspector can also be used as a convenient short-cut for querying the attributes of a single feature. 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.

string value = "";
var inspector = new Inspector();
await inspector.LoadAsync(layer, oid);
value = inspector["fieldName"].ToString();

// OR
// value = inspector[2].ToString();

// OR
//foreach (var attrib in inspector)
//{
//  if (attrib.FieldName == "ZipCode")
//  {
//    value = attrib.CurrentValue.ToString();
//  }
//}

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);
        if (!op.IsEmpty)
        {
          var result = op.Execute();
          return new Tuple<bool, string>(result, op.ErrorMessage); //Can also use op.IsSucceeded instead of result
        }
        else
          return new Tuple<bool, string>(false, "Operation makes no changes to the database so if executed it will fail.");
    });

Raster Fields

Here's some examples for reading and writing raster fields using the Inspector.

  // Read a raster from a raster field as an InteropBitmap
  // the bitmap can then be used as an imagesource or written to disk
  var insp = new Inspector();
  insp.Load(layer, oid);
  var ibmp = insp["Photo"] as System.Windows.Interop.InteropBitmap;
  // Insert an image into a raster field
  // Image will be written with no compression
  var insp = new Inspector();
  insp.Load(layer, oid);
  insp["Photo"] = @"e:\temp\Hydrant.jpg";

  var op = new EditOperation();
  op.Name = "Raster Inspector";
  op.Modify(insp);
  if (op.IsEmpty)
    return;

  var result = op.Execute();

Use the following example if you wish to write a compressed image to a raster field.

  QueuedTask.Run(() =>
  {
    // Open the raster dataset on disk and create a compressed raster value dataset object
    var dataStore = new FileSystemDatastore(new FileSystemConnectionPath(new System.Uri(@"e:\temp"), FileSystemDatastoreType.Raster));
    using (var fileRasterDataset = dataStore.OpenDataset<ArcGIS.Core.Data.Raster.RasterDataset>("Hydrant.jpg"))
    {
      var storageDef = new ArcGIS.Core.Data.Raster.RasterStorageDef();
      storageDef.SetCompressionType(ArcGIS.Core.Data.Raster.RasterCompressionType.JPEG);
      storageDef.SetCompressionQuality(90);

      var rv = new ArcGIS.Core.Data.Raster.RasterValue();
      rv.SetRasterDataset(fileRasterDataset);
      rv.SetRasterStorageDef(storageDef);

      var sel = MapView.Active.Map.GetSelection();

      // insert a raster value object into the raster field
      var insp = new Inspector();
      insp.Load(layer, oid));
      insp["Photo"] = rv;

      var op = new EditOperation();
      op.Name = "Raster Inspector";
      op.Modify(insp);
      if (op.isEmpty)
        return;

      var result = op.Execute();

      ...
    }
  });

Blob Fields

The following code example illustrates how to read and write blob fields using the Inspector.

   QueuedTask.Run(() =>
   {
     var insp = new Inspector();
     insp.Load(layer, oid);

     // read a blob field and save to a file
     var msw = new MemoryStream();
     msw = insp["Blobfield"] as MemoryStream;
     using (FileStream file = new FileStream(@"d:\temp\blob.jpg", FileMode.Create, FileAccess.Write))
     {
       msw.WriteTo(file);
     }

     // read file into memory stream
     var msr = new MemoryStream();
     using (FileStream file = new FileStream(@"d:\images\Hydrant.jpg", FileMode.Open, FileAccess.Read))
     {
       file.CopyTo(msr);
     }

     // put the memory stream in the blob field and save to feature
     var op = new EditOperation();
     op.Name = "Blob Inspector";
     insp["Blobfield"] = msr;
     op.Modify(insp);
     if (op.isEmpty)
        return;

     var result = op.Execute();
   });
 

Row Templates

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

Map authors create and manage 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 or row.

Every template is associated with a feature layer or a standalone table. Standalone tables are used to create and maintain non-spatial data in a database. Each feature layer or standalone table can have a number of templates that facilitate the creation of records in that layer or table. When that object is persisted, as a layer file or in a project, the templates are stored as part of that object's definition.

Accessing Templates

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

//Get selected layer in the 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 templates in a standalone table:

//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

Row templates are automatically generated for editable layers and tables 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 or copied and altered from an existing template. Creating and modifying templates is done through the CIM classes. 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.

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 CIMRowTemplate;
  //Could also create a new one here
  //var newTemplate = new CIMRowTemplate();

  //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() as CIMRowTemplate;

  //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
   var 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 a 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:

//Get parcels layer
var featLayer = MapView.Active.Map.FindLayers("Parcels").First();
QueuedTask.Run(() =>
{
  //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.

Knowledge Graph Feature Editing

See the Knowledge Graph Concepts document and the section on Knowledge Graph Editing for specifics about Knowledge Graph and editing.

Snapping

Within ArcGIS Pro 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 other snapModes will be disabled.
Snapping.SetSnapModes(new List<SnapMode>() { SnapMode.Edge, SnapMode.Point });
      
//Set snapping options via get/set options
var snapOptions = Snapping.GetOptions(myMap);
//In 2.x, snapOptions.SnapToSketchEnabled = true;
snapOptions.IsSnapToSketchEnabled = true;
snapOptions.XYTolerance = 100;
//In 2.x, snapOptions.ZToleranceEnabled = true;
snapOptions.IsZToleranceEnabled = true;
snapOptions.ZTolerance = 0.6;
Snapping.SetOptions(myMap,snapOptions);

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

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

     //set 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 when sketching in the application 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 within the ArcGIS Pro session.

The following examples illustrates configuring 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 the GetSnapMode method
  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(null);

  // 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

As mentioned previously, 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.ObjectID;
    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
       // use (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, canceling 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. In Pro 2.x, Project.Current.EditedDatastores will return an array that is a newly constructed copy of the datastores, so any edits to the array doesn't make a change in the actual datastores. In Pro 3.0, it returns an IReadOnlyList of the datastores. 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 the 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 EditOperation in the EditStartedEventArgs 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 EditOperation in the EditCompletingEventArgs 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 or EditOperation.ExecuteAsync)
  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, modifies, and deletes by object ID per MapMember (layer or standalone table). Cancelation of the EditOperation is not posible within the EditCompletedEvent.

  private SubscriptionToken _token;

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

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

  //Event handler
  protected Task OnEditComplete(EditCompletedEventArgs args) {
   
     //Check for null
     if (args.Creates != null) {
       foreach (var kvp in args.Creates.ToDictionary()) {
         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
  if (!editOp.IsEmpty)
  {
    var result = editOp.ExecuteAsync(); //EditCompletedEvent fires when
                                        //ExecuteAsync has finished
    if (!result)
      var errorMessage = op.ErrorMessage;
  }

Row Events

In ArcGIS Pro, subscription to the row events is on a per-dataset basis. To listen to RowCreatedEvent, RowChangedEvent, and RowDeletedEvent, you subscribe 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 further 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 canceled.
*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 row event 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 row event 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 row event 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. Canceling within a row event will cancel the entire transaction (and the 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 are validated. If the "POLICE_DISTRICT" value fails validation, the edit operation is canceled.

  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 row event callbacks. Do not use a new edit operation object.

The following snippets subscribe to the RowChangedEvent and show how to make further modifications to an edited row by adding a record to an audit table.

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 the RowChangedEventArgs.Operation property. 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. 
      // 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 = args.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;
    }
    catch
    {
    }
  }

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. 
      // 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 = args.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;
    }
    catch
    {
    }
  }

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 args)
  {
    // get the row and the current operation
    var row = args.Row;
    var parentEditOp = args.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.

Any modifications performed within the row event 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, then consider tracking that row/feature's objectID to ignore the corresponding RowChangedEvent your attribute change will generate.

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

You cannot chain or execute the running operation (accessed from the RowChangedEventArgs.Operation property) within the events. Attempting to do so will cause exceptions to be thrown.

Row events fire from the main CIM thread when the row is modified on the main CIM thread. In the situation where the row is modified by a Geoprocessing tool that is working on a background thread, information about the table which has subscribed to the event is unknown. When the edit happens on the background thread it cannot jump to the main CIM thread to fire the event, resulting in the event not getting fired.

Sketch Events

As of 2.7, the ArcGIS Pro API 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, OnSketchCanceledAsync, and OnSketchCompleteAsync in the usual way).

*Custom sketch tools that want to publish sketch events should opt-in by setting their FireSketchEvents property to true. The default value for the property is false (no sketch events published). Custom sketch 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 tool's sketch method overrides. Custom sketch 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
     ...
   }
 });

SketchCanceledEvent

The SketchCanceledEvent fires whenever the sketch is canceled. The accompanying SketchCanceledEventArgs contains the map view the sketch cancellation originated on.

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

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 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 Task.CompletedTask;
      //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
   ...
 });

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. The following displays the default sketch feedback showing the vertex and segment symbols.

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 on the MapTool class:

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.

Customizing the Attributes Dockpane

Adding Tabs

The Attributes dockpane in ArcGIS Pro displays information for the selected features in a feature layer or selected rows in a standalone table. This information is displayed in a series of tabs in the lower half of the dockpane. Examples of these tabs are the Attributes and Geometry tabs.

attributesPane.png

At 3.1, the Attributes dockpane can be extended to display a custom tab. In the screenshot below, a tab called "Custom" has been added to the Attributes dockpane. In this example, the tab displays the name of the selected feature.

CustomTab

The following development process outlines how to achieve this special behavior.

Step 1: Implement the AttributeTabEmbeddableControl class

The AttributeTabEmbeddableControl represents the control that will be hosted by the custom tab. This class has a collection of properties and methods that can be overridden to create content and behavior in the custom tab for each selected row or feature.

In an add-in, implement an ArcGIS.Desktop.Editing.Attributes.AttributeTabEmbeddableControl. This is similar to the Visual Studio EmbeddableControl template and uses MVVM (Model View - View Model) design. The class that inherits from the AttributeTabEmbeddableControl will be the "View Model" for the custom tab. This class will contain the business logic needed for the custom UI. The UserControl (.xaml and code behind file .xaml.cs) will contain the UI elements to be displayed in the custom tab.

The table below illustrates some of the important members that can be overridden while implementing the AttributeTabEmbeddableControl class.

Member Description
Applies() method Gets if this control (and it's tab) apply to this MapMember. The default value is false.
IsDefault property Gets if the tab is the default tab displayed when a row is selected in the Attribute treeview. Default value is false.
SupportsMultiples property Gets if this control (and it's tab) support selection of multiple rows in the Attribute treeview. Default value is false.
ShowAutoApplyControls property Gets if the apply edit controls are displayed at the bottom of the tab. Default value is true.
LoadFromFeaturesAsync() method Called when attributes from one or more rows have been loaded into the Inspector

As mentioned in the table above, the return value of the Applies() callback determines if the new custom tab should be displayed for a specific MapMember. For example, in your workflow, you might want to only display the custom tab for point feature layers in a map. Within the Applies callback, test if the geometry of the MapMember is a point feature layer. If it is a point feature layer, return true, otherwise return false. The code snippet below illustrates how to use the Applies callback to refine the subset of MapMembers that display the custom tab.

        public  override bool Applies(MapMember mapMember)
        {
            var featureLayer = mapMember as FeatureLayer;
            if (featureLayer == null) return false;
            bool isPointMM = false;
            QueuedTask.Run(() =>
            {
                var featureLayer = mapMember as FeatureLayer;
                if (featureLayer != null)
                {
                    if (featureLayer.ShapeType == esriGeometryType.esriGeometryPoint)
                        isPointMM = true;
                }                
            });
            return isPointMM;
        }

If you are developing a custom tab that does not perform any editing (for example your tab displays information only such as an overview map) you may wish to override the ShowAutoApplyControls property and return false to hide the apply edit controls displayed at the bottom of the tab.

Here is a code snippet of the complete implementation of a class that derives from AttributeTabEmbeddableControl class.

internal class CustomAttributeTabViewModel : AttributeTabEmbeddableControl
{
  public CustomAttributeTabViewModel(XElement options, bool canChangeOptions) : base(options, canChangeOptions) { }

   /// <summary>
   /// Does this control (and it's parent tab) apply to this MapMember.  Default is false. 
   /// </summary>
   /// <param name="mapMember"></param>
   /// <returns>
   /// If the return value is false, a custom tab containing this control will not be displayed 
   /// when a row from Mapmember is highlighted in the attributes treeview. 
   /// </returns>
   public  override bool Applies(MapMember mapMember)
   {
         var featureLayer = mapMember as FeatureLayer;
         if (featureLayer == null) return false;            

         return true;
   }

   /// <summary>
   /// Is the parent tab default.  Default is false.
   /// </summary>
   public override bool IsDefault => false;

   /// <summary>
   /// Does this control (and it's parent tab) support selection of multiple rows in the Attribute treeview.
   /// Default is false.
   /// </summary>
   public override bool SupportsMultiples => base.SupportsMultiples;

   public async override Task LoadFromFeaturesAsync()
   {
        var inspector = Inspector;
        //Get the name field of the selected feature/row
        Name = inspector["NAME"].ToString();            
   }

   /// <summary>
   /// Text to be displayed in the Custom tab.
   /// In this example, the NAME field of the selected feature will be displayed.
   /// </summary>
   private string _name;
   public string Name
   {
       get => _name;
       set => SetProperty(ref _name, value, () => Name);
    }   
}

Step 2: Register the instance of AttributeTabEmbeddableControl with the “esri_editing_AttributeTabs” category

The custom class that derives from AttributeTabEmbeddableControl must be registered with the esri_editing_AttributeTabs categories. This registration is done in the config.daml file of your add-in. Here is a code snippet of the DAML.

 <categories>
    <updateCategory refID="esri_editing_AttributeTabs">
      <insertComponent id="CustomAttributeTab_CustomAttributeTab" className="CustomAttributeTabViewModel">
        <content guid="{f019fd26-4824-45d8-9788-2a0746973b31}" className="CustomAttributeTabView" />
      </insertComponent>
    </updateCategory>
  </categories>

In the daml code snippet above, note the component id "CustomAttributeTab_CustomAttributeTab" assigned to the "insertComponent" element. The className attribute is set to the custom class that derives from the AttributeTabEmbeddableControl (step 1).

Within the "Content" element, note the guid attribute. A unique guid value is set for this attribute. This is the "Extension ID". More information on this unique guid will be covered in the step 3 below. Along with the guid attribute, the content element also has a className attribute. This attribute's value is set to the user control class (defined in the xaml file) that contains the UI elements.

Using the config.daml, you can also configure the location of the custom tab in relation to Pro's tabs such as Attributes, Geometry, Attachments, etc. To do so, set the placeWith attribute in the content element to any one of these values listed below. Use the "insert" attribute to determine if the custom tab should come "before" or "after" the specified Pro tab.

  • "esri_editing_AttributeTabAttributes"
  • "esri_editing_AttributeTabRelationship"
  • "esri_editing_AttributeTabGeometry"
  • "esri_editing_AttributeTabAnnotation"
  • "esri_editing_AttributeTabAttachments"
  • "esri_editing_AttributeTabAssociations"
  • "esri_editing_AttributeTabDimensions"

The code snippet below shows how to insert a custom tab before Pro's Geometry tab.

 <categories>
    <updateCategory refID="esri_editing_AttributeTabs">
      <insertComponent ....
         <content guid="XXX"... placeWith="esri_editing_AttributeTabGeometry" insert="before" />
        </insertComponent>
    </updateCategory>

customTabLocation.png

In order to change the tab title of the custom tab you can use the 'L_name' attribute of the content tag as shown below.

 <categories>
    <updateCategory refID="esri_editing_AttributeTabs">
      <insertComponent ....
         <content guid="XXX"... L_name="My tab title" />
      </insertComponent>
    </updateCategory>

Step 3: Register the table(s) and feature class with a unique guid

Creating a custom tab in the Attributes dockpane for a feature layer or a table is a special behavior. Registering an extension id (guid) with a table allows it to participate in this special behavior. In a separate administrative process, or within a separate add-in, register the table(s) or feature classes which are to show the custom tab with the guid (i.e. "extension id") referenced in step 2 above. To register the table with the unique guid, use the AddActivationExtension method. Pass in the guid as a parameter to this method. The guid passed to AddActivationExtension method must match the guid that is set in the guid attribute in the content element in the config.daml. (Step 2 above).

Here is a code snippet to register the table or feature class with the unique guid used in our sample.

//note: These methods must be called on the Main CIM Thread. Use QueuedTask.Run.
var fc = featureLayer.GetFeatureClass();                    
Guid myExtension = new Guid("{f019fd26-4824-45d8-9788-2a0746973b31}");
fc.AddActivationExtension(myExtension);

After following these steps, the custom tab will appear in the Attributes dockpane when a feature or row from the registered feature class or table has been highlighted in the treeview and it meets the criteria (if any) in the Applies method.

Additional Notes

The following are Important notes regarding registering guids against a table or feature class.

  • Any number of extension ids can be added to a table or feature class.
  • Adding an extension id will limit the backward compatibility of the given table/feature class to ArcGIS Pro 3.1. This means the given table or feature class cannot be used or referenced within ArcGIS Pro 3.0 or earlier.
  • As with all properties that change the minimum client version, removing all activation extension ids will re-compute the minimum required client version. So removing activation extensions potentially allows older clients to open the table (depending on what other Geodatabase functionality is enabled on the table). Refer to RemoveActivationExtension
  • Activation extension ids are supported on registered classes in Enterprise, File, Mobile and on ArcObjects Feature Service geodatabases.
  • Calling AddActivationExtension when the table is being edited will fail.
  • Use the GetActivationExtensions method to get the readonly list of extension ids (guids) registered with the table or feature class.

There is one additional Important note regarding adding custom tabs to the Attributes dockpane.

  • Although multiple extension ids can be registered to a single table or feature class and each of these extension ids has an AttributeTabEmbeddableControl component defined (in the config.daml), only one of the registered components will be added as a custom tab in the attributes dockpane. That is; only ONE custom tab can be displayed in the Attributes dockpane for a feature class or table.

Adding Commands to the Context Menu

The process of adding a command to a context menu requires two steps:

  • a new ArcGIS Pro Button needs to be created.
  • the context menu needs to be updated with the new command inserted into the appropriate location.

In the case of adding a new command to the context menu of the treeview part of the Attributes dockpane, multiple context menus need to be updated as each context menu applies to different scenarios. The context menu DAML Ids used in the treeview part of the Attributes dockpane are:

  • esri_editing_Attributes_FeatureContextMenu - displayed when a feature is right-clicked
  • esri_editing_Attributes_NonFeatureRowContextMenu - displayed when a row from a table is right-clicked
  • esri_editing_Attributes_LayerContextMenu - displayed when a layer is right-clicked
  • esri_editing_Attributes_TableContextMenu - displayed when a table is right-clicked

Here's a screenshot of a custom button added to the esri_editing_Attributes_FeatureContextMenu.

AttributesContextMenu

When writing a command that is to be inserted into a context menu, the typical pattern is to use the FrameworkApplication.ContextMenuDataContext method to obtain a representation of the item that was clicked on; that is the context. Sometimes it is possible to cast this context directly to an API object. For example, when inserting a command into the esri_mapping_mapContextMenu menu (displayed when a map is right-clicked in the Table of Contents), the context is the Map object.

In other situations, the context has to be retrieved using a different methodology; that is the internal object representing the context is responsible for translating some parts of it's data into an API class. Programmatically this is achieved by using the FrameworkApplication.ContextMenuDataContextAs method.

The Attributes dockpane uses this second methodology to obtain it's context which is either a MapMember and object ID (when a single row is right clicked) or a MapMember and a set of object IDs (when the MapMember is right clicked or a set of rows are right-clicked). Both of these scenarios are represented by the SelectionSet class. Retrieving the context can be written as follows:

 var sSet = FrameworkApplication.ContextMenuDataContextAs<SelectionSet>();

Here's a complete code snippet illustrating how to obtain the context as a SelectionSet; the OnClick method of the button performs a Delete operation.

   protected override async void OnClick()
   {
     await QueuedTask.Run(async () =>
     {
       var sSet = FrameworkApplication.ContextMenuDataContextAs<SelectionSet>();
       if (sSet == null)
         return;

       int count = sSet.Count;
       if (count == 0)
         return;

       var op = new EditOperation();
       op.Name = "Delete context";
       op.Delete(sSet);
       await op.ExecuteAsync();
     });
   }

In addition to the code, the context menu also needs to be updated in the daml. You can achieve this by updating the Editing module and modifying the following set of menus as appropriate.

    <updateModule refID="esri_editing_EditingModule">
      <menus>
        <updateMenu refID="esri_editing_Attributes_FeatureContextMenu">
          <insertButton refID="_31_editing_AttributesContextMenu_MyButton" insert="after"  
                        placeWith="esri_editing_Attributes_DeleteSelectionContextMenuItem" />
        </updateMenu>
        <updateMenu refID="esri_editing_Attributes_NonFeatureRowContextMenu">
          <insertButton refID="_31_editing_AttributesContextMenu_MyButton" insert="after"
                        placeWith="esri_editing_Attributes_DeleteSelectionContextMenuItem" />
        </updateMenu>
        <updateMenu refID="esri_editing_Attributes_LayerContextMenu">
          <insertButton refID="_31_editing_AttributesContextMenu_MyButton" insert="after" 
                        placeWith="esri_editing_Attributes_DeleteSelectionContextMenuItem" />
        </updateMenu>
        <updateMenu refID="esri_editing_Attributes_TableContextMenu">
          <insertButton refID="_31_editing_AttributesContextMenu_MyButton" insert="after" 
                        placeWith="esri_editing_Attributes_DeleteSelectionContextMenuItem" />
        </updateMenu>
      </menus>
    </updateModule>

Topology

Topology Properties

Introduced at 3.1 are a number of classes that represent topology properties. TopologyProperties is the abstract base class. Deriving from this are the GeodatabaseTopologyProperties, MapTopologyProperties and NoTopologyProperties classes.

These classes represent the topological editing options shown in the Topology dropdown on the Edit tab as shown below. Note that topological editing only applies to 2D non-stereo maps.

TopologyProperties

Extension methods on the Map class can be used to interact with these topology property classes. Use GetAvailableTopologiesAsync to retrieve the set of topologies present in the current map or GetTopologyAsync to retrieve a specific topology by name.

The active topology properties can be retrieved using GetActiveTopologyAsyncreference/topic14665.html). Set the map topology as active using the combination of CanSetMapTopology and SetMapTopologyAsync or set a specific geodatabase topology as active by CanSetActiveTopology and SetActiveTopologyAsync. Set the active topology to "No topology" using the CanClearTopology and ClearTopologyAsync methods.

Here are some snippets illustrating the methods referenced above.

    var map = MapView.Active.Map;

    // get the active/current topology set for the map
    var activeTopology = await map.GetActiveTopologyAsync();
    var isMapTopology = activeTopologyProperties is MapTopologyProperties;
    var isGdbTopology = activeTopologyProperties is GeodatabaseTopologyProperties;
    var isNoTopology = activeTopologyProperties is NoTopologyProperties;


    //Get a list of all the available topologies for the map
    var availableTopologies = await map.GetAvailableTopologiesAsync();
    var gdbTopologies = availableTopologies.OfType<GeodatabaseTopologyProperties>();
    var mapTopologies = availableTopologies.OfType<MapTopologyProperties>();


    // get the GeodatabaseTopologyProperties specified by name
    var topoProperties = await map.GetTopologyAsync("TopologyName") as GeodatabaseTopologyProperties;
    var workspace = topoProperties.WorkspaceName;
    var topoLayer = topoProperties.TopologyLayer;
    var clusterTolerance = topoProperties.ClusterTolerance;


    // set the "TopologyName" to be the active topology
    if (map.CanSetActiveTopology("TopologyName"))
    {
        //Set the active topology for the map using the topology name
        await map.SetActiveTopologyAsync("TopologyName");
    }


    // set the MapTopology active
    if (map.CanSetMapTopology())
    {
        await map.SetMapTopologyAsync();
    }


    // set the "NoTopology" option as active
    if (map.CanClearTopology())
    {
        await map.ClearTopologyAsync();
    }

Map Topology Graph

A topology graph is an ordered graph of topologically connected edges and nodes; an in-memory planar representation of the geometries participating in the topology. Edges define shared linear boundaries, and nodes define connectivity across endpoints and intersections. The topology graph can be traversed by iterating through the connectivity between the topology edges and topology nodes.

Map Topology and geodatabase topology differ in the fact that a map topology operates on all features in the editable layers in a map, whereas a geodatabase topology limits topological editing to features participating in the rules of the topology.

Build a map topology graph using the extent of the MapView by calling MapView.BuildMapTopologyGraph().

  private async Task BuildGraphWithActiveView()
  {
    await QueuedTask.Run( () =>
    {
      //Build the map topology graph
      MapView.Active.BuildMapTopologyGraph<TopologyDefinition>(async topologyGraph =>
      {
        //Getting the nodes and edges present in the graph
        var topologyGraphNodes = topologyGraph.GetNodes();
        var topologyGraphEdges = topologyGraph.GetEdges();

        foreach (var node in topologyGraphNodes)
        {
          // do something with the node
        }
        foreach(var edge in topologyGraphEdges)
        { 
          // do something with the edge
        }

        MessageBox.Show($"Number of topo graph nodes are:  {topologyGraphNodes.Count}.\n" +
                         " Number of topo graph edges are {topologyGraphEdges.Count}.", "Map Topology Info");
      });
    });
  }

Note that the lifetime of the TopologyGraph is scoped to that of the client callback itself. Once the execution flow has returned from the callback, the TopologyGraph object will be disposed, and attempting to use the TopologyGraph object outside of the callback scope will raise an ArcGIS.Core.ObjectDisconnectedException.

Once you have the topology graph, navigate the nodes and edges to find features linked to specific features.

  private Task FindLinkedFeatures(Feature feature)
  {
    List<FeatureInfo> listOfLinkedfeatures = new List<FeatureInfo>();
    return QueuedTask.Run(() =>
    {
      MapView.Active.BuildMapTopologyGraph<TopologyDefinition>(async topologyGraph =>
      {
          var nodes = topologyGraph.GetNodes(feature); //Topology nodes via that feature
               
          foreach (TopologyNode node in nodes)
          {
            var edges = node.GetEdges();

            var parent = node.GetParentFeatures();
            foreach (var p in parent)
            {
              //Skipping the currently selected feature so as not to list it in the pane
              if (p.FeatureClassName.Equals(selectedFeat.Key.ToString()) && 
                               p.ObjectID.Equals(selectedFeature.GetObjectID()))
                continue;
              if (!listOfLinkedfeatures.Contains(p))
                listOfLinkedfeatures.Add(p);
            }
          }
      });
      System.Diagnostics.Debug.WriteLine(
        $"Number of topologically connected features are:  {listOfLinkedfeatures.Count}.");
    });
  }

You can find a sample related to map topology graphs here.

UI controls

Geometry Vertices

The Geometry control provides a UI for viewing vertices. The GeometryMode dependency property provides two options to configure the control - set it to GeometryMode.Geometry to view the vertices of a geometry or GeometryMode.Sketch to view the vertices of a sketch.

In GeometryMode.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. This geometry can be one 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.

 <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 mode, 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 canceled, 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.

Inspector Grid

A UI for displaying the field names and values of a row or set of rows in a grid-like manner can be achieved using the Inspector class. Create the inspector in the normal manner and then use the CreateEmbeddableControl method to obtain a View and ViewModel pair. Bind the View to a ContentPresenter on a dockpane or WPF control. Finally, use the Inspector.Load (or Inspector.LoadAsync) methods to see the attributes for the row or rows loaded.

Here is some sample code for how to achieve this.

   private EmbeddableControl _inspectorViewModel = null;
   private UserControl _inspectorView = null;
   private Inspector _inspector = null;

   /// <summary>
   /// Create the inspector and inspector grid.
   /// </summary>
   private void CreateControl()
   {
      // if inspector already exists, don't create it again
      if (_inspector!= null)
        return;

      // create a new instance for the inspector
      _inspector= new Inspector();
   
      // create an embeddable control from the inspector class
      var icontrol = _inspector.CreateEmbeddableControl();

      // get view and viewmodel from the inspector
      InspectorViewModel = icontrol.Item1;
      InspectorView = icontrol.Item2;
   }

   /// <summary>
   /// Access to the view model of the inspector
   /// </summary>
   public EmbeddableControl InspectorViewModel
   {
      get => _inspectorViewModel; 
      set
      {
         if (value != null)
         {
            _inspectorViewModel = value;
            // open
            _inspectorViewModel.OpenAsync();

         }
         else if (_inspectorViewModel != null)
         {
            // close the before nulling
            _inspectorViewModel.CloseAsync();
            _inspectorViewModel = value;
         }
         NotifyPropertyChanged(nameof(InspectorViewModel));
      }
   }

   /// <summary>
   /// Property for the inspector UI.
   /// </summary>
   public UserControl InspectorView
   {
      get => _inspectorView; }
      set => SetProperty(ref _inspectorView, value); }
   }

   /// <summary>
   /// Create and load the inspector grid.
   /// </summary>
   public void LoadInspector()
   {
      // find the first layer in the map
      var layer = MapView.Active.Map.GetLayersAsFlattenedList().FirstOrDefault();
      if (layer == null)
        return;

      // create the control
      CreateControl();
      // load a feature
      LoadInspector(layer, 1);
   }

   /// <summary>
   /// Loads a single row into the inspector.
   /// </summary>
   public void LoadInspector(MapMember mapMember, long oid)
   {
      if (_inspector== null)
        return;

      _inspector.LoadAsync(mapMember, oid);
   }
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    ... 

    <ContentPresenter Grid.Row="1" Content="{Binding InspectorView}"></ContentPresenter>
  </Grid>

InspectorEmbeddableControl

A more complete sample using the Inspector as an embeddable control within a MapTool can be found at InspectorTool.

Inspector Provider

From ArcGIS Pro 3.1 onwards, you can use the InspectorProvider to create a more customized version of the inspector grid. The InspectorProvider allows you to decide which fields are displayed, editable and highlighted, what order fields are displayed in, along with providing custom validation and overriding field name display.

First, define a class which derives from InspectorProvider to specify the customization details. Then instantiate that class and use the Create method to obtain an Inspector. Once the Inspector exists use the previously outlined CreateEmbeddableControl method to obtain the View and ViewModel pair.

Here is an example of a custom inspector provider:

    public class MyProvider : InspectorProvider
    {
        private System.Guid guid = System.Guid.NewGuid();
        internal MyProvider()
        {
        }
        public override System.Guid SharedFieldColumnSizeID()
        {
            return guid;
        }

        public override string CustomName(Attribute attr)
        {
            //Giving a custom name to be displayed for the field FeatureID
            if (attr.FieldName == "FeatureID")
                return "Feature Identification";

            return attr.FieldName;
        }
        public override bool? IsVisible(Attribute attr)
        {
            //The field FontStyle will not be visible
            if (attr.FieldName == "FontStyle")
                return false;

            return true;
        }
        public override bool? IsEditable(Attribute attr)
        {
            //The field DateField will not be editable
            if (attr.FieldName == "DateField")
                return false;

            return true;
        }
        public override bool? IsHighlighted(Attribute attr)
        {
            //ZOrder field will be highlighted in the feature inspector grid
            if (attr.FieldName == "ZOrder")
                return true;

            return false;
        }

        public override IEnumerable<Attribute> AttributesOrder(IEnumerable<Attribute> attrs)
        {
            //Reverse the order of display
            var newList = new List<Attribute>();
            foreach (var attr in attrs)
            {
                newList.Insert(0, attr);
            }
            return newList;
        }

        public override bool? IsDirty(Attribute attr)
        {
            //The field will not be marked dirty for FeatureID if you enter the value -1
            if ((attr.FieldName == "FeatureID") && (attr.CurrentValue.ToString() == "-1"))
                return false;

            return base.IsDirty(attr);
        }

        public override IEnumerable<ArcGIS.Desktop.Editing.Attributes.Attribute.ValidationError> Validate(Attribute attr)
        {
            var errors = new List<ArcGIS.Desktop.Editing.Attributes.Attribute.ValidationError>();

            if ((attr.FieldName == "FeatureID") && (attr.CurrentValue.ToString() == "2"))
                errors.Add(ArcGIS.Desktop.Editing.Attributes.Attribute.ValidationError.Create("Value not allowed", ArcGIS.Desktop.Editing.Attributes.Severity.Low));

            if ((attr.FieldName == "FeatureID") && (attr.CurrentValue.ToString() == "-1"))
                errors.Add(ArcGIS.Desktop.Editing.Attributes.Attribute.ValidationError.Create("Invalid value", ArcGIS.Desktop.Editing.Attributes.Severity.High));

            return errors;
        }
    }

And a snippet showing how to instantiate the custom provider, obtain the Inspector and create the embeddable control.

    // instantiate the custom provider
    var provider = new MyProvider();
    // otain an inspector
    Inspector _featureInspector = provider.Create();
    //Create the embeddable control from the inspector class to display on the pane
    var icontrol = _featureInspector.CreateEmbeddableControl();

    ... 


    // load the inspector with a row
    await _featureInspector.LoadAsync(layer, oid);

    // when accessing the attributes, the custom provider settings are honored
    var attribute = _featureInspector.Where(a => a.FieldName == "FontStyle").FirstOrDefault();
    var visibility = attribute.IsVisible; // Will return false

    attribute = _featureInspector.Where(a => a.FieldName == "DateField").FirstOrDefault();
    var editability = attribute.IsEditable; // Will return false

    attribute = _featureInspector.Where(a => a.FieldName == "ZOrder").FirstOrDefault();
    var highlighted = attribute.IsHighlighted; // Will return true
      

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; }


  // Gets and sets whether feature linked annotation automatically follows 
  // linked line features.
  public bool AutomaticallyFollowLinkedLineFeatures
  // Gets and sets whether feature linked annotation automatically follows 
  // linked polygon features.
  public bool AutomaticallyFollowLinkedPolygonFeatures
  // Gets and sets whether feature linked annotation uses placement properties 
  // defined in the annotation class.
  public bool UseAnnotationPlacementProperties
  // Gets and sets the feature linked annotation follow mode. This describes 
  // how new annotation aligns to the line or boundary feature it is following.
  public AnnotationFollowMode AnnotationFollowMode
  // Gets and sets the feature linked annotation placement mode for new annotation 
  // relative to the direction of the line or boundary feature it is following.
  public AnnotationPlacementMode AnnotationPlacementMode


  // 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();

  //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 an exception 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 an exception 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...
  var def_reg_unsel = 
    options.GetDefaultVertexSymbolOptions(VertexSymbolType.RegularUnselected);
  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
{
  //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; }
  //Gets and sets the value indicating if new geodatabase connections default to
  //traditional versioned.
  public bool IsTraditional { 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;
⚠️ **GitHub.com Fallback** ⚠️