PostSharp 4.3 / / Post­Sharp Documentation / Standard Patterns / Undo/Redo / Customizing Undo/Redo Operation Names
Customizing Undo/Redo Operation Names

The example of previous sections displays the list of operations appearing in the two UI buttons. That list of operations references the setters on the different individual properties in a very technical manner, for instance the operation of setting the first name is named set_FirstName, according to the name of the property in source code.

End users will want to see the operations described in meaningful business terms, not technical ones. This article will show you how to explicitly name the recording operations that will take place in your code.

This topic contains the following sections:

Understanding the default operation naming mechanism

From the end user's perspective, the undo/redo feature exposes a flat list of operations that can be undone or redone. From a system perspective, an operation is composed of changes to individual fields and collections. For instance, moving a picture on a design surface is seen as a single operation Move by the user, but it is composed of two changes in fields x and y.

Let's see this in a code example:

C#
                        [Recordable]
                        

                        public
                         
                        class
                         Picture
{
   
                        private
                         
                        double
                         x, y;

   
                        public
                         
                        double
                         X 
   { 
      
                        get
                         { 
                        return
                         x; }
      
                        set
                         { x = 
                        value
                        ; }
   }

   
                        public
                         
                        double
                         Y 
   { 
      
                        get
                         { 
                        return
                         y; }
      
                        set
                         { y = 
                        value
                        ; }
   }

   
                        public
                         
                        void
                         Move( 
                        double
                         x, 
                        double
                         y )
   {
      
                        this
                        .X = x;
      
                        this
                        .Y = y;
   }

}
                        


                        public
                         
                        static
                         
                        class
                         Program
{
   
                        public
                         
                        static
                         
                        void
                         Main()
   {
      
                        var
                         picture = 
                        new
                         Picture();

      picture.Move( 
                        10
                        , 
                        10
                         );

      
                        // 1 undo operation at this point: Move.
                        

                        
      picture.X = 
                        20
                        ;

      
                        // 2 undo operations at this point: set_X, Move.
                        

                        
      picture.Y = 
                        20
                        ;

      
                        // 3 undo operations at this point: set_Y, set_X, Move.
                        

                           }
}
                      

By default, the RecordableAttribute aspect will automatically open a new operation for any public method, unless the current Recorder already has an open operation. Therefore, invoking the Move method results in a single operation, even if it modifies two fields. Note that the Move method invokes the setters of public properties X and Y, which are themselves public methods, but they do not open new operations since they run from within the Move method. However, when properties X and Y are accessed from outside of the Picture class, new operations are created for the set_X and set_Y methods.

Setting operation names declaratively

By default, the name of an operation is set to the name of the method. There are various ways to customize this name, and the easiest is to add a RecordingScopeAttribute custom attribute to the public method.

In the following example, we're declaring a different name for the Move method:

C#
                        [Recordable]
                        

                        public
                         
                        class
                         Picture
{
   
                        private
                         
                        double
                         x, y;

   [RecordingScope(
                        "Moving the picture"
                        )]
   
                        public
                         
                        void
                         Move( 
                        double
                         x, 
                        double
                         y )
   {
      
                        this
                        .x = x;
      
                        this
                        .y = y;
   }

}
                      

With that RecordingScopeAttribute added, the recorded operation will now have a name of Moving the picture instead of just Move.

Setting operation names dynamically

Setting the operation name declaratively is convenient but relatively rigid. When more flexibility is needed, you can use the Recorder.OpenScope(String, RecordingScopeOption) method to control the creation and naming of scopes.

In the following example, we will modify the Move method to include the target position in the operation description.

To dynamically name an operation:

  1. Add the [RecordingScope(RecordingScopeOption.Skip)] custom attribute to the method, so that the method does not automatically define a new operation. Exclude from the Recorder the method which contains the block of code that you wish to encapsulate in a recording.

    C#
                                  [RecordingScope(RecordingScopeOption.Skip)]
                                  
    
                                  public
                                   
                                  void
                                   Move( 
                                  double
                                   x, 
                                  double
                                   y )
                                
    Note Note

    This step is not required if you are starting the operation from a non-recordable object.

  2. Invoke the OpenScope(String, RecordingScopeOption) method and wrap the code you want to record in a using block.

    C#
                                  [Recordable]
                                  
    
                                  public
                                   
                                  class
                                   Picture
    {
       
                                  private
                                   
                                  double
                                   x, y;
    
       [RecordingScope(RecordingScopeOption.Skip)]
       
                                  public
                                   
                                  void
                                   Move( 
                                  double
                                   x, 
                                  double
                                   y )
       {
          
                                  string
                                   scopeName = 
                                  string
                                  .Format( 
                                  "Moving to ({0}, {1})"
                                  , x, y );
    
          
                                  using
                                   (RecordingScope scope = RecordingServices.DefaultRecorder.OpenScope(scopeName))
          {
            
                                  this
                                  .x = x;
            
                                  this
                                  .y = y;
          }
       }           
    }
                                
    Note Note

    If you do not to add this custom attribute to the method, the RecordableAttribute aspect will automatically create a new scope to execute the method, and your call of the OpenScope(String, RecordingScopeOption) method will be ignored.

Using the OperationFormatter class

Explicitly declaring the name for every operation would be a large and tedious task. It is possible to write your own naming engine and apply that set of naming rules across the entire application. To achieve this, derive your own implementation from the OperationFormatter class. to the

In the following example, we will create a custom formatter that reads the operation name from the DisplayNameAttribute custom attribute and display the value to which a property has been set.

To create and register a custom OperationFormatter:

  1. Create a new class and inherit from the OperationFormatter class.

    C#
                                  public
                                   
                                  class
                                   MyOperationFormatter : OperationFormatter
    {
    }
                                
  2. Create a constructor for the new formatter class.

    C#
                                  public
                                   
                                  class
                                   MyOperationFormatter : OperationFormatter
    {
        
                                  public
                                   MyOperationFormatter(OperationFormatter next) : 
                                  base
                                  (next)
        {
        }
    }
                                
  3. Next you need to override the FormatOperationDescriptor(IOperationDescriptor) method and write your custom logic for generating a custom operation name.

    C#
                                  class
                                   MyOperationFormatter : OperationFormatter
     {
         
                                  public
                                   MyOperationFormatter( OperationFormatter next ) : 
                                  base
                                  ( next )
         {
         }
    
         
                                  protected
                                   
                                  override
                                   
                                  string
                                   FormatOperationDescriptor( IOperationDescriptor operation )
         {
             
                                  if
                                   ( operation.OperationKind != OperationKind.Method )
                 
                                  return
                                   
                                  null
                                  ;
    
             
                                  var
                                   descriptor = (MethodExecutionOperationDescriptor) operation;
    
    
             
                                  if
                                   ( descriptor.Method.IsSpecialName && descriptor.Method.Name.StartsWith( 
                                  "set_"
                                   ) )
             {
                 
                                  // We have a property setter.
                                  
    
                                  
                 
                                  var
                                   property = descriptor.Method.DeclaringType.GetProperty( 
                        descriptor.Method.Name.Substring( 
                                  4
                                   ), 
                        BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic );
    
                 
                                  var
                                   attributes = 
                      (DisplayNameAttribute[]) property.GetCustomAttributes(
                                  typeof
                                  (DisplayNameAttribute), 
                                  false
                                  );
    
                 
                                  if
                                   ( attributes.Length > 
                                  0
                                   )
                     
                                  return
                                   
                                  string
                                  .Format( 
                                  "Set {0} to {1}"
                                  , attributes[
                                  0
                                  ].DisplayName, descriptor.Arguments[
                                  0
                                  ] ?? 
                                  "null"
                                   );
             }
             
                                  else
                                  
             {
                 
                                  // We have another method.
                                  
    
                                  
                 
                                  var
                                   attributes =  (DisplayNameAttribute[]) 
                    descriptor.Method.GetCustomAttributes(
                                  typeof
                                  (DisplayNameAttribute), 
                                  false
                                  );
    
                 
                                  if
                                   ( attributes.Length > 
                                  0
                                   )
                     
                                  return
                                   attributes[
                                  0
                                  ].DisplayName;
             }
    
             
                                  return
                                   
                                  null
                                  ;
         }
     }
                                
    Note Note

    Formatters create a chain of responsibility. If one formatter is unable to provide a name it will ask the next formatter in the chain to attempt to provide a name. To make the hand-off occur the FormatOperationDescriptor(IOperationDescriptor) method needs to return null. If it returns anything else the chain is broken and the returned value is used as a name.

  4. Finally, you need to add your custom name formatter into the chain of responsibility.

    C#
                                  RecordingServices.OperationFormatter = 
                                  new
                                   MyOperationFormatter(RecordingServices.OperationFormatter);
                                

    Because the RecordingServices is making use of a chain of responsibility, you are able to insert as many custom name formatters as you want. You are also able to determine their order of execution based on the order that you insert them into the chain of responsibility.