Friday, November 26, 2010

A Pattern For Asynchronously Updating Revit Documents

With Revit 2011 we got a new event: UIApplication.Idling. This post is about how to use the Idling event to update a Revit document outside the scope of an IExternalCommand.

Imagine a dialog, opened by an IExternalCommand. Let us leave the dialog open and return Result.Succeeded. Let us also set the dialog to stay on top always. A bit like a tool window with some funky gadgets and commands. We want those commands to alter something in the Revit document and then update its gadgets to display stuff.

If you just stash away the ExternalCommandData and try to access the Revit application using that, you will crash Revit sooner or later. I forget the exact message, but something along the lines of reading/writing memory you shouldn't be. But you were warned by the docs, so, stop whining: Outside of the IExternalCommand.Execute method, the command data becomes stale and should not be used anymore.

Create a static Queue of Action<UIApplication>. You can store this in your IExternalApplication implementation and initialize it to an empty list on startup. This queue has to be static as you want it to persist while the class is loaded in memory (the class will be loaded by Revit on startup).

Next, hook up to the Idling event in your IExternalApplication.OnStartup method:

private static Queue<Action<UIApplication>> _tasks;
public Result OnStartup(UIControlledApplication revit)
{
    _tasks = new Queue<Action<UIApplication>>()
    revit.Idling += OnIdling;
}

Great. Now for the implementation of OnIdling:

private void OnIdling(object sender, IdlingEventArgs e)
{
    var app = (UIApplication)sender;
    lock (_tasks)
    {
        if (_tasks.Count > 0)
        {
            Action<UIApplication> task = _tasks.Dequeue();

            // execute the task!
            task(app);
        }
    }
}

This is one half of the equation. Next, we have to get the tasks into queue. Also, note that I'm locking the queue before reading it, since the dialog might access it at the same time as we are reading and I just want to be safe here.

To add a tasks to the queue, we just need a method in the IExternalApplication that will accept it:

public static void EnqueueTask(Action<UIDocument> task)
{
    lock (_tasks)
    {
        _tasks.Enqueue(task);
    }
}

This can then be called from your dialogs code, e.g. when a button is pressed. Assuming your IExternalApplication is called FooPlugin, an invocation could look something like this:

public void YouPushedMyButton(object sender, EventArgs args)
{
     FooPlugin.Enqueue((app) => {
         var doc = app.ActiveUIDocument.Document;
         doc.ProjectInformation.ClientName = "Boss Murphy";
     });
}

If you are not familiar with the (app) => { /* statements */ } this is a statement lambda. You might want to read the chapter Lambda Expressions from the C# Programming Guide on msdn.

A nice property of these lambda expressions is, that they create a closure around the variables that were in scope at the time of creation. Instead of "Boss Murphy" I could also have assigned some instance value from the dialog or a local variable from YouPushedMyButton.

The next time Revit fires an Idling Event, your tasks will be executed!

Oh, one thing: You will have to make sure you open a transaction. I haven't done this in the example, to keep things short and simple, but it's going to go boom on you! There are two possible places for this: Either in the lambda statement directly, or, if you know for sure that you are always going to need a transaction (all your tasks will be writing to the document), you could add it in the OnIdling event handler, wrapping the task(app) call.

Friday, November 12, 2010

Listing Level Elevations with RevitPythonShell

I recently stumbled apon the Revit APP blog and spotted a simple code listing for getting all level elevations. This should be a no-brainer using the RevitPythonShell (RPS), so I fired up a shell and started typing. This is a session transcript in the shell:

>>>import clr

>>>clr.AddReference('RevitAPI')

>>>import Autodesk.Revit.DB

>>>collector = Autodesk.Revit.DB.FilteredElementCollector(__revit__.ActiveUIDocument.Document)

>>>elems = collector.WhereElementIsNotElementType().OfClass(Autodesk.Revit.DB.Level).ToElements()

>>>for lv in elems:
...    print '%s: %.2f' % (lv.Name, lv.Elevation)
...
Level 1: 0.00
Level 2: 9.84
>>>

I am getting a bit fed up of typing import clr and referencing RevitAPI whenever I use RPS so my next step will be to try and add that by default. I'll keep you posted about that.

Wednesday, November 10, 2010

Unit Testing Revit Plugins

Yesterday, Jeremy Tammik brought an interesting thread on augi (Unit testing with Revit API) to my attention, suggesting that I might like to add something on the RevitPythonShell (RPS).

I couldn't register as a member on augi (some mail thing I don't want to digg into), so I thought I'd add my thoughts here.

First a disclaimer: I'm not test infected. I don't floss my teeth every day either - unit testing is just one of those things you should be doing, but nothing really bad happens if you don't. Especially if you use tooth picks etc.

Unit tests (as I understand the term) cannot be dependent on an environment (in this case: Revit). So running a test as a Revit plugin with a Revit document is not a unit test. It is an integration test. I do plenty of these. Here is how:

Using the RPS, I create a "driver" script that loads the assembly to be tested (see the loadplugin module) and executes its public methods, much the same way as the IExternalCommand implementation would. Note here, that I tend to keep my IExternalCommand implementations "dumb", refactoring the logic to other parts of the assembly, so that I don't have to test the IExternalCommand itself (it should just be evident that it works, e.g. instantiate plugin object, call public method on it passing some command data, return).

The "driver" script can be registered as an RPS command and will then show up in the Revit Ribbon. You can even assign a keyboard shortcut to it using the standard Revit procedure for shortcuts (you will have to restart Revit after registering the RPS command first, as it only then gets promoted to an IExternalCommand). Run it often (after every compile)!

I generally don't register plugins that are under development with Revit, so that Revit doesn't lock the assembly on startup - this way, I can keep the source open in Visual Studio and build to my hearts content. The "driver" script will just pick up the newest build and test it.

Anything written to "Debug.WriteLine" will be output in the RPS window. This is handy for testing stuff!

Once you are at the "driver" script level, you can employ pythons unit testing libraries: unittest and also use the doc tests. This also keeps your tests separate from your production code, especially since you are using another language to write the tests!

If you really want to do unit testing, you will have to go the full monty - this is the reason for unit testing anyway:

  • Split up your code into Revit dependent functionality and modules.
  • Make sure your plugin abstracts from the Revit stuff into a logical model that you can then mock and write tests to.

  • Regard the Revit API as an I/O interface or a database connection that is not part of unit testing.

Tuesday, November 9, 2010

Revit Room Boundaries

This came up today at work: For a given room, what are the room bounding objects?

Questions like these can easily be solved with RevitPythonShell. So I fired up an RPS shell and came up with the following script:

import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB.Architecture import Room
from Autodesk.Revit.DB import Wall

sel = __revit__.ActiveUIDocument.Selection.Elements
rooms = [e for e in sel if type(e) is Room]
for room in rooms:
    for bsa in room.Boundary:     # bsa = BoundarySegmentArray
        for bs in bsa:            # bs = BoundarySegment
            print bs.Element

This gets you quite close. I added a featured script to the RPS site that can be used as an RPS command and prints the element id for boundary segments that are model elements.