Eden Ridgway's Blog

.Net and Web Development Information

  Home :: Contact :: Syndication  :: Login
  105 Posts :: 1 Stories :: 78 Comments :: 3 Trackbacks

Search

Article Categories

Archives

Post Categories

Development

General

When looking at implementing undo functionality, two common patterns that are used are the Memento or Command design patterns. For an example of using the Memento pattern on JavaScript objects take a look at Lawrence Carvalho's ActAsUndoable post. This approach however did not suite the needs of the application I'm currently working on because the changes I'm making are on Data Transfer Objects (DTOs) that have been serialized from the server using ASP.Net web services. As such I don't want to add any functionality to them and I don't really care about breaking encapsulation because all object values are public. I take these client side objects and "databind" (logically speaking) them to various UI elements, such as tree nodes and form fields. What I want to be able to offer however is the ability to cancel out changes without going back to the server to fetch the original data. I also want any undone changes to apply to the original objects and therefore simply cloning the objects by serializing and deserializing them using ASP.Net JSON serialization methods is not an option (it would make the object to UI element association management a pain). If however you don't care about preserving object references then this is a perfect solution for you, and can be done like this:

function cloneObjectGraph(targetObject)
{
    
var jsonSerializedData Sys.Serialization.JavaScriptSerializer.serialize(targetObject);
    return 
Sys.Serialization.JavaScriptSerializer.deserialize(jsonSerializedData);
}

So what I have created is a very simple pair of objects, UndoManager and UndoItem, that are ideally suited to taking a snapshot of state on simple JavaScript object graph, where all the values you care about are "public", and then being able to rollback the object graph to the snapshot point while preserving the original object references. If you want functionality such as the ability store state of more complex objects that have getters/setters and "private" variables (as is present with the Module pattern and others) then don't use these objects, rather take a look at the ActAsUndoable memento approach. Also if you want to implement an "action by action" undo/redo interface like word then rather look at using the command pattern.

So getting on to UndoManager and UndoItem objects. As you can see from the diagram below, they are quite straightforward. The UndoManager exists to allow you to simply have a stack of changes that you can undo on a last in last out basis. The UndoItem object handles the storing of a single undo operation on an object graph.

The way UndoItem takes a snapshot of an objects state is to "iterate" over all the object attributes using a for..in loop and copy any simple type to the _undoState variable. Any complex object it encounters, such as an object, date or array, it creates another UndoItem for that and adds it to _undoState in the attributes place. Hence it builds up a hierarchy of Undo Items it can call on when undo is called. When undoing the changes it uses the _objectReference it stored when constructing the UndoItem to copy the values back from the _undoState on to the original object.

This gives one a really simple and unobtrusive way of capturing and restoring state. Say you have a person object that you want to save to save the state for, make changes and then undo them, you use an UndoItem to do it in the following way:

var person 

    IsActive: 
true,
    Team: { Name: 
'Admin' }
}
;

var 
undoItem = new UndoItem(person);

person.IsActive = false;
person.Team.Name 'General User';

undoItem.undo();

If you want to manage a stack of changes then you use the UndoManager like so:

var team { Name: 'Admin' };
var 
person { Name: 'Joe' };

var 
undoManager = new UndoManager();

undoManager.saveUndoPoint(team);
undoManager.saveUndoPoint(person);

team.Name 'General User';
person.Name 'Bob';

//Will undo the person name change
undoManager.undo();

//Will undo the team name change
undoManager.undo();

If you are keen to use the objects then you can download it here (the code is not minified). I have also created a set of Scriptaculous unit tests that you can run here.
posted on Wednesday, November 07, 2007 8:47 AM
Comments have been closed on this topic.