New in PostSharp 4.0: Undo/Redo, Part 4

by Gael Fraiteur on 17 Mar 2014

In the last three blog posts, I introduced our new undo/redo feature and described how to customize it. So far, we’ve lived with the assumption that there is only a single global recorder to which all objects would send their changes. In this post, I will show how to cope with several recorders. Then, I will see how recordable objects can react to undo/redo operations by implementing a callback interface.    

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.

Coping with several recorders in an application

In my previous examples, the global recorder was automatically and transparently assigned to all new recordable objects. This behavior is the result of the default value of two settings:

  • The AutoRecord property of the [Recordable] custom attribute (set to true by default,) determines whether the object should be assigned to the ambient recorder upon instantiation.

  • The RecordingServices.RecorderProvider property points to a chain of responsibility that provides recorders to recordable objects. The default implementation unconditionally returns a single global instance.

Note that the RecorderProvider is only used when AutoRecord is set to true. Otherwise, the recorder must be assigned manually, as we will see below.

Overriding the default RecorderProvider

If you want to customize which recorder is being assigned to recordable by default, you need to derive a class from RecorderProvider and implement the GetRecorderImpl method. The parameter contains the object for which the recorder is required. Note that instances of RecorderProvider form a chain of responsibility; if one of the instances return null, the next instance will be invoked.

class CustomRecorderProvider : RecorderProvider
{
    public CustomRecorderProvider(RecorderProvider next) : base(next)
    {
    }

    protected override Recorder GetRecorderImpl(object obj)
    {
    throw new NotImplementedException();
    }
}

The second step is to add the custom provider to the chain of responsibility:

RecordingServices.RecorderProvider = new CustomRecorderProvider(RecordingServices.RecorderProvider);

Assigning recorders manually

Another way to assign recorders to objects is to disable the AutoRecord behavior:

[Recordable(AutoRecord = false)]
class TableBooking
{
    // Details skipped.
}

Then you can attach a recorder to an object using the Recorder.Attach method:

Recorder recorder = new Recorder();
TableBooking booking = new TableBooking();
recorder.Attach(booking);

The symmetric method is Recorder.Detach. Note that invoking any of the Attach or Detach results in adding the attach or detach operation to the list of undoable operations. This seems a bit unintuitive but is important to conserve the ability to undo consistently.

Recordable and Aggregatable

As a rule, an object must always have the same recorder as its parent, unless the parent has no recorder. Therefore, the parent’s recorder is automatically assigned to any child object. Note that the recorder is not detached from a child when the child is detached from its parent.

This rule allows to easily work with real-world object models, where the undo/redo behavior of an object is often derived from its parent.

Undo/redo callbacks

There are situations where you may want to execute custom code before or after an object is affected by an undo or redo operation. For instance, if you have a custom implementation of INotifyPropertyChanged, you may want to raise the PropertyChanged event after undo or redo. This can be done by having your recordable object implement the IRecordableCallback interface.

This is illustrated in the following example, where we want to count the number of operations performed on a child list, but we don’t want this counter to be affected by undo/redo operations.

[Recordable]
class IntList : IRecordableCallback
{
    [NotRecorded]
    private bool replaying;
    [Child]
    public readonly AdvisableCollection<int> List = new AdvisableCollection<int>();
        [NotRecorded]
    public int OperationCount { get; private set; }
    public IntList()
    {
        this.List.CollectionChanged += ListOnCollectionChanged;
    }
    private void ListOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
    {
if ( !this.replaying ) this.OperationCount++; } void IRecordableCallback.OnReplaying(ReplayKind kind, ReplayContext context) { this.replaying = true; } void IRecordableCallback.OnReplayed(ReplayKind kind, ReplayContext context) { this.replaying = false; } }

Conclusion

This is the last of a 4-part blog series about the new undo/redo feature found in PostSharp 4.0. We tried to design it to be super easy to get started with, but customizable enough for real-world applications. Thanks to PostSharp, implementing undo/redo in your own applications should become affordable and reasonably easy.

You may wonder how we tested the feature. Of course we have dozens of unit tests, but unit tests don’t make good usability tests. Since as a compiler-building company we don’t have any UI application to dogfood, we chose an open-source project and added the undo/redo feature to it. More in my next blog post.

Happy PostSharping!

-gael

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