Creating and running a custom rule with PerformanceAdviser

As an introduction to the big bunch of code below, I think PerformanceAdviser is a misleading name that understates the power of this API feature. While the default rules do focus on performance, you can use it for a wide variety of applications, and your custom rules don’t need to have anything to do with performance.

At Revit Forum it was asked if it is possible to retrieve all current warnings from the active document. This can be done with the PerformanceAdviser, but not with the default PerformanceAdviser rules. To do this you will need to make custom rules that match the warnings that Revit creates.

For this example, I made a custom rule to detect rooms that are not in properly enclosed regions. Here are screenshots comparing the output from Revit’s standard Review Warnings dialog and the output from the macro below.

roomwarnings

I’ve tried to be generous with the comments in this code, but please post comments with your questions. Figuring it out the first time is the hardest – creating additional rules is easier.

private void Module_Startup(object sender, EventArgs e)
{
    try
    {
        // Get the one instance of PerformanceAdviser in the Application
        PerformanceAdviser pa = PerformanceAdviser.GetPerformanceAdviser();

        // Create an instance of the RoomNotEnclosed rule class
         RoomNotEnclosed roomNotEnclosed = new RoomNotEnclosed();

         // Add this roomNotEnclosed rule to the PerformanceAdviser
        pa.AddRule( roomNotEnclosed.Id, roomNotEnclosed );
    }    
    // An exception need to be caught here because compiling the module causes Module_Startup to be invoked, which calls RoomNotEnclosed()
    // which calls CreateFailureDefinition which is only allowed during Revit start-up.
    catch (Autodesk.Revit.Exceptions.ApplicationException)
    {}
}

public void RunRoomRule()
{
    Document doc = this.ActiveUIDocument.Document;
    PerformanceAdviser pa = PerformanceAdviser.GetPerformanceAdviser();
    string s = "";

    // Create a list of rules to run
    IList<PerformanceAdviserRuleId> ruleList = new List<PerformanceAdviserRuleId>();

    // Find the "Room not enclosed" rule in the set of all rules
    foreach (PerformanceAdviserRuleId ruleId in pa.GetAllRuleIds())
    {
        if (pa.GetRuleName(ruleId) == "Room not enclosed.")
        {
            // Add this rule to the list of rules to run
            ruleList.Add(ruleId);
            break;
        }
    }

    // Execute the rule and loop through every FailureMessage that results
    foreach (FailureMessage message in pa.ExecuteRules(doc,ruleList))
    {
        // for each failure get its description and the names & ids of the associated elements
        s += message.GetDescriptionText() + " Elements:" + getElementsFromList(doc, message.GetFailingElements()) + "\n\n";
    }

    if (s.Length == 0)
        s = "No warnings found";

    TaskDialog.Show("Room Warnings",s);
}

// This is the implementation of the RoomNotEnclosed. It is derived from the IPerformanceAdviserRule Interface which
// defines the methods that must be included.
public class RoomNotEnclosed : IPerformanceAdviserRule
{
      private FailureDefinition m_warning;
      private FailureDefinitionId m_warningId;

      // The id that uniquely identifies the rule
      public PerformanceAdviserRuleId Id = new PerformanceAdviserRuleId(new Guid("DB21C266-743E-4771-B783-CC49BE7F2A60"));

      // The name of the rule
      public string name = "Room not enclosed.";

      // This list will hold the ids of elements found by this rule
      private IList<ElementId> ids;

  // InitCheck runs once at the beginning of the check. If the rule checks document as a whole (which this rule doesn't), the check can be performed in this method. 
  public void InitCheck(Document document)
  {
        // Create the list of ids if it does not already exist
      if( ids == null )
        ids = new List<ElementId>();
      // Clear the list of ids if it does already exist
      else
        ids.Clear();
  }

  // GetElementFilter gets a filter that restrict the set of elements to be checked. 
  public ElementFilter GetElementFilter(Document document)
      // I want to filter for only Rooms, but it is not possible to filter on this class
      // because it is not part of Revit's native object model. Instead we must filter on
      // SpatialElement, the parent class of Room which also includes the Area and Space classes.
  {  return new ElementClassFilter(typeof(SpatialElement)); }

  // ExecuteElementCheck runs once for each element that passes the ElementFilter defined in GetElementFilter
  public void ExecuteElementCheck(Document document, Element element)
  {
      // Because of the inability to filter on the Room class, this cast and null check is used to restrict
      // the check to rooms only.
      Room room = element as Room;
      if (room != null && room.Area == 0)
          ids.Add(room.Id);
  }

  // FinalizeCheck runs once at the end of the check. 
  public void FinalizeCheck(Document document)
  {    
      if (ids.Count > 0)
      {
        // Create a new failure message
          FailureMessage fm = new FailureMessage(m_warningId);

          // Set the element ids for this failure message
          fm.SetFailingElements(ids);

          // Post a warning for this failure message
          using (Transaction t = new Transaction(document, "Failure"))
          {
              t.Start();
              PerformanceAdviser.GetPerformanceAdviser().PostWarning(fm);
             t.Commit();
          }
      }
  }

  // Return true because the rule needs to be executed on individual elements.
  // False would be used if the check applieed to the document instead of individual elements
  public bool WillCheckElements()
  { return true; }

  // This is a constructor that runs when the new object is created
  public RoomNotEnclosed()
    {
      m_warningId = new FailureDefinitionId(new Guid("E7BC1F65-781D-48E8-AF37-1136B62913F5"));
      m_warning = FailureDefinition.CreateFailureDefinition(m_warningId, FailureSeverity.Warning, name);
    }

  public string GetDescription()
  { return "This room is not enclosed. Volume, area, and perimeter will not be computed."; }

  public string GetName()
  { return name; }
}

private string getElementsFromList(Document doc, ICollection<ElementId> ids)
{
    string s = "";
    foreach (ElementId id in ids)
    {
        Element e = doc.GetElement(id);
        s += " " + e.Name + " (" + id + "),";
    }
    // A comma is being added after each element id, but we don't want that comma after the last id
    return s.TrimEnd(',');
}

Using Module_Startup to run macro code when Revit starts

In previous posts, I have recommended staying away from this code that Revit creates in your macro file.  Now I will explain a situation when we need to get into it.

private void Module_Startup(object sender, EventArgs e)
{
}

The short summary is that code in Module_Startup runs automatically when Revit starts. It can be useful for subscribing to events, registering updaters for Dynamic Model Update, and for using the FailureDefinition.CreateFailureDefinition which is why it is relevant to this series of posts on PerformanceAdviser.

While working on a code sample to run a custom rule with the Performance Adviser, I ran the macro and Revit threw this exception:

RegistryLocked

The RevitAPI.chm help file tells us more about this restriction of FailureDefinition.CreateFailureDefinition:

The newly created FailureDefinition will be added to the FailureDefinitionRegistry. Because FailureDefinition could only be registered when Revit starting up, this function cannot be used after Revit has already started. Throws InvalidOperationException if invoked after Revit start-up is completed.

So we can’t start Revit normally and then, in the middle of our Revit session, run a macro that registers a FailureDefinition. Therefore we need a way to do this registration when Revit starts.

The Revit API Wiki provides the solution:

The Module_Startup method is called when a module loads and Module_Shutdown is called when a module unloads. For Application-level macro modules, Module Startup is called when Revit starts

So I move the code that calls CreateFailureDefinition from my RunRoomRule macro into Module_Startup. (RunRoomRule will be discussed in its own upcoming post). My Module_Startup now looks like this:

private void Module_Startup(object sender, EventArgs e)
{
    // Get the one instance of PerformanceAdviser in the Application
    PerformanceAdviser pa = PerformanceAdviser.GetPerformanceAdviser();

    // Create an instance of the RoomNotEnclosed rule class. Calling the RoomNotEnclosed() constructor is what calls CreateFailureDefinition.
     RoomNotEnclosed roomNotEnclosed = new RoomNotEnclosed();

     // Add this roomNotEnclosed rule to the PerformanceAdviser
    pa.AddRule( roomNotEnclosed.Id, roomNotEnclosed );
}

But when I compile my macro code I get the same exception shown in the screenshot above! Why? Because, in addition to Module_Startup running when Revit starts, it is also called when the macro project is rebuilt.

This creates a bit of a puzzle. I need to move the code to Module_Startup to only have it run when Revit starts. But to do this I need to compile the macro in the middle of my Revit session. But compiling the code in the middle of the Revit session fails because it calls Module_Startup.

The way out of this situation is to add try/catch handling of this exception. I have written about try/catch before and noted that, in general, for this blog I am not going to catch every exception that might occur. But here I have no choice. The exception that occurs during compilation needs to be caught so that compilation can succeed. On startup, the exception will not occur because at that time it will be legal to call CreateFailureDefinition.

The final code is:

private void Module_Startup(object sender, EventArgs e)
{
    try
    {
        // Get the one instance of PerformanceAdviser in the Application
        PerformanceAdviser pa = PerformanceAdviser.GetPerformanceAdviser();

        // Create an instance of the RoomNotEnclosed rule class
         RoomNotEnclosed roomNotEnclosed = new RoomNotEnclosed();

         // Add this roomNotEnclosed rule to the PerformanceAdviser
        pa.AddRule( roomNotEnclosed.Id, roomNotEnclosed );
    }    
    // Need to catch this exception because otherwise Revit will throw every time this is compiled because
    // Module_Startup and Module_Shutdown are called when the macro project is compiled.
    // And because the macro project will compile in the middle of the Revit session, calling RoomNotEnclosed()
    // will throw because it calls CreateFailureDefinition
    catch (Autodesk.Revit.Exceptions.ApplicationException)
    {}
}

(In my initial code I was catching the InvalidOperationException which is the specific sub-class of Autodesk.Revit.Exceptions.ApplicationException that is thrown by CreateFailureDefinition. But there is also another exception to deal with, an ArgumentException that occurs when the failure definition id has already been used to register an existing failure definition. ApplicationException is the parent class of both InvalidOperationException and ArgumentException, so catching ApplicationException takes care of both cases)

Running rules with the Performance Adviser

With the Performance Adviser introduced in the last post, you can run one or more rules and learn which, if any, are being violated.

Here is a macro that runs the 3 default rules whose names include “View”. In my test model it results in 3 warnings:

ViewWarnings

public void PerfAdviserRunViewRules()
{
    Document doc = this.ActiveUIDocument.Document;
    PerformanceAdviser pa = PerformanceAdviser.GetPerformanceAdviser();

    // this list will contain the rules we want to run
    IList<PerformanceAdviserRuleId> ruleList = new List<PerformanceAdviserRuleId>();
    string s = "";

    // Loop through all rules
    foreach (PerformanceAdviserRuleId ruleId in pa.GetAllRuleIds())
    {
        // If the rule name contains "View" add it to the list
        if (pa.GetRuleName(ruleId).Contains("View"))
            ruleList.Add(ruleId);
    }

    TaskDialog.Show("d",ruleList.Count.ToString());

    // Run all rules in the list and loop through all resutling failure messages
    foreach (FailureMessage message in pa.ExecuteRules(doc,ruleList))
    {
        // for each failure get its description and the names & ids of the associated elements
        s += message.GetDescriptionText() + " " + getElementsFromList(doc, message.GetFailingElements()) + "\n\n";
    }
    TaskDialog.Show("Warnings",s);
}

private string getElementsFromList(Document doc, ICollection<ElementId> ids)
{
    string s = "";
    foreach (ElementId id in ids)
    {
        Element e = doc.GetElement(id);
        s += e.Name + " (" + id + "),";
    }
    // A comma is being added after each element id, but we don't want that comma after the last id
    return s.TrimEnd(',');
}

The default rules of the Performance Adviser

Over at the Revit Forum there is talk about Revit Warnings, which is a good excuse to write about PerformanceAdviser. This class lets you analyze the model and warn the user about issues that might harm performance. You can also create your own warnings based on any condition that you can detect with the API.

There are 17 rules hard-coded into Revit.

Get the rules with this code:

public void PerfAdviserGetAllRules()
{
    PerformanceAdviser pa = PerformanceAdviser.GetPerformanceAdviser();
    string s = "";
    foreach (PerformanceAdviserRuleId ruleId in pa.GetAllRuleIds())
    {
        s += pa.GetRuleName(ruleId) + ": " + pa.GetRuleDescription(ruleId) + "\n\n";
    }
    TaskDialog.Show(pa.GetNumberOfRules() + " Rules", s);
}

The rules are:

rules