Revit add-ins run on a separate thread from Revit's main event loop. Reading data is straightforward from an IExternalCommand, but modifying the model requires two things: an ExternalEvent that signals Revit to invoke your handler at the next idle moment (ensuring the right context), and a Transaction that guarantees atomic, ACID-compliant writes.
ACID Transactions
A Revit Transaction follows the same ACID guarantees as database transactions — Atomicity (all-or-nothing), Consistency (only valid model states), Isolation (concurrent operations produce the same result as sequential ones), and Durability (committed changes survive failures). If any statement inside the transaction fails you roll back the entire set, leaving the model unchanged.
Architecture
Two classes wrap the Revit plumbing behind a single shared interface. This lets the same UI code run in a real Revit add-in and in a standalone WPF test project — the context difference is hidden inside the implementation:
- SaveStateEventHandler — implements
IExternalEventHandler; the only class with direct Revit API context. Stores the action and wraps its execution in a transaction. - RevitActionHandler — implements
IRevitActionHandler; exposesRaiseEventwhich posts the action toSaveStateEventHandlerand callsExternalEvent.Raise().
The Shared Interface
// Shared interface — used by both the test project and the Revit add-in.
// Neither consumer needs to know whether Revit context is available.
public interface IRevitActionHandler
{
// Raised after the transaction commits
event Action OnExecuted;
// Execute actionInRvtContext inside a named Revit transaction (or directly in tests)
void RaiseEvent(string transactionName, Action actionInRvtContext);
}Test Project Implementation
Without a Revit process, the mock handler executes the action inline:
// Test project implementation — no Revit context required.
// RaiseEvent just executes the action directly, making the UI testable in plain WPF.
public class MokRevitActionHandler : IRevitActionHandler
{
public event Action OnExecuted;
public void RaiseEvent(string transactionName, Action actionInRvtContext)
{
actionInRvtContext();
OnExecuted?.Invoke();
}
}Add-in Implementation
The real handler delegates to ExternalEvent.Raise(), which queues execution for Revit's idle moment:
// Add-in implementation — wires the action into a Revit ExternalEvent.
// RevitExternalEvent.Raise() signals Revit to call Execute() at the next idle moment,
// which runs in the correct Revit API context.
public class RevitActionHandler : IRevitActionHandler
{
private SaveStateEventHandler SaveStateEventHandler { get; set; }
private ExternalEvent RevitExternalEvent { get; set; }
public event Action OnExecuted
{
add { if (SaveStateEventHandler != null) SaveStateEventHandler.OnExecuted += value; }
remove { if (SaveStateEventHandler != null) SaveStateEventHandler.OnExecuted -= value; }
}
public RevitActionHandler(Autodesk.Revit.DB.Document document)
{
SaveStateEventHandler = new SaveStateEventHandler(document);
RevitExternalEvent = ExternalEvent.Create(SaveStateEventHandler);
}
public void RaiseEvent(string transactionName, Action action)
{
SaveStateEventHandler.TransactAction = action;
SaveStateEventHandler.TransactionName = transactionName;
RevitExternalEvent.Raise();
// Revit will call SaveStateEventHandler.Execute() at next idle
}
}// The only class that directly touches Revit API context.
// Wraps the action in a Transaction and fires OnExecuted when the commit succeeds.
public class SaveStateEventHandler : IExternalEventHandler
{
public Action TransactAction { get; set; }
public string TransactionName { get; set; }
public event Action OnExecuted;
public Autodesk.Revit.DB.Document Document { get; set; }
private bool DocumentIsOpen { get; set; }
public SaveStateEventHandler(Document document)
{
Document = document;
DocumentIsOpen = true;
Document.DocumentClosing += (s, e) => DocumentIsOpen = false;
}
public void Execute(UIApplication app)
{
if (!DocumentIsOpen || TransactAction == null) return;
try
{
using (Transaction t = new Transaction(Document, TransactionName))
{
t.Start();
TransactAction.Invoke();
t.Commit();
OnExecuted?.Invoke();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
public string GetName() => "RHAPPSAVEEVTHND";
}Usage
// Revit add-in: changes committed inside a real transaction
RevitActionHandler handler = new RevitActionHandler(document);
handler.RaiseEvent("Set Parameter", () =>
{
// statements here run inside a Revit transaction
element.get_Parameter(BuiltInParameter.ALL_MODEL_MARK).Set("A1");
});
// Test project: same call — action executes immediately, no transaction
MokRevitActionHandler testHandler = new MokRevitActionHandler();
testHandler.RaiseEvent("Set Parameter", () =>
{
// same action, no Revit required
});