我需要为我的窗口应用程序(如 powerpoint 之类的编辑器)实现撤消/重做框架工作,应该遵循的最佳实践是什么,如何处理我的对象的所有属性更改以及它在 UI 上的反映。
5 回答
有两种经典模式可供使用。第一个是备忘录模式,用于存储完整对象状态的快照。这可能比命令模式更加系统密集,但它允许非常简单地回滚到较旧的快照。您可以将快照存储在磁盘上,例如 PaintShop/PhotoShop,或者将它们保存在内存中以用于不需要持久性的较小对象。你所做的正是这个模式的设计目的,所以它应该比其他人建议的命令模式更符合要求。
另外,需要注意的是,因为它不需要您使用互惠命令来撤消以前完成的操作,这意味着任何潜在的单向功能[例如散列或加密]都无法使用互惠来轻松撤消只需回滚到较旧的快照,仍然可以非常简单地撤消命令。
同样正如所指出的,命令模式可能占用较少的资源,所以我承认在特定情况下:
- 有一个大的对象状态需要持久化和/或
- 没有破坏性的方法和
- 可以非常简单地使用互惠命令来逆转所采取的任何行动
命令模式可能更适合[但不一定,这将在很大程度上取决于情况]。在其他情况下,我会使用备忘录模式。
我可能会避免使用两者的混搭,因为我倾向于关心将在我身后出现并维护我的代码的开发人员,以及我对雇主的道德责任是让这个过程变得简单和便宜可能的。我看到这两种模式的混搭很容易成为无法维护的不适感,维护成本很高。
这里有三种可行的方法。备忘录模式(快照)、命令模式和状态差异。它们都有优点和缺点,这实际上取决于您的用例、您正在使用的数据以及您愿意实施的内容。
如果您可以摆脱它,我会选择 State Diffing,因为它结合了内存减少与易于实现和可维护性。
我将引用一篇描述这三种方法的文章(参考下文)。
请注意,文中提到的 VoxelShop 是开源的。所以你可以在这里看看命令模式的复杂性: https ://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history
以下是文章的摘录。但是,我确实建议您完整阅读它。
纪念品图案
每个历史状态都存储一个完整副本。一个动作创建一个新状态,一个指针用于在状态之间移动以允许撤消和重做。
优点
- 实施独立于应用的操作。一旦实现,我们就可以添加操作而不必担心破坏历史。
- 快速推进到历史中的预定义位置。当在当前和期望的历史位置之间应用的动作在计算上很昂贵时,这很有趣。
缺点
- 与其他方法相比,内存要求可能要高得多。
- 如果快照很大,加载时间可能会很慢。
命令模式
类似于备忘录模式,但不是存储完整状态,而是仅存储状态之间的差异。差异存储为可以应用和不应用的操作。引入新动作时,需要执行应用和取消应用。
优点
- 内存占用很小。我们只需要将更改存储到模型中,如果这些更改很小,那么历史堆栈也很小。
缺点
- 我们不能直接去任意位置,而是需要取消应用历史堆栈,直到我们到达那里。这可能很耗时。
- 每个动作及其相反的动作都需要封装在一个对象中。如果你的行动不是微不足道的,这可能很困难。(反向)操作中的错误确实很难调试,并且很容易导致致命的崩溃。即使是看起来很简单的动作通常也包含大量的复杂性。例如,在 3D 编辑器的情况下,添加到模型的对象需要存储添加的内容、当前选择的颜色、覆盖的内容、镜像模式是否激活等。
- 当动作没有简单的反向时,例如在模糊图像时,实现起来可能具有挑战性并且需要大量内存。
状态差异
与命令模式类似,但通过简单地对状态进行异或操作来独立于操作存储差异。引入新动作不需要任何特殊考虑。
优点
- 实施独立于应用的操作。一旦添加了历史功能,我们就可以添加操作而不必担心破坏历史。
- 内存要求通常比快照方法低得多,并且在很多情况下与命令模式方法相当。然而,这在很大程度上取决于所应用的操作类型。例如,使用命令模式反转图像的颜色应该非常便宜,而状态区分将保存整个图像。相反,当绘制一条自由格式的长线时,如果命令模式方法将每个像素的历史条目链接起来,它可能会使用更多内存。
缺点/限制
- 我们不能直接去任意位置,而是需要取消应用历史堆栈,直到我们到达那里。
- 我们需要计算状态之间的差异。这可能很昂贵。
- 根据您的数据模型,实现模型状态之间的异或差异可能很难实现。
参考:
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 行左右实现了撤消重做内容。您可以下载源代码。它可以由任何类型的应用程序实现。