Pages

Google+

Thursday, September 17, 2009

AS3 Undo / Redo With Display Objects

There are a few examples of undo & redo floating around, but I wanted to give it a shot myself. I didn't look at anyone else's solution and I'm glad I didn't, because I came up with a very different and clean solution.



Most examples of undo and redo on the web involve either the Command Pattern, or the somewhat deprecated Memento Pattern. These are solid patterns in the right context, but they have a good deal of boilerplate code. Another drawback of these approaches is how the designer must create a method that alters the display object in a particular way, so it can be recorded.  Lastly, it makes manipulating multiple properties ( at once ) of the object either not possible or extremely difficult. These issues make the fore mentioned patterns neither easy nor portable.

My approach is to define, at run time, what properties to record, examine the display object directly, and extract a sort of value object that records these values.  This value object is either stored for later retrieval or cleaned and GC'ed.

Example:
[SWF]http://blog.alanklement.com/files/examples/undo_redo/Main.swf, 550, 400[/SWF]
The code implementation is very easy:
// Create recorder and tell what properties you want to record
// add as many display object properties you want: e.g. width, height, scaleX, scaleY, alpha.....
propertyRecorder = new PropertyRecorder(redSquare, ["x","y","rotation"]);

//When desired, record the history
propertyHistory.recordNewHistory();

// Call undo() or redo() anytime you want
propertyHistory.undo();
propertyHistory.redo();


This implementation uses 2 lightweight objects, but if desired they could be merged together into one. The history value object was abstracted for readability.

The above example can be downloaded here. If you have FDT, you can import this project along with all dependencies and a launch configuration. Without FDT, just unzip it and the source is still in there.

Property Recorder:


public class PropertyRecorder
{
private var objectToRecord : *;
private var currentHistoryIndex : uint;
private var historyStates : Array = [];
private var propertiesToWatch : Array;

public function PropertyRecorder(objectToRecord : *, propertiesToWatch : Array)
{
this.propertiesToWatch = propertiesToWatch;
this.objectToRecord = objectToRecord;
recordNewHistory();
}

public function undo() : void
{
if(currentHistoryIndex > 0)
{
currentHistoryIndex--;
var historyToRestore : PropertyVO = PropertyVO(historyStates[currentHistoryIndex]);
historyToRestore.restoreProperties(objectToRecord);
}
}

public function redo() : void
{
if(currentHistoryIndex < historyStates.length - 1)
{
currentHistoryIndex++;
var historyToRestore : PropertyVO = PropertyVO(historyStates[currentHistoryIndex]);
historyToRestore.restoreProperties(objectToRecord);
}
}

public function recordNewHistory() : void
{
checkForOldHistoryToClean();
}

private function checkForOldHistoryToClean() : void
{
while(currentHistoryIndex + 1 < historyStates.length)
{
PropertyVO(historyStates.pop()).dispose();
}

createNewHistoryVO();
}

private function createNewHistoryVO() : void
{
var historyVO : PropertyVO = new PropertyVO(propertiesToWatch);
historyVO.extractCurrentProperties(objectToRecord);

historyStates.push(historyVO);
currentHistoryIndex = historyStates.length - 1;
}


And PropertyVO:


public class PropertyVO
{
private var dictionary : Dictionary = new Dictionary(true);

public function PropertyVO(propertiesToWatch : Array)
{
for each (var item : String in propertiesToWatch)
{
dictionary[item] = "";
}
}

public function extractCurrentProperties(objectToRecord : *) : void
{
for (var property : String in dictionary)
{
dictionary[property] = objectToRecord[property];
}
}

public function restoreProperties(objectToRestore : *) : void
{
for (var property : String in dictionary)
{
objectToRestore[property] = dictionary[property];
}
}

public function dispose() : void
{
for (var item : String in dictionary)
{
item = null;
}

dictionary = null;
}
}