Logging data when opening Revit files

Here’s a prototype of a new tool to track data about your Revit files, who opens them, how long the open takes, if they need to be upgraded, and more. Please leave a comment if you like this or have ideas about how to make it better.

Data Collected:

Open Started Open Completed Elapsed (HH:MM:SS) User File Name File Size (MB) Central File File Last Saved Version Saved in Current Version Revit Version Name Revit Version # Revit Build #

Sample Data:
Untitled Open times are rounded down to the nearest second.

Search the Uniformat file & Set the Parameter

Steve makes a good observation in this post:

It would be nice if we could search for keywords when attempting to assign the parameters for both Omniclass and Uniformat. Sometimes the best category to use in Revit isn’t necessarily the correct category in these classification systems. This means a fair bit of time wasted hunting for an appropriate value. I search the source files to find keywords first which isn’t the most straightforward.

Here’s a proof-of-concept showing how it could be done without too much heavy lifting. I haven’t coded a UI for this as I mention in the source code comments, but getting the search string from the text note is a fine simplification to show what to do with the search string.

public void uniformatSearch()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = this.ActiveUIDocument.Document;

    // select the element whose Uniformat parameter will be set
    Element e = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select the element to set Assembly Code."));

    // select a text note whose text indicates the string to search for
    // in a real app there would be UI to enter this search string, but this works for a proof of concept macro
    TextNote text = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select the text note with the search term.")) as TextNote;

    // the location of the Uniformat file
    string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
    string uniformatFile = Path.Combine(appData, @"Autodesk\revit\Autodesk Revit Architecture 2013", "UniformatClassifications.txt");

    IList<string> matches = new List<string>();
    // read the uniformat file one line at a time
    using (StreamReader read = new StreamReader(uniformatFile))
    {
        string line;
        while ((line = read.ReadLine()) != null)
       {
            // add the line to the list of matches if it contains the search string
            if (line.Contains(text.Text))
                matches.Add(line);
       }
    }
    // show all matches
    TaskDialog.Show("Matches for " + text.Text, listToString(matches));

    // get the type of the selected element and its uniformat parameter
    ElementType eType = doc.GetElement(e.GetTypeId()) as ElementType;
    Parameter p = eType.get_Parameter(BuiltInParameter.UNIFORMAT_CODE);

    // set the value of the parameter to the first entry in the match list - good enough for this proof-of-concept
    using (Transaction t = new Transaction(doc,"Set Uniformat"))
    {
        t.Start();
        // the uniformat file is a tab-delimited file
        // the string to use to set the parameter is the first entry [0] in the array created by splitting the complete string
        p.Set(matches.First().Split('\t')[0]);
        t.Commit();
    }
}
private string listToString(IList<string> stringList)
{
    string ret = "";
    foreach (string s in stringList)
    {
        ret += s.Replace("\t"," ") + "\n";
    }
    return ret;
}

Getting Data From the Central File While Working in the Local File

A while back I was thinking about using the API while working in a local file to collect data about the central file. This came from Steve’s post about warnings after Synchronize with Central if rooms have been added in your local file that are in the same region as rooms in the central file (that presumably were created by some other user since your last Synchronize).

Instead of being surprised by this warning as part of a complex and possibly time-consuming Synchronize, maybe it would be nicer to run some quick checks via the API. To do this, we would start with the local file open, open the central file, collect some data, and then return the user to the local file. But because of these two restrictions, several intermediate steps are needed.

FileOpenRestrictions

flowchart
public void GetCentralFileData()
{
    Autodesk.Revit.ApplicationServices.Application app = this.Application;
    Document localFileDocument = this.ActiveUIDocument.Document;

    // Get an instance of the ModelPath class describing the central file
    ModelPath modelPath = localFileDocument.GetWorksharingCentralModelPath();

    // Get the string of the path for the central and local files 
    string centralFilePath = ModelPathUtils.ConvertModelPathToUserVisiblePath(modelPath);
    string localFilePath = localFileDocument.PathName;

    // Local and central can't both be open at the same time in the same session
    // But trying to call doc.Close() here doesn't work. It throws an InvalidOperationException
    // when attempting to close the currently active document
    // So before trying to close the currently open local file, first create and open a temporary document
    Document tempDoc = app.NewProjectDocument(@"C:\ProgramData\Autodesk\RAC 2013\Templates\US Imperial\default.rte");
    // But NewProjectDocument does not make the new document active, so the local file (which is the active document) still can't be closed

    // Save the temporary file into the TEMP folder
    string tempFileName = Path.Combine(Environment.GetEnvironmentVariable("TEMP"),"tempFile.rvt");
    // Delete an existing file if there is one from a previous run of this macro
    File.Delete(tempFileName);
    tempDoc.SaveAs(tempFileName);
    tempDoc.Close();

    // Use OpenAndActivateDocument to make the temp file the active document
    Autodesk.Revit.UI.UIApplication uiapp = new UIApplication(app);
    uiapp.OpenAndActivateDocument(tempFileName);

    // Now that the temp file is open and active, the local file can be closed
    localFileDocument.Close();

    // With the local file closed, we can now finally open the central file
    // OpenDocumentFile opens the document into memory but does not make it visible in the Revit UI.
    // Use OpenAndActivateDocument if you want the document to become active
    Document centralFileDoc = app.OpenDocumentFile(centralFilePath);

    int centralFileWallCount = new FilteredElementCollector(centralFileDoc).OfClass(typeof(Wall)).Count();

    // Open the temp file
    UIDocument tempfileUIDoc = uiapp.OpenAndActivateDocument(tempFileName);

    // Close the central file
    centralFileDoc.Close();

    // Open the local file
    uiapp.OpenAndActivateDocument(localFilePath);

    // Close the temp file
    tempfileUIDoc.Document.Close();

    // Finally we are back to having only the local file open!
    // Display the data about the central file that was collected when the central file was open
    TaskDialog.Show("Central File Data", centralFileWallCount + " walls in " + centralFilePath);
}

Opening a local file’s central file – Attempt #2

public void OpenCentralFile()
{
    Autodesk.Revit.ApplicationServices.Application app = this.Application;
    Document doc = this.ActiveUIDocument.Document;

    // Get an instance of the ModelPath class describing the central file
    ModelPath modelPath = doc.GetWorksharingCentralModelPath();

    // Get the string of the path for the modelPath 
    string centralFilepath = ModelPathUtils.ConvertModelPathToUserVisiblePath(modelPath);

    // Local and central can't both be open at the same time in the same session
    // But trying to call doc.Close() here doesn't work. It throws an InvalidOperationException
    // when attempting to close the currently active document
    doc.Close();

    Document centralDoc = app.OpenDocumentFile(centralFilepath);
    TaskDialog.Show("status", "Central file opened");
}

How to Not Open a Local File’s Central File

The last post about errors when synchronizing with the central file got me thinking about how the API might be used to run checks in the central file before saving.

At first, it seems like it should be simple to open the local file’s central file. Maybe it should be, but it isn’t.

Here is a straightforward first attempt. The user opens the local file. Then this macro runs to open and investigate the central file.

public void OpenCentralFile()
{
    Document doc = this.ActiveUIDocument.Document;

    // Get an instance of the ModelPath class describing the central file
    ModelPath modelPath = doc.GetWorksharingCentralModelPath();

    // Get the string of the path for the modelPath 
    string centralFilepath = ModelPathUtils.ConvertModelPathToUserVisiblePath(modelPath);

    Autodesk.Revit.ApplicationServices.Application app = this.Application;
    // Open the Central File
    Document centralDoc = app.OpenDocumentFile(centralFilepath);

    // We never get here because the local and central can't both be open at the same time in the same session
    TaskDialog.Show("status", "Central file opened");
}

error

Revit users may have seen this restriction in the past, and with the API the restriction is no different. The next post will look at how to workaround this limitation.

Macro to determine if a Family is In-Place (with output to Text and Excel Files)

A recent post at whatrevitwants mentions some tools that can be used to find in-place families. Here is a tiny macro to do the same

public void findInPlaceFamilies()
{
    string info = "";
    Document doc = this.ActiveUIDocument.Document;
    foreach (Family family in new FilteredElementCollector(doc).OfClass(typeof(Family)).Cast<Family>())
    {
        if (family.IsInPlace)
            info += family.Name + "\n";
    }
    TaskDialog.Show("In Place Families", info);
}

This is also a good excuse to show how to do something other than show the string inside Revit in a TaskDialog. For example, we might want to write the data to a text file or to an Excel file.

Text File:

public void findInPlaceFamilies()
{
    Document doc = this.ActiveUIDocument.Document;

    // Use the @ before the filename to avoid errors related to the \ character
    // Alternative is to use \\ instead of \
    string filename = @"C:\Users\HP002\Documents\In Place Families.txt";

    // Create a StreamWriter object to write to a file
    using (System.IO.StreamWriter writer = new System.IO.StreamWriter(filename))
    {
        foreach (Family family in new FilteredElementCollector(doc).OfClass(typeof(Family)).Cast<Family>())
        {
            if (family.IsInPlace)
                writer.WriteLine(family.Name);
        }
    }    
    // open the text file
    System.Diagnostics.Process.Start(filename);
}

Excel File:

  1. Select “Project – Add Reference” from the SharpDevelop menu bar and add a reference to Microsoft.Office.Interop.Excel
    AddExcelReference
  2. Add these lines to the other “using” statements at the top of your file:
    using Microsoft.Office.Interop.Excel;
    using System.Reflection;
  3. Adding “Microsoft.Office.Interop.Excel” may result in errors like this if elsewhere in your macros you are using the Revit Parameter class:
    Ambiguous
    This happens because now you can no longer define variables as “Parameter” because both Autodesk.Revit.DB and Microsoft.Office.Interop.Excel contain definitions for “Parameter”. The compiler can’t guess which one you want to use.
    Go through these errors and replace “Parameter” with  “Autodesk.Revit.DB.Parameter” like this:

    Autodesk.Revit.DB.Parameter parameter = element.get_Parameter("Material");
    
public void findInPlaceFamilies()
{
    Document doc = this.ActiveUIDocument.Document;

    // set up Excel variables
    Application excelApp = new Application();
    Workbook workbook = excelApp.Workbooks.Add(Missing.Value);
    Worksheet worksheet = (Worksheet)workbook.ActiveSheet;

    string filename = @"C:\Users\HP002\Documents\In Place Families.xls";
    int rowCtr = 1;
    foreach (Family family in new FilteredElementCollector(doc).OfClass(typeof(Family)).Cast<Family>())
    {
        if (family.IsInPlace)
        {
            // write data to the next row in the spreadsheet
            worksheet.Cells[rowCtr,1] = family.Name;
            rowCtr++; // increment counter by one
        }
    }

    // Save the file
    workbook.SaveAs(filename,XlFileFormat.xlWorkbookNormal, 
        Missing.Value, Missing.Value, Missing.Value, Missing.Value, 
        XlSaveAsAccessMode.xlExclusive, Missing.Value, Missing.Value, Missing.Value, 
           Missing.Value, Missing.Value); 
    // Clean up the excel resources that were created
    workbook.Close();
    excelApp.Quit();

    System.Diagnostics.Process.Start(filename);
}

Don’t mind all that crazy “Missing.Value” stuff. The Excel API is strange but it works.