Introduction
Almost anyone who has operated a word processor has used undo/redo to some extent. This is the feature that lets you effectively roll back (undo) and roll forward (redo) any state changes (i.e. add words) you have made to the document while editing. This concept can be quite useful beyond text editors and documents.
The idea is to provide automatic Undo/Redo functionality for .NET classes. The functionality is implemented as an IMessageSink
. This article will demonstrate how to intercept method invocations 'in-flight' before they reach the target, allowing for pre and post processing of method calls.
Design goals/requirements
Classes wanting automatic Undo/Redo functionality need to do nothing more than sub-class ContextBoundObject
or MarshalByRefObject
(in the MarshalByRefObject
case, you must insure all calls must come through a TransparentProxy
) and 'mark' the class with the custom attribute (UndoRedoAttribute
) included in this article. One other small requirement is that the class must also implement IUndoRedo
. Implementing is actually odd in this context because the class just needs to provide the methods, they can be empty as they are never called. An abstract base class is also provided that performs these steps for you (described later).
Design notes
This technique is built using undocumented features of the CLR, namely IContributeObjectSink
. I don�t find this to be an issue; however a similar technique can be applied using a RealProxy
approach (which is a documented class).
The RealProxy
approach may not be quite as performant (MSFT), but clearly performance is not a consideration when MarshalByRefObject
is sub-classed. Note: calls on MarhsalByRefObject
can�t be inlined, plus calls on TransparentProxy
objects force the call stack to be serialized. Reflection is also used, which isn�t known for its performance, but allows for some very powerful techniques.
The code can also be very easily modified to support Undo/Redo for field setter operations, but I�ll leave that to the reader.
The functionality is exposed through the IUndoRedo
interface.
public interface IUndoRedo
{
void Undo();
void Redo();
void Flush();
}
How it works
The basic idea is to track object state via interception of property set operations. To do this code must be injected between the caller and the target. The CLR provides a great facility for doing this using custom attributes and implementing IContributeObjectSink
. I will briefly explain how code injection works.
When an object that derives from ContextBoundObject
is created the CLR will look for attributes (class level) that implement IContextAttribute
. Here is the custom attribute implementation:
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;
}
}
The CLR will first invoke IsContextOK
. This allows the attribute to determine if the current Context
is going to be appropriate (UndoRedoProperty
is 'installed'). If it returns false
, GetPropertiesForNewContext
will be invoked, allowing the attribute to 'install' the UndoRedoProperty
into the Context
.
The UndoRedoProperty
is responsible for injecting the interception class (a class that implements 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){}
}
The main thing to notice on the Context
property is the implementation of IContributeObjectSink
. The CLR will call GetObjectSink
every time a new object is created that has the UndoRedoAttribute
. Now all invocations will come through the custom message sink.
Now that we have all the boilerplate code out of the way let's look at the IMessageSink
implementation:
internal sealed class UndoRedoSink : IMessageSink
{
private IMessageSink _nextSink;
private object _target;
private Type _targetType;
private ArrayList _actions;
private int _index;
private int _cnt;
public IMessageSink NextSink
{
get
{
return _nextSink;
}
}
private void Add(SetAction sa)
{
if(_actions.Count <= _index)
{
_actions.Add(sa);
}
else
{
_actions[_index] = sa;
}
_index++;
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;
if (mcm != null)
{
if (mcm.MethodName.StartsWith("set_"))
{
string propertyName = mcm.MethodName.Substring(4);
SetAction action = new SetAction();
action.propertyName = propertyName;
action.newVal = mcm.InArgs[0];
PropertyInfo pi = _targetType.GetProperty(propertyName,
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
action.oldVal = pi.GetValue(_target,new object[0]);
Add(action);
}
if (mcm.MethodName == "Undo")
{
if(CanUndo())
{
_cnt++;
--_index;
PropertyInfo pi =
_targetType.GetProperty(
((SetAction)_actions[_index]).propertyName,
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public);
pi.SetValue(_target,
((SetAction)_actions[_index]).oldVal,
new object[0]);
}
mrm = new ReturnMessage(null, mcm);
return mrm;
}
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;
}
}
if (mcm.MethodName == "Flush")
{
_actions.Clear();
_cnt = 0;
_index = 0;
mrm = new ReturnMessage(null, mcm);
return mrm;
}
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;
}
}
The sink handles four types of invocations:
- Property set calls
- Flush calls
- Undo calls
- Redo calls
All the rest of the invocations are simply forwarded onto the next sink.
Using the code
Using the sink couldn't be easier. As stated earlier, just implement IUndoRedo
, sub-class ContextBoundObject
and place the UndoRedoAttribute
above your class. A convenience class is also provided, which does the required steps on your behalf:
[UndoRedo]
public abstract class UndoRedo : ContextBoundObject,
IUndoRedo
{
public void Undo() { }
public void Redo() { }
public void Flush() { }
}
Simply derive your class from UndoRedo
and your ready to go.
Conclusion
Using this technique, other interesting services can also be implemented like:
- Security
- Just-In-Time Activation
- Logging
Whether you find this sink useful or not, I hope you can see the power of method interception. The notion of message sinks provide new and interesting ways to reuse code.