为.NET类自动实现撤销/重做功能






4.49/5 (17投票s)
通过方法调用拦截实现.NET类通用撤销/重做功能的独特方法...
引言
几乎所有使用过文字处理软件的人都或多或少地使用过撤销/重做功能。此功能可以让您有效地回退(撤销)和前进(重做)对编辑时对文档所做的任何状态更改(即添加单词)。这个概念在文本编辑器和文档之外也很有用。
其想法是为.NET类提供自动撤销/重做功能。该功能被实现为 IMessageSink
。本文将演示如何在方法调用到达目标之前“在途中”拦截方法调用,从而允许对方法调用进行预处理和后处理。
设计目标/要求
希望具有自动撤销/重做功能的类只需要子类化 ContextBoundObject
或 MarshalByRefObject
(在 MarshalByRefObject
的情况下,您必须确保所有调用都必须通过 TransparentProxy
) 并用本文中包含的自定义属性 (UndoRedoAttribute
) "标记" 该类。另一个小要求是该类还必须实现 IUndoRedo
。在这种情况下实现实际上很奇怪,因为该类只需要提供方法,它们可以是空的,因为它们永远不会被调用。还提供了一个抽象基类,它会为您执行这些步骤(稍后描述)。
设计说明
此技术使用 CLR 的未公开功能,即 IContributeObjectSink
。我不认为这是一个问题;但是,可以使用 RealProxy
方法应用类似的技术(这是一个有文档记录的类)。
RealProxy
方法可能没有那么高的性能 (MSFT),但当子类化 MarshalByRefObject
时,显然性能不是一个考虑因素。注意:对 MarhsalByRefObject
的调用不能内联,此外,对 TransparentProxy
对象的调用会强制调用堆栈被序列化。反射也被使用,这并不能以其性能而闻名,但它允许一些非常强大的技术。
该代码也可以很容易地修改,以支持字段设置操作的撤销/重做,但这将留给读者。
该功能通过 IUndoRedo
接口公开。
public interface IUndoRedo
{
void Undo();
void Redo();
void Flush();
}
工作原理
基本思想是通过拦截属性设置操作来跟踪对象状态。为此,必须在调用者和目标之间注入代码。CLR 提供了一个很好的工具,可以使用自定义属性并实现 IContributeObjectSink
来做到这一点。我将简要解释代码注入的工作原理。
当创建派生自 ContextBoundObject
的对象时,CLR 将查找实现 IContextAttribute
的属性(类级别)。这是自定义属性实现
internal sealed class UndoRedoAttribute : Attribute,
IContextAttribute
{
public void GetPropertiesForNewContext(
IConstructionCallMessage msg)
{
IContextProperty prop = new UndoRedoProperty();
msg.ContextProperties.Add(prop);
}
public bool IsContextOK(Context ctx,
IConstructionCallMessage msg)
{
if(ctx.GetProperty("UndoRedo Property") != null)
{
return true;
}
return false;
}
}
CLR 将首先调用 IsContextOK
。这允许该属性确定当前的 Context
是否合适 (UndoRedoProperty
被“安装”)。如果它返回 false
,将调用 GetPropertiesForNewContext
,允许该属性将 UndoRedoProperty
“安装”到 Context
中。
UndoRedoProperty
负责注入拦截类(一个实现 IMessageSink
的类)。
public sealed class UndoRedoProperty : IContextProperty,
IContributeObjectSink
{
public IMessageSink GetObjectSink(MarshalByRefObject obj,
IMessageSink nextSink)
{
return new UndoRedoSink(obj, nextSink);
}
public string Name
{
get
{
return "UndoRedo Property";
}
}
public bool IsNewContextOK(Context newCtx){return true;}
public void Freeze(Context newContext){}
}
在 Context
属性上要注意的主要内容是 IContributeObjectSink
的实现。CLR 将在每次创建具有 UndoRedoAttribute
的新对象时调用 GetObjectSink
。现在所有调用都将通过自定义消息接收器。
现在我们已经完成了所有样板代码,让我们看一下 IMessageSink
的实现
internal sealed class UndoRedoSink : IMessageSink
{
private IMessageSink _nextSink; /*ref to next sink in chain*/
private object _target; /*target of method invocations*/
private Type _targetType;/*type of target(cached for perf)*/
private ArrayList _actions; /*set history*/
private int _index; /*current index of set buffer*/
private int _cnt; /*count of undo calls*/
public IMessageSink NextSink
{
get
{
return _nextSink;
}
}
private void Add(SetAction sa)
{
/*
* store set operation and update current index...
*/
if(_actions.Count <= _index)
{
_actions.Add(sa);
}
else
{
_actions[_index] = sa;
}
_index++;
/*
* clear
*/
for(int i = _index;i<_actions.Count;i++)
{
_actions[i] = null;
}
}
private bool CanUndo()
{
return _index > 0 ;
}
private bool CanRedo()
{
return _cnt > 0 && _actions.Count >_index &&
_actions[_index] != null;
}
public IMessage SyncProcessMessage(IMessage msg)
{
IMethodCallMessage mcm = msg as IMethodCallMessage;
IMethodReturnMessage mrm = null;
/*
* is the message a method call?...
*/
if (mcm != null)
{
/*
* is the message a property setter?...
*/
if (mcm.MethodName.StartsWith("set_"))
{
/*
* grab property name...
*/
string propertyName = mcm.MethodName.Substring(4);
/*
* record the set operation...
*/
SetAction action = new SetAction();
action.propertyName = propertyName;
action.newVal = mcm.InArgs[0];
/*
* capture old value...
*/
PropertyInfo pi = _targetType.GetProperty(propertyName,
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
action.oldVal = pi.GetValue(_target,new object[0]);
/*
* store...
*/
Add(action);
}
/*
* undo last action...
*/
if (mcm.MethodName == "Undo")
{
if(CanUndo())
{
_cnt++;
--_index;
/*
* set the state back to the prior value...
*/
PropertyInfo pi =
_targetType.GetProperty(
((SetAction)_actions[_index]).propertyName,
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public);
pi.SetValue(_target,
((SetAction)_actions[_index]).oldVal,
new object[0]);
}
/*
* no need to forward on...
*/
mrm = new ReturnMessage(null, mcm);
return mrm;
}
/*
* redo last action...
*/
if (mcm.MethodName == "Redo")
{
if(CanRedo())
{
_cnt--;
SetAction action =
(SetAction)_actions[_index];
PropertyInfo pi =
_targetType.GetProperty(action.propertyName,
BindingFlags.Instance | BindingFlags.NonPublic |
BindingFlags.Public);
pi.SetValue(_target, action.newVal, new object[0]);
_index++;
}
mrm = new ReturnMessage(null, mcm);
return mrm;
}
}
/*
* are we being told to 'empty' the
* undo/redo history?
*/
if (mcm.MethodName == "Flush")
{
_actions.Clear();
_cnt = 0;
_index = 0;
/*
* no need to forward to actual object,
* we are all done...
*/
mrm = new ReturnMessage(null, mcm);
return mrm;
}
/*
* forward to terminator sink...
*/
return _nextSink.SyncProcessMessage(msg);
}
public IMessageCtrl AsyncProcessMessage(IMessage msg,
IMessageSink replySink)
{
return _nextSink.AsyncProcessMessage(msg,replySink);
}
public UndoRedoSink(MarshalByRefObject target,
IMessageSink nextSink)
{
_target = (UndoRedo)target;
_targetType = _target.GetType();
_nextSink = nextSink;
_actions = new ArrayList();
_index = 0;
}
}
接收器处理四种类型的调用
- 属性设置调用
- 刷新调用
- 撤销调用
- 重做调用
所有其余的调用都简单地转发到下一个接收器。
使用代码
使用接收器再简单不过了。如前所述,只需实现 IUndoRedo
,子类化 ContextBoundObject
并将 UndoRedoAttribute
放置在您的类上方。还提供了一个便利类,它会代表您执行所需的步骤
[UndoRedo]
public abstract class UndoRedo : ContextBoundObject,
IUndoRedo
{
public void Undo() { }
public void Redo() { }
public void Flush() { }
}
只需从 UndoRedo
派生您的类,您就可以开始了。
结论
使用此技术,还可以实现其他有趣的服务,例如
- 安全
- 即时激活
- 日志记录
无论您是否认为此接收器有用,我都希望您能看到方法拦截的强大功能。消息接收器的概念提供了重复使用代码的新的和有趣的方式。