Understanding the Recordable Aspect
This section describes how the RecordableAttribute aspect is implemented. It helps developers and architects to understand the behavior and limitations of the aspect.
This topic contains the following sections:
When the RecordableAttribute aspect is applied to a class, the aspect records changes performed on instances of this class. Changes are represented as instances of the Operation class. For instance, the FieldOperation<T> class represents the operation of changing the value to a field. All operations implement the Undo(ReplayContext) and Redo(ReplayContext) methods. For instance, the FieldOperation<T> class stores both the new and old value so that the operation can be undone and redone.
The changes are recorded into the Recorder object. The Recorder maintains two collections of operations: UndoOperations and RedoOperations. The Recorder.Undo() method takes the last operation from the UndoOperations collection, invokes Operation.Undo(ReplayContext) for this operation, and moves the operation to the RedoOperations collection. The Recorder.Redo() method works symmetrically.
It would not be safe, however, to allow users to undo changes in the object model back to any arbitrary point in history. Users don't want to undo primitive changes to an object model, but to undo whole operations understood from a user's perspective. This is why the UndoOperations and RedoOperations collections don't expose primitive changes on the object model but logical operations.
By default, logical operations are automatically opened when calling a public or internal method of a recordable object, and closed when the same method exits. The principal use case of scopes is to define user-friendly operation names.
There is typically a single instance of the Recorder class per application, but there could be many if needed (for instance in a multi-document application). The default single instance is accessible from the RecordingServices.DefaultRecorder property. By default, recordable objects are attached to the default recorder immediately after completion of the constructor. See Assigning Recorders Manually to learn how to customize this behavior.
Scopes are a mechanism to aggregate several primitive operations into logical operations that make sense for the end-user. Logical operations are represented by the CompositeOperation class.
In general, logical operations form a flat structure: the UndoOperations and RedoOperations collections are flat double linked lists, and each CompositeOperation typically contains primitive operations such as a field value change.
Scopes define boundaries of logical operations. Scopes can be opened using the Recorder.OpenScope(RecordingScopeOption) method, which returns an object of type RecordingScope. This class implements the IDisposable interface, making it convenient to define scopes with the using statement.
By default, the RecordableAttribute aspect encloses all instance public and internal methods with an implicit scope. That is, by default, public and internal methods define boundaries of logical operations.
Unlike logical operations, scopes are generally nested. Scope nesting typically happens when a public method directly or indirectly invokes another public method. In general, only the outermost scope results in creating a logical operation. This is why, in general, logical operations form a flat structure.
Because they are visible to users, logical operations must be given a user-friendly name. PostSharp defines default names that are not user-friendly. The responsibility of generating operation names is implemented by the OperationFormatter class. You can provide your own OperationFormatter to generate operations names on demand, or you can set the name explicitly in source code for each operation.
Scope names can be declaratively defined using the RecordingScopeAttribute custom attribute, or programmatically using the Recorder.OpenScope(String, RecordingScopeOption) method. To learn more about operation names, see Customizing Undo/Redo Operation Names.
Atomic scopes are scopes whose changes are automatically rolled back when it does not complete successfully, typically when an exception occurs. The rollback is implemented using the undo mechanism. Atomic scopes are a concept similar to transactions, but multithreading is not taken into account. Therefore, other threads may see changes that have not been "committed", because the Recordable\ pattern does not have a notion of transaction isolation.
Atomic scopes cause composite operations to have a tree structure. However, the concept of atomic structure does not surface to the users. Therefore, from a user's perspective, the UndoOperations and RedoOperations collections still present linear lists of logical operations.
Represents the operation of setting a field to a different value.
Represents operations on collections.
Represents operations on dictionaries.
Represents the operation of attaching or detaching an object to or from a Recorder.
Additionally to these system-defined operations, it is possible to implement custom operations by deriving from the Operation abstract class. You can then use the Recorder.AddOperation(Operation) method to append the custom operation to the Recorder.
Logical operations, which are presented to the end user, are typically represented as instances of the CompositeOperation class.
Restore points act like bookmarks in the list of operations. They allow to undo or redo operations up to a specific point. You can use the Recorder.AddRestorePoint(String) method to create a restore point. The method returns an instance of the RestorePoint class, which derives from the Operation class. Unlike other operations, you can safely remove a restore point from the history thanks to the Remove() method.
You can use the EditableObjectAttribute custom attribute to automatically implement the IEditableObject interface. The implementation is based on the RecordableAttribute aspect. It creates a RestorePoint when the BeginEdit method is invoked, removes the restore point upon EndEdit, and undoes changes up to the restore point when CancelEdit is called.
Because of this implementation strategy, it is possible that CancelEdit actually cancels changes done to other objects that share the same Recorder.
The Recorder will invoke the OnReplaying(ReplayKind, ReplayContext) and OnReplayed(ReplayKind, ReplayContext) methods of any recordable object implementing the IRecordableCallback interface, whenever the object is affected by an undo or redo operation.
The order in which these methods are ordered on several objects is nondeterministic; in particular, the aggregation structure is not respected.
From callback methods, it is not allowed:
The UndoOperations and RedoOperations collections hold strong references to all objects that have changes that can be undone or redone. This means that these objects cannot be garbage-collected and will remain in memory.
You can define the maximal number of operations available for undo thanks to the Recorder.MaximumOperationsCount property.