Rabih Kodeih's

Implementing Undo/Redo using Closures

Today, I was thinking about adding undo/redo functionality in a form editor I have been working on lately. So I decided to look around to see if there are any libraries or implementations I could use. Couldn’t find anything to my liking, most of them use elaborate patterns (memento, etc….) or simply resort to saving the whole application state which is overkill, so I developed my own undo/redo thing.

In any Flex application, when you perform an action against some component, what you are actually doing is applying a forward transformation to the application state at one time to produce a different state at another time. Likewise, when an inverse transformation is applied to the current application state, we end-up with the original state.

The main idea is to simply use a closure to capture any transformation with all the necessary data which are used in the transformation logic. The result of this will be a stateless function that can be called any time. Thus for undo/redo, the forward transformation and its reverse are captured resulting in two distinct stateless functions. Both of undo and redo functions could be stored in an array and called at will without worrying about the current state of the application.

But what are the conditions that would ensure the consistency of the application state with the undo/redo logic at all times? Basically just one, it is the requirement that every action performed in the application be captured in a separate closure.

For example:

for which the code is:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
    width="349" height="162">

<mx:Script>
    <![CDATA[
        import mx.controls.Label;
       
        /* *********************
             UNDO - REDO LOGIC
           ********************* */
          
        private var _history:Array = [];
        private var _index:int = -1;
       
        private var closureFactory:Function = function(transform:Function, ...args):Function
        {
            var closure:Function = function():void {
                transform.apply(null, args);
            }
            return closure;
        }
       
        private function updateHistory(undoTransform:Function, redoTransform:Function):void
        {
            _index++;
            _history.splice(_index);
            _history.push(
                {forwardTransfrom:redoTransform, reverseTransform:undoTransform}
            );
        }
       
        private function undo():void
        {
            if (_index >= 0) {
                var closure:Function = _history[_index].reverseTransform;
                _index--;
                closure();
            }   
        }
       
        private function redo():void
        {
            if (_index < _history.length - 1) {
                _index++;
                var closure:Function = _history[_index].forwardTransfrom;
                closure();
            }
        }
       
       
        /* *********************
             APPLICATION LOGIC
           ********************* */
       
        private function moveLabel(label:Label, dx:Number, dy:Number):void
        {
            label.x += dx;
            label.y += dy;
        }
       
        private function moveLogic(dx:Number, dy:Number):void
        {
            if (myLabel.x + dx < 0) return;
            if (myLabel.y + dy < 0) return;
           
            var undo:Function = closureFactory(moveLabel, myLabel, -dx, -dy);
            var redo:Function = closureFactory(moveLabel, myLabel, dx, dy);
            updateHistory(undo, redo);
            redo();
        }
       
        private function upHandler(e:Event):void
        {
            moveLogic(0, -10);
        }
       
        private function downHandler(e:Event):void
        {
            moveLogic(0, 10);
        }
       
        private function leftHandler(e:Event):void
        {
            moveLogic(-10, 0);   
        }
       
        private function rightHandler(e:Event):void
        {
            moveLogic(10, 0);
        }
    ]]>
</mx:Script>

    <mx:Canvas x="73" y="10" width="200" height="139" backgroundColor="#87FBFF">
        <mx:Label id="myLabel" x="51" y="53" text="move me" fontSize="20"/>
    </mx:Canvas>
    <mx:Button x="10" y="54" label="Undo" click="undo()"/>
    <mx:Button x="10" y="84" label="Redo" click="redo()"/>
    <mx:Button x="281" y="24" label="Up" width="58" click="upHandler(event)"/>
    <mx:Button x="281" y="54" label="Down" width="58" click="downHandler(event)"/>
    <mx:Button x="281" y="84" label="Left" width="58" click="leftHandler(event)"/>
    <mx:Button x="281" y="114" label="Right" width="58" click="rightHandler(event)"/>
       
</mx:Application>

The array _history holds the undo/redo history, and _index points to the most recent undo or redo operation that has been executed. The function closureFactory takes a generic function along with its arguments then returns a closure. This function is used in application logic (moveLogic) to create the two undo and redo closures and to save them into _history using the function _updateHistory. In closureFactory, I have used apply to pass a variable number of arguments into transforms’ argument list.

Note that only when the label is actually entitled to move, _history is updated with a new closure. Otherwise we would end up with a lot of dummy do-nothing undo/redo actions!

Comments, questions and suggestions are welcomed,

more ...