List All Import Instances

Have you ever wondered whether you have any duplicate imported CAD instances in your model?

My colleague Nikolay Shulga from the Revit development team implemented a nice little end user utility to answer this question. By the way, Nikolay and I go back a long time, way back in the beginning of the IAI and IFC project, decades ago, in previous lives...

In Nikolay's own words:

A while ago I wrote a prototype app to list ImportInstance objects in a Revit project. The idea is to list duplicate instances – people importing the same data multiple times – and perhaps do other useful things.

I don’t think I can maintain that app – not enough bandwidth and demand to make it a part of Revit. I’m wondering whether you could make a blog entry out of the idea – hopefully someone picks it up and makes something out of it. If you can figure out a way to make it an open source app, even better.

Here is the entire implementation:

#region Namespaces
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using Autodesk.Revit.ApplicationServices;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Autodesk.Revit.UI.Selection;
using System.IO;
#endregion
 
namespace ListImportInstances
{
  /// <summary>
  /// A generic interface to report imported 
  /// data found in a specific project.  
  /// </summary>
  interface IReportImportData
  {
    bool init( string projectName );
    void startReportSection( string sectionName );
    void logItem( string item );
    void setWarning();
    void done();
    string getLogFileName();
  }
 
  class SimpleTextFileBasedReporter : IReportImportData
  {
    public SimpleTextFileBasedReporter()
    {
    }
 
    public bool init( string projectFileName )
    {
      bool outcome = false;
      m_currentSection = null;
      m_warnUser = false;
 
      if( 0 != projectFileName.Length )
      {
        m_projectFileName = projectFileName;
      }
      else
      {
        m_projectFileName = "Default";
      }
 
      m_logFileName = System.IO.Path.Combine(
        System.IO.Path.GetDirectoryName( m_projectFileName ),
        System.IO.Path.GetFileNameWithoutExtension(
          m_projectFileName ) ) + "-ListOfImportedData.txt";
 
      // Construct log file name from projectFileName 
      // and try to open file. Project file name is 
      // assumed to be valid (expected to be called 
      // on an open doc).
 
      try
      {
        m_outputFile = new StreamWriter( m_logFileName );
        m_outputFile.WriteLine( "List of imported CAD data in "
          + projectFileName );
        outcome = true;
      }
      catch( System.UnauthorizedAccessException )
      {
        TaskDialog.Show( "FindImports",
          "You are not authorized to create "
            + m_logFileName );
      }
      catch( System.ArgumentNullException ) // oh, come on.
      {
        TaskDialog.Show( "FindImports",
          "That's just not fair. Null argument for StreamWriter()" );
      }
      catch( System.ArgumentException )
      {
        TaskDialog.Show( "FindImports",
          "Failed to create " + m_logFileName );
      }
      catch( System.IO.DirectoryNotFoundException )
      {
        TaskDialog.Show( "FindImports",
          "That's not supposed to happen: directory not found: "
          + System.IO.Path.GetDirectoryName( m_projectFileName ) );
      }
      catch( System.IO.PathTooLongException )
      {
        TaskDialog.Show( "FindImports",
          "The OS thinks the file name " + m_logFileName
          + " is too long" );
      }
      catch( System.IO.IOException )
      {
        TaskDialog.Show( "FindImports",
          "An IO error has occurred while writing to "
          + m_logFileName );
      }
      catch( System.Security.SecurityException )
      {
        TaskDialog.Show( "FindImports",
          "The OS thinks your access rights to "
          + System.IO.Path.GetDirectoryName( m_projectFileName )
          + " are insufficient" );
      }
      return outcome;
    }
 
    public void startReportSection( string sectionName )
    {
      endReportSection();
      m_outputFile.WriteLine();
      m_outputFile.WriteLine( sectionName );
      m_outputFile.WriteLine();
 
      m_currentSection = sectionName;
    }
 
    public void logItem( string item )
    {
      m_outputFile.WriteLine( item );
    }
 
    public void setWarning()
    {
      m_warnUser = true;
    }
 
    public void done()
    {
      endReportSection();
      m_outputFile.WriteLine();
      m_outputFile.WriteLine( "The End" );
      m_outputFile.WriteLine();
      m_outputFile.Close();
 
      // Display "done" dialog, potentially open log file
 
      TaskDialog doneMsg = null;
 
      if( m_warnUser )
      {
        doneMsg = new TaskDialog(
          "Potential issues found. Please review the log file" );
      }
      else
      {
        doneMsg = new TaskDialog(
          "FindImports completed successfully" );
      }
 
      doneMsg.AddCommandLink(
        TaskDialogCommandLinkId.CommandLink1,
        "Review " + m_logFileName );
 
      switch( doneMsg.Show() )
      {
        default:
          break;
 
        case TaskDialogResult.CommandLink1:
          // Display the log file
          Process.Start( "notepad.exe", m_logFileName );
          break;
      }
    }
 
    public string getLogFileName()
    {
      return m_logFileName;
    }
 
    private void endReportSection()
    {
      if( null != m_currentSection )
      {
        m_outputFile.WriteLine();
        m_outputFile.WriteLine( "End of "
          + m_currentSection );
        m_outputFile.WriteLine();
      }
    }
 
    private string m_projectFileName;
    private string m_logFileName;
    private StreamWriter m_outputFile;
    private string m_currentSection;
 
    /// <summary>
    /// Tell the user to review the log file
    /// </summary>
    private bool m_warnUser;
  }
 
  [Transaction( TransactionMode.ReadOnly )]
  public class Command : IExternalCommand
  {
    private void listImports( Document doc )
    {
      FilteredElementCollector col
        = new FilteredElementCollector( doc )
          .OfClass( typeof( ImportInstance ) );
 
      NameValueCollection listOfViewSpecificImports
        = new NameValueCollection();
 
      NameValueCollection listOfModelImports
        = new NameValueCollection();
 
      NameValueCollection listOfUnidentifiedImports
        = new NameValueCollection();
 
      foreach( Element e in col )
      {
        // Collect all view-specific names.
 
        if( e.ViewSpecific )
        {
          string viewName = null;
 
          try
          {
            Element viewElement = doc.GetElement(
              e.OwnerViewId );
            viewName = viewElement.Name;
          }
          catch( Autodesk.Revit.Exceptions
            .ArgumentNullException ) // just in case
          {
            viewName = String.Concat(
              "Invalid View ID: ",
              e.OwnerViewId.ToString() );
          }
 
          if( null != e.Category )
          {
            listOfViewSpecificImports.Add(
              importCategoryNameToFileName(
                e.Category.Name ), viewName );
          }
          else
          {
            listOfUnidentifiedImports.Add(
              e.Id.ToString(), viewName );
          }
        }
        else
        {
          listOfModelImports.Add(
            importCategoryNameToFileName(
              e.Category.Name ), e.Name );
        }
      }
 
      IReportImportData logOutput
        = new SimpleTextFileBasedReporter();
 
      if( !logOutput.init( doc.PathName ) )
      {
        TaskDialog.Show( "FindImports",
          "Unable to create report file" );
      }
      else
      {
        if( listOfViewSpecificImports.HasKeys() )
        {
          logOutput.startReportSection(
            "View Specific Imports" );
 
          listResults( listOfViewSpecificImports,
            logOutput );
        }
 
        if( listOfModelImports.HasKeys() )
        {
          logOutput.startReportSection( "Model Imports" );
          listResults( listOfModelImports, logOutput );
        }
 
        if( listOfUnidentifiedImports.HasKeys() )
        {
          logOutput.startReportSection(
            "Unknown import instances" );
          listResults( listOfUnidentifiedImports,
            logOutput );
        }
 
        if( !sanityCheckViewSpecific(
          listOfViewSpecificImports, logOutput ) )
        {
          logOutput.setWarning();
          //TaskDialog.Show("FindImportedData", 
          //"Possible issues found. Please review the log file");
        }
 
        logOutput.done();
      }
    }
 
    /// <summary>
    /// This is an import category. It is created from 
    /// a CAD file name, with appropriate (number) added. 
    /// We want to use the file name as a key for our 
    /// list of import instances, so strip off the 
    /// brackets. 
    /// </summary>
    private string importCategoryNameToFileName(
      string catName )
    {
      string fileName = catName;
      fileName = fileName.Trim();
 
      if( fileName.EndsWith( ")" ) )
      {
        int lastLeftBracket = fileName.LastIndexOf( "(" );
 
        if( -1 != lastLeftBracket )
          fileName = fileName.Remove( lastLeftBracket ); // remove left bracket
      }
 
      return fileName.Trim();
    }
 
    private void listResults(
      NameValueCollection listOfImports,
      IReportImportData logFile )
    {
 
      foreach( String key in listOfImports.AllKeys )
      {
        logFile.logItem( key + ": "
          + listOfImports.Get( key ) );
      }
    }
 
    /// <summary>
    /// Run a few basic sanity checks on the list of 
    /// view-specific imports. 
    /// View-specific sanity is not the same as model 
    /// sanity. Neither is necessarily sane.
    /// True means possibly sane, false means probably 
    /// not.
    /// </summary>
    private bool sanityCheckViewSpecific(
      NameValueCollection listOfImports,
      IReportImportData logFile )
    {
      logFile.startReportSection(
        "Sanity check report for view-specific imports" );
 
      bool status = true;
 
      // Count number of entities per key.
 
      foreach( String key in listOfImports.AllKeys )
      {
        string[] levels = listOfImports.GetValues( key );
        if( levels != null && levels.GetLength( 0 ) > 1 )
        {
          logFile.logItem( "CAD data " + key
            + " appears to have been imported in "
            + "Current View Only mode multiple times. "
            + "It is present in views "
            + listOfImports.Get( key ) );
          status = false;
        }
      }
      return status;
    }
 
    public Result Execute(
      ExternalCommandData commandData,
      ref string message,
      ElementSet elements )
    {
      UIApplication uiapp = commandData.Application;
      UIDocument uidoc = uiapp.ActiveUIDocument;
      Document doc = uidoc.Document;
 
      listImports( doc );
 
      return Result.Succeeded;
    }
  }
}

Many thanks to Nikolay for sharing this!

I followed his instructions and created the following trivial minimal sample model with three imports of a 2D DWG to test it:

Three DWG import instances

Launching the external command generates a report of duplicate instances, stores it in a text file and displays the following task dialogue, with a command link to view the file:

ListImportInstances task dialogue with command link

Clicking the command link opens the text in the default application, in this case Notepad:

ListImportInstances report on duplicate instances

As always, the most up-to-date version including the full source code, Visual Studio solution and add-in manifest is provided by the ListImportInstances GitHub repository.

The version presented above is release 2015.0.0.1.

I am looking forward to hearing from you how you make use of and enhance this.

Please feel free to fork and submit pull requests.