Undo/Redo Python AddIn edits on SDE feature classes?

3340
4
03-13-2013 12:12 PM
KerryAlley
Occasional Contributor
I have a Python AddIn that edits SDE feature classes.  I???d like to have the option to Undo/Redo edits made with the AddIn.  So far I???ve tried several approaches (below), and am wondering if anybody can shed some insight on my observations.  If you have any other ideas about how to undo/redo AddIn edits, let me know!  Alternatively, if this is not possible using Python AddIns, what would be my best approach to reproduce some VBA tools requiring user input that are now obsolete?

Unsuccessful attempts to undo Python AddIn edits: 
1) Managing the edit session from within the Addin.
Code structure:
edit = arcpy.da.Editor(workspace)
edit.startEditing() 
edit.startOperation()
Cur.insertRow([geometry])
edit.stopOperation()
edit.stopEditing(True) 
arcpy.SelectLayerByAttribute_management(layer, "CLEAR_SELECTION")

Observation:  When I run my AddIn, the Undo button activates with a single operation in the stack, and I can apparently undo the clear selection (but only if the initial selection was made using a tool in my AddIn rather than the standard ArcMap ???select by rectangle??? button).  I???m not surprised that the AddIn edits are not visible to the operation stack in ArcMap, but have no idea what the selection is doing there.  Any insights?

2) Alternatively, if I starting an edit session in ArcMap before running the AddIn (with the editing code commented out) I see this: The AddIn edits are undoable only incidentally when there are manually generated edits on the operation stack before the AddIn is run (i.e. undoing the last manual edit also undoes any AddIn edits that were made after the manual edits).
Tags (2)
0 Kudos
4 Replies
KerryAlley
Occasional Contributor
Based on a post in a similar thread:
http://forums.arcgis.com/threads/60725-Python-Addin-and-Undo-Redo?p=280913&viewfull=1#post280913
I now think that .Net is probably the way to go for addin editing flexibility. 

More addin editing observations in case someone finds them useful:
I've found that the behavior I observed in scenario #2 (in my previous post) is not consistently reproducible, and addins that edit SDE data only run reliably if the edit session is controlled from within the addin.   However, if I am already in an ArcMap editing session when I run my addin (addin with code controlling its own editing session), that the features are indeed created because the addin doesn't break until it reaches the line where I attempt to stop editing and save edits.  New features created by the "unsuccessful" run of the addin can then be seen with a "refresh", and saved via "Save Edits" in the ArcMap editing session, or discarded via "Stop Editing" without saving edits.
 
At least this means that if an absentminded user (like me) runs an addin editing tool when already in an editing session, the edits can be "salvaged" even if the addin doesn't finish its execution.  I'm reluctant to use "try" statements to bypass legitimate errors since they might be useful when the tool is used "properly."

Time to learn .Net!
0 Kudos
JohnDye
Occasional Contributor III
Kalley,

You could try directly referencing the .NET Editing Functions from your Addin. Jason Pardy did a really good blog post on this in August.

What you would need to do is directly reference the .NET ProgID for the builtin function you are looking for in the Config.xml. In your case, you need to alter your Python Addin button refID to directly reference the .NET ProgID for the Start/Stop/Save Edit Commands.Since you would then be directly accessing the .NET Editor functions, that should perform the edits in the fashion you desire.

Give it a read, I think it will solve your problem.

Hope it helps,
John
0 Kudos
KerryAlley
Occasional Contributor
Thanks for the tip John...putting ArcMap commands in buttons using the Config.xml file is a cool shortcut for commands that have a ProgID!

However, I haven't been able to figure out how to use buttons to allow me to undo edits made with the add-in.  As far as I can tell, the command buttons would be functionally equivalent to the buttons (or menu options) that are in ArcMap.  My limitation is that the arcpy.da.editor in an add-in is always a unique instance limited to the the scope of the add-in. Actually, it's limited to whichever class it was created in.  In short, I can't find a way to refer to an ArcMap editing session from a tool class, or a way to refer to a tool class's arcpy.da.editor editing session from outside the class.  If only editing sessions were properties of add-in tool classes...

I did consider using an add-in extension class because it can be aware of a series of ArcMap events before, during, and after an editing session within the class.  Unfortunately extensions don't have the same functions/methods as tool classes.  Even though an extension can access tool properties (e.g. change the "enabled" property), they are still two distinct classes and can't access the same arcpy.da.editor.

I'm exploring the possibility of using comtypes (accessing ArcObjects with Python wrappers), but it's still a little over my head so it's going slow!
0 Kudos
ThomasLaxson
New Contributor III

Since the edit stack is not preserved outside of a given add-in edit session, you can simply set a global variable that tracks the edits and is then accessible to other buttons.

For example, depending on the complexity of the edits performed by your add-in, you could have a global list to which you append information about the edits.

Here's an example of a simple edit button (it just buffers the selected features) with undo and redo buttons. Note that these are not the built-in undo/redo buttons, but Add-In buttons that you create. The edits here will be in a separate stack from the built-in edits.

import arcpy
import pythonaddins

MXD = arcpy.mapping.MapDocument('CURRENT')
FN_SHAPE = 'SHAPE@'

# Global variables that will store the edit histories forward and backward
undo_stack = list()
redo_stack = list()

class MakeEdits(object):
    def __init__(self):
        self.enabled = True
        self.checked = False

    def onClick(self):
        # Get the selected layer in the table of contents
        lyr = pythonaddins.GetSelectedTOCLayerOrDataFrame()

        # Get a count of selected features in the selected layer
        fid_set = arcpy.Describe(lyr).FIDSet
        if fid_set == '':
            count = 0
        else:
            count = len(fid_set.split(';'))

        # If at least one feature is selected
        if count > 0:
            # Enable global modification of the undo_stack
            global undo_stack

            # Get the name of the layer's OID field
            fn_oid = arcpy.Describe(lyr).OIDFieldName

            with arcpy.da.UpdateCursor(lyr, [fn_oid, FN_SHAPE]) as cur:
                for row in cur:
                    # Add the relevant details to the editing stack
                    undo_stack.append((lyr, fn_oid, row[0], row[1]))
                    # An arbitrary example edit
                    row[1] = row[1].buffer(500)
                    cur.updateRow(row)

            arcpy.RefreshActiveView()


class RedoEdits(object):
    def __init__(self):
        self.enabled = True
        self.checked = False

    def onClick(self):
        global undo_stack
        global redo_stack

        if len(redo_stack) > 0:
            # Get the last item in the edit stack
            edit = redo_stack.pop()
            # Clear any existing selection on the layer, or else the cursor may
            #   not hit the necessary feature
            arcpy.SelectLayerByAttribute_management(edit[0], 'CLEAR_SELECTION')
            # A where clause to select the most recently edited feature
            wc = '{} = {}'.format(edit[1], edit[2])

            with arcpy.da.UpdateCursor(edit[0], FN_SHAPE, wc) as cur:
                for row in cur:
                    # Update the undo edit stack
                    edit_out = list(edit[:-1])
                    edit_out.append(row[0])
                    undo_stack.append(edit_out)
                    # Apply the edit
                    row[0] = edit[3]
                    cur.updateRow(row)
                    break

        arcpy.RefreshActiveView()


class UndoEdits(object):
    def __init__(self):
        self.enabled = True
        self.checked = False

    def onClick(self):
        global undo_stack
        global redo_stack

        if len(undo_stack) > 0:
            edit = undo_stack.pop()

            arcpy.SelectLayerByAttribute_management(edit[0], 'CLEAR_SELECTION')
            wc = '{} = {}'.format(edit[1], edit[2])

            with arcpy.da.UpdateCursor(edit[0], FN_SHAPE, wc) as cur:
                for row in cur:
                    edit_out = list(edit[:-1])
                    edit_out.append(row[0])
                    redo_stack.append(edit_out)
                    row[0] = edit[3]
                    cur.updateRow(row)
                    break

        arcpy.RefreshActiveView()

        return