lsgsanxiao

前言

大佬走过,小菜留下。

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

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

第一想法

起初第一想法是保存整个操作对象,然后撤销就重新换整个对象就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框架-动态代理和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特征就行了,现在回头看也就那么回事,捣鼓一天就弄了个这么点东西

我只有一个要求:

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

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

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

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

那为什么不删(懒)

 

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

太难了。

 

分类:

技术点:

相关文章:

  • 2022-12-23
  • 2021-11-27
  • 2022-12-23
  • 2022-12-23
  • 2021-09-23
  • 2021-04-25
猜你喜欢
  • 2021-09-30
  • 2021-10-07
  • 2021-05-23
  • 2021-11-27
  • 2022-12-23
  • 2021-05-28
相关资源
相似解决方案