我需要为我的窗口应用程序(类似于PowerPoint的编辑器)实现撤销/重做框架,应该遵循哪些最佳实践?如何处理对象的所有属性更改以及它们在UI上的反映。
我需要为我的窗口应用程序(类似于PowerPoint的编辑器)实现撤销/重做框架,应该遵循哪些最佳实践?如何处理对象的所有属性更改以及它们在UI上的反映。
我可能会避免使用这两种模式的混合,因为我关心接替我的开发人员能够维护我的代码,并且这也是我对雇主的道德责任,使这个过程尽可能简单和廉价。我认为这两种模式的混合很容易变成一个难以维护的不适之处,需要花费大量成本来维护。
这里有三种可行的方法:备忘录模式(快照)、命令模式和状态差分。它们都有利有弊,实际上取决于你的用例、你使用的数据以及你愿意实现的内容。
如果可以适用状态差分,则建议采用它,因为它将内存减少与易于实现和维护相结合。
我将引用一篇介绍这三种方法的文章(参见下文引用)。
注意,在文章中提到的VoxelShop是开源的。因此,您可以查看此处命令模式的复杂性: https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history
以下是文章的摘录。但我建议您全文阅读。
备忘录模式
每个历史状态都存储完整的副本。动作创建一个新状态,并使用指针在状态之间移动,允许撤消和重做。
优点
缺点
命令模式
类似于备忘录模式,但不是存储完整状态,而是仅存储状态之间的差异。差异以可应用和可取消的操作形式存储。引入新操作时,需要实现应用和取消应用。
优点
缺点
状态差异
类似于命令模式,但是差异是独立于操作而存储的,只需通过异或状态即可。引入新操作不需要任何特殊考虑。
优点
缺点/限制
参考:
https://www.linkedin.com/pulse/solving-history-hard-problem-lukas-siemon
经典的做法是遵循命令模式。
您可以使用命令封装执行操作的任何对象,并使用Undo()方法执行相反的操作。您可以将所有操作存储在堆栈中,以便轻松地通过它们进行倒带。
看一下命令模式。 你需要将对模型的每个更改封装到单独的命令对象中。
我编写了一个非常灵活的系统来跟踪更改。我有一个绘图程序,它实现了两种类型的更改:
基类:
public abstract class Actie
{
public Actie(Vorm[] Vormen)
{
vormen = Vormen;
}
private Vorm[] vormen = new Vorm[] { };
public Vorm[] Vormen
{
get { return vormen; }
}
public abstract void Undo();
public abstract void Redo();
}
用于添加形状的派生类:
public class VormenToegevoegdActie : Actie
{
public VormenToegevoegdActie(Vorm[] Vormen, Tekening tek)
: base(Vormen)
{
this.tek = tek;
}
private Tekening tek;
public override void Redo()
{
tek.Vormen.CanRaiseEvents = false;
tek.Vormen.AddRange(Vormen);
tek.Vormen.CanRaiseEvents = true;
}
public override void Undo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Remove(v);
tek.Vormen.CanRaiseEvents = true;
}
}
用于删除形状的派生类:
public class VormenVerwijderdActie : Actie
{
public VormenVerwijderdActie(Vorm[] Vormen, Tekening tek)
: base(Vormen)
{
this.tek = tek;
}
private Tekening tek;
public override void Redo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Remove(v);
tek.Vormen.CanRaiseEvents = true;
}
public override void Undo()
{
tek.Vormen.CanRaiseEvents = false;
foreach(Vorm v in Vormen)
tek.Vormen.Add(v);
tek.Vormen.CanRaiseEvents = true;
}
}
属性更改的派生类:
public class PropertyChangedActie : Actie
{
public PropertyChangedActie(Vorm[] Vormen, string PropertyName, object OldValue, object NewValue)
: base(Vormen)
{
propertyName = PropertyName;
oldValue = OldValue;
newValue = NewValue;
}
private object oldValue;
public object OldValue
{
get { return oldValue; }
}
private object newValue;
public object NewValue
{
get { return newValue; }
}
private string propertyName;
public string PropertyName
{
get { return propertyName; }
}
public override void Undo()
{
//Type t = base.Vorm.GetType();
PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
foreach(Vorm v in Vormen)
{
v.CanRaiseVeranderdEvent = false;
info.SetValue(v, oldValue, null);
v.CanRaiseVeranderdEvent = true;
}
}
public override void Redo()
{
//Type t = base.Vorm.GetType();
PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName);
foreach(Vorm v in Vormen)
{
v.CanRaiseVeranderdEvent = false;
info.SetValue(v, newValue, null);
v.CanRaiseVeranderdEvent = true;
}
}
}
每次 Vormen = 提交更改的项目数组。 应该像这样使用:
声明堆栈:
Stack<Actie> UndoStack = new Stack<Actie>();
Stack<Actie> RedoStack = new Stack<Actie>();
添加新形状(例如,点)
VormenToegevoegdActie vta = new VormenToegevoegdActie(new Vorm[] { NieuweVorm }, this);
UndoStack.Push(vta);
RedoStack.Clear();
删除所选形状
VormenVerwijderdActie vva = new VormenVerwijderdActie(to_remove, this);
UndoStack.Push(vva);
RedoStack.Clear();
注册属性更改
PropertyChangedActie ppa = new PropertyChangedActie(new Vorm[] { (Vorm)e.Object }, e.PropName, e.OldValue, e.NewValue);
UndoStack.Push(ppa);
RedoStack.Clear();
最后是撤销/重做操作
public void Undo()
{
Actie a = UndoStack.Pop();
RedoStack.Push(a);
a.Undo();
}
public void Redo()
{
Actie a = RedoStack.Pop();
UndoStack.Push(a);
a.Redo();
}
我认为这是实现撤销重做算法最有效的方法。 例如,可以查看我网站上的此页面:DrawIt。
我在文件Tekening.cs的第479行左右实现了撤销重做功能。您可以下载源代码。它可以被任何类型的应用程序实现。