【IT168 专稿】
关于Undo/Redo实现的基本思想
正如我们所知,应用程序在每次操作后改变其状态。当操作应用程序时,它的状态会发生改变。所以,若有人想要做撤销,他不得不回到先前的状态。因此,为了能够回到先前状态,我们需要在应用程序运行时存储它的状态。要支持重做,我们不得不从目前状态跳到下一个状态。
第一篇:C#中使用单个对象的方法实现Undo/Redo
为了实现Undo/Redo,我们不得不存储应用程序的状态并在撤销时跳到前一个状态而在重做时跳到下一个状态。因此我们需要维护应用程序的状态来支持Undo/Redo。在所有三种方法中,应用程序状态的维护用到了两个栈。一个栈包含用于撤销操作的状态,第二个包含用于重做的状态。撤销操作弹出撤销栈以获取前一个状态并将其设置给应用程序。同样的,重做操作弹出重做栈以获取下一个状态并将其设置给应用程序。
现在,我们知道了Undo/Redo的实现操作都是关于保持应用程序每次操作后的状态。现在的问题是该方法如何保存状态。在命令模式中,我们将单个操作的变化保存在一个ICommand对象中,它的目的在于把这种特殊类型的操作看作状态。
关于命令模式
这里我并不打算讨论命令模式,你可从这和这了解该模式相关信息。
如何应用命令模式对任意应用程序Undo/Redo操作建模?
命令模式如何对任意应用程序Undo/Redo操作建模将在以下步骤中讨论:
步骤1
首先识别出你希望哪些操作能支持Undo/Redo,并识别出操作将要应用的那些对象以及Undo/Redo操作的容器。
步骤2
对每个识别出的操作,使命令类继承自ICommand。每个命令类将包含它所代表的操作进行Undo/Redo时所需的属性。ICommand接口如下:
{
void Execute();
void UnExecute();
}
在Execute()方法中,你将利用命令的属性来完成动作。在Unexecuted ()方法中,你将利用命令的属性完成撤销操作。这里命令的属性保存它对应的命令完成Undo/Redo需要的变化,以及它改变的那些对象的引用。
特别注意:如果同一个操作对不同对象表现不同的行为,那么你不得不为此操作准备多重命令。这是针对每个对象类型的操作命令。
步骤3
然后创建一个名为UndoRedo的类,它包含两个栈。第一个用于Undo操作,第二个用于Redo操作。该类实现Undo方法,Redo方法和许多的InsertInUnDoRedo方法以在Undo / Redo系统中插入ICommand对象。当InsertInUnDoRedo方法被调用,然后ICommand对象被插入Undo栈,使Undo / Redo操作可用并清除Redostack。
在Undo操作中:
•首先检查UndoStack是否为空。如果为空则返回否则继续。
•然后从UndoStack中弹出一个ICommand对象。
•再次把该命令压入RedoStack。
•最后调用Icommand对象的Unexecute方法。
在Redo操作中:
•首先检查RedoStack是否为空。若为空则返回,否则继续。
•然后从RedoStack中弹出一个Icommand对象。
•再次把该命令压入Undostack。
•最后调用Icommand对象的execute方法。
步骤4
当你执行应用程序内不同的操作时,创建该操作类型的Command对象并通过名为InsertInUnDoRedo的方法将其压入Undo/Redo系统。当你需要做Undo操作时,只要调用应用程序中UndoRedo类的Undo方法,而当你想做Redo操作时,你只要调用应用程序的redo操作。
示例应用程序描述
这个示范WPF绘制应用程序用来作为结合Undo/Redo操作的案例。该WPF应用程序示例支持四种操作:插入对象、删除对象、移动对象和调整对象的尺寸,它还有两种类型的几何对象:矩形和多边形。它使用画布作为包含这些几何对象的容器。
现在,在此系列文章中,我们可以看到如何让这四个操作支持Undo/Redo。在第一部分,使用单个对象表示变化的方法实现。在第二部分,使用命令模式实现而在第三部分,使用备忘录模式实现。
使用命令模式实现示例应用程序的Undo/Redo
利用命令模式对示范应用程序Undo/Redo的实现将在以下步骤中讨论:
步骤1
在示范应用程序中有四种操作,它们是移动、调整尺寸、插入和删除。对象是矩形、椭圆,而容器是一个WPF画布。
步骤2
现在我们要为四种操作创建四个继承ICommand接口的命令类。
{
private Thickness _ChangeOfMargin;
private FrameworkElement _UiElement;
public MoveCommand(Thickness margin, FrameworkElement uiElement)
{
_ChangeOfMargin = margin;
_UiElement = uiElement;
}
#region ICommand Members
public void Execute()
{
_UiElement.Margin = new Thickness(_UiElement.Margin.Left +
_ChangeOfMargin.Left, _UiElement.Margin.Top
+ _ChangeOfMargin.Top, _UiElement.Margin.Right +
_ChangeOfMargin.Right, _UiElement.Margin.Bottom +
_ChangeOfMargin.Bottom);
}
public void UnExecute()
{
_UiElement.Margin = new Thickness(_UiElement.Margin.Left -
_ChangeOfMargin.Left, _UiElement.Margin.Top -
_ChangeOfMargin.Top, _UiElement.Margin.Right -
_ChangeOfMargin.Right, _UiElement.Margin.Bottom -
_ChangeOfMargin.Bottom);
}
#endregion
}
由于移动对象仅改变了几何对象的边距,移动命令将包含边距的变化,几何对象的引用。在移动命令中,Execute方法通过增加边距变化改变几何对象UIelement,而Unexecute方法通过减去刚应用的变化来撤销操作。这样,它减掉了几何对象UIelement的边距变化。
{
private Thickness _ChangeOfMargin;
private double _ChangeofWidth;
private double _Changeofheight;
private FrameworkElement _UiElement;
public ResizeCommand(Thickness margin, double width,
double height, FrameworkElement uiElement)
{
_ChangeOfMargin = margin;
_ChangeofWidth = width;
_Changeofheight = height;
_UiElement = uiElement;
}
#region ICommand Members
public void Execute()
{
_UiElement.Height = _UiElement.Height + _Changeofheight;
_UiElement.Width = _UiElement.Width + _ChangeofWidth;
_UiElement.Margin = new Thickness
(_UiElement.Margin.Left + _ChangeOfMargin.Left,
_UiElement.Margin.Top
+ _ChangeOfMargin.Top, _UiElement.Margin.Right +
_ChangeOfMargin.Right, _UiElement.Margin.Bottom +
_ChangeOfMargin.Bottom);
}
public void UnExecute()
{
_UiElement.Height = _UiElement.Height - _Changeofheight;
_UiElement.Width = _UiElement.Width - _ChangeofWidth;
_UiElement.Margin = new Thickness(_UiElement.Margin.Left -
_ChangeOfMargin.Left, _UiElement.Margin.Top -
_ChangeOfMargin.Top, _UiElement.Margin.Right -
_ChangeOfMargin.Right, _UiElement.Margin.Bottom -
_ChangeOfMargin.Bottom);
}
#endregion
}
调整尺寸方法改变了几何对象的边距、高度、宽度,因此调整尺寸命令带有边距的变化、高度的变化、宽度的变化以及几何对象的引用。在调整尺寸的命令中,Execute方法通过增加边距变化,高度变化和宽度变化改变几何对象UIelement,而Unexecute方法通过减去刚应用的变化来撤销操作。这样,它减掉了几何对象UIelement的边距变化、高度变化和宽度变化。
{
private FrameworkElement _UiElement;
private Canvas _Container;
public InsertCommand(FrameworkElement uiElement, Canvas container)
{
_UiElement = uiElement;
_Container = container;
}
#region ICommand Members
public void Execute()
{
if (!_Container.Children.Contains(_UiElement))
{
_Container.Children.Add(_UiElement);
}
}
public void UnExecute()
{
_Container.Children.Remove(_UiElement);
}
#endregion
}
插入操作插入一个几何对象到面板,插入命令包含几何对象和对Canvas的引用。在插入命令中,Execute方法添加对象到画布,而Unexecute方法从画布中删除该对象。
{
private FrameworkElement _UiElement;
private Canvas _Container;
public DeleteCommand(FrameworkElement uiElement, Canvas container)
{
_UiElement = uiElement;
_Container = container;
}
#region ICommand Members
public void Execute()
{
_Container.Children.Remove(_UiElement);
}
public void UnExecute()
{
_Container.Children.Add(_UiElement);
}
#endregion
}
因为删除操作从面板中删除一个几何对象,因此删除命令带有几何对象和画布的应用。在删除对象中,Execute方法从画布中删除对象,而Unexecute方法添加这个对象到画布。
步骤3
现在我们将根据通用方法的描述实现UndoRedo类。
{
private Stack<ICommand> _Undocommands = new Stack<ICommand>();
private Stack<ICommand> _Redocommands = new Stack<ICommand>();
private Canvas _Container;
public Canvas Container
{
get { return _Container; }
set { _Container = value; }
}
public void Redo(int levels)
{
for (int i = 1; i <= levels; i++)
{
if (_Redocommands.Count != 0)
{
ICommand command = _Redocommands.Pop();
command.Execute();
_Undocommands.Push(command);
}
}
}
public void Undo(int levels)
{
for (int i = 1; i <= levels; i++)
{
if (_Undocommands.Count != 0)
{
ICommand command = _Undocommands.Pop();
command.UnExecute();
_Redocommands.Push(command);
}
}
}
#region UndoHelperFunctions
public void InsertInUnDoRedoForInsert(FrameworkElement ApbOrDevice)
{
ICommand cmd = new InsertCommand(ApbOrDevice, Container);
_Undocommands.Push(cmd);_Redocommands.Clear();
}
public void InsertInUnDoRedoForDelete(FrameworkElement ApbOrDevice)
{
ICommand cmd = new DeleteCommand(ApbOrDevice, Container);
_Undocommands.Push(cmd);_Redocommands.Clear();
}
public void InsertInUnDoRedoForMove
(Point margin, FrameworkElement UIelement)
{
ICommand cmd = new MoveCommand(new Thickness
(margin.X, margin.Y, 0, 0), UIelement);
_Undocommands.Push(cmd);_Redocommands.Clear();
}
public void InsertInUnDoRedoForResize
(Point margin, double width, double height, FrameworkElement UIelement)
{
ICommand cmd = new ResizeCommand(new Thickness
(margin.X, margin.Y, 0, 0), width, height, UIelement);
_Undocommands.Push(cmd);_Redocommands.Clear();
}
#endregion
}
第一个栈_Undocommands进行撤销操作而第二个栈_Redocommands进行重做操作。当InsertInUnDoRedo方法被调用,Icommand对象被插入使之可以进行撤销操作并清空Redostack。这里,级别决定了你想要撤销的次数。这就是为什么你级别是多少,就不得不在undoredo方法中运行撤销,重做方法多少次。
步骤4
当你完成应用程序中不同的操作,创建该操作类型的命令对象并调用InsertInUnDoRedo方法将其压入Undo/Redo系统。当用户界面上点击Undo,我们调用UndoRedo类的Undo方法,而当用户界面上点击Redo,我们调用UndoRedo类的redo方法。
这里,我们没有明确设置Undo栈和Redo栈的大小,因此,应用程序能具有的状态数目取决于系统的内存。
使用命令模式时的变更管理
当你需要为你应用程序中新的操作支持Undo/Redo时,你只需要添加一个新的命令对象。我认为这具有很好的可维护性。
使用命令模式的优缺点
它的可维护性很好而且没有任何冗余信息。它不是内存密集型,从我的观点来看,命令模式是实现多级Undo/Redo的最好方式。
命令模式的唯一缺点在于你不得不使命令的类型与操作的数目相等而无论一个操作是小或大。随着操作地增加,命令也在增加。