How to make custom edits undoable

Tips & Tricks

Summary

This article describes how to create custom undo units to make custom edits undoable.

Description

The common edit operations like adding items or changing styles are supported by the yFiles FLEX undo mechanism. Adding undo support for custom edits, however, is easy to implement. This article describes how to write custom IUndoUnit implementations and how to add them to the existing undo support.

For this example, we will making changes to an exisiting style undoable. While setting a new style to a graph item is already covered by the build-in undo mechanism, changing a style's property is not.

Example: Changing a ShapeNodeStyle's fill property (not undoable)

The following example changes the color of all selected nodes with shape node style and triggers a repaint of those nodes:

private function newColor():void {
   for (var it:Iterator=graphCanvas.selection.selectedNodes.iterator(); it.hasNext();) {
         var node:INode = it.next() as INode;
         var sns:ShapeNodeStyle = node.style as ShapeNodeStyle;
         if (sns != null) {
              sns.fill = new SolidColor(0xFF0000);

              // mark for repaint
              graphCanvas.invalidateItem(node);
         }
    }
}
Note that the method GraphCanvasComponent.invalidateItem(IModelItem) is only available since yFiles FLEX 1.6. For yFiles FLEX versions prior to 1.6 the same functionality can be achieved by the code below:
private function newColor():void {
   for (var it:Iterator=graphCanvas.selection.selectedNodes.iterator(); it.hasNext();) {
         var node:INode = it.next() as INode;
         var sns:ShapeNodeStyle = node.style as ShapeNodeStyle;
         if (sns != null) {
              sns.fill = new SolidColor(0xFF0000);
          }
          // mark for repaint
          for each (var obj:ICanvasObject in graphCanvas.graphModelManager.getCanvasObjects(node)) {
               obj.dirty = true;
          }
    }
    graphCanvas.treePartiallyDirty = true;
    graphCanvas.invalidateDisplayList();
}

To make the changes undoable, two steps are necessary:

  • Create a IUndoUnit implementation.
  • Add the custom undo unit to the undo queue.

Create the undo unit

An undo unit is an object which can undo and redo the operation for which it is created. The class AbstractUndoUnit faciliates the task of creating such a class. In order to undo an operation we have to remember the object which will be changed by the operation and the object's state before the operation. In our example the object is the style and the state is the style's fill property. We can use the undo unit's constructor to pass these values to the instance. With this knowledge, we can start with the implementation:

package demo.test
{
    import com.yworks.graph.drawing.ShapeNodeStyle;
    import com.yworks.support.AbstractUndoUnit;

    import mx.graphics.IFill;

    public class StyleColorUndoUnit extends AbstractUndoUnit
    {

        private var _style:ShapeNodeStyle;
        private var _oldFill:IFill;

        public function StyleColorUndoUnit(style:ShapeNodeStyle, oldFill:IFill)
        {
            this._style = style;
            this._oldFill = fill;
            super("Color Change", "Color Change"); //names for undo and redo operation
        }
    }
}

The next thing to do is to add the method for undo. This method sets the old color to the style's fill property. It also has to remember the current value of that property, so this undo operation can also be redone:

override protected function undoImpl():void {
    this._newFill = this._style.fill;
    this._style.fill = this._oldFill;
}

The declaration of _newFill:

private var _newFill:IFill;

The implementation of the redo operation has to re-set _newFill to the style:

override protected function redoImpl():void {
    this._style.fill = this._newFill;
}

Add the undo unit to the undo queue

To make an operation undoable, its corresponding undo unit has to be added to the graph's IUndoSupport using the method addUnit(). First we have to get the IUndoSupport:

var support:IUndoSupport = graphCanvas.graph.lookup(IUndoSupport) as IUndoSupport;

Before setting the new color, we create the undo unit and add it to the support:

if (support != null) {
    var unit:IUndoUnit = new StyleColorUndoUnit(sns, sns.fill);
    support.addUnit(unit);
}

Collect undo units

Sometimes, one wants to collect more than one operations in one single undo unit. In our above example, all selected nodes are re-colored upon a single call to the newColor method. It makes sense to create only one undo unit for this. To do so, one can use an ICompoundEdit. The edit is started by calling the method beginCompoundEdit():

var edit:ICompoundEdit = null;
if (support != null) {
    edit = support.beginCompoundEdit("New Color", "New Color");
}

and stopped with the edit's end() method:

if (edit != null) {
    edit.end();
}

Example: Changing a ShapeNodeStyle's fill property (undoable)

So, in recapitulation, our undoable newColor() method now looks like:

private function newColor():void {
  var support:IUndoSupport = graphCanvas.graph.lookup(IUndoSupport) as IUndoSupport;
  var edit:ICompoundEdit = null;
  if (support != null) {
    edit = support.beginCompoundEdit("New Color", "New Color");
  }
  for (var it:Iterator=graphCanvas.selection.selectedNodes.iterator(); it.hasNext();) {
    var node:INode = it.next() as INode;
    var sns:ShapeNodeStyle = node.style as ShapeNodeStyle;
    if (sns != null) {
      if (support != null) {
        var unit:IUndoUnit = new StyleColorUndoUnit(sns, sns.fill);
        support.addUnit(unit);
      }
      sns.fill = new SolidColor(0xFF0000);

      // mark for repaint. For yFiles FLEX versions prior to 1.6 see the invalidation code 
      // in the Example: Changing a ShapeNodeStyle's fill property (not undoable)
      graphCanvas.invalidateItem(node);
    }
  }
  if (edit != null) {
    edit.end();
  }
}

Automatic merging

You may have noticed that even if you don't use the compound edit, the re-coloring of multiple nodes is merged into one single undo operation. The reason for this is that the UndoEngine merges undo operations in a certain time span, determined by autoAddTimeSpan (default is 100 milliseconds), into one single unit. This feature is useful because the undo support is meant to undo changes done by the user, and more than one operation in 100 milliseconds are most likely the result of a single user gesture.

Categories this article belongs to:
yFiles FLEX > Displaying and Editing Graphs > User Interaction
Applies to:
yFiles FLEX: 1.4, 1.5, 1.6, 1.7, 1.8
Keywords:
undo - redo - IUndoUnit - IUndoSupport - UndoEngine