Archive

We live in a paradoxical world: all office applications support undo/redo, but very few business applications include this feature. Is this because undo/redo seems so difficult to implement? This is a problem we decided to address in PostSharp 4.0 with the Recordable pattern.

This article is part of a series of 5 about undo/redo:

  1. Announcement and introduction
  2. Getting started – tutorial
  3. Logical operations, scopes, naming
  4. Recorders, recorder providers, callbacks
  5. Case study: Visual Designer

NOTE: This blog post is about an available pre-release of PostSharp. You can install PostSharp 4.0 only using NuGet by enabling the “Include pre-release” option. Undo/Redo is implemented in the package PostSharp.Patterns.Model.

Recordable Pattern

There are several approaches to the undo/redo problem. The design patterns literature recommends the Memento pattern, which basically relies on a snapshotting mechanism: you take a snapshot before an operation, and restore the object model to that snapshot if the operation needs to be undone. An alternative approach is to record changes to the object model and to undo/redo these changes. This is the approach we chose.

The two principal parts of the Recordable pattern is the [Recordable] aspect, which injects into an class the ability to record changes, and the Recorder class, which stores these changes and expose the undo/redo semantics.

Let’s show how this works on a simple example:

[Recordable]
class TableBooking
{
    public string CustomerName { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
    public int NumberOfPersons { get; set; }
    public string TableId { get; set; }

    public void Postpone(TimeSpan time)
    {
        this.StartTime += time;
        this.EndTime += time;
    }
}

The [Recordable] custom attribute causes changes in all TableBooking objects to be recorded. These objects can be bound directly to the UI using XAML bindings or can be modified using C#.

By default, all changes are stored in a global recorder available from the RecordingServices.DefaultRecorder property. This property is provided for convenience; there are more options to assign different recorders if you need more flexibility. We’ll cover these advanced feature in later posts or in our final documentation.

As you would expect, the Recorder class gives you the methods Undo and Redo:

booking.NumberOfPersons = 2;
booking.NumberOfPersons = 4;
RecordingServices.DefaultRecorder.Undo();
// At this point, booking.NumberOfPersons == 2;
RecordingServices.DefaultRecorder.Redo();
// At this point, booking.NumberOfPersons == 4;

How it works

Under the cover, our implementation of the Recordable pattern records changes in all fields of the target class. When calling the Undo or Redo methods, fields are simply set to their previous value. This has the effect to restore the object to their previous state. Note that you can use the [NotRecorded] attribute to prevent a field or automatic property to be recorded.

Taking this approach naively could cause some issues with atomicity. Consider the Postpone method in the code above. This method changes two properties in such a way that the total duration of the table booking is unaffected. Now suppose that the user hits the Undo button just after having postponed the booking. We don’t want the undo operation to affect just EndTime but not StartTime. The user would expect all operations performed by the Postpone method to be undone.

PostSharp solves this problem by introducing the concept of recording scope. By default, all public methods of an object define an atomic scope. That is, all changes performed by the Postpone method are going to be undone atomically. This default behavior can be overridden by the [RecordingScope] attribute, which I will cover in a future article.

Explicit scopes

There are situations where you will want to programmatically control the recording scope. Suppose for instance that you bind the NumberOfPersons property to a slider control. While the user moves the slider, the value of the NumberOfPersons property is subsequently set to all values ranging from 1 to 10 and then back to 5, as the user releases the mouse button. While the user perceives this as a single operation, it actually results in 15 operations in the undo/redo list. This unpleasant experience can be improved by defining the recording scope programmatically, using the OpenScope method of the Recorder class.

The following code snippet shows how to define a recording scope that matches a scope of source code.

using (RecordingServices.DefaultRecorder.OpenScope())
{
    booking.NumberOfPersons = 4;
    booking.NumberOfPersons = 5;
    booking.NumberOfPersons = 6;
    booking.NumberOfPersons = 7;
}

To open a scope when the user starts manipulating a Slider and close the scope when the user stops, we would need to call OpenScope from the GotFocus event and call Dispose from LostFocus.

Object Graphs and Collections

Up to now, we’ve only tracked changes to fields of an intrinsic type. But what if the fields are of more complex types, such as other objects and collections? The solution is simple: all classes that need to be involved in an undo/redo operation need to be made [Recordable]. Of course, we would not want to ask you to develop your own recordable collections. We don’t even want to have specific collection classes that implement the Recordable behavior.

Do you remember our Aggregatable pattern? We faced the problem of providing collections that implement the parent-child relationship. Instead of providing specific classes, we develop collections to which the Aggregatable behavior can be dynamically injected. We’ve done exactly the same with Recordable. Our Recordable pattern relies on the Aggregatable patterns. All children collections will be automatically made recordable.

[Recordable]
public class Invoice
{
    [Child]
    public readonly AdvisableCollection<InvoiceLine> Lines = new AdvisableCollection<InvoiceLine>();

    [Child]
    public readonly AdvisableCollection<InvoiceDiscount> Discounts = new AdvisableCollection<InvoiceDiscount>();

    [Reference]
    public Customer Customer;
}

[Recordable]
public class InvoiceLine
{
    public decimal Quantity;

    [Reference]
    public Product Product;

    [Parent]
    public Invoice ParentInvoice { get; private set; }
}


[Recordable]
public class Customer
{
    public string Name { get; set; }
}

[Recordable]
public class InvoiceDiscount
{
    public decimal Percent;
    public string Reason;

    [Parent]
    public Invoice ParentInvoice { get; private set; }
}

Other features

In this article, I just wanted to introduce the basic features of our Recordable pattern implementation. There are many more features I will describe in a later post:

  • Multiple recorders: You can have several recorders in your application instead of the default single global one.

  • Callback methods that make your objects aware of undo/redo operations.        

  • Fully customizable operation naming so you can display the list of undoable operations to the user.

  • History trimming.

Summary

PostSharp 3.2 makes it much easier to implement the undo/redo in your Windows or Windows Phone application. You can try it yourself today by downloading the PostSharp.Patterns.Model pre-release package from NuGet and playing with the [Recordable] attribute and the Recorder class.

What do you think? Would you have implemented your current project differently if this feature was available? How much code would you have saved?

Happy PostSharping!

-gael

UPDATE: Change product version from PostSharp 3.2 to PostSharp 4.0.

Pingbacks and trackbacks (1)+

Comments are closed