撤銷重作功能實現

前言

大佬走過,小菜留下。html

該文講述我如何把撤銷重作功能作到讓我本身滿意。編程

這篇隨筆起於公司項目須要一個撤銷重寫功能,由於是圖形設計。數組

第一想法

起初第一想法是保存整個操做對象,而後撤銷就從新換整個對象就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項目中以前綁定的都會失效,由於不是原來的對象了。函數

初版本

既然是撤銷重寫,應該只需記錄下改變的東西,其餘不須要記錄,因此我須要兩個棧,一個記錄歷史棧(撤銷),一個重作棧,和壓入棧的數據類。post

數據類以下:學習

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

效果圖

 

 

是否須要再次升級

其實這裏有個爭議(我跟本身吵),是否須要把這個撤銷重作進一步升級。

裏面的棧數據,重寫,撤銷,相關的方法不寫死了,讓使用者的繼承,本身寫本身的棧數據和具體怎麼處理。

若是大家用起來有情緒,能夠抽象接口,反正我用起來沒情緒(你本身寫的)

反正源碼和思路都在這裏了,大家還有更好的想法,也搞起來,優化以後,評論通知我一下,灰常感謝。告辭。。

 

我只有一個要求:

看到這裏的道友,就不要翻我以前的隨筆了大部分都是我在網上轉發或者抄的。

你爲何這麼作,還這麼多(我想要找的方便呀)

你能夠收藏呀(他刪了怎麼辦)

之前還覺得能夠學習,但除了第一次看,後面幾乎沒看過。

那爲何不刪(懶)

 

並且我弄了一個底部加版權提示以後,以前的全部隨筆都給我加上了,方了呀。

太難了。

相關文章
相關標籤/搜索