撤消重做实现的最佳实践

时间:2009-12-16 16:35:57

标签: c# frameworks undo-redo

我需要为我的窗口应用程序(像powerpoint这样的编辑器)实现撤消/重做框架工作,应该遵循的最佳实践,如何处理我的对象的所有属性更改以及它在UI上的反射。

5 个答案:

答案 0 :(得分:29)

有两种经典模式可供使用。第一个是memento pattern,用于存储完整对象状态的快照。这可能比命令模式更加系统密集,但它允许非常简单地回滚到较旧的快照。您可以将快照存储在磁盘上,然后将其存储在PaintShop / PhotoShop中,或者将它们保存在内存中,以用于不需要持久性的较小对象。你正在做的正是这个模式的设计,所以它应该比其他人建议的命令模式略好。

此外,另外需要注意的是,因为它不需要您使用相反的命令撤消先前完成的操作,这意味着任何可能无法撤消的可能单向函数[例如散列或加密]通过回滚到较旧的快照,可以简单地使用互惠命令撤消。

同样正如所指出的那样command pattern可能资源密集程度较低,所以我会在特定情况下承认:

  • 要保留一个大对象状态和/或
  • 没有破坏性的方法和
  • 可以非常简单地使用互惠命令来撤销所采取的任何行动

命令模式可能更适合[但不一定,它将在很大程度上取决于情况]。在其他情况下,我会使用memento模式。

我可能会避免使用这两者的混搭,因为我倾向于关心开发人员,这些开发人员会支持我,并维护我的代码以及我的雇主的道德责任,使这个过程变得简单而且价格便宜。我看到这两种模式的混搭很容易变成一种难以维护的老鼠洞,这种老鼠难以维持。

答案 1 :(得分:6)

经典的做法是遵循Command Pattern

您可以使用命令封装执行操作的任何对象,并让它使用Undo()方法执行反向操作。您可以将所有操作存储在堆栈中,以便通过它们轻松地进行重绕。

答案 2 :(得分:5)

这里有三种可行的方法。纪念模式(快照),命令模式和状态差异。它们都有优点和缺点。

我会选择State Diffing,因为它可以将内存减少与易于实现和可维护性相结合。

请注意,文章中提到的VoxelShop是开源的。所以你可以在这里看一下命令模式的复杂性: https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

以下摘自文章:

Memento Pattern

enter image description here

赞成

  • 实施与所应用的行动无关。一旦实施,我们可以添加操作,而不必担心破坏历史。
  • 快速前进到历史中的预定义位置。当在当前和期望的历史位置之间应用的动作在计算上是昂贵的时,这是有趣的。

缺点

  • 与其他方法相比,内存要求可能会显着提高。
  • 如果快照很大,加载时间可能会很慢。

命令模式

enter image description here

赞成

  • 内存占用量很小。我们只需要将更改存储到模型中,如果这些更改很小,那么历史堆栈也很小。

缺点

  • 我们不能直接去任意位置,而是需要取消应用历史堆栈直到我们到达那里。这可能非常耗时。
  • 每个动作及其反向都需要封装在一个对象中。如果你的行动不重要,那么这可能很困难。 (反向)动作中的错误实际上很难调试,很容易导致致命的崩溃。即使是简单的行动通常也会涉及很多复杂性。例如。对于3D编辑器,添加到模型的对象需要存储添加的内容,当前选择的颜色,覆盖的内容,镜像模式是否有效等。
  • 当动作没有简单的反转时,例如模糊图像时,实施和记忆密集可能具有挑战性。

状态差异

enter image description here

赞成

  • 实施与所应用的行动无关。添加历史记录功能后,我们可以添加操作,而不必担心破坏历史记录。
  • 内存要求通常远低于快照方法,并且在许多情况下与命令模式方法相当。但是,这在很大程度上取决于所应用的操作类型。例如。使用命令模式反转图像的颜色应该非常便宜,而状态差异将保存整个图像。相反,在绘制长自由格式线时,如果命令模式方法链接每个像素的历史条目,则它可能会使用更多内存。

缺点/限制

  • 我们不能直接进入任意位置,而是需要取消应用历史堆栈直到我们到达那里。
  • 我们需要计算状态之间的差异。这可能很昂贵。
  • 根据您的数据模型,实现模型状态之间的xor差异可能很难实现。

参考:

https://www.linkedin.com/pulse/solving-history-hard-problem-lukas-siemon

答案 3 :(得分:2)

看看Command Pattern。 您必须将模型的每个更改封装到单独的命令对象中。

答案 4 :(得分:0)

我写了一个非常灵活的系统来跟踪变化。我有一个绘图程序,它实现了两种类型的更改:

  • 添加/删除形状
  • 形状的属性更改

基类:

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>();

添加新形状(例如Point)

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();
}

我认为这是实现undo-redo算法的最有效方法。 例如,请查看我的网站上的此页面:DrawIt

我在Tekening.cs文件的第479行附近实现了undo redo stuff。您可以下载源代码。它可以通过任何类型的应用程序实现。