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.