前言
大佬走过,小菜留下。
该文讲述我如何把撤销重做功能做到让我自己满意。
这篇随笔起于公司项目需要一个撤销重写功能,因为是图形设计。
第一想法
起初第一想法是保存整个操作对象,然后撤销就重新换整个对象就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特征就行了,现在回头看也就那么回事,捣鼓一天就弄了个这么点东西
我只有一个要求:
看到这里的道友,就不要翻我之前的随笔了,大部分都是我在网上转发或者抄的。
你为什么这么做,还这么多(我想要找的方便呀)
你可以收藏呀(他删了怎么办)
以前还以为可以学习,但除了第一次看,后面几乎没看过。
那为什么不删(懒)
而且我弄了一个底部加版权提示之后,之前的所有随笔都给我加上了,方了呀。
太难了。