Revit Exceptions are System Exceptions but the .NET run-time prefers the System namespace over Revit namespace

What’s wrong with this code? Why doesn’t it catch the exception thrown by SelectNewPrintDriver if the PDF995 is not installed?

try
{
pm.SelectNewPrintDriver(“PDF995”);
}
catch (InvalidOperationException)
{
TaskDialog.Show(“Error”, “Cannot find printer PDF995”);
return;
}

Because SelectNewPrintDriver throws Autodesk.Revit.Exceptions.InvalidOperationException, not System.InvalidOperationException. So I’d suggest that you include:

using Autodesk.Revit.Exceptions;

in your files so that the compiler will give this error and remind you to resolve the ambiguity about which exception you are trying to catch.
‘InvalidOperationException’ is an ambiguous reference between ‘System.InvalidOperationException’ and ‘Autodesk.Revit.Exceptions.InvalidOperationException’

Advertisements

Dynamic Model Update, Exceptions, and Transactions (hint – look in the journal file)

In the last post I casually mentioned that during Dynamic Model Update we could delete the just-created elements. My first attempt was to add a transaction and Document.Delete(). That doesn’t work, and it also brings up a useful tip.

The error dialog shown on the right is Revit’s way of saying that your updater threw an exception, though it doesn’t give any technical details about the nature of the execption. “But what was the exception?” you will rightfully wonder! The answer is in the journal file, where in this case it explains:

' 2:< Exception caught from managed method RevitAPI::Autodesk.Revit.DB.TransactionStatus Start() <Autodesk.Revit.Exceptions.InvalidOperationException> <Starting a new transaction is not permitted. It could be because another transaction already started and has not been completed yet, or the document is in a state in which it cannot start a new transaction (e.g. during failure handling or a read-only mode, which could be either permanent or temporary).>

errorpublic void Execute(UpdaterData data)
{
    Document doc = data.GetDocument();
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;
    foreach (ElementId addedElemId in data.GetAddedElementIds())
    {
        ImportInstance ii = doc.GetElement(addedElemId) as ImportInstance;
        if (ii.IsLinked == false)
            TaskDialog.Show("Hey!", app.Username + " - Maybe should should have linked that CAD instead of importing it.");

        // THIS DOESN'T WORK!
        Transaction t = new Transaction(doc,"delete that");
        t.Start();
        doc.Delete(ii);
        t.Commit();
    }
}

Thanks to that info in the journal file, we learn that we need to get rid of the transaction code. Doing so gives us this, which works fine to delete the import instance that was just created.

public void Execute(UpdaterData data)
{
    Document doc = data.GetDocument();
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;
    foreach (ElementId addedElemId in data.GetAddedElementIds())
    {
        ImportInstance ii = doc.GetElement(addedElemId) as ImportInstance;
        if (ii.IsLinked == false)
            TaskDialog.Show("Hey!", app.Username + " - Maybe should should have linked that CAD instead of importing it.");
        doc.Delete(ii);
    }
}

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)

Error Handling (checking for null) and the GetSheetParams macro

In some previous posts I have used the following macro which will extract the value of the CADFile parameter from every sheet in your project.

public void GetSheetParams()
{
    Document doc = this.ActiveUIDocument.Document;
    string sheetInfo = "";
    foreach (Element e in new FilteredElementCollector(doc).OfClass(typeof(ViewSheet)))
    {
        Parameter parameter = e.get_Parameter("CADFile");
        string cadFileData = parameter.AsString();
        sheetInfo += cadFileData + "\n";
    }
    TaskDialog.Show("CADFile", sheetInfo);
}

There are a couple ways this could go astray:

1) Your project has no sheets, so when you run the macro you get an empty dialog box.

emptydialog

This can be be handled as follows (new code in bold):

public void GetSheetParams()
{
    Document doc = this.ActiveUIDocument.Document;
    string sheetInfo = "";
    ICollection<Element> sheets = new FilteredElementCollector(doc).OfClass(typeof(ViewSheet)).ToElements();
    if (sheets.Count > 0)
    {
        foreach (Element e in sheets)
        {
            Parameter parameter = e.get_Parameter("CADFile");
            string cadFileData = parameter.AsString();
            sheetInfo += cadFileData + "\n";
        }
    }
    else
    {
        sheetInfo = "There are no sheets in this project";
    }
    TaskDialog.Show("CADFile", sheetInfo);
}

2) Another problem occurs if there is a sheet but that sheet does not have the “CADFile” parameter.

exception

This happens because

e.get_Parameter("CADFile")

returns null (i.e. nothing) when the parameter “CADFile” does not exist. Then the exception is thrown on the next line

parameter.AsString()

Because you can’t call AsString() (or much of anything else) on a null variable.

This can be handled with:

public void GetSheetParams()
{
    Document doc = this.ActiveUIDocument.Document;
    string sheetInfo = "";
    ICollection<Element> sheets = new FilteredElementCollector(doc).OfClass(typeof(ViewSheet)).ToElements();
    if (sheets.Count > 0)
    {
        foreach (Element e in sheets)
        {
            string parameterName = "CADFile";
            string cadFileData = "";
            Parameter parameter = e.get_Parameter(parameterName);
            if (parameter != null)
                cadFileData = parameter.AsString();
            else
                cadFileData = "Sheet '" + e.Name + "' has no parameter named '" + parameterName + "'";
            sheetInfo += cadFileData + "\n";
        }
    }
    else
    {
        sheetInfo = "There are no sheets in this project";
    }
    TaskDialog.Show("CADFile", sheetInfo);
}

Which results in the much nicer error dialog:
noparameter

The “if” statement that checks if parameter is null is known as a “null check”. Many many software bugs are caused by programmers who forget to check for null values and programs that assume that a variable will not be null. There are different ways to handle these situations, and different people have different opinions about what is most robust, readable, etc..

Catching Exceptions

As Arnost mentioned in a comment to an earlier post, the methods Selection.PickObject and Select.PickObjects throw exceptions when the user cancels the pick operation. This most commonly happens when the user presses the ESC key. Speaking of the ESC, did you read this http://www.nytimes.com/2012/10/07/magazine/who-made-that-escape-key.html?

If you run this macro and push ESC instead of making a selection, Revit will throw the exception, your code will not catch the exception, and you will see this:

Reference r = uidoc.Selection.PickObject(ObjectType.Element);
TaskDialog.Show("Thanks!", "You selected " + doc.GetElement(r).Name);

exception

Much better is for your code to handle the error by using a try/catch block. For example, this code displays a TaskDialog when the exception is thrown and then caught.

try
{
    Reference r = uidoc.Selection.PickObject(ObjectType.Element);
    TaskDialog.Show("Thanks!", "You selected " + doc.GetElement(r).Name);
}
catch (Autodesk.Revit.Exceptions.OperationCanceledException exception)
{
    TaskDialog.Show("Oh No!", "You did not make a selection so PickObject threw an exception: " + exception.Message);
}

catch