相信有不少RIA应用都有undo/redo功能。这里我就拿自己做过的画图板为例子说明一下它的实现原理(没有啥有用的代码,理解原理就行)。
分析
undo是什么?在用word的时候,写了一行字后悔了,执行一下undo那行字就消失了。undo就这么简单,将做过的事情再倒退回去。说专业一点,就是执行一个逆向动作。拿画图板里最简单的画直线来讲,画了一条直线,对应的undo就是擦掉这个线。很多的命令也都是类似的。所以我们看到,一个命令可以被“undo”,它至少包含了两种动作,一个是do,一个是undo,他们是相互逆向的动作。下面的事情就明朗了,如果一个命令被认为是可以undo的,那么它至少要实现两个方法,do和undo(大多数时候redo就是do)。但是命令千变万化,比如画直线和画曲线就不同,这里就需要用到设计模式中的Command。
代码:
{
public interface ICommand
{
public function doIt():void //do
public function undoIt():void //undo
}
}
我定义了一个ICommand接口,它按照我分析的定义了两个方法do和undo,所以认为它是可以被undo的。Command代表的不是一个实实在在的组件,而是一个动作。所有的具体动作都是来自ICommand。比如画直线,它就可以这么定义。
代码:
如果需要额外参数,从构造函数或者public方法注入都可以。当实际使用的时候,只要直接调用doIt或者undoIt就行了,根本不必管它是啥方法。
undoManager一统天下
分析完了undo原理,下面要做的就是把很多可以undo的命令组合起来思考。o(∩_∩)o…如果很多undo,redo混在一起就会发现事情也不是那么简单。这里有必要搞个undoManager来管理一下。还是先从原理开始分析,观察一下word,如果你分别写了三行字,undo两回后再写了一行字,然后你又想把原来的第二行恢复,发现无论如何也不能恢复!是不是有点乱?这里你只要明白一点就可以了:如果有一个新动作执行,那么原来已经被undo的动作就不能再redo了。用下面的图可以说明。
如果把command看成一条链的话,先执行ABCD,undo一次就退回倒C,再undo就退回到B,如果有新的command加进来而不是继续undo或redo,新的command链ABE便会形成,CD就会被抛弃,再也不能redo了。从AS3角度,准备两个堆栈结构(Array),分别代表现存的已经执行过的command(如AB),和已经被undo过的command(以后可以被redo也可以被抛弃,如CD)。看代码。
代码:
{
public class UndoManager implements ICommand
{
public function UndoManager()
{
}
// 一条队列存放已经执行过的command
private var queue:Array = new Array();
// 一条队列存放已经undo过的command
private var undoQueue:Array = new Array();
public function addNewCmd(cmd:ICommand):void
{
undoQueue = new Array();//新command加入的时候,undo队列便被清空
queue.push(cmd);
}
/////////////////////////////////
//redo
public function doIt():void
{
//TODO: implement function
if(undoQueue.length>0)
{
var ic:ICommand = undoQueue.pop() as ICommand;
ic.doIt();//redo
queue.push(ic);
}
}
//undo
public function undoIt():void
{
//TODO: implement function
if(queue.length>0)
{
//把队列尾部的command拿出来执行undo
var ic:ICommand = queue.pop() as ICommand;
ic.undoIt();
//执行完后插入undoQueue
undoQueue.push(ic);
}
}
}
}
实际用的时候很简单了,执行完一个command就addNewCmd到UndoManager,如果想undo或redo就直接操作UndoManager,根本不必管现在是啥状态,是不是很简单。