推荐:介绍一个UndoFramework
由于其他工作,好多天又没有对MetaModelEngine进行思考了,这两天又腾出时间可以思考一下了,本篇介绍一下在图形编辑器中对操作如何实现Undo操作。
在图形设计器操作中,每个操作按钮都对应到一个命令,很多情况下我们都应该允许用户执行操作后回滚这些操作,或者回滚后又再次执行。在我做的报表引擎中,我是在每次操作后把设计文件都保留下来,这个在报表设计中是没有问题,但是在毕竟不是很好的设计。接下来要考虑对OpenExpressApp提供建模支持了,所以也需要考虑一下如何让图形设计器更好的支持这种Undo操作。
在公司的一个项目组中内部是使用命令模式,只是在传统命令模式中增加了一个UnExecute方法,这个方法就是用来做Undo操作的。在codeplex上我找到了一类似的轻量级UndoFramework,后期准备就用它了,在这里我就给大家介绍一下。
UndoFramework项目
Codeplex网站地址:http://undo.codeplex.com/
下载地址:http://undo.codeplex.com/releases/view/29440
项目描述:
It's a simple framework to add Undo/Redo functionality to your applications, based on the classical Command design pattern. It supports merging actions, nested transactions, delayed execution (execution on top-level transaction commit) and possible non-linear undo history (where you can have a choice of multiple actions to redo).
The status of the project is Stable (released). I might add more stuff to it later, but right now it fully satisfies my needs. It's implemented in C# 3.0 (Visual Studio 2008) and I can build it for both desktop and Silverlight. The release has both binaries.
现有应用
A good example of where this framework is used is the Live Geometry project (http://livegeometry.codeplex.com). It defines several actions such as AddFigureAction, RemoveFigureAction, MoveAction and SetPropertyAction.
如何使用
我学习这些东西一般都喜欢先看如何使用,因为从使用方式就能看出封装得是否简单易用。
以下是一个控制台的演示程序,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | using System; using GuiLabs.Undo; namespace MinimalSample { class Program { static void Main( string [] args) { Console.WriteLine( "Original color" ); SetConsoleColor(ConsoleColor.Green); Console.WriteLine( "New color" ); actionManager.Undo(); Console.WriteLine( "Old color again" ); using (Transaction.Create(actionManager)) { SetConsoleColor(ConsoleColor.Red); // you never see Red Console.WriteLine( "Still didn't change to Red because of lazy evaluation" ); SetConsoleColor(ConsoleColor.Blue); } Console.WriteLine( "Changed two colors at once" ); actionManager.Undo(); Console.WriteLine( "Back to original" ); actionManager.Redo(); Console.WriteLine( "Blue again" ); Console.ReadKey(); } static void SetConsoleColor(ConsoleColor color) { SetConsoleColorAction action = new SetConsoleColorAction(color); actionManager.RecordAction(action); } static ActionManager actionManager = new ActionManager(); } class SetConsoleColorAction : AbstractAction { public SetConsoleColorAction(ConsoleColor newColor) { color = newColor; } ConsoleColor color; ConsoleColor oldColor; protected override void ExecuteCore() { oldColor = Console.ForegroundColor; Console.ForegroundColor = color; } protected override void UnExecuteCore() { Console.ForegroundColor = oldColor; } } } |
下载代码你会看到,它还自带一个Form的例子,感兴趣可以自己去看看
Actions
所有操作都从 IAction继承下来,必须实现两个操作:一个是执行操作,一个是反执行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /// <summary> /// Encapsulates a user action (actually two actions: Do and Undo) /// Can be anything. /// You can give your implementation any information it needs to be able to /// execute and rollback what it needs. /// </summary> public interface IAction { /// <summary> /// Apply changes encapsulated by this object. /// </summary> void Execute(); /// <summary> /// Undo changes made by a previous Execute call. /// </summary> void UnExecute(); /// <summary> /// For most Actions, CanExecute is true when ExecuteCount = 0 (not yet executed) /// and false when ExecuteCount = 1 (already executed once) /// </summary> /// <returns>true if an encapsulated action can be applied</returns> bool CanExecute(); /// <returns>true if an action was already executed and can be undone</returns> bool CanUnExecute(); /// <summary> /// Attempts to take a new incoming action and instead of recording that one /// as a new action, just modify the current one so that it's summary effect is /// a combination of both. /// </summary> /// <param name="followingAction"></param> /// <returns>true if the action agreed to merge, false if we want the followingAction /// to be tracked separately</returns> bool TryToMerge(IAction followingAction); /// <summary> /// Defines if the action can be merged with the previous one in the Undo buffer /// This is useful for long chains of consecutive operations of the same type, /// e.g. dragging something or typing some text /// </summary> bool AllowToMergeWithPrevious { get ; set ; } } |
ActionManager
ActionManager负责跟踪undo/redo记录,提供RecordAction(IAction)来记录操作步骤,提供ActionManager.Undo(), ActionManager.Redo(), CanUndo(), CanRedo()等其他方法。
其完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | /// <summary> /// Action Manager is a central class for the Undo Framework. /// Your domain model (business objects) will have an ActionManager reference that would /// take care of executing actions. /// /// Here's how it works: /// 1. You declare a class that implements IAction /// 2. You create an instance of it and give it all necessary info that it needs to know /// to apply or rollback a change /// 3. You call ActionManager.RecordAction(yourAction) /// /// Then you can also call ActionManager.Undo() or ActionManager.Redo() /// </summary> public class ActionManager { public ActionManager() { History = new SimpleHistory(); } #region Events /// <summary> /// Listen to this event to be notified when a new action is added, executed, undone or redone /// </summary> public event EventHandler CollectionChanged; protected void RaiseUndoBufferChanged( object sender, EventArgs e) { if (CollectionChanged != null ) { CollectionChanged( this , e); } } #endregion #region RecordAction #region Running /// <summary> /// Currently running action (during an Undo or Redo process) /// </summary> /// <remarks>null if no Undo or Redo is taking place</remarks> public IAction CurrentAction { get ; internal set ; } /// <summary> /// Checks if we're inside an Undo or Redo operation /// </summary> public bool ActionIsExecuting { get { return CurrentAction != null ; } } #endregion /// <summary> /// Defines whether we should record an action to the Undo buffer and then execute, /// or just execute it without it becoming a part of history /// </summary> public bool ExecuteImmediatelyWithoutRecording { get ; set ; } /// <summary> /// Central method to add and execute a new action. /// </summary> /// <param name="existingAction">An action to be recorded in the buffer and executed</param> public void RecordAction(IAction existingAction) { if (existingAction == null ) { throw new ArgumentNullException( "ActionManager.RecordAction: the existingAction argument is null" ); } // make sure we're not inside an Undo or Redo operation CheckNotRunningBeforeRecording(existingAction); // if we don't want to record actions, just run and forget it if (ExecuteImmediatelyWithoutRecording && existingAction.CanExecute()) { existingAction.Execute(); return ; } // Check if we're inside a transaction that is being recorded ITransaction currentTransaction = RecordingTransaction; if (currentTransaction != null ) { // if we're inside a transaction, just add the action to the transaction's list currentTransaction.AccumulatingAction.Add(existingAction); if (!currentTransaction.IsDelayed) { existingAction.Execute(); } } else { RunActionDirectly(existingAction); } } void CheckNotRunningBeforeRecording(IAction existingAction) { string existing = existingAction != null ? existingAction.ToString() : "" ; if (CurrentAction != null ) { throw new InvalidOperationException ( string .Format ( "ActionManager.RecordActionDirectly: the ActionManager is currently running " + "or undoing an action ({0}), and this action (while being executed) attempted " + "to recursively record another action ({1}), which is not allowed. " + "You can examine the stack trace of this exception to see what the " + "executing action did wrong and change this action not to influence the " + "Undo stack during its execution. Checking if ActionManager.ActionIsExecuting == true " + "before launching another transaction might help to avoid the problem. Thanks and sorry for the inconvenience." , CurrentAction.ToString(), existing ) ); } } object recordActionLock = new object (); /// <summary> /// Adds the action to the buffer and runs it /// </summary> void RunActionDirectly(IAction actionToRun) { CheckNotRunningBeforeRecording(actionToRun); lock (recordActionLock) { CurrentAction = actionToRun; if (History.AppendAction(actionToRun)) { History.MoveForward(); } CurrentAction = null ; } } #endregion #region Transactions public Transaction CreateTransaction() { return Transaction.Create( this ); } public Transaction CreateTransaction( bool delayed) { return Transaction.Create( this , delayed); } private Stack<ITransaction> mTransactionStack = new Stack<ITransaction>(); public Stack<ITransaction> TransactionStack { get { return mTransactionStack; } set { mTransactionStack = value; } } public ITransaction RecordingTransaction { get { if (TransactionStack.Count > 0) { return TransactionStack.Peek(); } return null ; } } public void OpenTransaction(ITransaction t) { TransactionStack.Push(t); } public void CommitTransaction() { if (TransactionStack.Count == 0) { throw new InvalidOperationException( "ActionManager.CommitTransaction was called" + " when there is no open transaction (TransactionStack is empty)." + " Please examine the stack trace of this exception to find code" + " which called CommitTransaction one time too many." + " Normally you don't call OpenTransaction and CommitTransaction directly," + " but use using(var t = Transaction.Create(Root)) instead." ); } ITransaction committing = TransactionStack.Pop(); if (committing.AccumulatingAction.Count > 0) { RecordAction(committing.AccumulatingAction); } } public void RollBackTransaction() { if (TransactionStack.Count != 0) { var topLevelTransaction = TransactionStack.Peek(); if (topLevelTransaction != null && topLevelTransaction.AccumulatingAction != null ) { topLevelTransaction.AccumulatingAction.UnExecute(); } TransactionStack.Clear(); } } #endregion #region Undo, Redo public void Undo() { if (!CanUndo) { return ; } if (ActionIsExecuting) { throw new InvalidOperationException( string .Format( "ActionManager is currently busy" + " executing a transaction ({0}). This transaction has called Undo()" + " which is not allowed until the transaction ends." + " Please examine the stack trace of this exception to see" + " what part of your code called Undo." , CurrentAction)); } CurrentAction = History.CurrentState.PreviousAction; History.MoveBack(); CurrentAction = null ; } public void Redo() { if (!CanRedo) { return ; } if (ActionIsExecuting) { throw new InvalidOperationException( string .Format( "ActionManager is currently busy" + " executing a transaction ({0}). This transaction has called Redo()" + " which is not allowed until the transaction ends." + " Please examine the stack trace of this exception to see" + " what part of your code called Redo." , CurrentAction)); } CurrentAction = History.CurrentState.NextAction; History.MoveForward(); CurrentAction = null ; } public bool CanUndo { get { return History.CanMoveBack; } } public bool CanRedo { get { return History.CanMoveForward; } } #endregion #region Buffer public void Clear() { History.Clear(); CurrentAction = null ; } public IEnumerable<IAction> EnumUndoableActions() { return History.EnumUndoableActions(); } private IActionHistory mHistory; internal IActionHistory History { get { return mHistory; } set { if (mHistory != null ) { mHistory.CollectionChanged -= RaiseUndoBufferChanged; } mHistory = value; if (mHistory != null ) { mHistory.CollectionChanged += RaiseUndoBufferChanged; } } } #endregion } |
参考
http://blogs.msdn.com/kirillosenkov/archive/2009/06/29/new-codeplex-project-a-simple-undo-redo-framework.aspx
http://blogs.msdn.com/kirillosenkov/archive/2009/07/02/samples-for-the-undo-framework.aspx
其他Undo框架
- http://dejavu.codeplex.com/ by Sergei Arhipenko
- http://www.codeproject.com/KB/cs/undoredobuffer.aspx by Marc Clifton
- http://msmvps.com/blogs/matthieu/archive/2009/06/08/ef-undo-redo.aspx - Undo/Redo for Entity Framework by Matthieu MEZIL
欢迎转载,转载请注明:转载自周金根 [ http://zhoujg.cnblogs.com/ ]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架