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.

#RTCEUR Wish 5 – Who Created Those Warnings?

To help BIM Managers train or punish their Revit users, I was asked at RTC in Delft if we can use the API to figure out which users are responsible for each warning in your Revit file.

Here’s how that can be done by writing all warnings to a log file and then comparing warnings in the current file with those in the log.

public void RegisterFailureReporter()
{
    Application app = this.Application;
    Document doc = this.ActiveUIDocument.Document;
    if (doc.PathName == "")
    {
        TaskDialog.Show("Error","Please save the file and then repeat this command.");
        return;
    }
    app.FailuresProcessing += FailureReporter;
}

private void FailureReporter(object sender, Autodesk.Revit.DB.Events.FailuresProcessingEventArgs args)
{
    FailuresAccessor fa = args.GetFailuresAccessor();
    Document doc = fa.GetDocument();

    using (StreamWriter sw = new StreamWriter(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Path.GetFileName(doc.PathName) + " Warning Creation Log.csv"), true))
    {
        foreach (FailureMessageAccessor fma in fa.GetFailureMessages(FailureSeverity.Warning))
        {
            sw.Write(DateTime.Now + "," + fa.GetDocument().Application.Username + ",");
            sw.Write(fma.GetDescriptionText().Replace(Environment.NewLine,"") + "," );
            foreach (ElementId id in fma.GetFailingElementIds())
            {
                // use UniqueId instead of ElementId because the UniqueId is stable across Save To Central while the ElementId property may change.  
                sw.Write(doc.GetElement(id).UniqueId + ",");
            }
            sw.Write(Environment.NewLine);
        }
    }
}

public void CheckCurrentWarningsInLog()
{
    Document doc = this.ActiveUIDocument.Document;
    if (doc.PathName == "")
    {
        TaskDialog.Show("Error","Please save the file and then repeat this command.");
        return;
    }    
    using (StreamReader warningsHTMLReader = new StreamReader(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Path.GetFileNameWithoutExtension(doc.PathName) + "_Error Report.html")))
    {
        string line = "";
        while ((line = warningsHTMLReader.ReadLine()) != null)
        {
            if (!line.Contains(": id "))
                continue;

            string[] row = System.Text.RegularExpressions.Regex.Split(line, "id ");

            string id1 = row[1].Split(' ')[0];
            string uniqueId1 = doc.GetElement(new ElementId(int.Parse(id1))).UniqueId;
            string uniqueId2 = null;
            if (row.Count() == 3) // there are two element ids in this row
            {
                string id2 = row[2].Split(' ')[0];
                uniqueId2 = doc.GetElement(new ElementId(int.Parse(id2))).UniqueId;
            }

            using (StreamReader warningLogReader = new StreamReader(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Path.GetFileName(doc.PathName) + " Warning Creation Log.csv")))
            {
                string lineLog = "";
                while ((lineLog = warningLogReader.ReadLine()) != null)
                {
                    if (lineLog.Contains(uniqueId1) || (uniqueId2 != null && lineLog.Contains(uniqueId1) && lineLog.Contains(uniqueId2)))
                    {
                        using (StreamWriter sw = new StreamWriter(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Path.GetFileName(doc.PathName) + " Current Warnings With User Info.csv"), true))
                        {
                                   sw.WriteLine(lineLog);
                        }
                        break;
                    }
                }
            }
        }
    }
}

Automatically Synchronize With Central every hour

I don’t know why Erik wants to call this FaceMelter, but here is an example of how to use the Idling event and 2014’s new SynchronizeWithCentral method to automatically Synchronize With Central at a desired interval.

For testing purposes I set it to auto-synch every 3 minutes. You would probably want an interval a bit longer 🙂

autosynch

public static class myCommand
{
    // store the time when the last save occurred
    static DateTime lastSaveTime;

    public static void idleUpdate(object sender, IdlingEventArgs e)
    {
        // set an initial value for the last saved time
        if (lastSaveTime == DateTime.MinValue)
            lastSaveTime = DateTime.Now;

        // check the current time
        DateTime now = DateTime.Now;

        TimeSpan elapsedTime = now.Subtract(lastSaveTime);
        double minutes = elapsedTime.Minutes;

        UIApplication uiApp = sender as UIApplication;
        // write a comment to the journal file for diagnostic purposes
        uiApp.Application.WriteJournalComment("Idle check. Elapsed time = " + minutes,true);

        // don't do anything if less than 3 minutes since last auto-save
        if (minutes < 3)
            return;

        Document doc = uiApp.ActiveUIDocument.Document;
        if (!doc.IsWorkshared)
            return;

        TransactWithCentralOptions transact = new TransactWithCentralOptions();
        SynchronizeWithCentralOptions synch = new SynchronizeWithCentralOptions();
        synch.Comment = "Autosaved by the API at " + DateTime.Now;
        RelinquishOptions relinquishOptions = new RelinquishOptions(true);
        relinquishOptions.CheckedOutElements = true;
        synch.SetRelinquishOptions(relinquishOptions);

        uiApp.Application.WriteJournalComment("AutoSave To Central", true);
        doc.SynchronizeWithCentral(transact, synch);

        // update the last saved time
        lastSaveTime = DateTime.Now;
    }
}

class rtcApplication : IExternalApplication
{
    public static FailureDefinitionId failureDefinitionId = new FailureDefinitionId(new Guid("E7BC1F65-781D-48E8-AF37-1136B62913F5"));
    public Autodesk.Revit.UI.Result OnStartup(UIControlledApplication application)
    {
        // register the idling event when Revit starts
        application.Idling += new EventHandler<IdlingEventArgs>(myCommand.idleUpdate);
        return Result.Succeeded;
    }
    public Result OnShutdown(UIControlledApplication application)
    { return Result.Succeeded; }
}

How to Automatically Dismiss a Revit dialog

For my friend who asked:

Is it possible to disable the pop-up, when you edit a floor: -Would you like walls that go up to his floor’s level to attach to its bottom?- I never want to do this, it’s annoying

Here is how to automatically dismiss that or any other Message Box or Task Dialog

private void Module_Startup(object sender, EventArgs e)
{
    UIApplication uiapp = new UIApplication(this.Application);
    // Subscribe to the DialogBoxShowing event to be notified when Revit is about to show a dialog box or a message box. 
    uiapp.DialogBoxShowing += new EventHandler(dismissFloorQuestion);
}

private void dismissFloorQuestion(object o, DialogBoxShowingEventArgs e)
{
    // DialogBoxShowingEventArgs has two subclasses - TaskDialogShowingEventArgs & MessageBoxShowingEventArgs
    // In this case we are interested in this event if it is TaskDialog being shown. 
    TaskDialogShowingEventArgs t = e as TaskDialogShowingEventArgs;
    if (t != null && t.Message == "The floor/roof overlaps the highlighted wall(s). Would you like to join geometry and cut the overlapping volume out of the wall(s)?")
    {
        // Call OverrideResult to cause the dialog to be dismissed with the specified return value
        // (int) is used to convert the enum TaskDialogResult.No to its integer value which is the data type required by OverrideResult
        e.OverrideResult((int)TaskDialogResult.No);
    }
}

Remember to add this line to the top of the file to avoid compilation errors:
using Autodesk.Revit.UI.Events;

Capture Comments When Saving to Central

Steve at Revit OpEd noted recently that occasionally he encounters a bigger problem (room warnings during Save To Central) and a smaller problem (comments entered in the Synchronize With Central dialog are lost):

If you are like me you enter a comment each time (using SwC) for tracking and troubleshooting purposes. It is a pain to get a message telling you your SwC didn’t work. It’s worse that what you typed is lost. I finally got slightly smarter. I try to remember to copy the comment text to the clipboard before clicking OK. This way I can just paste it back in if/or when the error pops up.

We can’t use the API to fix the bigger problem (though there may be some API solution to help mitigate it). But we can definitely use the API to make sure that the comments aren’t lost.

If we subscribe to the DocumentSynchronizingWithCentralEvent, we can use the DocumentSynchronizingWithCentralEventArgs.Comments property to get the comments entered in the Synchronize With Central dialog box. Then we can write them to a text file, database, or other location where they will be safe, regardless of Revit’s success or failure to complete the save.

See my previous post on Events for an introduction to the functionality.

public void SynchWithCentralEventRegister()
{
    this.Application.DocumentSynchronizingWithCentral += new EventHandler<DocumentSynchronizingWithCentralEventArgs>(mySynchronizingWithCentralEvent);
}        

private void mySynchronizingWithCentralEvent(object sender, DocumentSynchronizingWithCentralEventArgs args)
{
    string comments = args.Comments;
    
        // Use the @ before the filename to avoid errors related to the \ character
    // Alternative is to use \\ instead of \
    string filename = @"C:\Users\HP002\Documents\SynchronizingWithCentral Comments.txt";

    // Create a StreamWriter object to write to a file
    using (System.IO.StreamWriter writer = new System.IO.StreamWriter(filename))
    {
        writer.WriteLine(DateTime.Now + " - " + comments);
    }    
    // open the text file
    System.Diagnostics.Process.Start(filename);
}

Making Lines Not Print Improved (View Viz instead of Line Color)

David makes a great observation regarding my post at https://boostyourbim.wordpress.com/2012/12/12/making-lines-not-print-with-events/

‘That all works great if the background is in fact white. What happens if you have the same situation you were showing above with a floor tile pattern? You’d see the white line.”

The code I wrote in that post was I hope good for a relatively gentle introduction to the concept of Revit API Events. But this is not what anyone wants to see when they print.

whiteonpattern

I used the approach of changing the Object Styles – Line Color to white because it offered a straightforward way to globally change the appearance of the lines in all views. A better approach that requires greater API complexity could be to turn off the visibility of the Do Not Print sub-category in each view that is being printed.

vis

In the previous post’s code, myPrintingEvent and myPrintedEvent both had input arguments DocumentPrintingEventArgs and DocumentPrintedEventArgs that went unused. Every event has arguments like these that are used to pass data about what is happening in this event. For the printing events, this information includes the list of views to be printed and the list of views that were printed. This will be needed for the new approach of changing  category visibility for each view that is printed.

The structure of the code is the same as the previous post, but the CategoryLineColor method has been replaced with categoryVisInViews which uses View.SetVisibility(category, boolean).

public void PrintingEventRegister()
{
    // myPrintingEvent will run just BEFORE Revit prints
    this.Application.DocumentPrinting += new EventHandler<DocumentPrintingEventArgs>(myPrintingEvent);

    // myPrintedEvent will run just AFTER Revit prints
    this.Application.DocumentPrinted += new EventHandler<DocumentPrintedEventArgs>(myPrintedEvent);
}

private void myPrintingEvent(object sender, DocumentPrintingEventArgs args)
{
    // turn OFF visibility of the ""Generic Annotations - Do Not Print" subcategory in the views being printed
    categoryVisInViews(args.Document, args.GetViewElementIds(), false);
}

private void myPrintedEvent(object sender, DocumentPrintedEventArgs args)
{
    // list of views that both printed and failed to print
    List<ElementId> views = new List<ElementId>();
    views.AddRange(args.GetFailedViewElementIds());
    views.AddRange(args.GetPrintedViewElementIds());

    // turn ON visibility of the ""Generic Annotations - Do Not Print" subcategory in views that were printed & that failed to print
    categoryVisInViews(args.Document, views, true);
}

// This function takes as inputs:
// 1) The document
// 2) The list of views in which the "Do Not Print" subcategory visibility should be changed
// 3) A boolean (true or false) indicating the desired state of the category visibility 
private void categoryVisInViews(Document doc, IList<ElementId> viewList, bool visible)
{
    foreach (ElementId id in viewList)
    {
        View view = doc.GetElement(id) as View;
        // Get all categories in the document
        Categories categories = doc.Settings.Categories;

        // The "Generic Annotations" category
        Category genericAnnotations = categories.get_Item("Generic Annotations");

        // The ""Do Not Print" subcategory of the "Generic Annotations" category
        Category doNotPrint = genericAnnotations.SubCategories.get_Item("Do Not Print");

        // Create transaction with the view name & state in the transaction name
        using (Transaction t = new Transaction(doc,"Set Visibility: " + view.Name + ": " + visible))
        {            
            t.Start();
            // set the visibility of the category
            view.setVisibility(doNotPrint, visible);
            t.Commit();
        }            
    }
}

Making Lines Not Print (with Events)

A recurring question that has come up at least twice at AUGI (here and here) and many other times over the years has been how to put things in your Revit model that will not print. There were good reasons in the early days of Revit that we had a strong commitment to WYSIWYP (What You See Is What You Print) and there are also good reasons why Revit users do not want to print everything that they see in Revit. These “hide” options in the Print dialog are good, but often you will want to go beyond this.

printhide

Two API functions to the rescue!

  1. The ability to modify line colors defined in Object Styles and Line Styles
  2. Events, which let you run API commands before and after various file actions such as open, close, save, print

In this example, a Revit model contains two annotation families where the lines are assigned to a subcategory named “Do Not Print”

family

For this application, use two events:

  1. DocumentPrinting – runs API code just before Revit prints
  2. DocumentPrinted – runs API code just after Revit prints

Before Revit prints, the myPrintingEvent will change this subcategory’s line color to white.
After Revit prints, the myPrintedEvent will change this subcategory’s line color to black.

objectstyle

To enable the magic, run the macro PrintingEventRegister. This associates a function with each of the two print events mentioned above.

public void PrintingEventRegister()
{
    // myPrintingEvent will run just BEFORE Revit prints
    this.Application.DocumentPrinting += new EventHandler<DocumentPrintingEventArgs>(myPrintingEvent);

    // myPrintedEvent will run just AFTER Revit prints
    this.Application.DocumentPrinted += new EventHandler<DocumentPrintedEventArgs>(myPrintedEvent);
}

// These macro is private, instead of public, because it will not be run from the Macros dialog
// It will only be available to other macros
private void myPrintingEvent(object sender, DocumentPrintingEventArgs args)
{
    // set color to white (255, 255, 255)
    CategoryLineColor(new Color(Byte.MaxValue,Byte.MaxValue,Byte.MaxValue));
}

private void myPrintedEvent(object sender, DocumentPrintedEventArgs args)
{
    // set color to black (0, 0, 0)
    CategoryLineColor(new Color(Byte.MinValue,Byte.MinValue,Byte.MinValue));
}

The results of printing are shown in the PDF on the right side of this screenshot. The lines in the families are “invisible” because they are white! After the printing is completed they are set back to be black.

print

And here is the code for the CategoryLineColor function that sets the line color.

private void CategoryLineColor(Color newColor)
{
    Document doc = this.ActiveUIDocument.Document;

    // Get all categories in the document
    Categories categories = doc.Settings.Categories;

    // The "Generic Annotations" category
    Category genericAnnotations = categories.get_Item("Generic Annotations");

    // The ""Do Not Print" subcategory of the "Generic Annotations" category
    Category doNotPrint = genericAnnotations.SubCategories.get_Item("Do Not Print");

    // Create transaction, with the value of the color (0 or 255) in the transaction name
    using (Transaction t = new Transaction(doc,"Do Not Print color = " + newColor.Blue))
    {            
        t.Start();
        // set the style's line color to the "newColor"
        doNotPrint.LineColor = newColor;                
        t.Commit();
    }            
}