In the previous posts, I described the new undo/redo feature of PostSharp 4.0 and looked at most common use cases. In this post, I will show how you can customize its default behavior.

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.

This blog post covers the following topics:

  • A conceptual introduction to logical operations

  • Defining the operation name declaratively

  • Avoiding automatic scopes

  • Defining atomic scopes declaratively

  • Defining scopes imperatively

  • Customizing scope names

Logical Operations

The Recordable patterns record primitive changes to an object model into a Recorder object. Primitive changes are typically field value changes or primitive collections operations such as Add or Remove. The Recorder itself is basically a list (i.e. a linear collection) of atomic changes. This simple and beautiful structure is sufficient to implement the undo changes and revert the object model to any point in the past.

In practice however, you don’t want to undo to any random point in the past, but only to safe points. Typically, work on an object model is separated into logical operations, which must be undone or redone as a whole. Users always want to undo an operation, not a primitive change. Logical operations are implemented using the concept of recording scope. All changes done within a scope are grouped into a single operation.

By default, any public method of a Recordable object automatically executes inside a scope. That is, all changes done by a single method call will always be undone as a whole.

Note that scopes don’t necessarily define new operations. Because scopes can be nested but operations cannot, only the outermost will open an operation. Nested scope will be ignored unless they are atomic scopes (see below).

Defining the operation name declaratively

Because they are apparent to users, operations must have a name that is meaningful to the user. One way to do that is to use the [RecordingScope] custom attribute. Let’s see this on an example:

[Recordable]
class TableBooking
{
    public string CustomerName { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
[RecordingScope("Postpone booking")] public void Postpone(TimeSpan time) { this.StartTime += time; this.EndTime += time; } }

Avoiding automatic scopes

There are situations where you want to prevent the default behavior that executes each public method in a scope. To opt-out, you can use the [RecordingScope] custom attribute and define the RecordingScopeOption.Skip flag.

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

    [RecordingScope(RecordingScopeOption.Skip)]
    public void Postpone(TimeSpan time)
    {
        this.StartTime += time;
        this.EndTime += time;
    }
}

Defining atomic scopes declaratively

An atomic scope is a scope whose changes get rolled back if the code running inside the scope is not successful. Atomic scopes give the feeling that the code runs in a “transaction”. However, unlike real transactional systems, atomic recording scopes don’t provide transaction isolation. Changes are visible from other threads as soon as they are performed; there is no Commit semantic.

In the following code snippet, the [RecordingScope] attribute ensures that any change performed by the LoadFile method to the object will be rolled back to its initial state in case the input file contains a line that cannot be parsed into an integer.

[Recordable]
class IntList
{
    [Child] 
    private readonly AdvisableCollection<int> list = new AdvisableCollection<int>();
[RecordingScope(RecordingScopeOption.Atomic)] public void LoadFile(string file) { foreach (string line in File.ReadAllLines(file)) { this.list.Add(int.Parse(line)); } } }

Defining scopes imperatively

So far, we’ve seen how to define scopes declaratively using the [RecordingScope] custom attribute. Declarative programming is convenient, but sometimes more flexibility is needed. For these situations, the Recorder.OpenScope method must be used. It allows you to open a scope, set its name, and determine whether it should be atomic. The OpenScope method returns a RecordingScope object, which must be disposed at the end of the scope. For atomic scopes, you must invoke the Complete method before disposing the object, otherwise the changes performed in this scope will be rolled back.

The code snippet below uses the [RecordingScope] attribute to opt-out from the default scope, then uses OpenScope to open a new scope with a custom name.

[Recordable]
class IntList
{
    [Child] 
    readonly AdvisableCollection<int> list = new AdvisableCollection<int>();
[RecordingScope(RecordingScopeOption.Skip)] public void LoadFile(string file) { string scopeName = string.Format("Loading from {0}", file);
using ( RecordingScope scope = RecordingServices.DefaultRecorder.OpenScope(scopeName, RecordingScopeOption.Atomic)) { foreach (string line in File.ReadAllLines(file)) { this.list.Add(int.Parse(line)); } scope.Complete(); } } }

Customizing scope names

In the example above, I’ve shown how to specify a custom scope name when the scope is opened. This approach requires to mix UI code (generation of human-readable names) into domain code. It also demands more boilerplate code and can be cumbersome if you need have a large number of operations to customize. For instance, what if you need to have a proper name for the operation of setting every property in your classes?

This use case is covered by the OperationFormatter class. PostSharp comes with a default formatter, which can be overridden thanks to the RecordingServices.OperationFormatter property. Formatters form a chain of responsibility; if one formatter is not able to provide a name for an operation, it should return null, which causes the next formatter to be invoked.

Formatters have access to the operation descriptor, which is an object describing the operation. The IOperationDescriptor interface has a single property named OperationKind, which informs you to which class the descriptor must be cast.

In the following code snippet, we will show how to create a custom formatter that generates the same name as in the previous example.

class CustomFormatter : OperationFormatter
{
    public CustomFormatter(OperationFormatter next)
        : base(next)
    {
    }
protected override string FormatImpl(IOperationDescriptor operation) { if (operation.OperationKind != OperationKind.Method) return null;
MethodExecutionOperationDescriptor descriptor = (MethodExecutionOperationDescriptor) operation; if (descriptor.Method.DeclaringType == typeof (IntList) && descriptor.Method.Name == "LoadFile") { string file = (string) descriptor.Arguments[0]; return string.Format("Loading from {0}", file); } return null; } }

The following code inserts our custom formatter in the chain of responsibility:

RecordingServices.OperationFormatter = new CustomFormatter(RecordingServices.OperationFormatter);    

Of course, the example above is rather naïve because there is too much coupling between the formatter and the domain code. A more serious implementation would probably rely on other custom attributes such as [DisplayName] of the System.ComponentModel namespace. PostSharp is agnostic about the implementation of custom formatters and I just wanted to keep this example simple.

Summary

Scopes are the mechanism through which logical operations are defined. Logical operations are the “things” that are exposed to the user and that can be undone or redone. Therefore, they must be named. PostSharp provides three ways to define scope: implicitly, declaratively, and imperatively. It also provides, orthogonally, two ways to name operations: by setting the name when the scope is created, or lazily, based on the concept of operation formatter, which allows to better separate UI concerns from business logic. Finally, PostSharp defines the notion of atomic scope, which causes all changes to be rolled back in case of exception.

These few concepts allow to customize how your code maps to undoable operations.

But there is more. In the next post, I will talk about multiple recorders and custom operations.

Happy PostSharping!

-gael

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

In my previous blog post, I described how to make your model classes recordable, so that they can participate in the undo/redo feature. Today, I will show how you can expose the feature to your application’s user interface. There are two approaches: one is to simply include ready-made UndoButton or RedoButton controls to your XAML code. The other is to interact with the Recorder class in C#. Let’s see how.

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.

Undo/Redo Fast Track

For this post, I will use a popular sample application from Visual Studio Gallery: “Easy MVVM Example”. You can create a new project based on this project yourself easily; just open the New Project dialog in Visual Studio, select the Online tab, and search for the sample named “Easy MVVM Example”.

Step 1. Install NuGet packages

Go to the NuGet package manager dialog and add the pre-release package named PostSharp.Patterns.Model.Controls. This will add all dependent packages. Remember to display pre-release packages and not only stable ones.

Step 2. Clean up INotifyPropertyChanged

The sample was not written for PostSharp, so it has its custom INotifyPropertyChanged (INPC) implementation. We would need to write additional code to have our undo/redo feature work with their hand-written INPC, so let’s just clean the project and replace their implementation with our own.

To proceed, find the file Person.cs and replace it with the following content:

using PostSharp.Patterns.Model;

namespace MvvmExample.Model
{
    [NotifyPropertyChanged]
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

This incidentally removes 75% of boilerplate code.

Step 3. Make the Person class recordable

Add the [Recordable] attribute to the Person class.

The code becomes as follows:

using PostSharp.Patterns.Model;
using PostSharp.Patterns.Recording;

namespace MvvmExample.Model
{
    [NotifyPropertyChanged]
    [Recordable]
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

Step 4. Add undo/redo buttons to XAML

  1. Open the MainWindow.xaml file.

  2. Add the following namespace definition to the root <Window> tag:

    xmlns:model="clr-namespace:PostSharp.Patterns.Model.Controls;assembly=PostSharp.Patterns.Model.Controls"   
  3. Add the following controls before the closing </Window> tag.

<model:UndoButton HorizontalAlignment="Left" Margin="22,24,0,0" VerticalAlignment="Top" />
<model:RedoButton HorizontalAlignment="Left" Margin="64,24,0,0" VerticalAlignment="Top"/>

At this point, you can start the application and try to modify a data grid cell and try clicking the undo and redo buttons.

Interacting with the Recorder manually

In the previous section, we just inserted the UndoButton and RedoButton controls into our XAML code. If you want more control, you can access the Recorder class directly from C# code.

In this example, we are using a global Recorder object. You can get a reference to this instance from the RecordingServices.DefaultProvider class. This class provides the following members:

  • UndoOperations and RedoOperations collections: give access to operations that can be undone or redone. Operations are objects derived from the Operation class. This class has a Name property, which is the human-readable name of the operation.

  • Clear : remove all operations both from the UndoOperations and RedoOperations collection.

  • CanUndo and CanRedo properties: determines whether the Undo or Redo commands are available.

  • Undo and Redo methods: undo or redo one operation

  • AddRestorePoint method: creates a restore point, which basically is an empty operation that serves as a marker.

  • CanAddRestorePoint property: determines if the AddRestorePoint command is available

  • UndoTo and RedoTo methods: undo or redo all operations from the now to a given operation or restore point.

  • MaximumOperationsCount property: maximum number of operations in the UndoOperations collection before old operations are removed.

The Recorder class also has other methods and properties that will be explained in a later post.

Example

Let’s see how we can manually interact with the Recorder in practice.

If you click on the arrow of the Undo button just after the application started, you will see that the operation list is already populated with nine items. These operations were created during the application initialization.

If we don’t want users to be able to undo these operations, we can clear the Recorder at initialization. Go to the ViewModelMain class and append the following instruction to the constructor:

RecordingServices.DefaultRecorder.Clear();

Now, when you start the demo application, you will see that the undo and redo buttons are initially disabled.

Summary

Adding the undo/redo feature to your application can be dead easy if you already have a well-structure code, with a nice separation between the UI and the model. Basically, all you need to do is to make your model recordable, then drop the undo and redo buttons to your toolbar.

Of course, no real-world piece of software is so simple than a demo application, and I would not like you to believe that implementing Recordable is always so trivial. Yet, our implementation of the Recordable pattern makes it possible to customize the behavior to your needs.

In the next blog post, we’ll go deeper and see how you can control the creation and naming of operations.

Happy PostSharping!

-gael

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

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.