How to remove a layer from the undo stack, which has been removed from the map.

187
8
Jump to solution
08-10-2022 10:13 AM
DaveLewis3
Occasional Contributor

I am working in ArcGIS Pro 2.9 and I am attempting to remove a layer from the undo stack after the layer has been removed from the map.  I do not want the user to have the ability to undo the removal of the layer and bring it back into the map.  Once the layer has been removed then that is final.  With that being said, I am successfully using the OperationManager object to remove the layer from the undo stack if the layer is removed programmatically.  However, the same code does not work if the layer is removed from the GUI using the TOC.  This doesn't make any sense that I could do what I need programmatically, but not from the GUI.  I am using the LayersRemovedEvent to check for whether the layer has been added to the undo stack.  It does when I call "map.RemoveLayer," but not when the event fires from a GUI removal from the TOC.  Is this just the way that ArcGIS Pro is designed?  Does the GUI removal of the layer from the TOC not add the layer to the undo stack until after the LayersRemovedEvent fires?  If so, is there another way to get to the layer removal operation and remove it from the undo stack after the event?

The following is my method that removes operations from the undo stack:

public static void RemoveUndoOperations(string opName)
{
    if (MapView.Active?.Map == null) { return; }

    OperationManager opManager = MapView.Active.Map.OperationManager;
    List<Operation> ops = opManager.FindUndoOperations(o => o.Name.Contains(opName));
    foreach (Operation op in ops)
    {
        opManager.RemoveUndoOperation(op);
    }
}

0 Kudos
3 Solutions

Accepted Solutions
CharlesMacleod
Esri Regular Contributor

Hi Dave,

Internally, "Do" or "DoAync" is called on the OperationManager to execute the given operation - in this case the removing of a layer. Within the context of the Do or DoAsync, the operation is executed - i.e. removal of the layer(s) which, in turn, fires all relevant events - to include "Removing" and "Removed" events - _then_ , assuming completion was successful, the operation is added to the undo stack to allow it to be undone. Do now returns and/or DoAsync completes. An unsuccessful operation, conversely, would not be added (to be undone).

The best u can do, as u r trying to catch the operation "as it/when it" is added to the stack is to try a slight delay. So, something like:

 

//elsewhere - register for layers removed
ArcGIS.Desktop.Mapping.Events.LayersRemovedEvent.Subscribe((args) => {
  DelayedListUndoOperations("LayersRemovedEvent",
    args.Layers.Select(x => $"Remove layer: {x.Name}").ToList(),
    1000);
});

//delay before reading operations
private async void DelayedListUndoOperations(string when, 
                       List<string> opNames, int milliseconds) {
   //non-blocking wait
   await Task.Delay(milliseconds);
   ListUndoOperations($"delayed {milliseconds}, {when}", opNames);
}

private void ListUndoOperations(string when, List<string> opNames) {
  var opManager = MapView.Active?.Map?.OperationManager;
  var ops =
    opManager?.FindUndoOperations(o => !string.IsNullOrEmpty(o.Name)) ??
        new List<Operation>();
  System.Diagnostics.Debug.WriteLine(when);
  if (ops.Count() == 0) {
    System.Diagnostics.Debug.WriteLine("No Undo operations");
  }
  foreach (var op in ops) {
    var match = opNames.Contains(op.Name) ? " (match)" : "";
    System.Diagnostics.Debug.WriteLine($"{op.Name}{match}");
  }
}

 

 

There is still an edge case however, where, depending on the delay, the user still has the opportunity - albeit brief - to execute the undo.

View solution in original post

Wolf
by Esri Regular Contributor
Esri Regular Contributor

I found that this works in my test code and doesn't require a timer, however, you have to try this in your environment in order to verify that it works for you.  Don't use a debugger and breakpoints when you debug this snippet it will change the thread execution sequence.  Also as mentioned above your FindUndoOperations logic still requires some work.  Charlie mentioned that various async operations still have to complete before OperationManager's queue reflects the true state so i decided to try executing the OperationManager related code on the UI thread once the UI thread is idle again, clearly a workaround, but it seemed to work for me.

public static void RemoveUndoOperations(string opName)
{
  if (MapView.Active?.Map == null) { return; }
  QueuedTask.Run(async () =>
  {
    await System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
    {
      OperationManager opManager = MapView.Active.Map.OperationManager;
      List<Operation> ops = opManager.FindUndoOperations(o => o.Name.Contains(opName));
      foreach (Operation op in ops)
      {
        var opDetail = $@"Operation: {op.Name} canundo: {op.CanUndo} cat: {op.Category} sub: {op.SubCategory}";
        System.Diagnostics.Trace.WriteLine(opDetail);
        opManager.RemoveUndoOperation(op);
      }
    }), DispatcherPriority.ApplicationIdle);
  });
}

 

View solution in original post

DaveLewis3
Occasional Contributor

Wolf,

Thank you for your code snippet as well.  I am not sure which solution to use, your solution or Charlie's.  I wish I could accept both as a solution. Great work guys!

By the way, do you have any idea as to what I need to do to update my FindUndoOperations code to get around that problem of deleting multiple layers at the same time?

Dave

View solution in original post

0 Kudos
8 Replies
Wolf
by Esri Regular Contributor
Esri Regular Contributor

I was able to duplicate the problem and you are correct in that the Undo operation doesn't appear to get added until after the LayersRemovedEvent fires.   I am trying to find a workaround, but you will have to modify your .FindUndoOperations logic as well.  If you remove more than one layer at the same time the Operation Name doesn't contain the layer name.  Also, any other operation (like adding a layer) would be removed from the stack as well.  

0 Kudos
CharlesMacleod
Esri Regular Contributor

Hi Dave,

Internally, "Do" or "DoAync" is called on the OperationManager to execute the given operation - in this case the removing of a layer. Within the context of the Do or DoAsync, the operation is executed - i.e. removal of the layer(s) which, in turn, fires all relevant events - to include "Removing" and "Removed" events - _then_ , assuming completion was successful, the operation is added to the undo stack to allow it to be undone. Do now returns and/or DoAsync completes. An unsuccessful operation, conversely, would not be added (to be undone).

The best u can do, as u r trying to catch the operation "as it/when it" is added to the stack is to try a slight delay. So, something like:

 

//elsewhere - register for layers removed
ArcGIS.Desktop.Mapping.Events.LayersRemovedEvent.Subscribe((args) => {
  DelayedListUndoOperations("LayersRemovedEvent",
    args.Layers.Select(x => $"Remove layer: {x.Name}").ToList(),
    1000);
});

//delay before reading operations
private async void DelayedListUndoOperations(string when, 
                       List<string> opNames, int milliseconds) {
   //non-blocking wait
   await Task.Delay(milliseconds);
   ListUndoOperations($"delayed {milliseconds}, {when}", opNames);
}

private void ListUndoOperations(string when, List<string> opNames) {
  var opManager = MapView.Active?.Map?.OperationManager;
  var ops =
    opManager?.FindUndoOperations(o => !string.IsNullOrEmpty(o.Name)) ??
        new List<Operation>();
  System.Diagnostics.Debug.WriteLine(when);
  if (ops.Count() == 0) {
    System.Diagnostics.Debug.WriteLine("No Undo operations");
  }
  foreach (var op in ops) {
    var match = opNames.Contains(op.Name) ? " (match)" : "";
    System.Diagnostics.Debug.WriteLine($"{op.Name}{match}");
  }
}

 

 

There is still an edge case however, where, depending on the delay, the user still has the opportunity - albeit brief - to execute the undo.

DaveLewis3
Occasional Contributor

Charlie,

Thank you for the code snippet.  Everything works great!  This is just the kind of thing I was looking for (very straightforward).

Dave

0 Kudos
Wolf
by Esri Regular Contributor
Esri Regular Contributor

I found that this works in my test code and doesn't require a timer, however, you have to try this in your environment in order to verify that it works for you.  Don't use a debugger and breakpoints when you debug this snippet it will change the thread execution sequence.  Also as mentioned above your FindUndoOperations logic still requires some work.  Charlie mentioned that various async operations still have to complete before OperationManager's queue reflects the true state so i decided to try executing the OperationManager related code on the UI thread once the UI thread is idle again, clearly a workaround, but it seemed to work for me.

public static void RemoveUndoOperations(string opName)
{
  if (MapView.Active?.Map == null) { return; }
  QueuedTask.Run(async () =>
  {
    await System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
    {
      OperationManager opManager = MapView.Active.Map.OperationManager;
      List<Operation> ops = opManager.FindUndoOperations(o => o.Name.Contains(opName));
      foreach (Operation op in ops)
      {
        var opDetail = $@"Operation: {op.Name} canundo: {op.CanUndo} cat: {op.Category} sub: {op.SubCategory}";
        System.Diagnostics.Trace.WriteLine(opDetail);
        opManager.RemoveUndoOperation(op);
      }
    }), DispatcherPriority.ApplicationIdle);
  });
}

 

DaveLewis3
Occasional Contributor

Wolf,

Thank you for your code snippet as well.  I am not sure which solution to use, your solution or Charlie's.  I wish I could accept both as a solution. Great work guys!

By the way, do you have any idea as to what I need to do to update my FindUndoOperations code to get around that problem of deleting multiple layers at the same time?

Dave

0 Kudos
Wolf
by Esri Regular Contributor
Esri Regular Contributor

You have to check if the operation name starts with the string "Remove" and ends with your layer name or the string "layers".   If you have multi-language support, you have to use the proper translation of those strings (best implemented through the corresponding language resource file).

This will support the single layer delete and deletion of multiple layers:

Wolf_0-1660235635034.png

 

0 Kudos
DaveLewis3
Occasional Contributor

I am going to have to see if there is some other solution to removing multiple layers, because I only want to remove the undo capability on certain types of layers that have been removed from the map.  I am specifically dealing with certain image service layers that I don't want to have undo removal capability.  All other types of layers are ok to be removed and to then be able to undo the removal and restore them to the map.  Therefore, as you can see, having multiple layers removed at the same time is a rather serious concern.  I don't know what the user has selected from the TOC to remove.  I may want all the layers to have undo capability removed or just one/some layers depending on which ones are image service layers.  This is my dilemma.

0 Kudos
Wolf
by Esri Regular Contributor
Esri Regular Contributor

Regardless of knowing that a multi-layer removal included an 'allowed' layer, you can only remove the complete undo operation from OperationManager.   It's not possible to just remove one part of the undo operation (i.e., the layer you don't want to have re-added again).  Since it's an all or nothing [removal from the Undo stack] scenario, I would suggest removing an undo operation if the LayersRemovedEvent  contained at least one of the layers that cannot be re-added to the map.  If the LayersRemovedEvent only contained layers that can be re-added don't do anything with the undo stack.

0 Kudos