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

        }

    }

}

运行后界面如下:

推荐:介绍一个UndoFramework

下载代码你会看到,它还自带一个Form的例子,感兴趣可以自己去看看

推荐:介绍一个UndoFramework

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/ ]



你可能感兴趣的:(framework)