Cascaded Events and Renaming the Active Document

I waxed philosophical on the topic of change yesterday, and here is another issue that can be related to that. In fact, almost anything can be associated with change somehow or other, can't it?

A developer trying to trap the SaveAs command and rename the active document in order to force users to comply with certain naming conventions was attempting a convoluted solution and running into problems with that scenario.

Scott Conover, Software Development Manager in the Revit API team, came up with an elegant and effective alternative solution making use of "cascaded events", i.e. an event handler which in turn temporarily subscribes to another event.

In this case, we obviously make use of the SavingAs and SavedAs pre- and post-event notifications. They cannot be used to rename a document. The pre-event can however cancel the event if desired, and subscribe temporarily to the Idling event, in which you have full freedom to make any changes you like.

We already discussed a number of aspects of the Idling event, which was one of the most exciting new features provided by the Revit 2011 API:

The reason the convoluted workaround failed and this succeeds is the fact that save can be invoked while an editor transaction is open. That is one of the useful features of the Idling event, to be told by Revit when it is OK to proceed because the session is not in an unsupported state.

Before we look at the steps involved, and because it is short and sweet, here is the entire source code, to make it easier to understand the description. Scott's original implementation was based on the Revit 2011 API, since that is what the developer was asking about. Here is a Revit 2012 version, with a few small interesting differences which I will discuss further down:

class App : IExternalApplication
{
  public Result OnShutdown( UIControlledApplication a )
  {
    return Result.Succeeded;
  }
 
  public Result OnStartup( UIControlledApplication a )
  {
    a.ControlledApplication.DocumentSavingAs 
      += new EventHandler<DocumentSavingAsEventArgs>( 
        OnDocumentSavingAs );
 
    a.ControlledApplication.DocumentSavedAs 
      += new EventHandler<DocumentSavedAsEventArgs>( 
        OnDocumentSavedAs );
 
    return Result.Succeeded;
  }
 
  bool reentering = false;
 
  void OnDocumentSavingAs( 
    object sender, 
    DocumentSavingAsEventArgs e )
  {
    try
    {
      if( !reentering )
      {
        Document doc = e.Document;
 
        e.Cancel();
 
        reentering = true;
 
        UIApplication uiApp 
          = new UIApplication( doc.Application );
 
        uiApp.Idling 
          += new EventHandler<IdlingEventArgs>( 
            OnIdling );
      }
    }
    catch( System.Exception ex )
    {
      TaskDialog.Show( "Exception in SavingAs", 
        ex.ToString() );
    }
  }
 
  void OnDocumentSavedAs( 
    object sender, 
    DocumentSavedAsEventArgs e )
  {
    try
    {
      if( e.Status != RevitAPIEventStatus.Cancelled )
      {
        reentering = false;
      }
    }
    catch( System.Exception ex )
    {
      TaskDialog.Show( "Exception in SavedAs", 
        ex.ToString() );
    }
  }
 
  void OnIdling( object sender, IdlingEventArgs e )
  {
    //Application app = (Application) sender; // 2011
    //UIApplication uiApp = new UIApplication( app ); // 2011
 
    UIApplication uiApp = sender as UIApplication; // 2012
 
    Document doc = uiApp.ActiveUIDocument.Document;
 
    string filename = @"C:\foo.rvt";
 
    // warning CS0618: 
    // Autodesk.Revit.DB.Document.SaveAs(string, bool) is obsolete: 
    // This method is obsolete, use SaveAs(String, SaveAsOptions) instead.
    //doc.SaveAs( filename, true );
 
    // SaveAsOptions.Rename essentially does what the old option did 
    // – it either kept the document in memory with the original name, 
    // or renamed it in memory.  If the latter, the UI is one place 
    // where the new name appears.
 
    SaveAsOptions options = new SaveAsOptions();
    options.OverwriteExistingFile = true;
    options.Rename = true;
 
    doc.SaveAs( filename, options );
 
    uiApp.Idling -= OnIdling;
  }
}
 

Here is a top-level description of the functionality and execution sequence of the code:

I made the following additional notes while running the application and stepping through these steps one by one in the debugger:

Here are two interesting little differences between the code for Revit 2011 and Revit 2012:

Both SaveAsOptions.Rename and the obsolete 'changeDocumentFilename' argument choose between either keeping the document in memory with the original name, or renaming it in memory.

For completeness' sake, here is the original Revit 2011 version RenDocOnSave2011.zip and the cleaned up Revit 2012 migration RenDocOnSave2012.zip.

Idling Event Handler Sender Argument Changed

The second change above actually affects the migration of every single Revit 2011 add-in subscribing to the Idling event and using the event handler 'sender' argument to access the Revit application, for example to retrieve the active document from it.

The Building Coder sample command CmdIdling used the following code in the Revit 2011 API:

void OnIdling( object sender, IdlingEventArgs e )
{
  // access active document from sender:
 
  Application app = sender as Application;
 
  Debug.Assert( null != app, 
    "expected a valid Revit application instance" );
 
  if( app != null )
  {
    UIApplication uiapp = new UIApplication( app );
    UIDocument uidoc = uiapp.ActiveUIDocument;
    Document doc = uidoc.Document;
 
    Log( "OnIdling with active document " 
      + doc.Title );
  }
}

This will fail in Revit 2012, since the sender argument is no longer an Application instance, but an UIApplication one. The updated code is much simpler and looks like this:

void OnIdling( object sender, IdlingEventArgs e )
{
  // access active document from sender:
 
  UIApplication uiapp = sender as UIApplication;
  Document doc = uiapp.ActiveUIDocument.Document;
 
  Log( "OnIdling with active document " 
    + doc.Title );
}
 

Here is an updated version 2012.0.87.2 of The Building Coder sample code including this change.

Disclaimer: Please note that the code presented here is just a test showing one possible workaround for the originally stated problem with the customized SaveAs. It is by no means a production-level solution, and even the concept of using the Idling event in the suggested way is not optimal, as it could lead to potentially undesired results. A more detailed discussion and analysis of the above presented workaround will be published in a future post.