推荐:介绍一个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://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/ ]
浙公网安备 33010602011771号