前言
大佬走過,小菜留下。
該文講述我如何把撤銷重做功能做到讓我自己滿意。
這篇隨筆起於公司項目需要一個撤銷重寫功能,因為是圖形設計。
第一想法
起初第一想法是保存整個操作對象,然后撤銷就重新換整個對象就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(); }
效果圖
是否需要再次升級
其實這里有個爭議(我跟自己吵),是否需要把這個撤銷重做進一步升級。
里面的棧數據,重寫,撤銷,相關的方法不寫死了,讓使用者的繼承,自己寫自己的棧數據和具體怎么處理。
如果你們用起來有情緒,可以抽象接口,反正我用起來沒情緒(你自己寫的)
反正源碼和思路都在這里了,你們還有更好的想法,也搞起來,優化之后,評論通知我一下,灰常感謝。告辭。。
我只有一個要求:
看到這里的道友,就不要翻我之前的隨筆了,大部分都是我在網上轉發或者抄的。
你為什么這么做,還這么多(我想要找的方便呀)
你可以收藏呀(他刪了怎么辦)
以前還以為可以學習,但除了第一次看,后面幾乎沒看過。
那為什么不刪(懶)
而且我弄了一個底部加版權提示之后,之前的所有隨筆都給我加上了,方了呀。
太難了。