RevitToolkit

repository·main·Indexed 41 days ago

https://github.com/nice3point/revittoolkit

A library providing a modern and flexible interface for the Revit API. It offers abstractions for common development tasks, including synchronous and asynchronous External Commands (AsyncExternalCommand), External Applications, and External Events. Features include a fluent API for selection filters via SelectionConfiguration, failure and dialog suppression scopes, and an attribute-based generator for External Events. Includes Roslyn analyzer rules in version 2027.0.0 to ensure correct implementation of generated events.

Tokens
4.9K
Snippets
12
Records
16
Agent score
51%

What's inside RevitToolkit

  1. Overview of RevitToolkit features

    main
    RevitToolkit provides a modern interface for the Revit API. It includes implementations for common interfaces encountered during Revit development, allowing developers to choose specific components to improve flexibility and reduce boilerplate.
  2. Use External Events for modeless windows

    main

    The toolkit provides several ExternalEvent implementations to allow executing Revit API code from outside the Revit execution context (e.g., from a modeless window or a different thread). The toolkit handles all initialization automatically.

    • ExternalEvent: A synchronous event. Calling Raise() queues the delegate to be executed in the next Revit event-processing cycle.
    • ExternalEvent<T>: A synchronous event that accepts an argument of type T via Raise(T arg).
    • AsyncExternalEvent: An asynchronous event. RaiseAsync() returns a Task that completes when Revit finishes the delegate. Warning: Do not use .Wait() or .Result on the Revit main thread as it will cause a deadlock; always use await.
    • AsyncExternalEvent<T>: An asynchronous event that accepts an argument of type T.
    • AsyncRequestExternalEvent<TResult>: An asynchronous event that returns a result of type TResult via RaiseAsync().
    • AsyncRequestExternalEvent<T, TResult>: A generic asynchronous event that accepts an argument T and returns TResult.
    // AsyncRequestExternalEvent<T, TResult> Example
    private readonly AsyncRequestExternalEvent<ElementId, bool> _deleteWindowRequestEvent = new((application, elementId) =>
    {
        var document = application.ActiveUIDocument.Document;
        using var transaction = new Transaction(document, "Delete window");
        transaction.Start();
        document.Delete(elementId);
        transaction.Commit();
        return true;
    });
    
    private async Task DeleteWindowAsync(ElementId elementId)
    {
        var result = await _deleteWindowRequestEvent.RaiseAsync(elementId);
    }
  3. Generate External Events with the [ExternalEvent] attribute

    main

    The [ExternalEvent] attribute eliminates boilerplate by automatically generating event properties for annotated methods in a partial class. The generator creates properties based on the method signature:

    • void method: Generates IExternalEvent and IAsyncExternalEvent properties.
    • Method with return value: Generates an IAsyncRequestExternalEvent<TResult> property.
    • Method with UIApplication parameter: The generated events pass the UIApplication instance to the handler.
    • Method with one extra parameter: Generates typed event properties (e.g., IExternalEvent<T>).
    • Method with multiple extra parameters: Generates a sealed record containing the arguments and extension methods to call RaiseAsync using individual parameters.

    Use [ExternalEvent(AllowDirectInvocation = true)] to allow the handler to be invoked directly on the calling thread when Revit is in API mode.

    public partial class MyViewModel
    {
        [ExternalEvent]
        private void DeleteWindow(UIApplication application, ElementId elementId)
        {
            // Implementation
        }
    }
    
    // Generates:
    // IExternalEvent<ElementId> DeleteWindowEvent
    // IAsyncExternalEvent<ElementId> DeleteWindowAsyncEvent
  4. Install RevitToolkit via NuGet

    main

    RevitToolkit can be installed as a NuGet package. Because the packages are compiled for specific Revit versions, you should use the RevitVersion property to manage version compatibility in your project file.

    <PackageReference Include="Nice3point.Revit.Toolkit" Version="$(RevitVersion).*"/>
  5. Use JournalData for non-interactive command execution

    main

    The JournalData property in ExternalCommand provides a way to pass data into your command during Revit journal playback. This allows you to implement commands that can run in a UI-less mode, which is essential for automated testing and non-interactive automation.

    • Writing Data: You can store string-to-string data in the JournalData dictionary at the end of your command execution. This data is then recorded in the Revit journal file.
    • Reading Data: When the command is executed via journal playback, the data stored in the journal is passed back into the JournalData property of your command instance.
  6. Manage Revit API Context and Failure Suppression

    main

    The toolkit provides context objects to access global application information and manage Revit's behavior using disposable scopes.

    RevitApiContext (Database Level)

    • BeginFailureSuppressionScope(): Automatically resolves all failures without user interaction. Use resolveErrors: false to cancel the transaction and undo all failed changes if a failure occurs.

    RevitContext (UI Level)

    • Properties: UiApplication, ActiveDocument, ActiveUiDocument, ActiveView, ActiveGraphicalView, IsRevitInApiMode.
    • BeginDialogSuppressionScope(): Suppresses dialogs. You can specify a resultCode (e.g., TaskDialogResult.Ok) or provide a custom handler to decide the result based on the DialogId.
    // Suppress failures and undo on error
    using (RevitApiContext.BeginFailureSuppressionScope(resolveErrors: false))
    {
        ModifyDocument();
    }
    
    // Suppress dialogs with a custom handler
    using (RevitContext.BeginDialogSuppressionScope(args =>
    {
        var result = args.DialogId == "TaskDialog_ModelUpdater" ? TaskDialogResult.Ok : TaskDialogResult.Close;
        args.OverrideResult((int)result;
    }))
    {
        LoadFamilies();
    }
  7. Resolve dependencies with ResolveHelper

    main

    The ResolveHelper provides scoped assembly resolution to ensure dependencies are found. This is useful when loading external libraries from specific paths.

    • BeginAssemblyResolveScope<T>(): Resolves dependencies for a specific type.
    • BeginAssemblyResolveScope(string path): Searches for dependencies in a specific directory.
    • Nesting: Scopes can be nested; dependencies are searched from the innermost scope to the outermost.
    ```c#
    using (ResolveHelper.BeginAssemblyResolveScope(@
  8. Implement asynchronous External Commands

    main

    Use the AsyncExternalCommand base class for async/await support. Override ExecuteAsync() to perform I/O-bound operations (like HTTP requests or database queries) without freezing the Revit UI. The toolkit uses dispatcher message pumping to keep the UI responsive.

    [Transaction(TransactionMode.Manual)]
    public class Command : AsyncExternalCommand
    {
        public override async Task ExecuteAsync()
        {
            using var httpClient = new HttpClient();
            var response = await httpClient.GetStringAsync("https://example.com");
    
            using var transaction = new Transaction(Application.ActiveUIDocument.Document, "Update Parameter");
            transaction.Start();
    
            var element = Application.ActiveUIDocument.Document.GetElement(id);
            element?.FindParameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS)?.Set(response);
    
            transaction.Commit();
        }
    }
  9. Implement External Applications

    main

    The toolkit provides base classes for Revit external applications with simplified method signatures and automatic dependency resolution.

    • ExternalApplication: Implement IExternalApplication. Override OnStartup() and optionally OnShutdown().
    • AsyncExternalApplication: Implement asynchronous startup/shutdown. Override OnStartupAsync() and optionally OnShutdownAsync(). Ideal for I/O-bound tasks during startup.
    • ExternalDBApplication: Implement IExternalDBApplication. Use this when you do not need access to the Revit UI.
    public class Application : AsyncExternalApplication
    {
        public override async Task OnStartupAsync()
        {
            using var httpClient = new HttpClient();
            var configuration = await httpClient.GetStringAsync("https://api.example.com/config");
    
            var panel = Application.CreatePanel(configuration.PanelTitle);
            panel.AddPushButton<Command>(configuration.ButtonTitle);
        }
    
        public override async Task OnShutdownAsync()
        {
            await SaveSettingsAsync();
        }
    }
  10. Configure Selection Filters with SelectionConfiguration

    main

    The SelectionConfiguration class implements ISelectionFilter and provides a fluent API to define what elements or references are selectable during a PickObject operation.

    • Default: All elements are allowed.
    • Customization: Use .Allow.Element(predicate) to filter elements and .Allow.Reference(predicate) to filter references.
    var selectionConfiguration = new SelectionConfiguration()
            .Allow.Element(element => element.Category.Id.AreEquals(BuiltInCategory.OST_Walls))
            .Allow.Reference((reference, xyz) => false);
    
    uiDocument.Selection.PickObject(ObjectType.Element, selectionConfiguration.Filter);
  11. Implement synchronous External Commands

    main

    Use the ExternalCommand base class to implement IExternalCommand. Instead of the full interface, you only need to override the Execute() method. The toolkit automatically handles dependency resolution to prevent FileNotFoundException by searching the plugin folder.

    [Transaction(TransactionMode.Manual)]
    public class Command : ExternalCommand
    {
        public override void Execute()
        {
            var document = Application.ActiveUIDocument.Document;
        }
    }
  12. Review shipped analyzer rules in Release 2027.0.0

    main

    The Nice3point.Revit.Toolkit.Analyzers package includes specific Roslyn analyzer rules for the ExternalEventGenerator to ensure correct implementation of Revit external events. In version 2027.0.0, the following rules are enforced:

    Rule IDCategorySeverityDescription
    RVTTK0001ExternalEventGeneratorErrorMethod must return Task or Task<T>
    RVTTK0002ExternalEventGeneratorWarningMethod should not be async void
    RVTTK0003ExternalEventGeneratorErrorMethod cannot be generic
    RVTTK0004ExternalEventGeneratorErrorDuplicate method overloads are not allowed
    RVTTK0005ExternalEventGeneratorErrorThe containing type must be marked as partial
    | Rule ID   | Category               | Severity | Notes |
    |------------|------------------------|----------|--------------------------------|
    | RVTTK0001 | ExternalEventGenerator | Error    | Method returns Task or Task<T> |
    | RVTTK0002 | ExternalEventGenerator | Warning  | Method is async void |
    | RVTTK0003 | ExternalEventGenerator | Error    | Method is generic |
    | RVTTK0004 | ExternalEventGenerator | Error    | Duplicate method overloads |
    | RVTTK0005 | ExternalEventGenerator | Error    | Containing type is not partial |