Transactions and the UndoManager

GoDiagram models and diagrams make use of an UndoManager that can record all changes and support undoing and redoing those changes. Each state change is recorded in a ChangedEvent, which includes enough information about both before and after to be able to reproduce the state change in either direction, backward (undo) or forward (redo). Such changes are grouped together into Transactions so that a user action, which may result in many changes, can be undone and redone as a single operation.

Not all state changes result in ChangedEvents that can be recorded by the UndoManager. Some properties are considered transient, such as Diagram.Position, Diagram.Scale, Diagram.CurrentTool, Diagram.CurrentCursor, or Diagram.IsModified. Some changes are structural or considered unchanging, such as Diagram.Model, any property of CommandHandler, or any of the tool or layout properties. But most GraphObject and model properties do raise a ChangedEvent on the Diagram or Model, respectively, when a property value has been changed.

Transactions

Whenever you modify a model or its data programmatically in response to some event, you should wrap the code in a transaction. Wrap the changes in a call to Diagram.Commit or Model.Commit. Although the primary benefit from using transactions is to group together side-effects for undo/redo, you should use transactions even if your application does not support undo/redo by the user.

As with database transactions, you will want to perform transactions that are short and infrequent. Do not leave transactions ongoing between user actions. Consider whether it would be better to have a single transaction surrounding a loop instead of starting and finishing a transaction repeatedly within a loop. Do not execute transactions within a property setter -- such granularity is too small. Instead execute a transaction where the properties are set in response to some user action or external event.

Transactions are unnecessary when initializing a model or a diagram before assigning the model to the Diagram.Model property. (A Diagram only gets access to an UndoManager via the Model, the Model.UndoManager property.)

Furthermore many event handlers are already executed within transactions that are conducted by Tools or CommandHandler commands, so you often will not need to start and commit a transaction within such functions. Read the API documentation for details about whether a function is called within a transaction. For example, setting GraphObject.Click to an event handler to respond to a click on an object needs to perform a transaction if it wants to modify the model or the diagram. Most custom click event handlers do not change the diagram.

But implementing an "ExternalObjectsDropped" DiagramEvent listener, which usually does want to modify the just-dropped Parts in the Diagram.Selection, is called within the DraggingTool's transaction, so no additional start/commit transaction calls are needed.

Finally, some customizations, such as the Node.LinkValidation predicate, should not modify the diagram or model at all.

Both model changes and diagram changes are recorded in the UndoManager only if the model's UndoManager.IsEnabled has been set to true. If you do not want the user to be able to perform undo or redo and also prevent the recording of any Transactions, but you still want to get "Transaction"-type ChangedEvents because you want to update a database, you can set UndoManager.MaxHistoryLength to zero.

To better understand the relationships between objects and transactions in memory, look at this diagram:

A typical case for using transactions is when some command makes a change to the model.


  // define a function named "AddChild" that is invoked by a button click
  void addChild() {
    var selnode = diagram.Selection.First();
    if (!(selnode is Node)) return;
    diagram.Commit(d => {
      var model = d.Model as MyModel;
      // have the Model add a new node data
      var newnode = new NodeData { Key = "N" };
      model.AddNodeData(newnode);  // this makes sure the key is unique
      // and then add a link data connecting the original node with the new one
      var newlink = new LinkData { From = (selnode.Data as NodeData).Key, To = newnode.Key };
      // add the new link to the model
      model.AddLinkData(newlink);
    }, "add node and link");
  }

  diagram.NodeTemplate =
    new Node("Auto")
      .Add(
        new Shape("RoundedRectangle") { Fill = "whitesmoke" },
        new TextBlock { Margin = 5 }
          .Bind("Text", "Key")
      );

  diagram.Layout = new TreeLayout();

  var nodeDataList = new List<NodeData> {
    new NodeData { Key = "Alpha" },
    new NodeData { Key = "Beta" }
  };
  var linkDataList = new List<LinkData> {
    new LinkData { From = "Alpha", To = "Beta" }
  };
  diagram.Model = new MyModel {
    NodeDataSource = nodeDataList,
    LinkDataSource = linkDataList
  };
  diagram.Model.UndoManager.IsEnabled = true;

In the example above, the addChild function adds a link connecting the selected node to a new node. When no Node is selected, nothing happens.

Supporting the UndoManager

Changes to data properties do not automatically result in any notifications that can be observed. Thus when you want to change the value of a property in a manner that can be undone and redone, you should call Model.Set. This will get the previous value for the property, set the property to the new value, and call Model.RaiseDataChanged, which will also automatically update any target bindings in the Node corresponding to the data.


  diagram.NodeTemplate =
    new Node("Auto")
      .Add(
        new Shape("RoundedRectangle") { Fill = "whitesmoke" },
        new TextBlock { Margin = 5 }
          .Bind("Text", "SomeValue", v => v.ToString())
      );

  diagram.Layout = new TreeLayout();

  var nodeDataList = new List {
    new NodeData { Key = "Alpha", SomeValue = 1 }
  };
  diagram.Model = new MyModel {
    NodeDataSource = nodeDataList
  };
  diagram.Model.UndoManager.IsEnabled = true;

  void incrementData() {
    diagram.Model.Commit(m => {
      var data = m.NodeDataSource.First() as NodeData;
      m.Set(data, "SomeValue", data.SomeValue + 1);
    }, "increment");
  }

The incrementData function increases the value of the "SomeValue" property on the first node data. Ctrl-Z and Ctrl-Y cn be used to undo and redo the value changes.