Fixed node with undo manager

We have a view configuration that does not appear to play nicely with the undo manager. On each view we have one fixed GoIconicNode, by fixed I mean that the node cannot be removed by the user – CanDelete is false, and the SkipsUndoManager property of the node is true (this is in fact the “Start” node from which the user must begin drawing a graph). Various oddities are observed during copy/paste and undo operations when such a node adjoins the nodes in the operation.
One such example is a view with 3 GoIconic nodes linked together: N0, L1, N1, L2, N3. N0 is the “Start” (fixed) node. We execute these operations in order:
1. Select All
2. Edit/Copy
3. Edit/Delete (now just N0 remains)
4. Edit/Paste (pasted are N1, L2, N3, as expected)
5. Undo (L2 and N3 are removed, but N1 remains (!!) )
Is there perhaps a better way to handle the “fixed node” situation in conjuction with the undo manager, or can you point us to an example of how to exert more control over the process? Many thanks.

I should also add that we of course implement a custom clip object and that during edit/paste deserialization, we ignore any instance of any fixed node type.

I just tried this in the unmodified Demo1 sample.
I created three nodes and two links between them. I used the Properties window to make N0’s Deletable and Copyable properties FALSE.
Then ^A, ^C, DELETE, ^V, ^Z did exactly what you would expect them to.
I don’t know what the problem is in your app, but I’m suspicious that leaving SkipsUndoManager TRUE might cause some problems. I wouldn’t do that if I were you.

If I’m wanting to wrap some document update code without exposing it to the undo manager, is there any effective difference between GoDocument.SuspendsUpdates and GoDocument.SkipsUndoManager?
I did read that section of the user guide (again), but it is still hazy.

Yes, there’s a big difference, since there are a lot more GoDocument.Changed event listeners than just the Undo Manager that won’t get any notifications if you turn on SuspendsUpdates. Whereas SkipsUndoManager only stops those updates for the GoUndoManager.
There are effectively no situations where you would ever want to turn on either GoDocument.SuspendsUpdates or GoObject.SuspendsUpdates. I regret ever having made the property public. We have changed the documentation to say that you probably don’t want to be using the property at all. Practically all situations are covered by either SkipsUndoManager or the GoView.BeginUpdate/EndUpdate methods (copied from various WinForms controls).

I wonder if you would be so kind as to help me analyze the undo stack state.
I’m really at a loss to figure this out. It appears to me that there is information in the undo buffer that does not correspond to the document Changed events. Following is the trace with my notes:
*** There is currently nothing in the undo buffer. One node is on the canvas, the Start node. Also, document.SkipsUndoManager and document.SuspendsUpdates are both false at all times in this exercise. I removed the code that displays those values so as to not clutter this example.
*** Here we drag a GoIconicNode onto the canvas. Our code recognizes that this is the first node, and links it to the Start node.
OnDocumentChanged 902 Metreos.Max.Drawing.MaxActionNode
OnDocumentChanged 901 Metreos.Max.Drawing.MaxStartNode+CustomPort
OnDocumentChanged 901 Metreos.Max.Drawing.MaxIconicNode+CustomPort
OnDocumentChanged 902 Metreos.Max.Drawing.MaxBasicLink
OnDocumentChanged 202 null
— undo mgr stack after link to start node —
undo stack is empty
OnDocumentChanged 202 null
*** At this point we have two nodes linked together.
We hit the delete key to remove the just-added node.
DEL
OnDocumentChanged 903 Metreos.Max.Drawing.MaxActionNode
OnDocumentChanged 903 Metreos.Max.Drawing.MaxBasicLink
OnDocumentChanged 901 Metreos.Max.Drawing.MaxStartNode+CustomPort
OnDocumentChanged 901 Metreos.Max.Drawing.MaxIconicNode+CustomPort
*** At this point we have just the Start node remaining.
We hit ctrl-Z to undo the above delete.
Ctrl-Z
Undoing
— undo mgr stack prior Undo —
1 Compound Delete Selection
1 edit 901: 1709 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
2 edit 901: 1709 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
3 edit 902: 0 Metreos.Max.Drawing.MaxBasicLink --> (Northwoods.Go.GoLayer) [38,22 261x100]
4 edit 202: 0 [0,0 48x63.09114] --> [0,0 299x122]
5 edit 902: 0 Metreos.Max.Drawing.MaxActionNode --> (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114]
6 edit 202: 0 [0,0 299x122] --> [0,0 322.0898x143.0911]
7 edit 903: 0 Metreos.Max.Drawing.MaxActionNode 1 (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114] -->
8 edit 903: 0 Metreos.Max.Drawing.MaxBasicLink 1 (Northwoods.Go.GoLayer) [38,22 261x100] -->
9 edit 901: 1710 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
10 edit 901: 1710 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
OnDocumentChanged 901 Metreos.Max.Drawing.MaxStartNode+CustomPort
OnDocumentChanged 902 Metreos.Max.Drawing.MaxBasicLink
OnDocumentChanged 902 Metreos.Max.Drawing.MaxActionNode
OnDocumentChanged 202 null
OnDocumentChanged 903 Metreos.Max.Drawing.MaxActionNode
OnDocumentChanged 202 null
OnDocumentChanged 903 Metreos.Max.Drawing.MaxBasicLink
OnDocumentChanged 901 Metreos.Max.Drawing.MaxStartNode+CustomPort
OnDocumentChanged 100 null
— undo mgr stack after Undo or Redo —
1 Compound Delete Selection
1 edit 901: 1709 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
2 edit 901: 1709 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
3 edit 902: 0 Metreos.Max.Drawing.MaxBasicLink --> (Northwoods.Go.GoLayer) [38,22 261x100]
4 edit 202: 0 [0,0 48x63.09114] --> [0,0 299x122]
5 edit 902: 0 Metreos.Max.Drawing.MaxActionNode --> (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114]
6 edit 202: 0 [0,0 299x122] --> [0,0 322.0898x143.0911]
7 edit 903: 0 Metreos.Max.Drawing.MaxActionNode 1 (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114] -->
8 edit 903: 0 Metreos.Max.Drawing.MaxBasicLink 1 (Northwoods.Go.GoLayer) [38,22 261x100] -->
9 edit 901: 1710 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
10 edit 901: 1710 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
*** At this point we still have just the one node on the canvas.
The undo operation appears to have added back the node and link, and then removed them again. We now hit Ctrl-Y to redo.
Ctrl-Y
Redoing
— undo mgr stack prior Redo —
1 Compound Delete Selection
1 edit 901: 1709 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
2 edit 901: 1709 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
3 edit 902: 0 Metreos.Max.Drawing.MaxBasicLink --> (Northwoods.Go.GoLayer) [38,22 261x100]
4 edit 202: 0 [0,0 48x63.09114] --> [0,0 299x122]
5 edit 902: 0 Metreos.Max.Drawing.MaxActionNode --> (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114]
6 edit 202: 0 [0,0 299x122] --> [0,0 322.0898x143.0911]
7 edit 903: 0 Metreos.Max.Drawing.MaxActionNode 1 (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114] -->
8 edit 903: 0 Metreos.Max.Drawing.MaxBasicLink 1 (Northwoods.Go.GoLayer) [38,22 261x100] -->
9 edit 901: 1710 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
10 edit 901: 1710 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
OnDocumentChanged 901 Metreos.Max.Drawing.MaxStartNode+CustomPort
OnDocumentChanged 902 Metreos.Max.Drawing.MaxBasicLink
OnDocumentChanged 202 null
OnDocumentChanged 902 Metreos.Max.Drawing.MaxActionNode
OnDocumentChanged 202 null
OnDocumentChanged 903 Metreos.Max.Drawing.MaxActionNode
OnDocumentChanged 903 Metreos.Max.Drawing.MaxBasicLink
OnDocumentChanged 901 Metreos.Max.Drawing.MaxStartNode+CustomPort
OnDocumentChanged 100 null
— undo mgr stack after Undo or Redo —
1 Compound Delete Selection
1 edit 901: 1709 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
2 edit 901: 1709 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
3 edit 902: 0 Metreos.Max.Drawing.MaxBasicLink --> (Northwoods.Go.GoLayer) [38,22 261x100]
4 edit 202: 0 [0,0 48x63.09114] --> [0,0 299x122]
5 edit 902: 0 Metreos.Max.Drawing.MaxActionNode --> (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114]
6 edit 202: 0 [0,0 299x122] --> [0,0 322.0898x143.0911]
7 edit 903: 0 Metreos.Max.Drawing.MaxActionNode 1 (Northwoods.Go.GoLayer) [287.9102,96 34.17969x47.09114] -->
8 edit 903: 0 Metreos.Max.Drawing.MaxBasicLink 1 (Northwoods.Go.GoLayer) [38,22 261x100] -->
9 edit 901: 1710 Metreos.Max.Drawing.MaxStartNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)
10 edit 901: 1710 Metreos.Max.Drawing.MaxIconicNode+CustomPort (Metreos.Max.Drawing.MaxBasicLink) --> (Metreos.Max.Drawing.MaxBasicLink)

*** The canvas appears the same after Redo as before – just the one node.
Am I reading this correctly, that the undo buffer contains events which cancel each other out? Is there any line of investigation you might suggest? Also, is there a way to hook the undo manager Add events in order to detect when individual edits are added to the undo manager?

It looks like you have a situation where a transaction was started without being finished. All the regular transactions, implemented by GoView and its GoTools, such as insertion due to a drag-and-drop or removal due to a “DELETE”, have just become nested transactions in your application.
This means that all changes have been lumped together into one big transaction, which is why you get both the insertion and the deletion together. [An aside: yes, it’s common to have multiple events that, when seen together in sequence, make some of them superfluous – it’s most common when moving objects in realtime.]
There are two ways you can have mismatched transaction calls – not enough end-transactions, or too many of them. The latter case you can narrow down easily by setting GoUndoManager.ChecksTransactionLevel to true, and watching a Trace listener such as the one that Visual Studio provides in its Output window.
But your case seems to be one where there was a call to StartTransaction without a matching call to FinishTransaction or AbortTransaction. You can certainly override GoUndoManager.DocumentChanged to monitor all calls. For your information, here’s the definition:
public virtual void DocumentChanged(Object sender, GoChangedEventArgs e) {
// changes caused by performing an undo or redo should be ignored!
if (this.IsUndoing || this.IsRedoing) return;
if (!SkipEvent(e)) {
GoUndoManagerCompoundEdit cedit = this.CurrentEdit;
if (cedit == null || cedit.IsComplete) {
cedit = new GoUndoManagerCompoundEdit();
this.CurrentEdit = cedit;
}
// make a copy of the event to save as an edit in the list
GoChangedEventArgs edit = new GoChangedEventArgs(e);
cedit.AddEdit(edit);
if (this.ChecksTransactionLevel && this.TransactionLevel <= 0)
GoObject.Trace(“Change not within a transaction: " + edit.ToString());
}
}
I suppose you could look at the TransactionLevel to see when it’s greater than zero when it shouldn’t be. One way I have done that in the past is to implement a Control.Paint event handler:
private void myView_Paint(Object sender, PaintEventArgs evt) {
if (myView.Document.UndoManager.TransactionLevel > 1) {
System.Diagnostics.Trace.WriteLine(”*** xactlevel = " + myView.Document.UndoManager.TransactionLevel.ToString());
}
}
But you’ll need to examine your code once you have narrowed down when this occurs. Perhaps you overrode some method and didn’t either call the base method or didn’t explicitly end the transaction. Each virtual method that is expected to end a transaction is documented to do so. Some methods where you might be tempted to do so but shouldn’t are also documented, where you call StartTransaction and there isn’t any end-transaction in our framework that we implement.