20
Asynchronous Interactions and Managing Modeless UI with the Autodesk ® Revit ® API Arnošt Löbel Autodesk, Inc. CP5381 Since the inception of the Revit API, many programmers have tried working outside of the standard, synchronous workflows. In a typical scenario clients desire to free Revit from waiting while their external application continues working on other threads or awaiting user's input in a modeless window. Such asynchronous approach is admirable, but was not really possible before the introduction of events in Revit 2010 and especially the Idling event in 2011. Asynchronous communications with Revit API is now supported and can be done safely if all recommendations are followed. On the other hand, delving outside of known restrictions can lead to unexpected failures, which would frustrate both programmers and end users. This class focuses on exploring a set of programming techniques suitable for asynchro- nous communication with Revit. Sample applications cover work threads, modeless dialogs, and re- sponses to dynamic changes in Revit models. Learning Objectives At the end of this class, you will: Understand the rules of safe interactions with the Revit API in all standard ways. Learn the concept of controlling Revit asynchronously and the common pitfalls of such tasks. Know how to use the Idling event to deliver results from an outside work thread back to Revit. Be able to streamline communications between an external modeless dialogs and Revit. Become familiar with using events to react to model changes, both synchronously and asynchronously. About the Speaker Arnošt is a senior principal software engineer on the Revit development team. He joined Autodesk in 2006 as a new member of the growing API team and since his first day the API has been his primary focus and passion. His major contributions include the redesigned transaction framework, events, dynamic model updaters, and API safety. Arnošt has a master degree in structural engineering, but only a few years after college he switched professions to become a full-time software developer. He first started developing add-ons for AutoCAD ® in a small start-up, and contributed to various 2D graphics and CAD oriented projects since. Arnošt was born in the Czech Republic and relocated with his family to Miami, FL in 1998. He and his wife now live in Boston, which they really enjoy, though he has a hard time accepting the fact their daughters now live in two other corners of the U.S. Arnošt is an avid bicyclist, and skier, and runner, and badminton player, and Ping-Pong player, and a seasonal gym-rat.

Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Embed Size (px)

Citation preview

Page 1: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions and Managing Modeless UI with the Autodesk

® Revit

® API

Arnošt Löbel – Autodesk, Inc.

CP5381 Since the inception of the Revit API, many programmers have tried working outside of the

standard, synchronous workflows. In a typical scenario clients desire to free Revit from waiting while their external application continues working on other threads or awaiting user's input in a modeless window. Such asynchronous approach is admirable, but was not really possible before the introduction of events in Revit 2010 and especially the Idling event in 2011. Asynchronous communications with Revit API is now supported and can be done safely if all recommendations are followed. On the other hand, delving outside of known restrictions can lead to unexpected failures, which would frustrate both programmers and end users. This class focuses on exploring a set of programming techniques suitable for asynchro-nous communication with Revit. Sample applications cover work threads, modeless dialogs, and re-sponses to dynamic changes in Revit models.

Learning Objectives

At the end of this class, you will:

Understand the rules of safe interactions with the Revit API in all standard ways.

Learn the concept of controlling Revit asynchronously and the common pitfalls of such tasks.

Know how to use the Idling event to deliver results from an outside work thread back to Revit.

Be able to streamline communications between an external modeless dialogs and Revit.

Become familiar with using events to react to model changes, both synchronously and asynchronously.

About the Speaker

Arnošt is a senior principal software engineer on the Revit development team. He joined Autodesk in 2006 as a new member of the growing API team and since his first day the API has been his primary focus and passion. His major contributions include the redesigned transaction framework, events, dynamic model updaters, and API safety. Arnošt has a master degree in structural engineering, but only a few years after college he switched professions to become a full-time software developer. He first started developing add-ons for AutoCAD

® in a small start-up, and contributed to various 2D graphics and CAD

oriented projects since.

Arnošt was born in the Czech Republic and relocated with his family to Miami, FL in 1998. He and his wife now live in Boston, which they really enjoy, though he has a hard time accepting the fact their daughters now live in two other corners of the U.S. Arnošt is an avid bicyclist, and skier, and runner, and badminton player, and Ping-Pong player, and a seasonal gym-rat.

Page 2: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

2

Knowledge is freedom

The Revit API team wants to give API clients as much freedom as possible, but there are certain rules everyone should follow (and understand):

One should not call the API unless one is invoked by it!

a. Because Revit does not expect it

b. And you may interrupt something that should not be interrupted

c. In a better-case scenario, clients of your applications will experience random, sporadic failures

d. When Revit crashes due to the API call, the Revit team will not be able to say why it happened

One should not call the API from a different thread!

a. Memory management - some containers are not thread-safe; therefore they should not get de-allocated from a thread other than the one in which it got allocated

b. Revit is switching the thread context when going to and coming from the API

c. This may improve in the future, and most likely it will, but so far it is still the current state

One should call the API only, never the UI indirectly!

a. Revit does not expect it

b. UI may have different rules for what can or cannot be triggered

c. It may work sometimes, but not always

Describing the details of the above mentioned rules and explaining the reasons behind them is the subject of this document.

Things can go awry in asynchronous workflows

There’s a jungle out there

Monk theme song, by Randy Newman

If you paid attention, you'd be worried too. You better pay attention or this world we love so much might just kill you. I could be wrong now, but I don't think so! 'Cause there's a jungle out there.

It seems to me that in few environments would Mr. Newman’s words be more fitting than in programming: “If you do not pay attention to details you may regret it!” The API may seem to be easy to use (and that is certainly the API team’s primary goal), but there are rules and details about how to use it properly and safely. It may look like nothing really happens when rules are not followed to the point or if they are slightly bent, but that could be just a very thin appearance which could fool the less experienced of us.

In any single month the API team gets a number of requests for explaining somehow “odd” and “strange” behavior of the API. Sometimes it is a transaction that refused to start for no apparent reason, another time it was an API call that failed, rather surprisingly, for it previously worked just fine under “apparently” the same conditions (except for the exact time.) On a few occasions Revit would record errors and/or warnings into the journal. One or two report Revit crashing. Naturally, some of the problems lead us to finding hidden bugs in Revit. A bunch of the reported problems were of a different nature though – they were not caused by bugs; they were caused by not using the API “properly”. It is not certain how many improper ways of using the API exist, but we know about one that is definitely quite dangerous – it is the attempt to interact with Revit asynchronously. Let’s take a closer look at the problem.

Page 3: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

3

Where’s the danger?

The fact: Revit does not expect external applications calling the API from other than the main Revit thread. Although it is possible to access the API from other threads and sometimes such calls may even succeed, it is not a recommended and supported technique, and the outcomes of such calls can be virtually unpredictable and often quite bad.

To give an example of what we mean, the following is one of the unsupported workflows:

1. Revit calls an external command by invoking the command’s Execute method

2. The command collects data from Revit API, initiates a modeless dialog, and returns control back to Revit

3. The modeless dialog waits for other data (could be the user’s input), and then calls the API

The obvious reason for the above scenario is the desire not to slow down Revit while the external application is performing a task. Though this workflow is thoughtful and – in theory – correct, it is not allowed in Revit because Revit does not have a multi-thread-ready API. Revit does safeguard the API in various ways, but it only has firewalls at the main gateways, not at each and every API method. When an external application bypasses the gateways, a lot of things will not happen but should (or vice-versa).

Documents will not be protected properly – no Firewall in effect!

There is another section later in this document explaining the API firewall in details. For now let’s just say it protects Revit and its open documents against unsafe external procedures.

Transaction and Regeneration mode not set

These modes are only around the scope of an external command. Once the command ends, the modes are not in effect.

Revit will not know the running application's ID

Revit knows the Id of application that it has been invoked. Therefore when a call is made to the API during the invocation time Revit knows the call is valid and knows what application has invoked it. When the API is called completely outside of the standard invocation point, Revit may think it is some native function calling. Because there are some differences between how API calls are handled versus internal calls, the outcome may be different.

The API scope is out of sync

Related to the point above – not only Revit knows what application it has invoked recently, it also keeps a stack of invocations if calls are propagated to other applications. When this gets messed up, Revit may mistake one application for another. This will definitely affect features which require that only certain application can make a call. Dynamic updaters are one of such examples.

Why aren’t we more protected?

Most of the above would either cause exceptions that would not happen otherwise, or miss on exceptions that should happen. Both cases could be equally dangerous.

Main reasons for not allowing ad-hoc asynchronous calls:

Revit does not have a multi-thread-ready API

Although Revit often used multithreading internally to speed up certain processes (such as document loading, work-sharing, some parts of regeneration, etc.), it is still a single-threaded application to the outside world. It is caused in part by historical reasons (MFC based), but mostly by the fact that model handling and regeneration is simply too complex to be divided into predetermined and easily identified threads.

Page 4: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

4

Revit uses a different thread context in the API

Also for historical reason, Revit needs to switch thread contexts when going out to and returning from the API. When a call comes on a different thread and Revit needs to allocate or reallocate memory (or sometimes even reading it), having a wrong thread context naturally leads to problems.

API Firewall is not set around each and every API call

Like it has been already mentioned, Revit makes a certain amount of preparation before it calls an API client, and also performs certain amount of house-cleaning when returning from it. This is to guarantee the integrity of Revit as the running application and also to protect open documents from getting corrupted. It is not possible to maintain this level of protection around every API method. Revit has it only around predetermined API gateways, such as external commands and event invocations.

To sum it up:

Revit does not expect API calls from other than the main Revit thread!

Revit does not expect applications calling the API outside of predefined gateways!

Secured gateways

The following are the only execution times during which Revit expects and allows API calls from outside:

A. The OnStartup and OnShutdown methods

B. The Execute method of an external command

C. Execution of a VSTA macro

D. Handling an API event

E. Invoking methods of a callback interface (e.g. an updater)

Outside of these processes Revit does not know your application!

When Revit calls these methods in an external application it expects that whatever the method does, it will completely finish it before returning from the method back to Revit. Once the method returns back, the call is considered completed and the API is "closed" for external calls (though it is not technically closed, just practically, at least currently).

What could possibly happen

The outcome of not following the API rules ranges so widely that it is impossible to say for sure, but we could certainly at least try to categorize:

A. Nothing bad at all happens

The call or calls succeed and all is fine. This could happen quite often, actually, but do not let it fool you – something bad is going to happen eventually.

B. Calls “appear” all right and returning “successfully”

Although the execution finished successfully as far as crashing and errors count, the results are wrong, which may or may not be obvious. Certain parts of the model can be incorrect – not in the sense of geometry, perhaps, but logically. Values in connected databases may be wrong or inconsistent with the actual state of the model.

Page 5: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

5

C. Calls fail, exceptions are thrown

Believe it or not, this is not that bad a scenario, assuming it happens on the developer’s machine or when he or she can afford to interact with the affected client. The bad part of this is that it often is hard or impossible to diagnose the root of problem. You may be getting exceptions with confusing or misleading message, or you do not get a message at all. Finding the real cause of the failure can be very time consuming. Unfortunately, the burden is often put on the Revit R&D team.

D. Revit crashes, document(s) must be closed

Crashes of this nature could be quite frequent or they happen only sometimes. Just like with the exceptions, the timing and outcome is unpredictable, because it always depends on what the current situation in Revit is when the API call is made.

E. Revit model is corrupted by two independent processes

Obviously this is the worst case, particularly when the corrupted document is saved! We honestly do not know how often this happens and what percentage of corrupted documents is caused by misbehaving external applications. We estimate it is not high, but we are afraid it will get higher, as the Revit API is getting more and more popular and used.

Scary stories to think about

Let’s take a look at a few actual scenarios to get some ideas about what an inappropriate utilizing the API looks like. With some experience it should be quite simple to recognize the patterns.

When reading is not just reading

It is a well-known fact that reading data from a Revit model does not always yield expected result – most of you are aware that one should avoid reading from a model after the model was modified and not yet regenerated. That makes sense, we assume, because there are complex relations between parts of the model and data may be inconsistent unless the related parts get synchronized. But what if you do not change the model at all and yet you still get wrong data? How could that happen?

Consider the following workflow when an asynchronous call (AC) is invoked from a modeless dialog or a worker-thread that was left open by an external command:

1. AC calls to get one parameter of a wall.

2. Another application changes the wall on Idling.

3. AC calls to get another parameter of the wall.

4. An updater reacts to the change by the Idling handler.

5. AC calls to get another parameter of the wall.

Though the calls were probably next to each other in the client’s code, they were technically executed at different times and the data acquired by each of the methods reflect the state of the model at those different times.

Note: One may think the problem can be mitigated but subscribing to the DocumentChanged event and start reading the data from the start again if and when the document gets changed. While this may somehow improve the situation, it cannot avoid it. That is because the event is raised only when changes are actually committed to the document, not when they are made.

Page 6: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

6

Crash is just waiting to happen

No one should be surprised by a crash when making unsafe asynchronous invocations to the Revit API, but some cases raise many an eyebrow. Such is the following workflow:

1. User invokes an external command.

2. Command splits a worker thread and waits for it to finish. There is nothing wrong with it; Revit does not require external applications to be single-threaded.

3. The thread attempts to start a transaction. Can we see it coming yet?

4. Revit throws an unhandled exception, possibly crashes. Now, there we have it!

“But why?” you may ask. “What did I do wrong now? I am still in the external command, am I not, and I was told that as long as I do my calls from the external command, I should be all right; right?”

Well, yes, but we also insisted you make calls from the main thread and the main thread only. That means the very thread on which Revit invoked your external command. As we already mentioned, Revit needs to switch context between the API and native side, and it does not expect the thread to be different when returning back from the API.

It might have used to be less obvious in the early days of Revit when the API was young and mostly manually written with major implementation happening on the managed side. With a little of luck calls would not need to cross the managed-native barrier at all. But in the API as we have it now, most of the methods and properties have their native counterparts which contain the actual implementation, while the managed parts have dwarfed to just thin shells. Therefore virtually every API call does have to cross the barrier.

In the case above, when a new transaction asked for memory to be allocated, the thread context was wrong and memory allocation failed.

When modeless dialogs do more than just reading

All you might get when you read asynchronously is wrong data. Sure, it may not be good, but it is not the end of the world. Nothing’s crashing, documents are safe and even the probability you actually get wrong data may be not that high. That changes when you want to do more than reading from your asynchronous process, for example when controlling Revit from a modeless dialog. The workflow is again similar:

1. Revit calls an external command.

2. Command launches a modeless dialog and returns; so far so good.

3. The dialog awaits the user’s command, a click on the dialog’s control, perhaps.

4. On such a command, the dialog calls Revit API and performs the action, whatever that may be.

We have already discussed the range of possible outcomes resulting from this scenario and we know it ranges from virtually nothing to corrupted documents. The lesson-learned from this is clear:

Outcome of unexpected asynchronous calls to the Revit API is unpredictable!

Page 7: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

7

Asynchronous workflows by the book

Fortunately, not all is as bad as we have pictured it so far. Revit actually allows applications to interact asynchronously and safely at the same time. It all started with introducing events in the API, particularly the Idling event.

Idling to the rescue

The Idling is an event like no other. While all the other events in Revit are raised when something somewhere in Revit happens, Idling is raised when nothing is happening at all.

By its nature Idling is a UI-concept; that is why the event is not available in RevitAPI, but in RevitAPIUI instead. The sender object is of type UIApplication.

Idling event is raised when Revit is not doing anything and is ready to do something. By “doing nothing” we mean that no command is being executed, no editor is running, no transaction or transaction groups are open.

Note: The DocumentChanged event could be seen as a counter-part of the Idling event. While Idling informs you that Revit is doing nothing but you can, DocumentChanged signals Revit has just changed a document but you may not. Those two events can make a powerful tool when used together.

Not surprisingly Idling is raised a lot; therefore it should not be abused by time demanding processes. Please, be considerate:

o Spend as little time in the handler as possible.

o If you need to do more, collect data quickly, give it to another thread, and consume the results on another (or another, or another…) Idling event.

Just being subscribed to Idling can slow your computer down significantly. That will be quite apparent on computers with fewer cores where even a void Idling can consume as much as 50 % of your CPU time. Please keep in mind that even when the handler of the event does nothing, Revit still needs to convert arguments, go from native code to managed code, find out there is nothing to do out there, and then cross the managed-native barrier again. That is definitely no no-op. Therefore:

o Only subscribe to Idling when you need it.

o Always unsubscribe as soon as you do not need it.

API calls from a handler of Idling still must be made on the main thread!

The basic asynchronous pattern

The workflow pattern Revit engineers recommend their users to use is not new, is not complex, and is relatively easy to implement. Let’s take a look at it a step by step:

1. An application registers itself and its command(s).

2. When invoked, the command kicks off a modeless dialog (or a work-thread) and leaves it there waiting (or working on something, respectively).

3. If the kick-off succeeds, the command registers a handler for the Idling event.

4. The command then returns, Revit continues running.

5. Whenever possible and appropriate, Revit will raise the Idling event.

6. The application's event handler gets the call.

Page 8: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

8

1. Now, it all depends on how the application communicates with the dialog (or the thread):

a. It could be that the thread periodically feeds data back to the external application, so when the event is raised, the data is either there already or not. If it is not ready, the handler simply returns. If there is data to use, the handler uses it and makes any API calls as it needs, assuming it follows all standard rules of the API calls.

b. Or it could be that the application queries the work thread (or dialog) somehow at the time of the event using one of the many synchronization tools (events, mutexes, etc.). If the thread is ready and waiting, the application gets data from it and uses it as it sees fit.

8. When the dialog (work-thread) finishes, it signals that to its application, so when the next Idling event is raised, the application unregisters from the Idling event.

9. The application that owns the modeless dialog (or work-thread) should make sure that when OnShutdown is invoked and the dialog (or thread) is still running, to close it, so it does not “outlive” Revit.

Flowchart of the modeless-dialog basic pattern:

This workflow is perfectly safe as long as the changes made on each round are atomic.

Page 9: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

9

A little more sophisticated pattern

Sometimes life is just not that easy. Sometimes the outside thread has to do more than simple atomic changes. Not unusual are processes which need more than mere minutes to run, particularly in structural and environmental analysis. And not only the calculations are time-consuming, but the results are often coming in chunks and it would be great if we could keep delivering it safely to Revit and make the end user aware of the progress.

Then, naturally, the above workflow may fail, because between individual iterations when the work-thread is ready to deliver another chunk of data, the Revit model for which the data was calculated may long have been changed several times. To make sure calculated data are in sync with the model, more than the Idling event is needed – the application has to utilize the DocumentChanged event as well.

The workflow is very similar to the one above, except we have to squeeze in a few more steps:

3. After the application subscribes to Idling, it also subscribes to DocumentChanged event.

Then, on each document-changed event the handler checks if elements the ongoing process depends on have been changed and if so, it removes any obsoleted results from the model and either completely stops the process or restarts it.

Note: Sometimes Dynamic updaters are used in this scenario instead of the event, but be aware that updaters are invoked only when a transaction is committed, not when it is undone or redone.

DocumentChanged event under a microscope

We should probably take a closer look at it, for the event is so important and so often used (and sometimes misused),

The event’s main purpose is to provide a way for keeping data which are external to the document (such as UI controls) synchronized with the Revit model.

Not surprisingly, it is the most often used event in Revit (after maybe SelectionChanged, which is not public).

It runs before a transaction is wrapped up but after dynamic updaters finished their job and the model has been fully regenerated.

It may be important to know that the event is raised when transaction is committed but also when it is rolled back. This is particularly important for the UI, which may need to refresh differently.

DocumentChanged also runs when transaction(s) is (are) undone or redone. That is not limited to just the end-user clicking in the Undo menu. When a transaction group is rolled back and some already committed transactions go with it, this event is also raised.

User have access to three sets of changed elements:

o Added elements

o Modified elements

o Deleted elements

The sets are mutually exclusive – elements in one will not be found in the other two.

The three sets are empty if the transaction has been rolled back (instead of committed).

When more than one transaction is undone (or redone, or rolled back in a group) in one single operation, the sets contain elements merged from all the transactions.

Users are not permitted to make changes during DocumentChanged events!

Page 10: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

10

The fact that applications are not allowed to make changes during this event has been discussed a lot. The reason behind it is that in order to accept and make sense of changes coming in from various applications that may have been fighting (unknowingly, of course) over contradicting modifications, Revit would have to surround the event with rather sophisticated a framework. After that the event would be more like the framework of Dynamic Mode Updates. Therefore:

A. If you do not need to react to model changes by modifying it too, use the DocumentChanged event.

B. If you do need to make modifications depending on how the model changes, use dynamic updates.

How stuff works in the Revit API

Understanding how asynchronous workflows are supported in Revit is only one prerequisite of developing safe and well behaved external applications on top of Revit API. There are many common rules a good application should follow no matter how it interacts with Revit. In this section we will briefly describe the most important rules of the most common API components. We will not go into depths of each of the framework we mention, but we’ll try to point out things that are likely to be misunderstood or not so commonly known.

Regeneration modes

It used to be that external applications had to specify how they wanted regeneration to behave in API methods. They had to specify whether they wanted the model regenerated after each call to the API (the automatic mode) or if they preferred to call regeneration manually as needed (the manual mode). Actually, before those modes were available, the API always worked as in the Automatic regeneration mode.

Both those modes are now history – there is no regeneration mode to be declared. Since R2012, the API always works as if in the manual mode. Users can still declare the manual mode, but the declaration will be ignored. Users will get an error if they declare their applications with the Automatic mode.

There are few things to remember about using regeneration:

Programmers invoke regeneration as they need it. The method to call is Document.Regenerate.

Regeneration may be called only in a transaction; it does not make sense outside of it.

A changed model should be regenerated before it is read from. It is to prevent getting obsolete data from a model that has not yet propagated all changes through out to all related elements.

Regeneration is smart – it only causes regenerations of the things that have actually been changed. If you call regeneration twice one after the other, the second time it will be a virtual no-op.

Ending a transaction always triggers regeneration. There is no need to regenerate manually before committing a transaction. On the other hand, ending a sub-transaction does nothing to the model, thus if you need to read from the model after a sub-transaction was committed or rolled back, you will likely need to regenerate explicitly.

A manual regeneration alone is sometimes not enough to guarantee the integrity of the model. In some cases, which may not be common but certainly exist, the current transaction must be committed. It is because there is a lot more going on at the end of transaction besides regeneration of the model and sometimes those other things are important in order to read from the model.

Let’s list just a few of the many things:

o Auto-joining of elements is processed

Page 11: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

11

o Dynamic updaters are invoked, possibly in several iterations

o Element assemblies get synchronized

o Analytical model may need adjustments

o Many work-sharing actions take place

o Failures are handled and mistakes corrected as needed

Transaction modes

Unlike the regeneration modes, Transaction modes are still in use and they will stay in the future. Even though the automatic transaction mode may be phased out eventually, we will probably always have at least two modes – Manual and Read-Only.

Automatic mode

This mode is available for backward compatibility only. We do not recommend using it in current and future external applications, for it will quite possibly not be supported in the future.

Pros:

a) Easy to use, not having to care about transactions at all, assuming you actually need them.

Cons:

b) A transaction is always created even when it is not going to be needed during a command. The good news is that the transaction will not be committed unless something actually changes – this is an improvement in R2012 over 2011.

c) You may not have your own transactions inside the command, because transaction may not be nested.

d) You may not even name the one transaction - it will bear the command’s name.

e) You may be limited in the number of API methods allowed to invoke in your command, because some methods are valid only while outside of a transaction.

f) You may not use your command if there are not documents open in Revit.

Manual mode

This is the preferred mode and the only mode Revit internal programmers can use. It gives API clients freedom and full control over transactions in their commands.

Pros and Cons: the set of advantages and disadvantages is the exact opposite of the Automatic transaction mode. For a small price, the user has all the freedom in his or her external command. There are not restrictions over what to call; there are only restrictions over when to call what – for some methods must be in a transaction and some must be not (and the rest of methods do not care).

Read-Only mode

A command running in this mode guarantees the active document will not be modified, which means:

a) The command in its implementation may not modify it.

b) No other registered API client may modify it while the command is being executed.

c) Not even Revit internally may modify the document.

d) No transaction may be started. Not even a transaction group may be started.

e) Methods that expect the document to be modifiable will throw an exception.

f) Methods that attempt to start a transaction internally will also throw when invoked.

Page 12: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

12

It should probably be mentioned that transactions modes apply to external commands only - they do not have any effect on anything else. For example, an event handler is completely on its own regardless of the transaction mode of the external command during which the event handler might have been registered. Also, declaring an external application with a transaction mode will be ignored.

It is recommended to use the Manual Transaction Mode in external commands.

The Transaction Framework

The Transaction Framework certainly is one of those 20-80 things where with just 20% knowledge you gain 80% of all the benefits. We certainly cannot cover all the features and rules of that framework in this short section, but you may rest assured that if you follow the few rules we do mention here, your applications will likely be all right.

There are three major components of the framework:

a) Transaction (T)

b) Transaction Group (TG)

c) Sub-Transaction (ST)

Only Transactions are necessary in order to make changes to a document. Neither TG nor ST is actually needed in order to work with a document. They are only used to better "organize" the changes.

Transactions may not nests, which means only one transaction per document at any time is allowed to be open.

An ST can only be started in an open transaction and it must be closed before the transaction ends.

Conversely a TG can only be started when no transaction is open yet, and must be closed only after all transactions were closed. In other words: If you want to group a couple of transactions together, you have to start the group first.

Both ST and TG can nest, but overlapping is not allowed. That means you can start one group inside another, but the child group must be closed before the parent group is. The same applies to sub-transactions.

Methods like Start, Commit, RollBack and Assimilate can fail. The caller should test the status value these methods return.

Transaction being committed may actually return the RolledBack status instead. It is because of failures that may have been posted during the transition. If they are severe or if the end-user decides they cannot be ignored, the transaction may not be committed. This is particularly important to watch if there is a sequence of transactions which all need to be committed successfully in order for the sequence to be considered successful.

API transaction failures are handled modally unless explicitly set to modeless handling. Please keep in mind that although switching to modeless handling is possible, it requires more careful approach to finishing transactions. If the transaction ends up in pending mode it is utterly important not to do anything until the transaction is completely resolved. This will most likely be possible only with using Transaction Finalizers (see the code snippet bellow).

Assimilating a transaction group is very like committing it, but not exactly. When a TG is committed, all transactions successfully committed inside the group will stay as they are, which means the end-user will be able to see them in the Undo menu. If a TG is assimilated instead, all inner transactions will be merged into just one which will bear the group’s name (unless there was only one transaction inside the group in which case the transaction will keep its name).

Page 13: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

13

Utilizing a Transaction Finalizer

// A very simple finalizer, which just closes an outer transaction group

// after a transaction is closed, depending on the result

class MyFinalizer : ITransactionFinalizer

{

private TransactionGroup m_group;

public MyFinalizer(TransactionGroup group) { m_group = group; }

public void OnCommitted(Document doc, string name) { m_group.Commit(); }

public void OnRolledBack(Document doc, string name) { m_group.RollBack(); }

};

private TransactionStatus CommitTransactionAndGroup

(

Transaction trans, // the transaction to commit

TransactionGroup group // an outer group (enclosing the transaction)

)

{

MyFinalizer finalizer = new MyFinalizer(group);

trans.SetFailureHandlingOptions(

trans.GetFailureHandlingOptions().

SetTransactionFinalizer(finalizer));

return trans.Commit();

}

Note: Transaction finalizers are particularly useful (and practically necessary) when using modeless failure handling during transactions, but they can be used with any transaction which needs a follow-up action after it is completed (committed or rolled back).

Document modifiability

I do not think transactions in Revit are confusing, but many people do (). What they usually are confused with is how and when one can actually modify a document. “Do I have to start a transaction now?” and “May I start a transaction now?” or “Do I need to end my transaction now?” are their typical questions. Let’s see if we can shed some light on that by describing three useful properties of the Document class.

Document is Modifiable

a) If it is not in a read-only state

b) and has an open, uncommitted transaction

Page 14: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

14

Document is Read-Only

a) During an external command declared with Read-Only transaction mode

b) Under other circumstances that temporarily put a document into a read only state - such as resolving posted transaction failures

c) During some events – rather unpredictably – some documents are put into this mode because they were modifiable at the time of the event and they have to stay untouched.

Document is Read-Only File

a) This property of a document only refers to the actual file on disk

b) Changes can be made to the document, but file cannot be saved (only saved-as)

c) (not always set dependably, it appears)

To be able to open a transaction, the document

1. must not be read-only

2. and must not be modifiable yet (only one transaction at a time is permitted)

Note: The same applies to transaction groups.

Element validity

As it has been already mentioned before, there is a close relation between the object an external programmer works with in his or her managed code and corresponding native object in Revit. There is mostly one-to-one relation between those two and while a native object can exist without its managed brethren, it is rarely the case the other way around. So, while you can keep your instances of simple classes such as XYZ for as long as you wish without any side effect, objects like elements (and particularly element) are valid in the API only as long as the corresponding instance still exists in Revit.

Access to elements ceases to exist when:

a) Element is deleted from the model

b) Creation of an element is undone

c) Deletion of an element is redone

d) Transaction during which an element was created was rolled back (i.e. not committed.) (Note: The same applies for sub-transactions within a transaction.)

e) The native object is physically deleted in Revit internal code (destroyed in memory)

On other hand, a deleted element can come back to life if:

a) Deletion of the element was undone

b) Creation of the element was redone

When an element is deleted, an API application may not attempt to access the element's properties. If such an attempt is made, Revit will throw an InvalidObjectException.

Note: The code guarding access to deleted objects works for all kind of objects, not just elements (try it with a document, for example), but the ability to bring an object back to life with undoing its deletion is only applicable to elements and their derivatives.

Note: It is probably obvious that should the above statements be true (and they are) one would not gain any memory benefits by deleting elements from the model. Deleted elements always wait for a chance to be resurrected. The cache of removed elements is cleared at some situations only, such as synchronizing with the central document in work-sharing environment, or when a document is closed.

Page 15: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

15

Example using of an expired element:

private void ElementTest(Document doc, Element element) { string name = element.Name; // OK, accessing a valid element

using( Transaction trans = new Transaction(doc) )

{

trans.Start( "Deleting" ); // allowing modifications of the model

name = element.Name; // ok, reading is allowed in a transaction

doc.Delete(element); // this deletes the element from the model

try

{

name = element.Name; // wrong! expect an exception!

}

catch( InvalidObjectException )

{

TaskDialog.Show( "Revit", "Accessing a deleted element." );

}

trans.RollBack(); // this effectively undoes the deletion

}

name = element.Name; // OK; element's alive again

}

Object lifespan

Not unrelated to the above is the concept of an object’s lifespan. Most programmers (and certainly C++ programmers) are well aware of the fact that once an object leaves the scope of a program block in which it was locally created (meaning – put on the stack), the object ceases to exist. It works differently in managed code when the garbage collector takes care of most of the programmer’s worries, but it does not mean it leaves the programmer worry-free. That is especially true in mixed environment such as Revit’s where managed object are mixed with and related to native objects.

Let’s take a look at some of the most important facts:

Most managed objects are light-weight wrappers around native instances.

Some wrappers own native resources, some don’t; most don’t.

Objects that may be explicitly destroyed (they have a Destroy method defined) are better if controlled with the using block (see using the transaction in the example above).

This is particularly important for transaction phases - Transaction, Transaction Group and Sub Transaction, because the using block (or calling Destroy explicitly) will invoke the object’s

destructor which will then make sure the transaction phase is not left open. If it has not been finished explicitly before reaching the end of the using block, the transaction phase will be rolled back. That is particularly useful when expecting exceptions.

Garbage collector takes care of managed objects, not necessarily their native resources.

Revit does not delete native resources of collected managed object. That is to avoid problems from switching thread context, because collecting of resources is invoked from other than the main thread.

Page 16: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

16

Purging releases API object

If you worry that your application may need more memory than what you originally anticipated because Revit seems to keep native objects alive longer, your worries may not be without a reason. Revit only releases collected native objects at the end of standard API invocations, such as commands and events. If your application is object hungry and needs to create and delete a lot of objects, it is likely you could be reaching some limits earlier than expected.

Fortunately the API now has a method you can use to purge collected native object explicitly: PurgeReleasedAPIObject. It does exactly what it sounds like – it looks at the pool of native objects

left behind by their managed counterparts, and destroys them.

Note: Native objects are put into the pool only when the managed objects are collected by the garbage collector. When you destroy an object explicitly, the native resources of it will be destroyed too.

API Events

We are quite confident to guess there are few Revit API developers who have not tried to use Revit events yet. We are not as confident when it comes to guessing how much our users know and do not know about the mechanism of Revit events. Sure, from the outside Revit events look and work much like any other events in the .NET framework, but there is a lot more under the uniform interface that probably deserves a closer look, mostly because – once again – Revit events are rather complicated a mixture of native and managed implementation.

Let’s start with the obvious:

Event handlers are called in a loop on first-come-first-served basis. Whichever event handler was registered first it will also be called first when the event is raised. There is no re-sorting of any kind.

There are three kinds of events:

a) Single events

b) Pre-events and Post events (they always come in pairs)

There is another way of categorizing Revit events:

a) Application-level events – they are either independent of any document, or they are raised no matter what the related document of the event is.

c) Document-level events – they are only raised to those who subscribed to them for a particular document.

Pre-event are raised before post-events. Post events are always raised, even when the corresponding pre-event cancelled the action, or if the action failed and was not finished.

It used to be (pre-R2012) that document-level handlers were always invoked before corresponding application-level handlers, but that is no longer the case. Though all document-level handlers of an event will be called in one batch and application-level handlers of the same level in another batch, the order is not predefined. Like with individual handlers of an event, these groups will be processed on first-come-first-served basis. For example, if it was a document-level event a client first subscribed to, than it will be the batch of document handlers that will be invoked first.

Some events are cancellable, some are not. Some events are never cancellable (post events), some are always cancellable, and some are cancellable conditionally. You can test the IsCancellable property in an event’s argument to see if the action for which the event is being

raised can be cancelled. To cancel the action, call the Cancel method – also available in the

event’s argument class.

Page 17: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

17

If a handler of a cancellable pre-event requests cancelling the action, other handlers queued up for the pre-event will not get called, but the related post-event will still be raised to anyone who subscribed to it.

Now let’s dive a bit deeper under the hood

There is a connection of managed events and their native brethren in Revit code. Internally, each event can have many observers, one of which serves all managed handlers of that particular event. There may be other observers for the same event in Revit - the API observer is just one of them. An API observer (of a particular event) is only instantiated when there is at least one API handler registered for the event. The observer is destroyed after the last API event handler unregisters.

Depending on the event and situation, event handlers can use transactions, but they are not permitted to use modeless failure handling in transaction. Any attempt to do so will be simply ignored. This is for the sake of other handlers who will not be of a pending transaction hanging around and they could easily get an exception for doing nothing obviously wrong.

Open documents other than the currently active document and the document of the event (if they are not the same) can typically be modified if the event handler wishes so. Documents can also be freely opened and closed (with some exceptions – e.g. during the Application Closing event.) There is one restriction to when a non-active document may be modified during an event:

If an inactive document is already modifiable at the time an event is raised, the document is put temporarily into a read-only state where it stays for the entire durations of the event. That means such document will not be modified during the event.

There is an API Firewall in effect around every call to every handler. We will take a closer look at the API firewall in following chapter; for now let’s just briefly state the firewall is to make sure an API client does not leave open what is not supposed to be left open, such as an unfinished transaction.

Unsubscribing from events during events is possible. One can unsubscribe from any events, even from the one currently being handled. This comes quite handy particularly for the Idling event as we have learned to unsubscribe from it as quickly as possible.

Application vs. Controlled-Application events

I have been asked about this several times: “Does it matter if I subscribe to an event in the ControlledApplication object and how is it different from subscribing through the Application object?” The answer is: It does not matter. These subscriptions are virtually identical. Subscribers are held at one common repository, there is just one common observer for both. One can even subscribe to an event in the application object and unsubscribe from it in the ControlledApplication object, and vice versa, more likely. The effect is the same.

External commands

We do not feel like we need to say a lot about external commands; most Revit API developers are intimately familiar with them and with all the ways of using them. Let’s just sum up only those few things our Developer’s Guide might have missed to point out:

An external command may be invoked only when:

a) No other command is being executed

b) No edit mode or editor is in effect

c) No transactions or groups are open in the active document

Revit does not guarantee lifetime of commands beyond their execution scope, even though the common practice (in Revit) is to keep them around for the entire Revit session. Since it is not guaranteed, it is recommended not to store there anything that may need to be used elsewhere,

Page 18: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

18

such as global variables or event handlers. We always recommend subscribing and unsubscribing to (from) an event in an external application. Using events from an external command is certainly possible, but both subscribing and unsubscribing should ideally be done during one invocation of the command, particularly when the event’s handler is a non-static method of the command class.

Failed commands are completely discarded. It does not matter how many successful transactions were committed during the command. If the command does not succeed, all its transactions will be rolled back. This applies to the active document only; inactive documents will stay as they are at the end of the command regardless of the command’s result.

Like with events, there is an API Firewall around every call out. A command that breaks the rules of the firewall will be discarded. We will look closely at the rules later.

API Callbacks

Starting with R2012, Revit API introduced a new kind of objects – API Callbacks (a.k.a. API interfaces). Callback objects are delegates that are given to Revit to be executed later. A few examples of commonly used callbacks:

Updaters (IUpdater)

Transaction finalizers (ITransactionFinalizer)

Failures processors and Failures preprocessors (IFailuresProcessor)

The callbacks are not pure managed objects. They always have their native implementation too. Revit actually calls the native part first, which then passes the calls onto the managed object (if is still exists) and returns back the obtained result, which may be both good or bad.

Callbacks are typically owned (held) by Revit for the necessary time being, but it is recommended that callers maintain their managed instances alive. When the native part finds the managed instance does not exist anymore, nothing should happen.

Dynamic Updaters

Like the transaction framework, the Dynamic Model Updates framework is too vast to be fully explained in this concise section. Here we want to focus on just the details that are more likely to be overlooked elsewhere.

In a sense, Dynamic updaters are an extension of the internal regeneration process. Regeneration essentially makes sure that all parts of the model are valid according to dependencies set between related parts. Revit programmers have built Revit with certain rules about those dependencies and those rules may not be broken (the model would be considered invalid). So the rules may not be broken, but they can be extended, and that is when updaters come in.

Updaters are invoked near the end of a committed transaction, but still before DocumentChanged event is raised.

o The Execute method will be called for every registered updater that got triggered by a particular change, depending on how the updater was set up.

o Execute may be invoked more than once in a single transaction.

o But there is a limit to the number of cycles (= total number of registered updaters + 2). Updates at the limit and above are considered illegal and those updaters which made them will be removed.

Unlike the DocumentChanged event, dynamic updates are not invoked for undone and redone transactions. The same applies when a group is rolled back, which effectively undoes committed

Page 19: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

19

transactions it contains. In neither of these cases updaters are invoked, while a DC event will be raised.

Unlike with events (any event), registering and unregistering updaters is not allowed during execution of a dynamic update, and it does not matter whether the registration is for the updater being executed or some other one. For the same reason, adding or removing triggers, and changing updaters’ priority is also prohibited.

Strictly technically speaking, it is not disallowed per se, but Revit would likely give an exception or would crash if someone tries. Between us, unregistering updaters during execution of a dynamic update does make a little sense anyway.

Obviously, trying to start or commit a transaction in the document that is being dynamically updated is not permitted, but it may be less obvious that committing transaction in other document is not allowed either. Even though it is not technically disabled, updating will not work correctly due to the fact dynamic updates are not designed as re-entrant (currently).

Updates provide access to “changed” elements:

o Added and Deleted sets rule each other out – an “Added” element cannot be among those “Deleted”, and vice versa.

o Changed elements were neither added nor deleted.

Certain API methods are forbidden in updater execution. The methods have comments about this in their description, but we can summarize the kind of methods:

o Methods that need a transaction internally (some exports), or methods that must be out of a transaction (Save)

o UI methods (picking, selections, etc.)

o Methods that introduce elements interdependency. This limitation is to avoid synchronization problems in work-sharing environments.

Memory footprint and execution time of updaters is not limited, but it is wise to keep an updater light; do not invoke methods that take a long time (opening document, exporting, iterating through all elements in the document, etc.)

And since you probably wanted to ask, yes, it is possible to find out in an updater’s execution what trigger was it that actually got hit. It is what the IsChangeTriggered method is for. Keep in mind it

only makes sense to use it if there is more than one trigger on an updater.

API Scope

Revit may have the rule about API calls being allowed only during certain times, but there are no rules requiring one client to finish his execution before another client can process its code. Not only are invocations allowed to be nested in each other, but executions can be done in different external applications.

For example, the following scenario is perfectly possible and allowed:

Revit invokes an external command, which does something…

triggering an event to be handled by another application…

triggering another event to be handled by yet another application…

causing a dynamic updater to be executed back in the first application.

Application developers should not be surprised by such scenarios; they should expect it and anticipate it.

Revit keeps a FILO stack of the currently executing applications, thus it always knows the application running atop. Having this information available allows Revit to forbid certain operations to an application

Page 20: Asynchronous Interactions with the Revit API - Autodeskaucache.autodesk.com/au2011/sessions/5381/class... · Asynchronous Interactions with the Revit API 2 Knowledge is freedom

Asynchronous Interactions with the Revit API

20

because it knows that the application should not be invoking those operations. For example, an application cannot change the settings of another application’s updater.

The bottom external process, the one Revit invoked first is sometime being held responsible for other application’s faults. It may not be exactly fair, but it is how Revit works currently. In order to save time by not running every possible test after every application that leaves the invocations scope, some tests are only done at the end when there is only one executor left. For example, consider this case:

1. An external command is invoked by Revit.

2. The command makes a few changes in the active document and creates another document.

3. Then it starts a transaction there, makes changes, and commits the transaction.

4. After that, the command saves the new document

5. There is another application which subscribed to the DocumentSaved event.

6. The handler starts a transaction, makes changes, but forgets to commit the transaction.

7. The event framework lets it pass, because it is now at the second tier of execution.

8. The process comes back to the first application, which does nothing else but returns.

9. Revit checks and finds there is an open transaction in one of the documents. It rolls it back and marks the command as not successful, which means whatever changes the command might have done in the active document would also be discarded.

Unfortunately there is nothing that the first application could have done to avoid the problem. The programmer may only hope the end user will notice that he gets more problems when the second application is installed and uninstalls it.

API Firewall

In the just described scenario, the decision to discard the command’s changes was made by the now infamous API firewall. What is it? Well, like any other firewall, its purpose is to protect, in this particular case to protect Revit application and its open documents from any harm caused by misbehaving external applications.

API Firewall wraps around API invocations, particularly about such invocation that may maintain their own transactions, namely External commands and Events.

The main objective is to make sure clients do not leave anything open that was opened during the invocation. Mostly transactions, sub-transactions and transaction groups, but also edit modes, potentially, or simply anything that has a start and an end.

During any invocation, nothing is allowed to be left open in the active document (and/or the document an event was raised for). When such situation occurs, the violating procedure is rendered as a failure and everything that was done during the process is discarded. In events this applies to the one event handler only; other event handlers will still be called.

There is one and only one exception to this rule, but it comes with another rule attached: External commands which finished with a pending modeless transaction are allowed to do that, but when the failure dialog is closed, the firewall looks if the command left any transaction groups open around that pending transaction. If it finds that is the case, it discards everything what the command did despite the fact it had ended already.

If the client is allowed to change the active document at all (by calling OpenAndActiveDocument), the firewall validates that the formerly active document is free of transactions and transaction groups.

--- THE END ---