Python Addins with comtypes/ArcObjects and Undo/Redo

1134
5
11-13-2013 10:23 AM
KerryAlley
Occasional Contributor
I believe there is a simple solution to the following dilemma.  Any insights/solutions would be greatly appreciated!

I have several Python AddIns that edit features in versioned SDE feature classes.  I want to be able to run these AddIns from ArcMap within an ongoing editing session, and to have the ability to undo/redo these edits using ArcMap???s Undo/Redo buttons.   My hope was to leverage comtypes in an AddIn to gain access to the ongoing ArcMap editing session (this works as expected), and that the AddIn edits would appear on the undo stack as long as I explicitly coded for start and stop edit operations (undo isn't working).  What else do I need to do to make addin edits undoable?  Do I need to fetch the existing Operation Stack and work with that?

This is a rough outline of the editing steps in the Addin, but I can provide the actual code and more detailed explanation if necessary:
edit = getIWorkspaceEditUsingComtypes()   #(this function returns IWorkspaceEdit successfully)
edit.StartEditOperation  
??? #(python code creating a feature using a Python Insert Cursor)
edit.StopEditOperation

Other observations:  The addin edits are permanent even if I don???t save my edits.  However, I did notice that if I make an edit manually and then run the AddIn, hitting the Undo button undoes the manual edit and the addin edit disappears as well.
Tags (2)
0 Kudos
5 Replies
T__WayneWhitley
Frequent Contributor
I'm interested in the rest of your code, having lately been trying to employ comtypes - I'm rusty with ArcObjects, specifically the editor objects and a little troubled with your statement "The addin edits are permanent even if I don�??t save my edits."  It appears the comtypes part is functional, but seems maybe without 1st explicitly targeting a version (as in your manual edits), maybe your code is somehow 'bypassing' the stack??  I'd need to see the rest of your code.  Undo should otherwise work if you can 'point' to a specific part of the stack (since you were able to do it with the undo button in part of your experimentation).

....this is curious and could be relevant to you, over on Stack Exchange:
http://gis.stackexchange.com/questions/3072/how-do-i-get-the-current-undo-and-redo-item-in-arcmap-op...

-Wayne
0 Kudos
KerryAlley
Occasional Contributor
That was a helpful link Wayne, especially if I end up having to write code to put edit operations on the operation stack! 

I assume that my issue reflects my shallow understanding of ArcObjects, but I guess there might actually be an unexpected consequence of using the Python addin framework.  If I remove all references to the Editor from the addin, it returns errors when run outside of an editing session (expected when editing SDE data).  However, if run from within an ongoing edit session, it runs as expected except that the features are not undoable and are permanent (ending without saving is futile).

I forgot to mention that this particular addin has two buttons, one to select a feature, the other to create features.  The undo button will actually select, in reverse order, the features that had been selected using that addin button (and usually an adjacent feature as well for good measure, for who knows what reason). The redo button reverses the sequence of selected features, but without including any adjacent features.    Another stack puzzle. 🙂  And further evidence that Python addins have a weird way of interacting with operation stacks!

I've pasted my addin script below.  I've also pasted the function "Snippets.ArcMap_GetIworkspaceEdit()" that I call from the addin.  That function is a very slightly modified version of the Mark Cederholm's function "Snippets.ArcMap_GetEditWorkspace()" from his Snippets.py script available from this page.  My function calls other functions from Mark's Snippets.py as well.

Addin Script:
#ShieldToolComtypes_addin.py
#KAlley 2013_11_11
#VT Agency of Transportation
#
#Based on ShieldTool_addin.py (KAlley 2013-03-12)
#In this script, lines of code relevent to using arcpy.da.Editor are commented out

import arcpy, time, os, sys, math
sys.path.append("V:\Projects\Shared\kalley\PythonScripts")
import comtypes, Snippets
import pythonaddins

class SelectRoadArcClass(object):
    """Implementation for TwoInputsAddin_addin_B.tool (Tool)"""
    def __init__(self):
        self.enabled = True
        self.shape = "NONE" # Can set to "Line", "Circle" or "Rectangle" for interactive shape drawing and to activate the onLine/Polygon/Circle event sinks.

    def onMouseDownMap(self, x, y, button, shift):
 #start_time = time.time() #only used to compare run-times of different versions of the script
 arcpy.env.overwriteOutput = True
 
 path = r'V:\Projects\Shared\kalley\ToolDevelopment\ShieldToolComtypes\Install'
 print "Comtypes Version of the Shield Tool AddIn"

 #rdsmall_dataSource = r"GDB_HMS.HMSADMIN.rdsmall_arc"
 rdsmall_dataSource = r"GDB_HMSDev.HMSADMIN.rdsmall_arc_kalley"

 mxd = arcpy.mapping.MapDocument('current')
 for lyr in arcpy.mapping.ListLayers(mxd):
     if lyr.supports("dataSource"):
  if str(lyr.dataSource).endswith(rdsmall_dataSource):
      rdsmall_lyr = lyr

 point = arcpy.Point()
 point.X = x
 point.Y = y
 pointGeometry = arcpy.PointGeometry(point)
 arcpy.SelectLayerByLocation_management(rdsmall_lyr, "WITHIN_A_DISTANCE", pointGeometry, "40 Meters", "NEW_SELECTION")
 #print time.time() - start_time, "seconds"


class CreateShieldClass(object):
    """Implementation for TwoInputsAddin_addin_A.tool (Tool)"""
    def __init__(self):
 self.enabled = True
 self.cursor = 3
 self.shape = "LINE" # Can set to "Line", "Circle" or "Rectangle" for interactive shape drawing and to activate the onLine/Polygon/Circle event sinks.
    def onLine(self, line_geometry):
 start_time = time.time() #only used to compare rough run-times of different versions of the script
 arcpy.env.overwriteOutput = True
 
 #Hard-wiring the exact feature classes that will be edited with shield tool:
 ##VTrans Data:
 ##shield_points_dataSource = r"GDB_HMS.HMSADMIN.hms_shields_points"
 ##shield_arcs_dataSource = r"GDB_HMS.HMSADMIN.hms_shields_arcs"
 ##rdsmall_dataSource = r"GDB_HMS.HMSADMIN.rdsmall_arc"
 #Development Sandbox copy of VTrans data:
 shield_points_dataSource = r"GDB_HMSDev.HMSADMIN.hms_shields_points_kalley"
 shield_arcs_dataSource = r"GDB_HMSDev.HMSADMIN.hms_shields_arcs_kalley"
 rdsmall_dataSource = r"GDB_HMSDev.HMSADMIN.rdsmall_arc_kalley"

 #mxd = arcpy.mapping.MapDocument(r"K:\ToolDevelopment\ToolDevelopmentSDE.mxd") #for testing script independently of add-in
 mxd = arcpy.mapping.MapDocument('current')

 #for new shields to appear in current mxd, this script must refer to layers in the current mxd
 lyrs = arcpy.mapping.ListLayers(mxd)
 for lyr in arcpy.mapping.ListLayers(mxd):
     if lyr.supports("dataSource"):
  if str(lyr.dataSource).endswith(shield_points_dataSource):
      shield_points_lyr = lyr
  if str(lyr.dataSource).endswith(shield_arcs_dataSource):
      shield_arcs_lyr = lyr
  if str(lyr.dataSource).endswith(rdsmall_dataSource):
      rdsmall_lyr = lyr
 if not (shield_points_lyr and shield_arcs_lyr and rdsmall_lyr):
     print "Shield Not Created..."
     print "Must have at least 3 layers in this mxd: shield points, shield arcs, and rdsmall"

 #only allow shield creation if a single road arc is selected
 num_road_arcs = int(arcpy.GetCount_management(rdsmall_lyr).getOutput(0))
 if num_road_arcs == 1:
     ##Code used if using arcpy.da.Editor
     ##workspace = rdsmall_lyr.workspacePath  #for  .gdb *and* SDE connection (refers to .sde file in DatabaseConnections)
     ##arcpy.env.workspace = workspace
     ##edit_py = arcpy.da.Editor(workspace)
     ##edit_py.startEditing() #with undo, multiuser mode
     
     #Code used if accessing preexisting editing session with comtypes/ArcObjects
     edit = Snippets.ArcMap_GetIWorkspaceEdit(bStandalone=False) 
     if edit is None:
  print "Must be in an editing session to use this tool"
  return
     
     #identify maximum SHI_GRP_ID value currently used in shields
     max_id = sorted(arcpy.da.SearchCursor(shield_points_lyr.dataSource, "SHI_GRP_ID"), reverse = 1)[0][0]

     #collect selected road arc attributes
     rdsmall_values = sorted(arcpy.da.SearchCursor(rdsmall_lyr, ["UA", "FAID_S", "CTCODE", "RTNUMBER", "AOTCLASS"]))

     #initialize the new line as the input line_geometry (will change if fix_length == True)
     new_line = line_geometry
    
     #create shield point and populate attributes
     new_points_Cur = arcpy.da.InsertCursor(shield_points_lyr, ["SHAPE@XY", "UA", "FAID_S", "CTCODE", "RTNO", "AOTCLASS", "SHI_GRP_ID", "ISVISIBLE", "SUBINSET"])
     test = (new_line.firstPoint,) + tuple(rdsmall_values) + (max_id + 1,)
     test_tuple = ((test[0].X, test[0].Y), test[1][0], test[1][1], str(test[1][2]), str(test[1][3]), test[1][4], test[2], 1, "N")
     ##edit_py.startOperation()
     edit.StartEditOperation
     new_points_Cur.insertRow(test_tuple)
     ##edit_py.startOperation()
     edit.StopEditOperation
     del new_points_Cur

     #create shield arc and populate SHI_GRP_ID
     new_arc_Cur = arcpy.da.InsertCursor(shield_arcs_lyr.dataSource, ["SHAPE@", "SHI_GRP_ID"])
     ##edit_py.startOperation()
     edit.StartEditOperation
     new_arc_Cur.insertRow([new_line, max_id + 1])
     ##edit_py.stopOperation()
     edit.StopEditOperation
     del new_arc_Cur

     ##edit_py.stopEditing(True) #save changes
     arcpy.SelectLayerByAttribute_management(rdsmall_lyr, "CLEAR_SELECTION")
     arcpy.RefreshActiveView()
 else:
     print "Shield Not Created..."
     print "Must select a single arc from rdsmall"
 #print time.time() - start_time, "seconds"


function Snippets.ArcMap_GetIWorkspaceEdit:
def ArcMap_GetIWorkspaceEdit(bStandalone=False):

    GetDesktopModules()
    if bStandalone:
        InitStandalone()
        pApp = GetApp()
    else:
        pApp = GetCurrentApp()
    GetModule("esriEditor.olb")
    import comtypes.gen.esriSystem as esriSystem
    import comtypes.gen.esriEditor as esriEditor
    import comtypes.gen.esriGeoDatabase as esriGeoDatabase
    pID = NewObj(esriSystem.UID, esriSystem.IUID)
    pID.Value = CLSID(esriEditor.Editor)
    pExt = pApp.FindExtensionByCLSID(pID)
    pEditor = CType(pExt, esriEditor.IEditor)
    if pEditor.EditState == esriEditor.esriStateEditing:
        pWS = pEditor.EditWorkspace
        pWSE = CType(pWS, esriGeoDatabase.IWorkspaceEdit)
        pDS = CType(pWS, esriGeoDatabase.IDataset)
        print "Workspace name: " + pDS.BrowseName
        print "Workspace category: " + pDS.Category
        return pWSE
    return
 
0 Kudos
KerryAlley
Occasional Contributor
I've been stubbornly chipping away at this issue so I have more information, and a more specific question:

Regardless of whether I attempt to start edit operations via IEditor or IWorkspaceEdit2, the attempts are not successful (because IWorkspaceEdit2.InEditOperation returns "False").  I would really like to know why "IEditor.StartOperation" doesn't seem to work, and what I need to do differently.  Any suggestions?  I'm pretty sure I correctly referenced IEditor, because IEditor.EditState is "esriStateEditing", and I get the correct value from IEditor.EditWorkspace.

Since I'm creating an "editing customization" for ArcMap, it seems that I should be using IEditor rather than IWorkspaceEdit2 (according to this help page and this help page).

Because I'm editing SDE feature classes (initiated from the Editor toolbar menu in ArcMap), successful edit operations should automatically be placed on the operation stack, and workspace and versions don't have to be specified in code.

Working with ArcObjects sure is humbling!!

Kerry
0 Kudos
DavidHollema
New Contributor III
Have you ever resolved this?  We too are building a Python add-in for a custom edit experience within ArcMap.  We want to leverage as much OOTB functionality in ArcMap as possible, including the edit session and undo/redo capabilities.  Any more info since this post?
0 Kudos
KerryAlley
Occasional Contributor
Alas, I gave up and rewrote the addins in VB.NET.

Although the VB.NET addins run significantly faster than the Python addins, I am still eager to find a Python solution to the problem.  I'll be at the Esri Developer's Summit in a couple weeks, and will seek out the Comtypes and ArcObjects gurus to pick their brains for a solution.  I'll also be checking to see if/when the ongoing editing capability will be added to the Python Addin framework.

Kerry
0 Kudos