Progress reporting plugin for Revit

hello and welcome to this blog post! today we’re going to explore some of the tools that are exposed to us through the Revit API (Application programming interface), in order to create a simple yet effective progress tracking addin that can help the team leaders and project managers to better supervise the overall design and documentation process. the main difference between using the API and Dynamo is that Dynamo has a set of predefined nodes that each perform a certain task once you run the dynamo graph. but as intuitive as that is, it also quite obviously creates a number of serious limitation for us as developers such as:

  • Dynamo cannot run in the background as the user is working.
  • A Dynamo graph is pretty much open to the public so anyone can read through it, thus it’s not an easy task to protect your code. (although ZeroTouch partially resolves this issue).
  • An optimized addin can run much faster than a dynamo graph (specially for more complex tasks across a larger project), not to mention that the user does not have to launch Dynamo at all.
  • you have the ability to work in a fully supported IDE with intellisense and other functionalities.
  • The ability to use either WinForms or WPF to design an easy to understand user interface.
  • Last but not least, it’s extremely easy to incorporate third party libraries and DLLs into our code.

so now that you’re familiar with the main benefits of developing a Revit addin, let us get started!


Before we start writing any code though, you need to prepare a suitable programming environment. that includes installing and setting up your IDE, as well as making sure that the code can execute once it has been written. If you’re not familiar with Visual Studio, C# and/or writing addins for Revit, I strongly recommend that you read and follow through this beginner’s tutorial on Autodesk Knowledge Network: My first Revit plugin, you’ll also need a solid understanding of C# and general programming concepts. this blog post is more or less an overview of the whole process and is not going to go through each and every detail.

The main entry point of our program is going to be a class with an IExternalApplication interface. Here we can tell Revit that We’re going to be creating an addin and that we want our addin to have it’s own panel inside of Revit. Then we’ll proceed with adding buttons to the predefined panel. Each button is then connected to a class that has an IExternalCommand interface. This is where the majority of our code lives, since it is considered a “valid API context” for us to make our API calls from. A “valid Revit API context” is a concept that you’ll be hearing about quite often if you’re into creating addins for Revit. What it means in simple terms is that: you cannot make API calls where ever you want in your code, rather Revit is very specific in how your addin can communicate with it. Usually the builtin methods of IExternalApplication and IExternalCommand are considered “valid API context”. now with that out of the way, let’s add our very own panel and buttons to Revit. For the sake of simplicity we’ll call our buttons ON and OFF. they will enable and disable the progress tracker respectively.

class App : IExternalApplication
    {       
        public Result OnStartup(UIControlledApplication a)
        {  
           //Creating the pannel  
           string tabName = "Automation";
           a.CreateRibbonTab(tabName);
           RibbonPanel panel = a.CreateRibbonPanel(tabName, "Custom Tools");
           PushButtonData SJO_BtnData = new PushButtonData("SJO_BtnData", "SJO", Assembly.GetExecutingAssembly().Location, "modlessForm1.Command")
           //Creating and adding the buttons
           //adding button for updater on
           PushButtonData UpdateOn = new PushButtonData("UpdateOn", "ON", Assembly.GetExecutingAssembly().Location, "modlessForm1.Command6")
            {
                ToolTip = "This turns the updater on",
            };
           PushButton UpdateOn_Btn = panel.AddItem(UpdateOn) as PushButton;
           //adding button for updater off
           PushButtonData UpdateOff = new PushButtonData("UpdateOff", "OFF", Assembly.GetExecutingAssembly().Location, "modlessForm1.Command7")
            {
                ToolTip = "This turns the updater off",
            };
           PushButton UpdateOff_Btn = panel.AddItem(UpdateOff) as PushButton;   
           return Result.Succeeded;
        }
    }
Our (ON and OFF) buttons have been successfully added to Revit.

The next step is to program each button. This is done through two IExternalCommand, one for each button. In the ON button command, we’ll have to trigger a windows form in order to collect relevant input data from the user. then we’ll proceed to subscribing to two very useful Revit events (OnDocumentChanged event and Idling event). The first event raises every time the user modifies the document in any way. The second event raises whenever Revit is ready to receive API calls, hence the name Idling event. we’ll be using both of these events in conjunction to run our command every time the user performs any changes to the document, but still we want to make sure that the code runs only when Revit is “Ready” to to receive calls. In other words when Revit is in Idling mode. This allows for our code to run alongside the normal tasks of Revit without any conflicts.

First we’ll be using FilteredElementCollector to collect all the views that exist in the project. Then we’ll store their names in variables so that we can populate our UI later on. Finally, we’ll call our form to show up.

UIApplication UiApp = commandData.Application;
            Application App = UiApp.Application;
            UIDocument UiDoc = UiApp.ActiveUIDocument;
            Document doc = UiDoc.Document;

            //assigning to global variable for the document
            GlobalVariables6.m_document = doc;
            GlobalVariables6.m_path = string.Empty;

            //select view
            FilteredElementCollector collector = new FilteredElementCollector(doc);
            List<Element> views = collector.OfCategory(BuiltInCategory.OST_Views).WhereElementIsNotElementType().ToList();

            //string viewName = "Progress";

            List<string> viewnames = new List<string>();
            foreach (Element item in views)
            {
                viewnames.Add(item.Name);
            }
            GlobalVariables6.m_viewNames = viewnames;

            //show the modal form
            using (Form3 viewForm = new Form3(this))
            {
                viewForm.ShowDialog();
            }

            foreach (Element item in views)
            {
                if (item.Name == GlobalVariables6.m_viewName)
                {
                    view = item;
                }
            }

            //subscribe to event if the conditions are met
            if (GlobalVariables6.m_run == true)
            {
                App.DocumentChanged += OnDocumentChanged;
                UiApp.Idling += OnIdlling;
            }

            return Result.Succeeded;

Since the form is modal, the code will stop executing right after the form opens and awaits user input. The form looks something like this:

Designing the user interface of our addin

Right after the user inputs the required information and presses OK, we’ll subscribe to the above mentioned events. keep in mind that subscribing to events must be handled with care. You should always unsubscribe from that event once you’re done using it. this is the rest of the command that will get executed post user input which includes both of the event handlers:

 public static void OnIdlling(object sender, IdlingEventArgs e)
        {
            if (GlobalVariables6.m_docChanged == true)
            {
                string path = GlobalVariables6.m_path + "/frame-" + frameNumber.ToString() + ".jpg";
                var opt = new ImageExportOptions
                {
                    Zoom = GlobalVariables6.m_zoom,
                    ZoomType = ZoomFitType.Zoom,
                    FilePath = path,
                    FitDirection = FitDirectionType.Horizontal,
                    HLRandWFViewsFileType = ImageFileType.JPEGLossless,
                    ImageResolution = ImageResolution.DPI_600,
                    ExportRange = ExportRange.SetOfViews,
                };
                IList<ElementId> viewId = new List<ElementId>();
                viewId.Add(view.Id);
                opt.SetViewsAndSheets(viewId);
                GlobalVariables6.m_document.ExportImage(opt);            
                frameNumber++;
                GlobalVariables6.m_docChanged = false;
            }
        }

        static int frameNumber = 1;

        public static void OnDocumentChanged(object sender, DocumentChangedEventArgs e)
        {
            GlobalVariables6.m_docChanged = true;
        }

The heart of our code is the OnIdling event handler. This is where all of the magic happens. a set of rules have been describe in this method that are responsible for the image import process. the process looks something like this:

  1. OnDocumentChanged event gets triggered by the user, which will in return assign a true value to the m_docChanged variable (this is a public static variable within a class called GlobalVariables6).
  2. Once the user is done modifying the document, Revit raises the Idling event which signals to our program that it’s safe to run.
  3. Our program checks to see whether or not the user has indeed made any changes using a conditional statement that checks the value of the m_docChanged variable.
  4. If it’s true, the program collects user inputs and uses them accordingly (save directory, view name and zoom level)
  5. We use document.ExportImage to export the images based on the predefined rules and condition.
  6. finally we increment the frameNumber integer that is used to name the frames.

Turning this off is very simple. We simply unsubscribe from the event and we’re done:

class Command7 : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {           
            commandData.Application.Application.DocumentChanged -= Command6.OnDocumentChanged;
            commandData.Application.Idling -= Command6.OnIdlling;
            GlobalVariables6.m_run = false;
            return Result.Succeeded;
        }
    }

The plugin is now complete!

Every time the user makes any changes to the model, this plugin exports a preview of the image to the designated directory.

Here is the result of our work: