Use undoManager for virtualizedForceLayout

Hello.

I’m trying to use undoManager in the demo virtualizedForceLayout to redo/undo nodes’ location, but a node is not always in diagram, and it’s seem useless to set myWholeModel.undoManager.isEnabled = true. Any advice, please?

I took a quick look at this, and I don’t see an easy answer.

Are the locations of nodes the only thing that you want to record in the UndoManager? What are all of the causes of node movement that you want to record?

Well, i have some controllers to change node’s color, shape, visibility and location by databinding. About node movement, drag or re-layout could change node’s location. I think i need to spend some time to customize a undoMananger now.

Anyway, thanks for the reply :-D.

Here’s one thing that might help. There’s a subtle bug in the Binding of the Node.position. Use this instead:

                new go.Binding("position", "bounds", function(b) { return b.position; })
                              .makeTwoWay(function(p, d) { return new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height); }),

Both the original binding and this binding update the data.bounds property of the node data in the models (the same node data object is shared by both models), but the original binding did not record the change in the UndoManager because the new value for data.bounds was the same Rect reference, so there did not seem to be a change, and thus the change to the Rect did not get recorded.

Another thing that I was trying until I ran out of time today (it’s a holiday today) is customizing the UndoManager not to record any ChangedEvents for Diagram objects – Nodes, Links, TextBlocks, Shapes, et al. In other words, only record changes to the model data.

Here’s the first part of implementing that:

      myDiagram.model.undoManager.skipsEvent = function(e) {
        if (e !== null && e.diagram !== null) return true;
        return go.UndoManager.prototype.skipsEvent.call(this, e);
      };

The reason this is probably useful is that the same Node instance won’t exist, and one wouldn’t want to retain references to Nodes and Links in memory anyway in the UndoManager, so it’s better to restrict all changes to be model only – no diagram changes.

However, an undo or a redo won’t work, because although the model data is modified appropriately, nothing in the diagram is modified to match the undo or redo effects.

Basically the Model needs to handle state changes (due to undo or redo) and update the Diagram: check the ChangedEvent to see if the Node exists for that model data. If it does, then it should update the bindings. But at this moment I’m not sure that such functionality is accessible in the library’s API.

OK. I will try this. Thank you very much!

This might work for supporting undo/redo of property changes that have been saved in the model data via TwoWay Bindings:

      myDiagram.model.undoManager.skipsEvent = function(e) {
        if (e !== null && e.model === null) return true;
        return go.UndoManager.prototype.skipsEvent.call(this, e);
      };

      myDiagram.model.addChangedListener(function(e) {
        if (e.change === go.ChangedEvent.Transaction &&
            (e.propertyName === "FinishedUndo" || e.propertyName === "FinishedRedo")) {
          setTimeout(function() {
            myDiagram.commit(function(d) {
              d.updateAllTargetBindings();
            }, null);
          }, 1);
        }
      });

I hope this helps you.

Hi. Sorry for late reply. I was working on this proplem until now.

I think my description was a little vague. What i want is one undoManager remembers all changes come from both mydiagram.model and myWholeModel. For example, change all nodeDatas’ color to “red”:

myWholeModel.nodeDataArray.forEach(function (part) {
     myWholeModel.setDataProperty(part, "color", "red");
 });

I get the answer from the sample Update Demo GoJS Sample. Then things become easy.

var undoManager = new go.UndoManager();
undoManager.isEnabled = true;
undoManager.skipsEvent = function (e) {
            if (e !== null && e.diagram !== null) return true;
            return go.UndoManager.prototype.skipsEvent.call(this, e);
        };
myDiagram.model.undoManager = undoManager;
myWholeModel.undoManager = undoManager;

So, it will remember model changes like this:

undoManager.startTransaction("do something");
// do something
undoManager.commitTransaction("do something");

And your advice is important to keep diagram being synchronized with model. Because my go.js version is old, there is no myDiagram.commit, so I made a little change.

var func = function (e) {
            if (!e) return;
            if (e.change === go.ChangedEvent.Transaction
                && (e.propertyName === "FinishedUndo"
                || e.propertyName === "FinishedRedo"
                || e.propertyName === "CommittedTransaction")) {
                setTimeout(function () {
                    myDiagram.updateAllTargetBindings();
                }, 1);
            }
        };
myDiagram.model.addChangedListener(func);
myWholeModel.addChangedListener(func);

It works well for now! Again, thanks for your advice ,Walter.

I suspect that you should not call updateAllTargetBindings when a transaction is committed.

Furthermore I believe it is important to call that method while skipsUndoManager is true. That’s the null argument to the new commit method.

OK, got it. I will do more tests on this.