许多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。但实现起来也最为麻烦。
基于篇幅所限,本文就不进一步讨论和实现了。