推荐:介绍一个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.

如何使用

  我学习这些东西一般都喜欢先看如何使用,因为从使用方式就能看出封装得是否简单易用。

  以下是一个控制台的演示程序,代码如下:

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继承下来,必须实现两个操作:一个是执行操作,一个是反执行操作

/// <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()等其他方法。

其完整代码如下:

  /// <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://zhoujg.cnblogs.com/ ]



posted on 2010-08-25 16:52  周 金根  阅读(3374)  评论(4编辑  收藏  举报

导航