撤销重做功能实现

前言

大佬走过,小菜留下。

该文讲述我如何把撤销重做功能做到让我自己满意。

这篇随笔起于公司项目需要一个撤销重写功能,因为是图形设计。

第一想法

起初第一想法是保存整个操作对象,然后撤销就重新换整个对象就ok了。在群里讨论的时候也只是说这种方式,可能隐藏大佬没出现

这种方法大佬群里直接丢出一个demo,我觉得挺好的,如果是小的对象的话,这样做完全没问题,下面我给出大佬的代码

public interface IUndoable<T>
    {
        bool CanRedo { get; }
        bool CanUndo { get; }
        T Value { get; set; }
        void SaveState();
        void Undo();
        void Redo();
    }
internal interface IUndoState<T>
    {
        T State { get; }
    }
public class Undoable<T> : IUndoable<T>
    {
        Stack<IUndoState<T>> _redoStack;
        Stack<IUndoState<T>> _undoStack;
        T _value;

        public Undoable(T value)
        {
            _value = value;
            _redoStack = new Stack<IUndoState<T>>();
            _undoStack = new Stack<IUndoState<T>>();
        }

        public T Value
        {
            get { return _value; }
            set { _value = value; }
        }

        public bool CanRedo
        {
            get { return _redoStack.Count != 0; }
        }

        public bool CanUndo
        {
            get { return _undoStack.Count != 0; }
        }

        public void SaveState()
        {
            _redoStack.Clear();
            _undoStack.Push(GenerateUndoState());
        }

        public void Undo()
        {
            if (_undoStack.Count == 0) throw new InvalidOperationException("Undo history exhausted");

            _redoStack.Push(GenerateUndoState());
            _value = _undoStack.Pop().State;
        }

        private UndoState<T> GenerateUndoState()
        {
            return new UndoState<T>(Value);
        }

        public void Redo()
        {
            if (_redoStack.Count == 0) throw new InvalidOperationException("Redo history exhausted");

            _undoStack.Push(GenerateUndoState());
            _value = _redoStack.Pop().State;
        }
    }
internal class UndoState<T> : IUndoState<T>
    {
        BinaryFormatter _formatter;
        byte[] _stateData;

        internal UndoState(T state)
        {
            _formatter = new BinaryFormatter();
            using (MemoryStream stream = new MemoryStream())
            {
                _formatter.Serialize(stream, state);
                _stateData = stream.ToArray();
            }
        }

        public T State
        {
            get
            {
                using (MemoryStream stream = new MemoryStream(_stateData))
                {
                    return (T)_formatter.Deserialize(stream);
                }
            }
        }
    }
class Program
    {
        static void Main(string[] args)
        {
            IUndoable<string> stuff = new Undoable<string>("State One");
            stuff.SaveState();
            stuff.Value = "State Two";
            stuff.SaveState();
            stuff.Value = "State Three";

            stuff.Undo();   // State Two
            stuff.Undo();   // State One
            stuff.Redo();   // State Two
            stuff.Redo();   // State Three
        }
    }

上面是大佬的全部代码,使用字节流来记录整个对象,撤销和重写就是把整个对象创建一遍,这种可以用到一些情况。

但是不适用我的项目中,因为每一次更改一点东西就需要把整个对象记下来,而且wpf项目中之前绑定的都会失效,因为不是原来的对象了。

第一版本

既然是撤销重写,应该只需记录下改变的东西,其他不需要记录,所以我需要两个栈,一个记录历史栈(撤销),一个重做栈,和压入栈的数据类。

数据类如下:

public class UnRedoInfo
    {
        /// <summary>
        /// 插入的对象
        /// </summary>
        public object Item { get; set; }
        /// <summary>
        /// 记录对象更改的属性和属性值
        /// </summary>
        public Dictionary<string, object> PropValueDry { get; set; }
    }
Item是更改的属性所属的对象,PropValueDry是key:属性名,value:属性值

撤销重做功能类如下
public class UnRedoHelp
    {
        //撤销和重做栈。
        public static Stack<UnRedoInfo> UndoStack;
        public static Stack<UnRedoInfo> RedoStack;

        static UnRedoHelp()
        {
            UndoStack = new Stack<UnRedoInfo>();
            RedoStack = new Stack<UnRedoInfo>();
        }
        //添加撤销命令
        /// <summary>
        /// 添加撤销命令
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propValueDry"></param>
        public static void Add(object item, Dictionary<string, object> propValueDry)
        {
            UnRedoInfo info = new UnRedoInfo();
            info.Item = item;
            info.PropValueDry = propValueDry;
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 添加撤销命令
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propNames">记录的属性名更改数组</param>
        public static void Add(object item, params string[] propNames)
        {
            UnRedoInfo info = new UnRedoInfo();
            info.Item = item;

            //添加属性和属性值
            Dictionary<string, object> propValueDry = new Dictionary<string, object>();
            for (int i = 0; i < propNames.Length; i++)
            {
                var obj = GetPropertyValue(item, propNames[i]);
                if (!propValueDry.ContainsKey(propNames[i]))
                {
                    propValueDry.Add(propNames[i],obj);
                }
            }

            info.PropValueDry = propValueDry;
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 撤销
        /// </summary>
        public static void Undo()
        {
            if (UndoStack.Count == 0)
            {
                return;
            }

            UnRedoInfo info = UndoStack.Pop();
            //设置属性值
            foreach (var item in info.PropValueDry)
            {
                SetPropertyValue(info.Item,item.Key,item.Value);
            }
            //将撤销的命令重新压到重做栈顶,重做时可恢复。
            RedoStack.Push(info);
        }
        /// <summary>
        /// 重做
        /// </summary>
        public static void Redo()
        {
            if (RedoStack.Count == 0)
            {
                return;
            }

            UnRedoInfo info = RedoStack.Pop();
            //设置属性值
            foreach (var item in info.PropValueDry)
            {
                SetPropertyValue(info.Item, item.Key, item.Value);
            }
            //将撤销的命令重新压到重做栈顶,重做时可恢复。
            UndoStack.Push(info);
        }
        /// <summary>
        /// 获取属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static object GetPropertyValue(object obj, string name)
        {
            PropertyInfo property = obj.GetType().GetProperty(name);
            if (property != null)
            {
                object drv1 = property.GetValue(obj, null);
                return drv1;
            }
            else
            {
                return null;
            }
        }
        /// <summary>
        /// 设置属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <param name="value"></param>
        public static void SetPropertyValue(object obj, string name,object value)
        {
            PropertyInfo property = obj.GetType().GetProperty(name);
            if (property != null)
            {
                property.SetValue(obj, value);
            }
        }
    }

上面用了反射获取属性的值和设置属性,这个功能类逻辑是有问题,因为我那时候心思没在哪方面,是我写到了最后面才发现的,并在新版里改正了,但是这个版本并没有 ,

而且这个代码是我后面版本撤回来才有的代码,不保证没有任何错误。

如果你也正好需要这样的功能,那为何不往下再看看呢

我以为上面的可以解决我的问题,然并卵,如果属性是集合,那根本就没用,因为栈的数据对象保存的属性值是对象,就是外面添加减少,栈里的也会改变。所以有了下一个版本

第二版本

我想用字节流来保存属性的值,所以

public class UnRedoHelp
    {
        static UnRedoHelp()
        {
            UndoStack = new Stack<UnRedoInfo>();
            RedoStack = new Stack<UnRedoInfo>();
            _formatter = new BinaryFormatter();
        }
        //撤销和重做栈。
        public static Stack<UnRedoInfo> UndoStack;
        public static Stack<UnRedoInfo> RedoStack;
        static BinaryFormatter _formatter;
        //添加撤销命令
        /// <summary>
        /// 添加撤销命令
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propValueDry"></param>
        public static void Add(object item, Dictionary<string, object> propValueDry)
        {
            UnRedoInfo info = new UnRedoInfo();
            info.Item = item;
            info.PropValueDry = propValueDry;
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 添加撤销命令
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propNames">记录的属性名更改数组</param>
        public static void Add(object item, params string[] propNames)
        {
            UnRedoInfo info = new UnRedoInfo();
            info.Item = item;

            //添加属性和属性值
            Dictionary<string, object> propValueDry = new Dictionary<string, object>();
            for (int i = 0; i < propNames.Length; i++)
            {

                if (!propValueDry.ContainsKey(propNames[i]))
                {
                    var obj = GetPropertyValue(item, propNames[i]);
                    //将属性值,序列化成字节流
                    using (MemoryStream stream = new MemoryStream())
                    {
                        _formatter.Serialize(stream, obj);
                        var bt = stream.ToArray();
                        propValueDry.Add(propNames[i], bt);
                    }

                }
            }

            info.PropValueDry = propValueDry;
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 撤销
        /// </summary>
        public static void Undo()
        {
            if (UndoStack.Count == 0)
            {
                return;
            }

            UnRedoInfo info = UndoStack.Pop();
            //设置属性值
            foreach (var item in info.PropValueDry)
            {
                object value = GetPropBytes(item.Value);
                SetPropertyValue(info.Item, item.Key, value);
            }
            //将撤销的命令重新压到重做栈顶,重做时可恢复。
            RedoStack.Push(info);
        }
        /// <summary>
        /// 重做
        /// </summary>
        public static void Redo()
        {
            if (RedoStack.Count == 0)
            {
                return;
            }

            UnRedoInfo info = RedoStack.Pop();
            //设置属性值
            foreach (var item in info.PropValueDry)
            {
                object value = GetPropBytes(item.Value);
                SetPropertyValue(info.Item, item.Key, value);
            }
            //将撤销的命令重新压到重做栈顶,重做时可恢复。
            UndoStack.Push(info);
        }
        /// <summary>
        /// 转换字节流获取属性的值
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private static object GetPropBytes(object value)
        {
            var bts = (byte[])value;
            using (MemoryStream stream = new MemoryStream(bts))
            {
                return _formatter.Deserialize(stream);
            }
        }

        /// <summary>
        /// 获取属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static object GetPropertyValue(object obj, string name)
        {
            PropertyInfo property = obj.GetType().GetProperty(name);
            if (property != null)
            {
                object drv1 = property.GetValue(obj, null);
                return drv1;
            }
            else
            {
                return null;
            }
        }
        /// <summary>
        /// 设置属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <param name="value"></param>
        public static void SetPropertyValue(object obj, string name, object value)
        {
            PropertyInfo property = obj.GetType().GetProperty(name);

            if (property != null)
            {
                property.SetValue(obj, value);
            }
        }
    }

上面这个版本很快就被我否定了,它是和大佬的字节流保存相结合的产儿,为什么用字节流保存属性值不行呢,

举个栗子,现在保存的是列表子项对象的属性,然后再保存列表,撤销,列表回到原先的值,但是里面的子项对象已经不是原来的对象,虽然值都一样,再撤销,是反应不到列表里的子项的。

船新版本

对第一个版本进行改造,因为属性要么是对象,要么就是对象集合,什么,你说int不是对象(你当我什么都没说)

我们需要记录属性更详细的信息

保存属性的类型(对象还是集合)

/// <summary>
    /// 保存属性的类型(对象,集合)
    /// </summary>
    public enum PropInfoType
    {
        /// <summary>
        /// 单个对象属性
        /// </summary>
        Object,
        /// <summary>
        /// 列表属性
        /// </summary>
        IList
    }
/// <summary>
    /// 撤销重做的属性信息
    /// </summary>
    public class PropInfo
    {
        /// <summary>
        /// 属性类型
        /// </summary>
        public PropInfoType InfoType { get; set; }
        /// <summary>
        /// 单对象属性的值
        /// </summary>
        public object PropValue { get; set; }
        /// <summary>
        /// 列表对象属性的值,记录当前列表属性的所有子项
        /// </summary>
        public List<object> PropValueLst { get; set; }
        /// <summary>
        /// 属性名称
        /// </summary>
        public string PropName { get; set; }
    }
/// <summary>
    /// 撤销重做信息
    /// </summary>
    public class UnRedoInfo
    {
        /// <summary>
        /// 插入的对象
        /// </summary>
        public object Item { get; set; }
        
        /// <summary>
        /// 记录对象更改的多个属性和属性值
        /// </summary>
        public Dictionary<string, PropInfo> PropValueDry { get; set; }
    }

这三个类连着看,根据注释,应该没什么问题,不要问我为什么key已经是属性名了,为什么PropInfo中还有?

public class UnRedoHelp
    {
        static UnRedoHelp()
        {
            UndoStack = new Stack<UnRedoInfo>();
            RedoStack = new Stack<UnRedoInfo>();
        }
        //撤销和重做栈。
        public static Stack<UnRedoInfo> UndoStack;
        public static Stack<UnRedoInfo> RedoStack;

        /// <summary>
        /// 说明功能是否在撤销或者重做,true正在进行操作
        /// </summary>
        public static bool IsUnRedo = false;
        //添加撤销命令
        /// <summary>
        /// 添加撤销命令
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propValueDry"></param>
        public static void Add(object item, Dictionary<string, PropInfo> propValueDry)
        {
            if (IsUnRedo) return;
            UnRedoInfo info = new UnRedoInfo();
            info.Item = item;
            info.PropValueDry = propValueDry;
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 添加撤销命令,普通对象属性
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propNames">记录的属性名更改数组</param>
        public static void Add(object item, params string[] propNames)
        {
            if (IsUnRedo) return;

            if (RedoStack.Count != 0)
            {
                //添加要把重做清空
                RedoStack.Clear();
            }
            UnRedoInfo info = GetPropertyValue(item,propNames);
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }


        /// <summary>
        /// 撤销
        /// </summary>
        public static void Undo()
        {
            if (UndoStack.Count == 0)
            {
                return;
            }
            IsUnRedo = true;
            try
            {
                UnRedoInfo info = UndoStack.Pop();
                //先压到重做栈,再改变值 重做时可恢复
                UnRedoInfo oldinfo = GetPropertyValue(info.Item, info.PropValueDry.Keys.ToArray());
                RedoStack.Push(oldinfo);

                SetPropertyValue(info);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                IsUnRedo = false;
            }
            
            
        }
        /// <summary>
        /// 重做
        /// </summary>
        public static void Redo()
        {
            if (RedoStack.Count == 0)
            {
                return;
            }
            IsUnRedo = true;
            try
            {
                UnRedoInfo info = RedoStack.Pop();
                //先压到撤销栈,再改变值 撤销时可恢复
                UnRedoInfo oldinfo = GetPropertyValue(info.Item, info.PropValueDry.Keys.ToArray());
                UndoStack.Push(oldinfo);
                //设置属性值
                SetPropertyValue(info);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                IsUnRedo = false;
            }

        }
        /// <summary>
        /// 获取属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static UnRedoInfo GetPropertyValue(object obj, string[] propNames)
        {
            UnRedoInfo info = new UnRedoInfo();
            info.Item = obj;

            //添加属性和属性值
            Dictionary<string, PropInfo> propValueDry = new Dictionary<string, PropInfo>();
            for (int i = 0; i < propNames.Length; i++)
            {
                //对象属性名
                string name = propNames[i];
                //获取属性相关信息
                PropertyInfo property = obj.GetType().GetProperty(name);

                if (property != null)
                {
                    #region 设置撤销重做的属性信息
                    //设置撤销重做的属性信息
                    PropInfo propInfo = new PropInfo();
                    propInfo.PropName = name;
                    //获取属性值
                    var prop = property.GetValue(obj);
                    if (prop is System.Collections.IList)
                    {
                        //列表
                        propInfo.InfoType = PropInfoType.IList;
                        propInfo.PropValueLst = new List<object>();
                        var lst = (IList)prop;
                        foreach (var item in lst)
                        {
                            propInfo.PropValueLst.Add(item);
                        }

                    }
                    else
                    {
                        //不是列表,单个对象
                        propInfo.InfoType = PropInfoType.Object;
                        propInfo.PropValue = prop;
                    }

                    if (!propValueDry.ContainsKey(propNames[i]))
                    {
                        propValueDry.Add(propNames[i], propInfo);
                    }
                    #endregion
                }

            }
            //设置对象
            info.PropValueDry = propValueDry;
            return info;
        }
        /// <summary>
        /// 设置属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <param name="value"></param>
        public static void SetPropertyValue(UnRedoInfo info)
        {
            //设置属性值
            foreach (var item in info.PropValueDry)
            {
                PropertyInfo property = info.Item.GetType().GetProperty(item.Key);

                if (property != null)
                {
                    if (item.Value.InfoType == PropInfoType.Object)
                    {
                        //单个对象值的,直接赋值
                        property.SetValue(info.Item, item.Value.PropValue);
                    }
                    else if (item.Value.InfoType == PropInfoType.IList)
                    {
                        //列表对象值,先清除该列表对象的子项,然后重新添加子项
                        var lst = (IList)property.GetValue(info.Item);
                        lst.Clear();
                        foreach (var x in item.Value.PropValueLst)
                        {
                            lst.Add(x);
                        }
                    }
                }
            }
            
        }
    }
}

上面这个是船新版本,测试过,符合我的要求,下面是main函数里的使用代码,栗子

static void Main(string[] args)
        {
            TestC testC = new TestC();
            TestD testD = new TestD();
            testD.W = 5;

            testC.TestD = testD;
            testC.Name = "name1";
            testC.Count = 2;

            testC.TestDs = new List<TestD>();
            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i });
            }
            //添加历史记录 需要记录的属性名
            UnRedoHelp.Add(testC, nameof(testC.Name), nameof(testC.Count), nameof(testC.TestDs));

            testC.TestDs[0].W = -2;

            UnRedoHelp.Add(testC.TestDs[0], nameof(testD.W));

            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i + 3 });
            }

            testC.Name = "name2";
            testC.Count = 3;
            testC.TestDs[0].W = -9;

            UnRedoHelp.Add(testC, nameof(testC.Name), nameof(testC.Count), nameof(testC.TestDs));

            testC.Name = "name3";
            testC.Count = 4;

            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i + 6 });
            }
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();
            UnRedoHelp.Redo();
            UnRedoHelp.Redo();
        }

好了,这样总该可以了叭,什么?还不行?你不想写一堆的UnRedoHelp.Add(testC, nameof(testC.Name), nameof(testC.Count), nameof(testC.TestDs));

一群乌鸦飞过。。。。怎么办?

AOP,以前有看过一点这东西,所以脑子有这个印象,不过一直没用过。

所以说这么个小小的功能,一点点代码,我几年累计下来的知识完全不够用。下面是资料广告时间:

利用C#实现AOP常见的几种方法详解

【原创】颠覆C#王权的“魔比斯环” — 实现AOP框架的终极利器(这个让我很兴奋)

使用 RealProxy 类进行面向方面的编程

推荐个非常简单好用的AOP -- MrAdvice

C#语法——反射,架构师的入门基础(不好意思,打扰了)

上面就是我找的,觉得有用的,可以学到点东西的资料,第一个资料我试了两个就是第二三种方式,然后我觉得不好用,然后群里推荐了

(大佬:AOP框架-动态代理和IL
微软企业库的PIAB Postsharp
Mr.Advice castle dynamicproxy sheepAspect PropertyChanged.Fody

大佬:你去找找,我用的是Mr.Advice)

嗯,大佬让我用Mr.Advice,然后我找了第四个资料,确实符合我的需求。

安装Mr.Advice,写UnRedoAttribute

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
    public class UnRedoAttribute :  Attribute, IMethodAdvice
    {
        public void Advise(MethodAdviceContext context)
        {
            //必须是属性改变,而且不是因为撤销重做引起的
            if (context.TargetName.Length > 4 && context.TargetName.StartsWith("set_") )
            {
                //属性改变
                //添加历史记录 需要记录的属性名 去掉get_和set_
                string prop = context.TargetName.Remove(0, 4);
                UnRedoHelp.Add(context.Target, prop);
            }
            //Console.WriteLine("test");
            // do things you want here
            context.Proceed(); // this calls the original method
            // do other things here
        }
    }

测试,使用

public interface ITest
    {
    }

public class TestC:ITest
    {
        public virtual string CName { get; set; }
        [UnRedo]
        public virtual string Name { get; set; }
        [UnRedo]
        public virtual int Count { get; set; }
        [UnRedo]
        public virtual TestD TestD { get; set; }
        [UnRedo]
        public virtual List<TestD> TestDs { get;set; }
    }
    [Serializable]
    public class TestD
    {
        [UnRedo]
        public int W { get; set; }
    }
static void Main(string[] args)
        {
            //ProxyGenerator generator = new ProxyGenerator();

            //var testC = generator.CreateClassProxy<TestC>(new TestInterceptor());
            //TestC testC = (TestC)RepositoryFactory.Create();
            TestC testC = new TestC();
            TestD testD = new TestD();
            testD.W = 5;

            testC.TestD = testD;
            testC.Name = "name1";
            testC.Count = 2;
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();

            testC.TestDs = new List<TestD>();
            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i });
            }
           
            testC.TestDs[0].W = -2;

            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i + 3 });
            }

            testC.Name = "name2";
            testC.Count = 3;
            testC.TestDs[0].W = -9;

            testC.Name = "name3";
            testC.Count = 4;

            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i + 6 });
            }
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();
            UnRedoHelp.Redo();
            UnRedoHelp.Redo();
            Console.ReadKey();
        }

这终于是实现我想要的效果,只需要在撤销的属性加UnRedo特征就行了,现在回头看也就那么回事,捣鼓一天就弄了个这么点东西、

补充注意:

使用MrAdvice过程中,所有需要用到UnRedoAttribute的项目都要引用MrAdvice,不然拦截无效,我已经标红了,不要当作看不见哈

 再次补充:

船新版本之最新版本

其实上面的还不是最新版本,后面需求要保存操作,这个怎么办?

第一步肯定得先改保存到栈里的数据,不要管怎么其他,数据最重要

保存到栈的数据类型,加入了保存方法

/// <summary>
    /// 撤销重做信息
    /// </summary>
    public class UnRedoInfo
    {
        /// <summary>
        /// 插入的对象
        /// </summary>
        public object Target { get; set; }
        /// <summary>
        /// 命令集合,key:命令名
        /// </summary>
        public Dictionary<string, CmdInfo> CmdDry { get; set; }
        /// <summary>
        /// 信息类型
        /// </summary>
        public UnRedoInfoType InfoType { get; set; } = UnRedoInfoType.Prop;
        /// <summary>
        /// 记录对象更改的多个属性和属性值
        /// </summary>
        public Dictionary<string, PropInfo> PropValueDry { get; set; }
    }

    #region 对象
    /// <summary>
    /// 撤销重做的属性信息
    /// </summary>
    public class PropInfo
    {
        /// <summary>
        /// 属性类型
        /// </summary>
        public PropInfoType InfoType { get; set; }
        /// <summary>
        /// 单对象属性的值
        /// </summary>
        public object PropValue { get; set; }
        /// <summary>
        /// 列表对象属性的值,记录当前列表属性的所有子项
        /// </summary>
        public List<object> PropValueLst { get; set; }
        /// <summary>
        /// 属性名称
        /// </summary>
        public string PropName { get; set; }
    }
    /// <summary>
    /// 保存属性的类型(对象,集合)
    /// </summary>
    public enum PropInfoType
    {
        /// <summary>
        /// 单个对象属性
        /// </summary>
        Object,
        /// <summary>
        /// 列表属性
        /// </summary>
        IList
    }
    #endregion

    #region 命令
    /// <summary>
    /// 撤销重做的属性信息
    /// </summary>
    public class CmdInfo
    {
        /// <summary>
        /// 命令名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 相反命令名称
        /// </summary>
        public string UnName { get; set; }
        /// <summary>
        /// 命令的参数列表
        /// </summary>
        public object[] Paras { get; set; }
    }

    /// <summary>
    /// 保存属性的类型(对象,集合)
    /// </summary>
    public enum UnRedoInfoType
    {
        /// <summary>
        /// 对象属性
        /// </summary>
        Prop,
        /// <summary>
        /// 命令
        /// </summary>
        Cmd
    }
    #endregion

不就是填加了一个分辨方法和属性的类型吗?不就是记录方法的集合,确实简单,这个方法集合意思就是说你可以传入多个方法进来,其实用一个就够了,因为你可以把多方法写成一个方法传进来。

那下面就看看怎么执行保存方法,撤销和重做了。

public class UnRedoHelp
    {
        static UnRedoHelp()
        {
            UndoStack = new Stack<UnRedoInfo>();
            RedoStack = new Stack<UnRedoInfo>();
        }
        //撤销和重做栈。
        public static Stack<UnRedoInfo> UndoStack;
        public static Stack<UnRedoInfo> RedoStack;

        /// <summary>
        /// 说明功能是否在撤销或者重做,true正在进行操作
        /// </summary>
        public static bool IsUnRedo = false;
        /// <summary>
        /// 把重做栈清空
        /// </summary>
        private static void RedoStackClear()
        {
            if (RedoStack.Count != 0)
            {
                //添加要把重做清空
                RedoStack.Clear();
            }
        }

        //添加撤销命令
        /// <summary>
        /// 添加撤销命令
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propValueDry"></param>
        public static void Add(object target, Dictionary<string, PropInfo> propValueDry)
        {
            if (IsUnRedo) return;

            RedoStackClear();
            UnRedoInfo info = new UnRedoInfo();
            info.Target = target;
            info.PropValueDry = propValueDry;
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 添加撤销命令,普通对象属性
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propNames">记录的属性名更改数组</param>
        public static void Add(object target, params string[] propNames)
        {
            if (IsUnRedo) return;

            RedoStackClear();
            UnRedoInfo info = GetPropertyValue(target, propNames);
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 添加撤销命令,消息命令
        /// </summary>
        /// <param name="target"></param>
        /// <param name="cmd">命令名</param>
        /// <param name="cmdDry">命令参数,key:方法名</param>
        public static void AddCmd(object target,  Dictionary<string, CmdInfo> cmdDry)
        {
            if (IsUnRedo) return;

            RedoStackClear();
            UnRedoInfo info = GetCmd(target, cmdDry);
            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }

        /// <summary>
        /// 添加撤销命令,消息命令
        /// </summary>
        /// <param name="target"></param>
        /// <param name="cmd">命令名</param>
        /// <param name="uncmd">反命令名</param>
        /// <param name="paras">命令参数</param>
        public static void AddCmd(object target, string cmd, string uncmd, params object[] paras)
        {
            if (IsUnRedo) return;

            RedoStackClear();
            //获取命令的基本信息
            Dictionary<string, CmdInfo> cmdDry = new Dictionary<string, CmdInfo>();
            CmdInfo cmdInfo = new CmdInfo();
            cmdInfo.Name = cmd;//方法名
            cmdInfo.UnName = uncmd;//反方法
            cmdInfo.Paras = paras;//参数
            cmdDry.Add(cmd,cmdInfo);

            UnRedoInfo info = GetCmd(target, cmdDry);

            //将命令参数压到栈顶。
            UndoStack.Push(info);
        }


        /// <summary>
        /// 撤销
        /// </summary>
        public static void Undo()
        {
            if (UndoStack.Count == 0)
            {
                return;
            }
            IsUnRedo = true;
            try
            {
                UnRedoInfo info = UndoStack.Pop();
                UnRedoInfo oldinfo;
                if (info.InfoType == UnRedoInfoType.Prop)
                {
                    //先压到重做栈,再改变值 重做时可恢复
                    oldinfo = GetPropertyValue(info.Target, info.PropValueDry.Keys.ToArray());
                    RedoStack.Push(oldinfo);
                    //使用栈数据进行属性赋值
                    SetPropertyValue(info);
                }
                else
                {
                    //命令
                    oldinfo = GetCmd(info.Target,info.CmdDry,true);
                    RedoStack.Push(oldinfo);

                    //使用栈数据进行执行命令
                    SetCmd(info);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                //LogHelp.WriteLog(e.Message + e.StackTrace);
            }
            finally
            {
                IsUnRedo = false;
            }


        }
        /// <summary>
        /// 重做
        /// </summary>
        public static void Redo()
        {
            if (RedoStack.Count == 0)
            {
                return;
            }
            IsUnRedo = true;
            try
            {
                UnRedoInfo info = RedoStack.Pop();
                UnRedoInfo oldinfo;
                if (info.InfoType == UnRedoInfoType.Prop)
                {
                    //先压到撤销栈,再改变值 撤销时可恢复
                    oldinfo = GetPropertyValue(info.Target, info.PropValueDry.Keys.ToArray());
                    UndoStack.Push(oldinfo);
                    //设置属性值
                    SetPropertyValue(info);
                }
                else
                {
                    //命令
                    oldinfo = GetCmd(info.Target, info.CmdDry, true);
                    UndoStack.Push(oldinfo);

                    SetCmd(info);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                //LogHelp.WriteLog(e.Message + e.StackTrace);
            }
            finally
            {
                IsUnRedo = false;
            }

        }

        #region 命令
        /// <summary>
        /// 获取关于命令数据的栈数据
        /// </summary>
        /// <param name="target"></param>
        /// <param name="cmd"></param>
        /// <param name="uncmd"></param>
        /// <param name="paras"></param>
        /// <param name="isUn">true是获取相反的命令</param>
        /// <returns></returns>
        public static UnRedoInfo GetCmd(object target, Dictionary<string, CmdInfo> cmdDry,bool isUn = false)
        {
            UnRedoInfo info = new UnRedoInfo();
            info.InfoType = UnRedoInfoType.Cmd;
            info.Target = target;
            Dictionary<string, CmdInfo> cmdDryTemp = new Dictionary<string, CmdInfo>();
            if (isUn)
            {
                //命令颠倒,所以两个正反方法的参数是一样的
                foreach (var x in cmdDry)
                {
                    CmdInfo cmdInfo = new CmdInfo();
                    cmdInfo.Name = x.Value.UnName;
                    cmdInfo.UnName = x.Value.Name;
                    cmdInfo.Paras = x.Value.Paras;
                    cmdDryTemp.Add(x.Value.UnName, cmdInfo);
                }
                info.CmdDry = cmdDryTemp;
            }
            else
            {
                info.CmdDry = cmdDry;
            }

            return info;
        }
        /// <summary>
        /// 执行命令
        /// </summary>
        /// <param name="info"></param>
        public static void SetCmd(UnRedoInfo info)
        {
            foreach (var x in info.CmdDry)
            {
                //获取方法信息 执行反命令
                MethodInfo methodInfo = info.Target.GetType().GetMethod(x.Value.UnName);
                methodInfo?.Invoke(info.Target,x.Value.Paras);
            }
        }
        #endregion

        #region 属性
        /// <summary>
        /// 获取属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static UnRedoInfo GetPropertyValue(object obj, string[] propNames)
        {
            UnRedoInfo info = new UnRedoInfo();
            info.Target = obj;

            //添加属性和属性值
            Dictionary<string, PropInfo> propValueDry = new Dictionary<string, PropInfo>();
            for (int i = 0; i < propNames.Length; i++)
            {
                //对象属性名
                string name = propNames[i];
                //获取属性相关信息
                PropertyInfo property = obj.GetType().GetProperty(name);

                if (property != null)
                {
                    #region 设置撤销重做的属性信息
                    //设置撤销重做的属性信息
                    PropInfo propInfo = new PropInfo();
                    propInfo.PropName = name;
                    //获取属性值
                    var prop = property.GetValue(obj);
                    if (prop is System.Collections.IList)
                    {
                        //列表
                        propInfo.InfoType = PropInfoType.IList;
                        propInfo.PropValueLst = new List<object>();
                        var lst = (IList)prop;
                        foreach (var item in lst)
                        {
                            propInfo.PropValueLst.Add(item);
                        }

                    }
                    else
                    {
                        //不是列表,单个对象
                        propInfo.InfoType = PropInfoType.Object;
                        propInfo.PropValue = prop;
                    }

                    if (!propValueDry.ContainsKey(propNames[i]))
                    {
                        propValueDry.Add(propNames[i], propInfo);
                    }
                    #endregion
                }

            }
            //设置对象
            info.PropValueDry = propValueDry;
            return info;
        }
        /// <summary>
        /// 设置属性值
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="name"></param>
        /// <param name="value"></param>
        public static void SetPropertyValue(UnRedoInfo info)
        {
            //设置属性值
            foreach (var item in info.PropValueDry)
            {
                PropertyInfo property = info.Target.GetType().GetProperty(item.Key);

                if (property != null)
                {
                    if (item.Value.InfoType == PropInfoType.Object)
                    {
                        //单个对象值的,直接赋值
                        property.SetValue(info.Target, item.Value.PropValue);
                    }
                    else if (item.Value.InfoType == PropInfoType.IList)
                    {
                        //列表对象值,先清除该列表对象的子项,然后重新添加子项
                        var lst = (IList)property.GetValue(info.Target);
                        lst.Clear();
                        foreach (var x in item.Value.PropValueLst)
                        {
                            lst.Add(x);
                        }
                    }
                }
            }

        }
        #endregion
    }

这里的IsUnRedo要注意一下,它是为了防止,在撤销重做Undo和Redo操作的里面改变一个属性,或者调用方法的时候也“添加进栈”,这是我们不愿意看到的。

其实如果之前的代码你整明白了,加一个保存方法,是一点问题都没有的。因为属性和方法分开处理的。

下面给出我使用撤销重做配合AOP使用需要注意的地方

/// <summary>
    /// 这个类需要创建与使用的特征的类型必须引用MrAdvice,不然不起作用
    /// </summary>
    [Serializable]
    //[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
    [AttributeUsage(AttributeTargets.All | AttributeTargets.Method, AllowMultiple = true)]
    public class UnRedoAttribute : Attribute, IMethodAdvice
    {
        /// <summary>
        /// 撤销重做类型
        /// </summary>
        public UnRedoInfoType UnRedoInfoType { get; set; } = UnRedoInfoType.Prop;
        /// <summary>
        /// 反命令,当撤销重做类型是命令类型可用 需要撤销的命令必须拥有正反方法的全部参数,并且互相赋值 而且参数对象是外面创建的
        /// 反正就是说我知道参数名称,但是不知道参数,所以要用到前面的方法参数,故正反用的参数是相同的
        /// </summary>
        public string UnCmd { get; set; }
        /// <summary>
        /// 命令是否在执行,true执行,
        /// 防止cmd中执行cmd或者修改撤销重做特征的属性然后添加进栈
        /// </summary>
        private static bool IsCmdRun { get; set; } = false;
        /// <summary>
        /// 切面方法
        /// </summary>
        /// <param name="context"></param>
        public void Advise(MethodAdviceContext context)
        {
            //如果有正在进行则返回,所以多线程不能记录
            MemberInfo memberInfo = context.TargetMethod;
            var unRedoAtr = memberInfo.GetCustomAttribute(typeof(UnRedoAttribute)) as UnRedoAttribute;
            //如果有正在进行则不记录,所以多线程不能记录
            if (!IsCmdRun)
            {
                //必须是属性改变
                if (context.TargetName.Length > 4 && context.TargetName.StartsWith("set_"))
                {
                    //属性改变
                    //添加历史记录 需要记录的属性名 去掉set_
                    string prop = context.TargetName.Remove(0, 4);
                    UnRedoHelp.Add(context.Target, prop);
                }
                else if (!context.TargetName.StartsWith("get_") && unRedoAtr.UnRedoInfoType == UnRedoInfoType.Cmd)
                {
                    UnRedoHelp.AddCmd(context.Target, context.TargetName, unRedoAtr.UnCmd, context.Arguments.ToArray());
                }
            }

            // do things you want here
            //执行方法,只对属性设置的设置控制,IsCmdRun = true,属性或者命令中修改涉及的撤消重做都不会生效
            if (!IsCmdRun && ((context.TargetName.Length > 4 && context.TargetName.StartsWith("set_"))
                || (unRedoAtr != null && unRedoAtr.UnRedoInfoType == UnRedoInfoType.Cmd)))
            {
                IsCmdRun = true;
                context.Proceed(); // this calls the original method
                IsCmdRun = false;
            }
            else
            {
                context.Proceed();
            }

            // do other things here
        }
    }

上面特性类是专门给这个撤销重做写的,IsCmdRun这里也注意一下就是防止执行的方法里重复执行撤销重做特性的方法,改变一个属性的同时去修改另一个属性,或者执行一个撤销重写操作时候去修改属性或者调用操作的时候“添加进栈”

说那么多,简单说,IsCmdRun防止很多不必要的栈数据添加进来。

IsCmdRun和IsUnRedo作用是不一样的:

IsUnRedo是针对Redo和Undo方法里面的调用方法或者修改属性会重复再次记录,所以限制;

IsCmdRun是针对执行方法本身里执行另一个拥有撤销重做特性的方法或者修改属性,所以限制;

使用例子

public class TestC:ITest
    {
        public  string CName { get; set; }
        [UnRedo]
        public  string Name { get; set; }
        [UnRedo]
        public  int Count { get; set; }
        [UnRedo]
        public  TestD TestD { get; set; }
        [UnRedo]
        public  List<TestD> TestDs { get;set; }

        #region 无参数
        [UnRedo(UnCmd = nameof(UnCmd), UnRedoInfoType = UnRedoInfoType.Cmd)]
        public void Cmd()
        {
            Console.WriteLine("testCmd");
        }

        [UnRedo(UnCmd = nameof(Cmd), UnRedoInfoType = UnRedoInfoType.Cmd)]
        public void UnCmd()
        {
            Console.WriteLine("testUnCmd");
        }
        #endregion

        #region 简单参数
        [UnRedo(UnCmd = nameof(ParaCmd2), UnRedoInfoType = UnRedoInfoType.Cmd)]
        public void ParaCmd(int temp)
        {
            Console.WriteLine($"testCmd:{temp}");
        }

        [UnRedo(UnCmd = nameof(ParaCmd), UnRedoInfoType = UnRedoInfoType.Cmd)]
        public void ParaCmd2(int temp)
        {
            Console.WriteLine($"testCmd2:{temp}");
        }
        #endregion
    }

加入无参和有参数的简单例子

static void Main(string[] args)
        {
            TestC testC = new TestC();
            TestD testD = new TestD();
            testD.W = 5;

            testC.TestD = testD;
            testC.Name = "name1";
            testC.Count = 2;
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();

            testC.Cmd();
            testC.ParaCmd(2);
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();

            testC.TestDs = new List<TestD>();
            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i });
            }

            testC.TestDs[0].W = -2;

            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i + 3 });
            }

            testC.Name = "name2";
            testC.Count = 3;
            testC.TestDs[0].W = -9;

            testC.Name = "name3";
            testC.Count = 4;

            for (int i = 0; i < 3; i++)
            {
                testC.TestDs.Add(new TestD() { W = i + 6 });
            }
            UnRedoHelp.Undo();
            UnRedoHelp.Undo();
            UnRedoHelp.Redo();
            UnRedoHelp.Redo();
            Console.ReadKey();
        }

效果图

 

 

是否需要再次升级

其实这里有个争议(我跟自己吵),是否需要把这个撤销重做进一步升级。

里面的栈数据,重写,撤销,相关的方法不写死了,让使用者的继承,自己写自己的栈数据和具体怎么处理。

如果你们用起来有情绪,可以抽象接口,反正我用起来没情绪(你自己写的)

反正源码和思路都在这里了,你们还有更好的想法,也搞起来,优化之后,评论通知我一下,灰常感谢。告辞。。

 

我只有一个要求:

看到这里的道友,就不要翻我之前的随笔了大部分都是我在网上转发或者抄的。

你为什么这么做,还这么多(我想要找的方便呀)

你可以收藏呀(他删了怎么办)

以前还以为可以学习,但除了第一次看,后面几乎没看过。

那为什么不删(懒)

 

而且我弄了一个底部加版权提示之后,之前的所有随笔都给我加上了,方了呀。

太难了。

 

posted @ 2019-10-23 18:31  三小  阅读(4610)  评论(3编辑  收藏  举报