Drive Revit through a WCF Service

Today, a special titbit for you:

How can I access Revit functionality from an external process?

As we have underlined many times in the past, the Revit API does not support any kind of asynchronous access.

The Idling event, however, provides a possibility to work around this limitation.

Daren Thomas, the author or RevitPythonShell, even presented a neat and clean pattern for semi asynchronous Idling API access.

Now Victor Chekalin, aka Виктор Чекалин, presents another very neat sample providing such an access. He says:

I have an idea how to access Revit from an external application.

The only official way to achieve this is through the Idling event and external events, of course.

The main idea:

There is no risk. All commands are executed in the one and only Revit thread.

Here is the scheme of work:

Revit external access WCF service workflow

In this implementation, I am using a WCF service. It would be equally possible to use any other technology or framework to handle the inter-application communication.

Everything you need to know about creating and using a WCF service is described in the WCF Getting Started Tutorial.

I created a simple demonstration project to test this idea. It implements the following minimal service demonstrating that we can access the Revit API, both to query it for information and execute and action that modifies the current BIM:

namespace RevitExternalAccessDemo
{
  [ServiceContract]
  public interface IRevitExternalService
  {
    [OperationContract]
    string GetCurrentDocumentPath();
 
    [OperationContract]
    bool CreateWall( XYZ startPoint, XYZ endPoint );
  }
}

It further requires:

Here is a short video showing how it works:

I am happy to make the project available for everyone in its current state.

The source code is available on Github, both directly and as a Github zip archive file.

It is just a demo and provided as is.

To launch it on your computer, you must run Revit as administrator, because this is required by the WCF service.

Remember, Revit must be idling to launch a command from the external application.

In this kind of situation, calling the SetRaiseWithoutDelay method is of utmost importance. Without the call, the demo pauses. Even with it, some delay exists, and the CPU load grows to over 25% even if no operations are active in Revit.

A better solution exists – follow Arnošt Löbel suggestion: subscribe to the Idling event only when you really need it, and unsubscribe immediately afterwards, as soon as possible.

Obviously, we need to handle Idling only when we have a request queued up from the WCF service. The idea would be to subscribe to the Idling event when we receive a request from the WCF Service and unsubscribe when it has been processed.

Unfortunately, I have not yet implemented this idea in the code presented here.

Here are some sample code snippets from the current implementation.

First, here is the main external application implementation including:

class App : IExternalApplication
{
  private const string serviceUrl =
    "http://localhost:56789/RevitExternalService";
 
  private ServiceHost serviceHost;
 
  public Result OnStartup( 
    UIControlledApplication a )
  {
    a.Idling += OnIdling;
 
    Uri uri = new Uri( serviceUrl );
 
    serviceHost = new ServiceHost( 
      typeof( RevitExternalService ), uri );
 
    try
    {
      serviceHost.AddServiceEndpoint( 
        typeof( IRevitExternalService ), 
        new WSHttpBinding(), 
        "RevitExternalService" );
 
      ServiceMetadataBehavior smb 
        = new ServiceMetadataBehavior();
 
      smb.HttpGetEnabled = true;
 
      serviceHost.Description.Behaviors.Add( smb );
 
      serviceHost.Open();
    }
    catch( Exception ex )
    {
      a.ControlledApplication.WriteJournalComment( 
        "Could not start WCF service.\r\n" 
        + ex.ToString(),
        true );
    }
    return Result.Succeeded;
  }
 
  private void OnIdling( 
    object sender, 
    IdlingEventArgs e )
  {
    var uiApp = sender as UIApplication;
 
    Debug.Print( "OnIdling: {0}", 
      DateTime.Now.ToString( "HH:mm:ss.fff" ) );
 
    // Be careful! This loads the CPU:
 
    e.SetRaiseWithoutDelay();
 
    if( !TaskContainer.Instance.HasTaskToPerform )
      return;
 
    try
    {
      Debug.Print( "Start execute task: {0}", 
        DateTime.Now.ToString( "HH:mm:ss.fff" ) );
 
      var task = TaskContainer.Instance.DequeueTask();
 
      task( uiApp );
 
      Debug.Print( "Ending execute task: {0}", 
        DateTime.Now.ToString( "HH:mm:ss.fff" ) );
    }
    catch( Exception ex )
    {
      uiApp.Application.WriteJournalComment(
        "RevitExternalService. An error occured "
        + "while executing the OnIdling event:\r\n"
        + ex.ToString(), true );
 
      Debug.WriteLine( ex );
    }
  }
 
  public Result OnShutdown( UIControlledApplication a )
  {
    a.Idling -= OnIdling;
 
    if( serviceHost != null )
    {
      serviceHost.Close();
    }
    return Result.Succeeded;
  }
}

Secondly, here is part of the RevitExternalService definition including one of its method implementations:

class RevitExternalService : IRevitExternalService
{
  private string currentDocumentPath;
 
  private static readonly object _locker 
    = new object();
 
  private const int WAIT_TIMEOUT = 10000; // 10 seconds timeout
 
  public string GetCurrentDocumentPath()
  {
    Debug.Print( "Push task to the container: {0}", 
      DateTime.Now.ToString( "HH:mm:ss.fff" ) );
 
    lock( _locker )
    {
      TaskContainer.Instance.EnqueueTask( GetDocumentPath );
 
      // Wait when the task is completed
 
      Monitor.Wait( _locker, WAIT_TIMEOUT );
    }
 
    Debug.Print( "Finish task: {0}", 
      DateTime.Now.ToString( "HH:mm:ss.fff" ) );
 
    return currentDocumentPath;
  }
 
  private void GetDocumentPath( 
    UIApplication uiapp )
  {
    try
    {
      currentDocumentPath 
        = uiapp.ActiveUIDocument.Document.PathName;
    }
    finally
    {
      // Always release locker in finally block
      // to ensure to unlock locker object.
 
      lock( _locker )
      {
        Monitor.Pulse( _locker );
      }
    }
  }
 
  public bool CreateWall( 
    XYZ startPoint, 
    XYZ endPoint )
  {
    // . . .
  }
}

For completeness' sake, here is also VCRevitExternalAccessDemo.zip containing my personal version of the code, mainly for my own convenience :-)

Many thanks to Victor for very clear and efficient sample implementation!

By the way, don't miss the additional neat little feature demonstrated by the sample code above, the use of the Application WriteJournalComment method to add a comment to the Revit journal file.

Programmatic Creation of a Structural Engineering Plan

Saikat Bhattacharya discusses the details and provides sample code to create a new Revit EngineeringPlan view using the static ViewPlan.Create method and a ViewFamily.StructuralPlan ViewFamilyType.