Revit External Event — Digitteck
Revit External Event
revit-api-dot-net·1 July 2018·5 min read

Revit External Event

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; exposes RaiseEvent which posts the action to SaveStateEventHandler and calls ExternalEvent.Raise().

The Shared Interface

csharp
// 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:

csharp
// 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:

csharp
// 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
    }
}
csharp
// 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

csharp
// 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
});

Tags

Revit APIC#.NETDesign Patterns
digitteck

© 2026 Digitteck