【问题标题】:Undo/Redo Implementation For Multiple Variables多个变量的撤消/重做实现
【发布时间】:2012-10-14 17:31:58
【问题描述】:

我正在尝试重构我拥有的撤消/重做实现,但不确定如何去做。

public class MyObject
{
    public int A;
    public int B;
    public int C;
}

public abstract class UndoRedoAction
{
    protected MyObject myobj;
    protected int oldValue;
    protected int newValue;

    public abstract void Undo();
    public abstract void Redo();
}

public class UndoRedoActionA : UndoRedoAction
{
    UndoRedoActionA(MyObject obj, int new)
    {
        myobj = obj;
        oldValue = myobj.A;
        newValue = new;
        myobj.A = newValue;
    }

    public override void Undo()
    {
        myobj.A = oldValue;
    }

    public override void Redo()
    {
        myobj.A = newValue;
    }    
}

public class UndoRedoActionB : UndoRedoAction
{
    UndoRedoActionB(MyObject obj, int new)
    {
        myobj = obj;
        oldValue = myobj.B;
        newValue = new;
        myobj.B = newValue;
    }

    public override void Undo()
    {
        myobj.B = oldValue;
    }

    public override void Redo()
    {
        myobj.B = newValue;
    }    
}

public class UndoRedoActionC : UndoRedoAction
{
    UndoRedoActionC(MyObject obj, int new)
    {
        myobj = obj;
        oldValue = myobj.C;
        newValue = new;
        myobj.C = newValue;
    }

    public override void Undo()
    {
        myobj.C = oldValue;
    }

    public override void Redo()
    {
        myobj.C = newValue;
    }    
}

显然,每个 UndoRedoAction 子类在访问不同字段时都具有自定义功能,但它们在这些字段上执行的功能是相同的。是否有任何干净的方法,除了将这些整数转换为属性并传递属性名称(我宁愿不这样做,魔术字符串等),将它们组合成一个通用的 UndoRedoAction 而不是制作一堆所有执行的子类对不同变量的操作完全相同?

我确实考虑过使用可以解决问题的 Memento 模式,但对于这么小的场景来说,这似乎有点矫枉过正,而且我无需担心单向操作,这正是 Memento 模式真正有用的地方。

谢谢。

澄清:这些 UndoRedoAction 对象被放置在一个 Stack 中,该 Stack 用作撤消缓存。更具体地说,有两个堆栈,一个用于撤消,一个用于重做,从一个弹出的操作被推送到另一个。此外,针对 Zaid Masud 的回应,变量不一定都是整数,甚至不一定都是相同的对象类型。我的例子只是为了简单起见。

【问题讨论】:

    标签: c# inheritance command undo-redo memento


    【解决方案1】:

    您可以通过使用动态方法或表达式来避免反射,从而根据 x => x.SomeProperty 之类的选择器表达式创建自定义属性 getter/setter。

    基本上你会想做这样的事情:

    public class PropertySetActionProvider<TObj, TProp>
    {
        private Func<TObj, TProp> _getter;
        private Action<Tbj, TProp> _setter;
    
        public PropertySetActionProvider(Expression<Func<TObj, TProp>> propertySelector)
        {
            _getter = propertySelector.Compile();
            _setter = SetterExpressionFromGetterExpression(propertySelector).Compile(); 
        }
    
        public IUndoRedoAction CreateAction(TObj target, TProp newValue)
        {
            var oldValue = _getter(target);
            return new PropertySetAction<TObj, TProp>(_setter, target, oldValue, newValue);             
        }
    }
    
    public class PropertySetAction<TObj, TProp> : IUndoRedoAction 
    {
       private Action<TObj, TProp> _setter;
       private TObj _target;
       private TProp _oldValue;
       private TProp _newValue;
    
       public PropertySetAction(Action<TObj, TProp> setter, TObj target, TProp oldValue, TProp newValue)
       {
            _setter = setter; 
            _target = target; 
            _oldValue = oldValue; 
            _newValue = newValue;
       }
    
       public void Do() 
       {
           _setter(_target, _newValue);
       }  
    
       public void Undo() 
       {
           _setter(_target, _oldValue);
       }   
    }
    

    然后您可以使用如下代码轻松创建新操作:

      // create the action providers once per property you'd like to change
      var actionProviderA = new PropertySetActionProvider<MyObject, int>(x => x.A);
      var actionProviderB = new PropertySetActionProvider<MyObject, string>(x => x.B);
    
      var someObject = new MyObject { A = 42, B = "spam" };
      actions.Push(actionProviderA.CreateAction(someObject, 43);
      actions.Push(actionProviderB.CreateAction(someObject, "eggs");
      actions.Push(actionProviderA.CreateAction(someObject, 44);
      actions.Push(actionProviderB.CreateAction(someObject, "sausage");
    
      // you get the point
    

    唯一困难的部分是从 getter 表达式创建一个 setter 表达式(上面的 SetterExpressionFromGetterExpression 方法),但这是一个已知已解决的问题。有关如何执行此操作的问题,请参阅例如 herehere

    在这种方法中,从表达式编译 getter/setter 委托的成本仅在您创建操作提供程序时产生一次,而不是每次创建操作或调用 RedoUndo 时。

    如果您想进一步优化,您可以将propertySelector 参数移动到操作构造函数中,并在后台按需创建操作提供程序并基于每个属性进行缓存。这将产生一些更易于使用的代码,但实现起来可能更棘手。

    希望这会有所帮助!

    【讨论】:

    • 在运行时创建方法?这太可怕了。聪明,做的正是我想要的,但很可怕......
    • 将表达式编译为委托是在运行时创建代码的最温和的形式;这不像我要你开始发射 IL。 :) 动作提供者可以在一些初始化/设置时间急切地初始化,如果它不那么可怕的话。
    • 叫我老同学。你编写程序,编译它,运行输出。拥有一个本质上可以修改自身的程序对我来说听起来像是可怕的 AI 接管世界的东西。但是概念和示例代码非常清楚,我肯定会使用它。只是有点令人兴奋,这样的事情是可能的:)
    【解决方案2】:

    对于这类问题,我更喜欢使用Stack&lt;T&gt;List&lt;T&gt; 并将我的整个对象复制到其中。这允许拥有比一个更多的撤消缓存,并且很简单。我使用此模型允许用户 10 撤消复杂对象。 顺便说一句,这需要可序列化的类用法。这是我的代码,用于制作我的课程的精确副本:

            private T DeepCopy<T>(T obj)
        {
            object result = null;
            if (obj == null)
            {
                return (T)result;
            }
            using (var ms = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj);
                ms.Position = 0;
    
                result = (T)formatter.Deserialize(ms);
                ms.Close();
            }
    
            return (T)result;
        }
    

    编辑:List 的简单用法

            List<MyClass> UndoCache = new List<MyClass>();
        MyClass myRealObject;
        int unDoCount = 10;
           void Undo()
        {
            myRealObject = UndoCache.Last();
            //remove this object from cache.
            UndoCache.RemoveAt(UndoCache.Count - 1);
        }
    

    //当你的对象改变时调用这个方法

            void ObjectChanged()
        {
            //remove the first item if we reach limit
            if (UndoCache.Count > unDoCount)
            {
                UndoCache.RemoveAt(0);
            }
            UndoCache.Add(DeepCopy<MyClass>(myRealObject));
        }
        public class MyClass { }
    

    【讨论】:

    • 据我所知,这是 Memento 模式,不是吗?而且,在我的示例中没有显示,这些“Action”对象然后被放置在用作撤消缓存的 Stack 中。
    • 你是对的。实际上我对这种模式了解不多,但就我而言,当我的对象发生更改时,我会在撤消列表中插入一个副本。当我需要撤消时,我会使用该存档。
    【解决方案3】:

    我想如果MyObject 中的所有字段都是int,就像您的代码示例一样,您可以将它们转换为固定长度的数组。代码示例(未编译)

    public class MyObject
    {
        public const int ARRAY_LENGTH = 3;
        public int[] Ints = new int[ARRAY_LENGTH]
    }
    
    public abstract class UndoRedoAction
    {
        private MyObject myobj;
        private int oldValues = new int[MyObject.ARRAY_LENGTH];
        private int newValues = new int[MyObject.ARRAY_LENGTH];
    
        public UndoRedoAction(MyObject obj)
        {
            myobj = obj;
        }
    
        public void SetValue(int index, int newValue)
        {
            oldValues[index] = myObj.Ints[index];
            newValues[index] = newValue;
            myObj.Ints[index] = newValue;
        }
    
        public void Undo(int index)
        {
            myObj.Ints[index] = oldValues[index];
        }
    
        public void Redo(int index)
        {
            myObj.Ints[index] = newValues[index];
        }
    }
    

    【讨论】:

    • 没错,但不幸的是事实并非如此。为了清楚起见,我只是在示例中这样做了。
    猜你喜欢
    • 2012-07-02
    • 2016-02-18
    • 2011-03-10
    • 1970-01-01
    • 2011-11-14
    • 2015-07-31
    • 2021-06-04
    • 2014-04-20
    • 1970-01-01
    相关资源
    最近更新 更多