由于其他工作,好多天又没有对MetaModelEngine进行思考了,这两天又腾出时间可以思考一下了,本篇介绍一下在图形编辑器中对操作如何实现Undo操作。
在图形设计器操作中,每个操作按钮都对应到一个命令,很多情况下我们都应该允许用户执行操作后回滚这些操作,或者回滚后又再次执行。在我做的报表引擎中,我是在每次操作后把设计文件都保留下来,这个在报表设计中是没有问题,但是在毕竟不是很好的设计。接下来要考虑对OpenExpressApp提供建模支持了,所以也需要考虑一下如何让图形设计器更好的支持这种Undo操作。
在公司的一个项目组中内部是使用命令模式,只是在传统命令模式中增加了一个UnExecute方法,这个方法就是用来做Undo操作的。在codeplex上我找到了一类似的轻量级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.
我学习这些东西一般都喜欢先看如何使用,因为从使用方式就能看出封装得是否简单易用。
以下是一个控制台的演示程序,代码如下:
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的例子,感兴趣可以自己去看看
所有操作都从 IAction继承下来,必须实现两个操作:一个是执行操作,一个是反执行操作
/// <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负责跟踪undo/redo记录,提供RecordAction(IAction)来记录操作步骤,提供ActionManager.Undo(), ActionManager.Redo(), CanUndo(), CanRedo()等其他方法。
其完整代码如下:
/// <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
欢迎转载,转载请注明:转载自周金根 [ http://zhoujg.cnblogs.com/ ]