ProConcepts Editing - kataya/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.

ArcGIS.Desktop.Editing.dll

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

In this topic

Editing in ArcGIS Pro

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

Editing Controls

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

Commands

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

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

Tools

Tools allow you to interact with the display in some manner. They can be used to define areas, create a sketch, or select features for an edit operation.

Sketch Tools

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

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

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

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

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

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

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

UseSelection

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

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

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

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

ActivateSelectAsync

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Construction Tools

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

You can use the ArcGIS Pro Construction Tool template in Visual Studio to create a construction tool. Configure the categoryRefID tag in the config.daml file for your tool to set the type of geometry the tool will be associated with.

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

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

EnableAndDisableEditing

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

ToggleEditing

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

Enable and Disable Editing via the API

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

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

Enable

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

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

    Project.Current.SetIsEditingEnabledAsync(true);
 }

Disable

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

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

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

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

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

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

Performing Edits in Pro

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

Edit Operations

Edit operations perform 3 key functions:

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

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

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

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

Here are 3 examples of editing using edit operations:

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

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

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

  //Execute the operations
  editOp.ExecuteAsync();

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

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

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

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

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

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

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

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

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

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

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

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

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

Chaining Edit Operations

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

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

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

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

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

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

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

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

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

Edit Operations and Utility Network Associations

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

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

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

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

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

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

   editOp.Create(poleAttachment);

   //Execute the EditOperation
   editOp.ExecuteAsync();

Refer to Utility Network Associations for more detail.

Edit Operation Callback

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

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

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

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

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

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

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

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

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

Edit Operation Callback Abort

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

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

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

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

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

Layer Editability

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

Feature Inspector and Working with Attributes

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

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

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

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

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

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

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

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

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

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

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

  //Elsewhere
  await inspector.ApplyAsync();

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

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

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

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

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

Editing Subtypes

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

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

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

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

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

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

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

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

Feature Templates

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

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

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

Accessing Feature Templates

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

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

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

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

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

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

Creating and Modifying Feature Templates

Feature templates are automatically generated for editable layers when they are added to a map. Templates are not regenerated if the renderer changes, for example, changing from single symbol to unique value. New templates can be created for a layer or copied and altered from an existing template. Creating and modifying templates is done through the CIM classes. New at ArcGIS Pro 2.2 is an extension method allowing template creation without needing the CIM. The CreateTemplate 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 CIMFeatureTemplate;
  //Could also create a new one here
  //var newTemplate = new CIMFeatureTemplate();

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

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

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

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

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

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

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

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

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

Removing Feature Templates

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

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

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

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

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

Annotation Feature Editing

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

Dimension Feature Editing

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

Snapping

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

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

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

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

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

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

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

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

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

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

Layer Snap Modes

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

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

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

The following examples illustrates the layer snap modes.

  // configure by layer

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

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


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

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


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


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

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


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


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

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

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

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

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

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

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

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

Snapping in Tools

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

internal class MyCustomTool1 : MapTool {

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

Snap Results

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

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

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

Edit Operations and Long and Short Transactions

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

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

Refer to the EditOperationType enum.

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

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

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

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

EditOperationType

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

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

EditOperationType by dataset at ArcGIS Pro 2.4 and earlier

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

EditOperationType by dataset at ArcGIS Pro 2.5+

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

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

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

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

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

Characteristics by dataset by participating EditOperationType at ArGIS Pro 2.5+

Dataset Cancel Edit Undo/Redo Save/Discard EditOperationType
Shape file Yes Yes Yes LONG
File GDB Yes Yes Yes LONG
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.

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

Edit Sessions

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

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

Save and Discard Edits

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

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

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

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

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

Checking for Unsaved Edits

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

Edit Events

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

EditStartedEvent

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

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

EditCompletingEvent

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

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

EditCompletedEvent

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

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

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

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

  private SubscriptionToken _token;

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

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

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

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

Row Events

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

           parentEditOp.Create(history, atts);
         }
      }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Sketch Events

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

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

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

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

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

SketchModifiedEvent

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

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

SketchCancelledEvent

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

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

BeforeSketchCompletedEvent

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

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

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

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

SketchCompletedEvent

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

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

UI controls

Geometry Vertices

The Geometry control provides a UI for viewing vertices.

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

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

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

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

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

geometryControlGeometry

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

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

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

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

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

geometryControlSketch

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

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