Introduction
Do your applications support Undo/Redo? They often should, because Undo/Redo improves usability by enabling user exploration. Nowadays, it can also be a great competitive advantage. Just think of a word processor without it. But, how can you implement such functionality in a .NET application? This article presents a new approach to make both new and existing classes support multi-level undo and redo using Generics, extension methods, and lambda expressions.
Even if your application does not need support user Undo and Redo, you still might be interested to use the reversible transaction support presented here to build much error-safer applications. If you have ever worked with databases, you should be familiar with how SqlTransaction
can group a number of changes as an atomic operation, which either fully succeeds (commits) or is completely rolled-back. With the framework presented here, you can do the same with in-memory objects.
The CodeProject article, Automatic Undo/Redo with .NET Generics, inspired me to start thinking of how to support undo and redo in applications. As shown there, property changes could easily be made undoable by wrapping every property field in a generics class. I have also used Generics extensively, but my solution does not require any changes to the field definitions. Actually, my solution can even quite easily be applied to existing non-reversible classes (and interfaces) by using C# 3.0 extension methods, as you will see shortly.
Using the code
Reversible transactions
A basic principle in my Reversible framework is that everything that is going to be reversible (undoable) has to be made within a transaction. A transaction can span any number of method calls and any number of reversible operations. A powerful feature of the transactions in this framework is that they can be nested. This means that when a new transaction is created while another is still active, part of the job can be rolled-back (in case of an error).
If a transaction spans a single method (that may call any number of other methods), it is best to scope the transaction in a using
-clause, as shown in the following example:
using (Transaction txn = Transaction.Begin())
{
txn.Commit();
}
This will start a new transaction (nested if there were any transactions ongoing before), and in case of any exception, all reversible operations will automatically be rolled-back since a call to Rollback
will be done in the IDisposable.Dispose()
implementation of Transaction
, called implicitly in the end of the using
-clause. A very important row here is therefore the call to Commit
in the very end. This call will end the transaction, preventing it to be rolled back during dispose. The attached project includes a code-snippet to add the code above (or wrap any existing code in such a scope).
Supporting Undo and Redo
To support Undo and Redo in your applications using this framework, you create a new UndoRedoSession
instance, which actually represents a long-lived transaction. UndoRedoSession
derives from Transaction
, making it possible to nest sessions and transactions to any level. To add a new undoable operation to an UndoRedoSession
, you create a new transaction using UndoRedoSession.Begin()
in a using
statement, as shown below. This transaction will be added as a new operation to the UndoRedoSession
when it is committed.
UndoRedoSession urSession = new UndoRedoSession();
using (Transaction txn = urSession.Begin("Name of operation"))
{
txn.Commit();
}
Please note that you can pass the name of the operation as a parameter to Transaction.Begin()
.
In a single application, you may have several undo sessions, but only one active at any moment. The attached demo is an MDI application with one session per document. When using several undo sessions, care must be taken to not access the same reversible resources from several undo sessions. Otherwise, undoing an operation in one session will easily interfere with undo operations in another one.
As you might expect, UndoRedoSession
has methods to perform Undo and Redo. A call to Undo()
will undo the last operation, and Redo()
will redo the last undone operation.
if( urSession.CanUndo() )
{
urSession.Undo()
}
To undo or redo several operations, an operation count can be passed as an argument to Undo and Redo. UndoRedoSession
also has a HistoryChanged
event that is raised whenever the list of undo and redo operations changes. This is, typically, used to keep the user interface updated, as shown in the attached demo. To retrieve the name of the last operation that can be undone, GetUndoText()
is called, and to get the names of all undoable operations, GetUndoTextList()
is used. Corresponding methods exists for Redo.
Usually, the user interface must be updated after a undo or redo operation to reflect the changes. This can be achieved by listening to the Reversed
event, which is fired whenever some operations are reversed. The demo shows how this can be done in a Windows Forms application. It also demonstrates one way of persisting the location and state of the form in the transactions so that the focus is also reversed.
Make existing properties reversible
In addition to initiating a Transaction
, you have to ensure that whenever you do a change, the original state is stored in that Transaction
. The rest of this article will focus on changes of properties and collections, as this should represent the most common in-memory changes in most applications. However, the Transaction
class can also accommodate other types of changes (such as deleting a file or starting a motor!).
This framework supports three ways of making property changes reversible. The first method uses C# 3.0 extension methods to almost seamlessly add reversible support to almost any existing class, including ones already in the .NET framework. As a demonstration, let us start with a simple non-reversible Person
class (used in the attached demo):
public class Person
{
public Person()
{
Children = new List<person>();
}
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public IList<person> Children { get; private set; }
}
Reversible property extension methods
To make reversible changes to the properties without touching the declaration, we create an extension class with one method per writable property of the class. For the Person
class, the final declaration looks like this:
public static class PersonReversibleExtension
{
public static void Name_SetReversible(this Person instance, string value)
{
Transaction.AddPropertyChange(Name_SetReversible, instance, instance.Name, value);
instance.Name = value;
}
public static void DateOfBirth_SetReversible(this Person instance, DateTime value)
{
Transaction.AddPropertyChange(DateOfBirth_SetReversible,
instance, instance.DateOfBirth, value);
instance.DateOfBirth = value;
}
public static IList<person> Children_Reversible(this Person instance)
{
return instance.Children.AsReversible();
}
}
All extension methods must be declared in a static class, and there should be one method per settable property. The keyword this
in front of the first parameter makes it an extension method. Each property setter method calls AddPropertyChange
to register the property change with the current Transaction
. The first parameter should be a reference to the method itself, which will be called in case of a rollback, undo, or redo.
This might look like too much code to write, but it is a lot of repetition for each property, and with the use of a code-snippet included in the demo project, the writing of these extension methods is very simple in Visual Studio. And, the reward is that you then can set properties of any class to be reversible in a very straightforward way, with full Intellisense support in Visual Studio 2008, as shown in Figure 1.
Figure 1: Intellisense support for reversible extension methods in Visual C# Express 2008
To set a Person
’s name, you write the following within a transactional scope:
currentPerson.Name_SetReversible(txtName.Text);
Unfortunately, there is no support for "extension properties" in C# 3.0. This would have made the syntax to set a property reversible more similar to the normal non-reversible syntax. However, by using an extension method name that starts with the property name, the two versions will always be shown below each other in the Intellisense popup menu. In this way, you will be reminded about its existence and how to use it.
Reversible collection extension methods
What about collections then? In the Person
example, we have a Children
collection property that we must be able to add and remove items reversibly as well. As extension methods can be applied to generic interfaces, I have been able to create some very reusable extensions methods that make any collection implementing a generic ICollection<T>
or IList<T>
reversible. Thanks to these, we can just write:
person.Children.Add_Reversible(newChild);
person.Children.Remove_Reversible(childToRemove);
to add and remove items from the Children
collection reversibly. And, thanks to the great Intellisense support for extension methods in VS2008, the reversible versions of the collection methods will show up when you are writing the code (provided that you have imported the Reversible.Extensions
namespace in the code file).
Use AsReversible() to make collections reversible
Another handy way of making collections reversible is to call the AsReversible()
extension method currently available for any generic List<T>
, IList<T>
, and ICollection<T>
. This will wrap the collection in a reversible container, enabling reversible calls to be made as usual, like this:
IList<person> children = person.Children.AsReversible();
children.Add(newChild);
children.Remove(childToRemove);
Thanks to MarsMarshall Rosenstein for this idea of using extension methods to wrap a collection.
Build-in reversibility
The method presented so far using extension methods is very powerful because it can be applied to many existing classes without accessing or changing the original code. In this way, you can write a non-reversible class as you normally do, and then selectively choose where to make reversible calls to it. In some context, you do not need or want reversibility, e.g., when initially loading a large amount of document data, or creating a new instance with some context-given default values. Then, you can simply call the original versions.
A drawback with the extension method technique is that you must ensure that you call the extension method in every situation where it is required, to make an operation reversible. In some situations, when you have full control over the implementation, it might be better to make a class’ methods inherently reversible. In this way, the caller doesn’t have to bother. Remember though that, then you will irrevocably add some overhead to the class that will negatively affect performance when it is to be used in a non-reversible context.
I have explored two different ways of making properties inherently reversible. The first method (recommended in most situations) is to simply add a line to the property setter to register the property change with the current transaction:
private string familyName;
public string FamilyName
{
get { return familyName; }
set
{
Transaction.AddPropertyChange(v => FamilyName = v, familyName, value);
familyName = value;
}
}
The version of AddPropertyChange
used here takes an instance delegate that simply calls the property setter as the first argument. This delegate will be invoked in the case of a reversal. Here, we use the new C# 3.0 lambda expression to greatly reduce the amount of code to write. In the attached project, there are also code-snippets (revprop
and revpropchg
) that reduce the amount of writing even more. The second parameter must be the current (old) value of the property, and the third parameter must be the new value. The former is passed to the delegate during rollback and undoing, and the latter is passed to the delegate in case of redoing.
In the example above, the property is simply stored in a private field, but you may add logic to do validity checks and change notification by raising an event. When the transaction is rolled back or undone, the property setter will be called again by the framework exactly in the same way it was originally called, but with the old value. Therefore, any event listener (like the GUI) is notified also when reversing.
To make collections inherently reversible, you can wrap a collection in a reversible one using the AsReversible()
extension method, as described above. Another option is to use the generic ReversibleList<T>
class I have implemented, which is a reversible version of the generic List<T>
class.
A second option I have explored to make simple properties reversible is very similar in use to the technique shown by Sergio Arhipenko. You simply wrap your property fields in a generic Reversible<T>
type, and uses the Value
property to retrieve and set the current value. An implementation of the Person
's Name
property would look like this:
Reversible<string> name;
public string Name
{
get { return name.Value; }
set { name.Value = value; }
}
In this way, all transaction support is hidden in the Reversible<T>
type, and the amount of code to write is minimized. A drawback is that it is less compatible with change notifications performed in the property setter. Each field will also consume more memory compared to the unwrapped version of the field. In contrast to the UndoRedo<T>
class presented by Sergio, the Reversible<T>
type is implemented as a structure, which reduces the total number of heap-allocated objects (only creates them when properties are changed within a transaction scope), and thereby minimizes the total memory consumption (as long as most of the properties are not changed). Each Reversible<T>
field will, however, still consume at least four extra bytes allocated to store a reference to a "storage object".
Implementation details
This framework heavily uses generic classes, generic methods, and generic delegates to improve type-safety and performance (avoid boxing of primitive types). As demonstrated, extension methods and lambda expressions also play a central role to reduce the amount of code to be written.
To enable rollbacks, undo, and redo, every change has to be stored in a list. Each change is represented by a subclass of the abstract base class Edit
. Its single abstract method, Reverse()
, dictates what should happen during reversing. You may recognize this is as a typical GoF Command Pattern implementation. For each property change, a new property edit instance is created and stored in the current transaction’s list. If no transaction is active, no Edit
instances are created or stored. The generic "property edit" created in AddPropertyChange
stores a reference to the property setter delegate passed to the AddPropertyChange
, together with the old and new values. When Reverse
is called, it simply calls the delegate with the new value. The old and new values are then switched to enable redo.
The reversible collection extension methods and classes implement and use other generic subclasses of Edit
to make operations reversible.
To support nested transactions, I simply made Transaction
inherit from Edit
, and in its Reverse
implementation, it calls Reverse
of every child edit.
Figure 2. Class diagram created in Visual Studio 2008
Anyone can extend the framework with new types of Edit
subclasses to support reversibility of other .NET framework classes and/or their own classes. A good exercise to learn more about extension methods and generics in general, and this framework in particular, is to implement reversibility support for the generic IDictionary
interface.
Conclusion
In this article, I have demonstrated a simple, yet powerful, approach to make .NET applications support atomic in-memory transactions and undo/redo using Generics and the new C# 3.0 features, extension methods and lambda expressions. I hope that this makes you consider implementing such support in future applications, and in this way, improving the usability and/or robustness of the software. However, I want to stress that implementing, and even more testing of, an undoable application can be still be a challenge.
Finally, I want to emphasize that the attached code is neither complete, nor thoroughly tested. It was published here to demonstrate the new approach and inspire you to think of other uses of the new .NET language features. I am also looking forward to any comments and suggestions for improvements.
Compatibility
This was developed and tested on Visual Studio 2008 Beta 2, targeting version 3.5 of the .NET framework. The Intellisense support for extension methods is excellent, but in the Beta 2 version, a bug exists, making the whole application to crash when editing a code file that uses the ReversibleList
class. In the later versions, this should not be an issue since the bug is fixed. I have verified that in Visual C# Express 2008, there is no such problem.
History
- Dec 28, 2007
- Original version published.
- Jun 2, 2008
- Version 1.0.1: Changed behaviour of
UndoRedoSession.Begin()
to restore the previous transaction when a new transaction ends, to avoid problems with operations adding between transaction scopes. Also fixed bugs in Clear
methods, and added support for non-generic IList
s (see the new TreeView example in the menu).