许多GUI程序中提供一个"撤销&重做"的功能,这个功能对用户来说非常友好;本文就简单的介绍一下如何用C#实现该功能。
实现Undo&Redo功能的基本模型是带撤销功能的命令模式,它将每步操作保存为一个命令对象,如下所示:
interface Icommand
{
void Do();
void Undo();
}
其中Do函数执行功能,Undo函数回滚功能。这样就把命令给实体化了,只要将命令对象给保存下来,需要撤销时执行Undo函数,重做时执行Do函数即可。
有了这个基本思路后,下面就是实现细节了:
-
申请两个Stack来保存命令对象:UndoStack和RedoStack
-
执行命令时,将命令序列化为Command对象,执行Do方法,存入UndoStack,清空RedoStack
-
撤销命令时,从UndoStack中取出命令,执行Undo方法,存入RedoStack
-
重做命令时,从RedoStack中取出命令,执行Do方法,存入UndoStack
一个简单的实现如下:
class
CommandManager
{
Stack<Command> redoStack = new
Stack<Command>();
Stack<Command> undoStack = new
Stack<Command>();
public
void AddCommand(Action doCmd, Action undoCmd)
{
var cmd = new
Command(doCmd, undoCmd);
cmd.Do();
undoStack.Push(cmd);
redoStack.Clear();
}
public
bool Undo()
{
if (undoStack.Count == 0)
return
false;
var cmd = undoStack.Pop();
redoStack.Push(cmd);
cmd.Undo();
return
true;
}
public
bool Redo()
{
if (redoStack.Count == 0)
return
false;
var cmd = redoStack.Pop();
undoStack.Push(cmd);
cmd.Do();
return
true;
}
class
Command
{
public
Action Do { get; private
set; }
public
Action Undo { get; private
set; }
public Command(Action doCmd, Action undoCmd)
{
this.Do = doCmd;
this.Undo = undoCmd;
}
}
}
用C#实现起来还是非常简洁的,就几十行代码。
遗留问题:命令对象何时释放
前面的实现虽然非常简单,但存在一个遗留问题:每一个命令对象都保存在UndoStack中了,这样随着程序的执行,UndoStack中记录的命令越来越多,占用内存得不到释放。对于这个问题,一般有如下几种策略:
-
不释放命令对象。一般需要Undo&Redo功能都是些GUI程序,这些程序大多不会持续运行,并且对内存的占用也没有太大限制,命令对象一般也不会占用多少内存。保存所有命令对象不会对程序造成什么影响。
-
命令堆栈维持固定的长度:当命令堆栈的长度超过阈值的时候,删除最开始压入的命令。这种策略用得最多,但这样带来的问题就是无法实现无限Undo。
-
将命令堆栈保存到文件:将命令序列化保存到文件,需要使用时从文件中还原。这种方式可以实现无限Undo,但序列化命令往往是件比较麻烦的事情,反序列化时也要消耗时间。
-
综合2,3两种方案:内存中保持固定长度的命令对象,超过阈值的保存到文件。这种方式能有效解决反序列化的耗时问题,也能实现无限Undo。但实现起来也最为麻烦。
基于篇幅所限,本文就不进一步讨论和实现了。