65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.49/5 (17投票s)

2005 年 7 月 29 日

CPOL

3分钟阅读

viewsIcon

58519

downloadIcon

604

通过方法调用拦截实现.NET类通用撤销/重做功能的独特方法...

引言

几乎所有使用过文字处理软件的人都或多或少地使用过撤销/重做功能。此功能可以让您有效地回退(撤销)和前进(重做)对编辑时对文档所做的任何状态更改(即添加单词)。这个概念在文本编辑器和文档之外也很有用。

其想法是为.NET类提供自动撤销/重做功能。该功能被实现为 IMessageSink。本文将演示如何在方法调用到达目标之前“在途中”拦截方法调用,从而允许对方法调用进行预处理和后处理。

设计目标/要求

希望具有自动撤销/重做功能的类只需要子类化 ContextBoundObjectMarshalByRefObject(在 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;
 }
}

接收器处理四种类型的调用

  1. 属性设置调用
  2. 刷新调用
  3. 撤销调用
  4. 重做调用

所有其余的调用都简单地转发到下一个接收器。

使用代码

使用接收器再简单不过了。如前所述,只需实现 IUndoRedo,子类化 ContextBoundObject 并将 UndoRedoAttribute 放置在您的类上方。还提供了一个便利类,它会代表您执行所需的步骤

[UndoRedo]
public abstract class UndoRedo : ContextBoundObject, 
                                            IUndoRedo
{
 public void Undo() { }
 public void Redo() { }
 public void Flush() { }
 
}

只需从 UndoRedo 派生您的类,您就可以开始了。

结论

使用此技术,还可以实现其他有趣的服务,例如

  • 安全
  • 即时激活
  • 日志记录

无论您是否认为此接收器有用,我都希望您能看到方法拦截的强大功能。消息接收器的概念提供了重复使用代码的新的和有趣的方式。

为.NET类自动实现撤销/重做功能 - CodeProject - 代码之家
© . All rights reserved.