Drive Revit via a WCF Service, Wall Directions and Parameters

Exciting news from Russia, and some mundane updates on other repetitive topics:

Driving Revit from a Modeless Context via a WCF Service

Over the years, we explored numerous different ways to drive Revit from outside, from a modeless context, via the Idling and external events.

The need for this kind of approach is diminishing as the Forge platform gets closer towards adding support for Revit to its Design Automation API, sometimes referred to as Revit I/O.

One popular method, widely used in Russian-speaking areas of the world, is Victor Chekalin's solution to drive Revit through a WCF service, cf. his RevitExternalAccessDemo on GitHub, originally implemented for Revit 2013.

Unfortunately, it stopped working in the Revit 2018 or 2019 timeframe.

The problem was extensively debated in the community of Autodesk programmers in the CIS and its discussion forum, which includes a lively Revit API board, in the thread on access to Revit from an external application.

Now Alexander @aignatovich Ignatovich, aka Александр Игнатович, stepped in and fixed the problem in his CADBIMDeveloper fork of RevitExternalAccessDemo.

In Alexander's own words:

В Собственных Словах Александра

I recently started to answer queries in the Russian ADN forum.

There was a discussion about driving Revit from outside.

Several years ago, Victor developed a sample that starts a WCF service in Revit.

It stopped working in Revit 2019.

This is my exploration of the problem:

The source code relies on independent calling of WCF service methods processing an OnIdling event, e.g., in this service method:

  lock (Locker)
  {
    // Enque GetDocumentPath task:

    TaskContainer.Instance.EnqueueTask(GetDocumentPath); 

    // Waiting for invoking Monitor.Pulse(Locker)
    // somewhere in desired time interval

    Monitor.Wait(Locker, WaitTimeout);
  }
  return currentDocumentPath;

The calling of Monitor.Pulse in task processing method:

  private void GetDocumentPath(UIApplication uiapp)
  {
    try
    {
      currentDocumentPath = uiapp.ActiveUIDocument.Document.PathName;
    }
    finally
    {
      lock (Locker)
      {
        Monitor.Pulse(Locker); // <- HERE!
      }
    }
  }

Actually, this code branch is invoked in the OnIdling event handler.

This worked for years and still works fine in Revit 2017.

However, since Revit 2019 (or maybe Revit 2018), we are waiting forever at the call to Monitor.Wait in the line:

  Monitor.Wait(Locker, WaitTimeout);

Within the Wait timeout period, there is no OnIdling event handler call.

So, I let the service host be opened in another thread:

private const string ServiceUrlHttp = "http://localhost:9001/RevitExternalService";
private const string ServiceUrlTcp = "net.tcp://localhost:9002/RevitExternalService";

// ...

public Result OnStartup(UIControlledApplication application)
{
  application.Idling += OnIdling;

  try
  {
    Task.Factory.StartNew(() =>
      {
        var serviceHost = new ServiceHost(
          typeof(RevitExternalService),
          new Uri(ServiceUrlHttp),
          new Uri(ServiceUrlTcp));

        serviceHost.Description.Behaviors.Add(
          new ServiceMetadataBehavior());

        serviceHost.AddServiceEndpoint(
          typeof(IRevitExternalService),
          new BasicHttpBinding(),
          ServiceUrlHttp);

        serviceHost.AddServiceEndpoint(
          typeof(IRevitExternalService),
          new NetTcpBinding(),
          ServiceUrlTcp);

        serviceHost.AddServiceEndpoint(
          typeof(IMetadataExchange),
          MetadataExchangeBindings.CreateMexHttpBinding(),
          "mex");

        serviceHost.Open();

      }, TaskCreationOptions.LongRunning);
  }
  catch (Exception ex)
  {
    //....
  }
  return Result.Succeeded;
}

With that, it started working again.

I use http binding, so Revit should be started as administrator, or you should use the ServiceModel registration tool to register WCF service for specific application.

Look at my forked RevitExternalAccessDemo repository.

I also was interested in why it stopped working.

Was it due to some Revit mechanism change or due to changes in .Net 4.7 framework?

I created a simple application that starts a WCF service, but real request processing is in separate thread (I use the same TaskContainer):

public partial class Form1 : Form
{
  private const string ServiceUrlHttp
    = "http://localhost:9001/ExternalService";

  private const string ServiceUrlTcp
    = "net.tcp://localhost:9002/ExternalService";

  public Form1()
  {
    InitializeComponent();
  }

  private void Form1_Load(object sender, EventArgs e)
  {
    Task.Factory.StartNew(() =>
      {
        while (true)
        {
          OnIdling();

          Thread.Sleep(TimeSpan.FromSeconds(1));
        }
      }, TaskCreationOptions.LongRunning);

    var serviceHost = new ServiceHost(
      typeof(ExternalService),
      new Uri(ServiceUrlHttp),
      new Uri(ServiceUrlTcp));

    serviceHost.Description.Behaviors.Add(
      new ServiceMetadataBehavior());

    serviceHost.AddServiceEndpoint(
      typeof(IExternalService),
      new BasicHttpBinding(),
      ServiceUrlHttp);

    serviceHost.AddServiceEndpoint(
      typeof(IExternalService),
      new NetTcpBinding(),
      ServiceUrlTcp);

    serviceHost.AddServiceEndpoint(
      typeof(IMetadataExchange),
      MetadataExchangeBindings.CreateMexHttpBinding(),
      "mex");

    serviceHost.Open();
  }

  private static void OnIdling()
  {
    if (!TaskContainer.Instance.HasTaskToPerform)
      return;

    var task = TaskContainer.Instance.DequeueTask();
    task();
  }
}

It works as expected, so this change is probably a side effect of some changes in Revit itself.

I forked Victor's repository and made this thing work again in Revit 2019.

For the sake of completeness, here is also the CADBIMDeveloper/RevitExternalAccessDemo GitHub repo documentation:

How to Host a WCF Service in Revit

This code shows how to deploy a WCF service in Autodesk Revit and call it from an external application.

This code initially was written by Victor Chekalin for Revit 2013.

However, it does not work in Revit 2019. There is a discussion in the adn-cis forum.

The latest changes make it work again.

Content

This solution contains 2 projects:

Starting the WCF Service in Revit

You should start Revit as administrator or use the ServiceModel Registration Tool to register WCF.

Room Walls' Directions

Several similar questions on determining the direction that walls are facing have cropped up in the last few weeks.

Here is a recent one from the Revit API discussion forum thread on room walls direction:

Question: How to get the direction of the walls of a room? I mean the interior side of wall is facing in what direction? East, West, North, South, Northeast, Northwest, Southeast, Southwest, etc.

You can assume I am inside the room and I am looking at the wall. I need to get this direction.

Answer: To start with, you can look at the DirectionCalculation Revit SDK sample and The Building Coder discussion of it on south facing walls.

Benoit provides some further suggestions on:

For the first, he suggests using the most simple solution: draw a semi-infinite line – a ray – from the point you are checking to a point on the face of the wall. Determine the number of times it crosses the walls of the room. If you cross them an even number, you are inside the room. Elseway, you are outside.

For the direction you need, draw a line between your position and a point of the face of the wall. Then, compute the angle to the North. You have your direction.

Retrieving All Exterior Walls

While I am writing this, another related question popped up, on how to get all the outermost walls in the model:

Question: How do I get all the outermost walls in the model?

Here is a picture showing what I mean:

Exterior walls

Answer: Just as in the previous answer, the DirectionCalculation Revit SDK sample and The Building Coder discussion of it on south facing walls will provide a good starting point for you.

It uses the built-in wall function parameter FUNCTION_PARAM to filter for exterior walls.

I updated the code presented there and added it to The Building Coder samples for you, in the CmdCollectorPerformance.cs module lines L293-L323.

First, we implement a predicate method IsExterior that checks this parameter value to determine whether a wall type is exterior or not:

  /// <summary>
  /// Wall type predicate for exterior wall function
  /// </summary>
  bool IsExterior( WallType wallType )
  {
    Parameter p = wallType.get_Parameter(
      BuiltInParameter.FUNCTION_PARAM );

    Debug.Assert( null != p, "expected wall type "
      + "to have wall function parameter" );

    WallFunction f = (WallFunction) p.AsInteger();

    return WallFunction.Exterior == f;
  }

With that, you can retrieve all exterior walls with a filtered element collector like this:

  /// <summary>
  /// Return all exterior walls
  /// </summary>
  IEnumerable<Element> GetAllExteriorWalls(
    Document doc )
  {
    return new FilteredElementCollector( doc )
      .OfClass( typeofWall ) )
      .Cast<Wall>()
      .Where<Wall>( w =>
        IsExterior( w.WallType ) );
  }

Since the wall function filter is just checking a parameter value, the performance of this filtering process could be significantly enhanced by using a parameter filter instead of the slow .NET based IsExterior method post-processing.

Unfortunately, the wall function parameter is not always correctly set. In that case, of course, the GIGO principle applies: Garbage in, garbage out.

If you wish to avoid the dependency on the wall type and its parameters, you can try to judge whether a wall is exterior based on geometrical analysis instead.

The Revit API also provides a BuildingEnvelopeAnalyzer class that should help with this, but there seem to be problems using it, cf.:

Some related challenges and solutions that might help here are discussed in The Building Coder topic group on 2D Booleans and adjacent areas.

Yet another workaround was suggested: Place some room separation lines outside the building envelope and create a huge room around the entire building. Then, it’s just a matter of getting room boundaries, filtering out the RSLs, appending the remaining elements to your list, deleting the room and RSLs, and moving up to the next level. It may not work for some bad modelling cases, but catches most.

After further discussion with the development team, they asked: Is the building model enclosed? It needs to be in order for the analyzer to work. In other words, do you have Roof and Floor elements to form enclosed spaces in the model?

Getting and Setting a Shared parameter Value

Let's end for today with a very basic question asked on StackOverflow, on how to get the value a room parameter defined by a third party:

Question: How to get the value a room parameter, which was defined by a third party?

I am working on a shared project with many other people. One defined a custom parameter, which we must fill. I now want to fill that custom parameter with values from my database. But I can figure out, how to access this custom parameter which was created by a third party, since I don`t have a GUID...

Here is a screenshot of the parameter I want to change:

Third party shared parameter

Answer: Reading and writing to a shared parameter is a very basic operation and is covered by the Revit API getting started material. Please work through the video tutorials first of all, so you get an understanding of the basics.

After that, you can also take a look at the AdnRevitApiLabsXtra on GitHub. It includes many examples of reading and writing parameters, both shared and built-in.

Finally, the FireRating and FireRatingCloud samples both demonstrate one single simple workflow that illustrates all aspects of what you need to know: