Registering multiple custom undo events in a command

Hi everyone,

I like to register a custom undo event handler multiple times during a command. Each time I pass the undo instructions as CustomUndoEventArgs. When Invoking Undo by pressing Ctrl-Z, the event handler is called several times as expected, but it receives each time the same argument.

public class CsUndoRedoSequence : Command
{

    public override string EnglishName
    {
        get { return "CsUndoRedoSequence"; }
    }

    static void OnUndo(object sender, Rhino.Commands.CustomUndoEventArgs e)
    {
        RhinoApp.WriteLine($"List of Objects when Undo Handler No. {e.Tag} ist called:");
        foreach (var obj in e.Document.Objects)
        {
            RhinoApp.WriteLine(obj.Id.ToString());
        }

        // Add Event Handler for Redo
        e.Document.AddCustomUndoEvent("Dummy Undo", OnUndo, e.Tag);
    }

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        // create a circle
        Circle circle;
        circle = new Circle(5);
        doc.Objects.AddCircle(circle);
        doc.Views.Redraw();

        // register first undo handler
        doc.AddCustomUndoEvent("Dummy Undo", OnUndo, 1);

        // create another circle
        circle = new Circle(10);
        doc.Objects.AddCircle(circle);
        doc.Views.Redraw();

        // register second handler
        doc.AddCustomUndoEvent("Dummy Undo", OnUndo, 2);

        return Result.Success;
    }
}

When I run the command and Undo and Redo, the output is the following:

Command: CsUndoRedoSequence

Command: _Undo

List of Objects when Undo Handler No. 1 ist called:

cca77a0e-b9f7-4dcb-8635-b007f783f596

f6b20c35-e18a-4a36-b513-4cdc11218664

List of Objects when Undo Handler No. 1 ist called:

f6b20c35-e18a-4a36-b513-4cdc11218664

Undoing CsUndoRedoSequence

Command: _Redo

List of Objects when Undo Handler No. 1 ist called:

f6b20c35-e18a-4a36-b513-4cdc11218664

List of Objects when Undo Handler No. 1 ist called:

cca77a0e-b9f7-4dcb-8635-b007f783f596

f6b20c35-e18a-4a36-b513-4cdc11218664

Redoing CsUndoRedoSequence

The list of objects the handler sees is correct, but each time it receives the same argument (integer 1). But it should be 1 and 2 as it is passed when registering the custom undo event.

Any idea how to solve this issue?

The targeted use of using multiple undo events is to implement proper undo for UserText and UserDictionary changes, since Rhino does not undo this. My workaround for the moment is postpone all user dictionary writing to the end of the command and put all the undo instruction into one custom undo event.

Thanks,

Samuel

Hi @samuel.hartmann,

The purpose of registering a custom undo event handling method with RhinoDoc.AddCustomUndoEvent is so you can undo changes to data managed by your own plug-in.

Never change any setting in the Rhino document or application from your undo event handling method. Rhino handles all changes to the application and document, and you will break the Undo/Redo commands if you make any changes to the application or document.

I’ve attach an example of how RhinoDoc.AddCustomUndoEvent should be used.

TestUndo.cs (2.5 KB)

Let me know if this helps.

– Dale

Hi Dale,

Thank you for your reply and sending me the code. The code works as long as you only register the UndoHandler once per Command. I extended the code with the Command TestEarnTwice. The command first earns 5 then it earns 10. It ends up to 15. Undo should decrease the value by 10 and then by 5 and result in 0. But Rhino does decrease the value two times by 5 and end up with a balance of 5, not 0:

Loading Rhino Render, version 1.50, May 26 2020, 07:11:30
Command: TestEarnTwice
New balance: 5
New balance: 15
Command: _Undo
New balance: 10
New balance: 5
Undoing TestEarnTwice

And regarding “Rhino handles all changes”: No, Rhino does not handle changes to user text and the user dictionary attached to the objects. It does restore this data when an object is deleted and restored by undo. But if a command just changes data in the user dictionary or the user text of objects, Rhino does not undo these changes.

Samuel

/// <summary>
/// TestEarnTwice command
/// </summary>
public class TestEarnTwice : Command
{
    public override string EnglishName => "TestEarnTwice";

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        // Earn 5
        double amount = 5.0;
        doc.AddCustomUndoEvent(EnglishName, TestCustomUndoHandler.OnCustomUndo, amount);
        var balance = TestBalanceHolder.Instance.Balance;
        balance += amount;
        TestBalanceHolder.Instance.Balance = balance;
        RhinoApp.WriteLine("New balance: {0}", balance);

        // Earn 10
        amount = 10.0;
        doc.AddCustomUndoEvent(EnglishName, TestCustomUndoHandler.OnCustomUndo, amount);
        balance = TestBalanceHolder.Instance.Balance;
        balance += amount;
        TestBalanceHolder.Instance.Balance = balance;
        RhinoApp.WriteLine("New balance: {0}", balance);

        return Result.Success;
    }
}

Hi @samuel.hartmann,

Rhino’s undo mechanism does not provide for multiple undo records per command iteration. If you want your command to provide multiple undos, then you’l need to code this up yourself. For example, Rhino’s Polyline command has it’s on in-command Undo option.

Can you provide me some code that is not working for you?

Thanks,

– Dale

Hi Dale,

I created the Command CsWriteUserText that writes some user text.20200611CsWriteUserText.cs (1.2 KB)

In Rhino I do the following:
I draw a line and select it.
Call the command CsWriteUserText
List all User Text entires of this line using GetUserText, this lists the entries as expected.
Call Undo by Ctrl-Z.
List again all User Text entries. The user text should be deleted, but it is still there.

This is the Rhino Command Log:

Loading Rhino Render, version 1.50, May 26 2020, 07:11:30
Command: Line
Start of line ( BothSides  Normal  Angled  Vertical  FourPoint  Bisector  Perpendicular  Tangent  Extension )
End of line ( BothSides )
1 curve added to selection.
Command: CsWriteUserText
Command: GetUserText
Text key <All keys>
2 attributes user strings.
  <Weather> cloudy
  <Temperature> 15°C
Command: _Undo
Undoing CsWriteUserText
Command: GetUserText
Text key <All keys>
2 attributes user strings.
  <Weather> cloudy
  <Temperature> 15°C

In the command I had to add a Dummy CustomUndoEvent for that does nothing. Without that, Rhino does not recognize that there is something to undo.
If this dummy Undo is not present, Rhino would instead of Undoing the CsWriteUserText command, go further back in history undo the previous command (in the example it would delete the line).

The same behaviour of Rhino with User Text I experience with the handling of UserDictionary.
There is also already a Issue created by you regarding the UserText Undo Topic:
https://mcneel.myjetbrains.com/youtrack/issue/RH-49758

Regards,

Samuel

The other topic is Custom Undo:

Dale wrote:

Rhino’s undo mechanism does not provide for multiple undo records per command iteration. If you want your command to provide multiple undos, then you’l need to code this up yourself. For example, Rhino’s Polyline command has it’s on in-command Undo option.

Implementing the possibility for the user to do Undo within a command as Polyline does it is not what I intend to to. I simply want to implement undo of my own data. A command typically does modifications of data in several locations on several objects (Rhino objects or own data objects). The clean method to implement undo for the own data is registering a custom undo handler each time a modification is done on an own data object. Rhino would then, when the user presses undo, call the undo handler in the right sequence (backwards and typically with Rhino document changes in between). Rhino lets you register multiple undohandler with AddCustomUndoEvent (it does not throw an Exception when you use it multiple times). During Undo Rhino also calls the number of Custom UndoHandlers registered. It does is it even in the right place (between Rhino handles its internal Undo Handlers on Objects). This is what my initial example in my first post shows. The problem is that Rhino always calles the first handler registered even when it should call the second.

Using the following very simple code illustrates that Rhino calls the wrong Handlers:

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        // register first undo handler
        doc.AddCustomUndoEvent("Handler 1", (sender, e) => { RhinoApp.WriteLine("Undo Handler 1 is called."); });

        // register second handler
        doc.AddCustomUndoEvent("Handler 2", (sender, e) => { RhinoApp.WriteLine("Undo Handler 2 is called"); });

        return Result.Success;
    }

The output is the following:

Command: CsMultipleUndoEvents
Command: _Undo
Undo Handler 1 is called.
Undo Handler 1 is called.
Undoing CsMultipleUndoEvents

I hope that Rhino improves there. The two ways for proper handling undo of own data should be to my opinion:

  1. Put your data as UserText orUserDictionary entry and let Rhino do the Undo/Redo job for you. Unfortunately Rhino does not the Undo/Redo on that data.
  2. Implement your own Undo with CustomUndoHandlers. Unfortunately Rhino calls the wrong handlers when several are undo changes are registered per command.

This leaves me with the only work around:

Record all changes on own data during a command. Register all undo to these changes at the end of the command. Not so elegant.

Samuel

Hi @samuel.hartmann,

Here is the property way to add attribute user text to an object, from a command, and have undo work:

protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
  var rc = RhinoGet.GetOneObject("Select an object for writing User Text.", false, ObjectType.AnyObject, out ObjRef objRef);
  if (rc != Result.Success)
    return rc;

  var obj = objRef.Object();
  if (obj == null)
    return Result.Failure;

  var attribute_copy = obj.Attributes.Duplicate();

  if (!attribute_copy.SetUserString("Weather", "cloudy"))
    return Result.Failure;
  if (!attribute_copy.SetUserString("Temperature", "15°C"))
    return Result.Failure;

  doc.Objects.ModifyAttributes(obj.Id, attribute_copy, true);

  return Result.Success;
}

– Dale

Thank you Dale for posting this solution. This works for me. The solution for my plug-in now is using user dictionaries together with Attributes.Duplicate() and ModifyAttributes(). With this, Rhino correctly handles undo of the user dictionary. And everytime a command of my plug-in runs I read all my data from the user dictionary. So I don’t need anymore to use AddCustomUndoEvent(). Anyway I would appriciate if Rhino improves the behaviour of AddCustomUndoEvent(), it does not behave as expected and makes it hard work to implement undo of own plugin-data.

Samuel

Handling undo with data attached to object using user dictionaries works well with the method proposed by Dale. However my PlugIn also stores data in user dictionaries attached to Layers. As the UserDictionary is directly a property of the Layer, it is not possible to use Dale’s proposed method replacing the Attributes containing the user dictionary. As a consequence again undo is broken.

In my case the data attached to layer tells my PlugIn how to handle the objects within this layer. This data is typically changed by the user using a dialog. When the user modifies the layer data, my Plug-in updates the object within the layer accordingly. When calling undo, rhino does undo the object updates but not undo the layer data change. The object state and the layer data state are inconsistent and the user experiences this as soon as he further modifies the objects.

Does anybody (may be Dale?) have an idea how to best implement undo for user dictionaries stored with layers?

Samuel

Can you provide sample code that does not work for you?

– Dale

Hi Dale,

2020-07-20 CsLayerUserDictUndo.cs (3.0 KB)

I attached an example illustrating what I mean. The example consists of two commands. CsSetRadius and CsDrawCircles. Select a layer and set a radius using CsSetRadius. The radius will be stored in the UserDict of the layer. Then draw some circles using CsDrawCircles. The radius will be read from the UserDict. Then use CsSetRadius to change the radius. All circles are updated to the new radius. Then call Undo (Ctrl+Z). All circles will go back to the old radius. But when you add another circle using CsDrawCircles, the new radius will be used because Rhino does not undo the radius change in the UserDict of the layer.

The example illustrates the concept I am using for my plug-in. Instead of circles, my plug-in draws wires used in electronics products. Wire layouts are organized in layers. The wire diameter and other parameters are defined on layout level. Thats why these parameters are saved in the corresponding layer user dicionary.

Readers who like to have an impression of this plug-in, visit this page:

At the current stage of my plug-in the user would experience exactly the same inconsistency, when adjusting the wire diameter of a wire layout. When undoing the wire diameter change, new wires objects will be created using the wrong diameter. Also the existing wires will get the wrong diameter when updated.

regards,

Samuel

Hi @samuel.hartmann,

See my modified code.

CsLayerUserDictUndo.cs (3.2 KB)

In a nutshell, the layer doesn’t know the user dictionary has been modified, so the layer is never copied and pushed on to Rhino’s undo stack. The “trick” is a easy work-around until we get this issue resolved.

https://mcneel.myjetbrains.com/youtrack/issue/RH-59573

– Dale

Thank you, Dale.
One line and my plug in behaves as expected!

Samuel