Thursday, February 10, 2011

Status Bar Text and WPF

Yesterday, Jeremy Tammik posted how to change the status bar text in Revit. Toward the end of his post, he mentions using GetCurrentProcess to find the HWND of the main Revit window.

Yay! says I: Now I can print out status text, without cluttering up my shiny WPF window.

Nay! says WPF: It doesn't work.

Imagine my frustration... I spent some time analysing this using SPY++ and found out, that WPF dialogs are not children of the main application window. If your IExternalCommand starts a WPF window, then you should note the HWND first and then show the dialog. You can keep the HWND in a class variable, since it won't change. I ended up with a utility static class StatusText:

public static class StatusText
{
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern int SetWindowText(IntPtr hWnd, string lpString);

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter,
                                      string lpszClass, string lpszWindow);

    private static IntPtr _hwndRevit = new IntPtr();

    /// <summary>
    /// Set the handle to the revit main window here before calling
    /// SetStatusText.
    /// (also, set it in the IExternalCommand.Execute thread, not
    /// in your WPF thread)
    /// </summary>
    public static void SetRevitWindowHandle(IntPtr hwnd)
    {
        _hwndRevit = hwnd;
    }

    /// <summary>
    /// Set the status text of the current revit instance.
    /// </summary>
    public static void SetStatusText(string text)
    {
        IntPtr statusBar = FindWindowEx(_hwndRevit, IntPtr.Zero,
                                        "msctls_statusbar32", "");

        if (statusBar != IntPtr.Zero)
        {
            SetWindowText(statusBar, text);
        }
    }

}

You can then just issue a

StatusText.SetStatusText("hello, world!");

whenever you need to display something cool. Bonus points for hooking this up to a TraceListener ;)

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.

Wednesday, October 6, 2010

Obfuscating an Autodesk Revit Plugin

Jeremy Tammik mentions obfuscation for Revit plugins in his post Obfuscation Tools. I would like to mention two free obfusctators I have used:

Both of these can be hooked up to your build script so that they obfuscate automatically. Please read their documentation on how to do this.

I liked Eazfuscator a lot and used it for all dependant assemblies of my project, but sadly, it chokes on references to the assembly RevitAPI. So for developing Autodesk Revit Plugins, it will not work.

Babel .NET can obfuscate assemblies that reference RevitAPI.dll. I tied it into my build process in Visual Studio 2010 as a Post-build event command line:

  1. select your project in the Solution Explorer

  2. right-click and choose Properties from the context menu

  3. in the tab Build Events, enter the following in the field Post-build event command line:

    if /I "$(ConfigurationName)" NEQ "Debug" "C:\Program Files\Babel\Babel.exe" "$(TargetPath)" --stringencrypt --output "$(TargetPath)"
    

This assumes, of course, that you have installed Babel .NET to C:\Program Files\Babel. Also note, that the build configuration "Debug" is excluded from obfuscation.

Thursday, August 19, 2010

Deleting a bunch of Elements By ElementId in RevitPythonShell

I know I promised examples for RevitPythonShell and never delivered. Well, here is an example I came across this morning:

I had to delete a bunch of elements from a document. I new the ids of the elements and was about to manually delete them using the "Select by ID" tool in Revit and deleting them one by one. I know this works, because I did the exact same thing yesterday on the exact same document, but forgot to save the changes. I'd just have to do all that tedious work again.

But then I thought: Wait a minute... aren't we supposed to be scripting this stuff?

So, here is a session in the shell part of RevitPythonShell

>>>ids = [136116, 136119, 136120, 136121, 136122, 136131, 136132, 136133, 136134, 136135, 136136, 136137, 136138, 136139, 136140, 136141, 136142, 136143, 136144, 136145, 136155, 136165, 136206, 136306, 136309, 136419, 136422]

>>>import clr

>>>clr.AddReference("RevitAPI")

>>>from Autodesk.Revit.DB import *

>>>transaction = Transaction(doc, "find all the mutants!")

>>>transaction.Start()

>>>for id in ids:
...    doc.Delete(doc.get_Element(ElementId(id)))
...
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000002D [136116]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000002E [136119]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000002F [136120]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000030 [136121]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000031 [136122]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000032 [136131]>, <Autodesk.Revit.DB.ElementId object at 0x0000000000000033 [170627]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000034 [136132]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000035 [136133]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000036 [136134]>, <Autodesk.Revit.DB.ElementId object at 0x0000000000000037 [170628]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000038 [136135]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000039 [136136]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000003A [136137]>, <Autodesk.Revit.DB.ElementId object at 0x000000000000003B [170629]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000003C [136138]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000003D [136139]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000003E [136140]>, <Autodesk.Revit.DB.ElementId object at 0x000000000000003F [170630]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000040 [136141]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000041 [136142]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000042 [136143]>, <Autodesk.Revit.DB.ElementId object at 0x0000000000000043 [170631]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000044 [136144]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000045 [136145]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000046 [136155]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000047 [136165]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000048 [136206]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x0000000000000049 [136306]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000004A [136309]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000004B [136419]>])
List[ElementId]([<Autodesk.Revit.DB.ElementId object at 0x000000000000004C [136422]>])
>>>transaction.Commit()

<Autodesk.Revit.DB.TransactionStatus object at 0x000000000000004D [Committed]>
>>>

Here a few notes that might illuminate some parts of working with RevitPythonShell:

  • access to the Revit API is added by the following lines
import clr
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
  • the document is provided via the __revit__ variable
doc = __revit__.ActiveUIDocument.Document
  • you need to start a transaction before changing the document
transaction = Transaction(doc, "call this whatever you like")
transaction.Start()
# do stuff
transaction.Commit()
  • to get an element given an integer, use this technique
element = doc.get_Element(ElementId(myinteger))
  • Document.Delete() returns a list of deleted elements on each call. That is what the clutter (List[ElementId]([Autodesk.Revit.DB.ElementID object at...) is all about in the session transcript above.