Table of Contents

Hooks

Hooks are the extensibility mechanism that allows DBTools modules to respond to Revit application events without coupling directly to the Revit API event system. They provide a clean, testable abstraction for behaviors like auto-updating elements when views change or injecting contextual ribbon UI.

Overview

What Are Hooks?

Hooks are interfaces that define contracts for responding to specific application-level events. Rather than subscribing directly to Revit's UIControlledApplication events, tools register hook handlers that the central DbtHookHost dispatches to when events occur.

Why Use Hooks?

Direct Revit Events DBTools Hooks
Scattered subscriptions across tools Centralized event management
Manual exception handling Automatic error handling via ISafeExecutor
Tight coupling to Revit API Testable abstractions
Risk of blocking UI thread Async-first design
Complex lifecycle management Automatic attach/detach

Key Benefits

  1. Centralized Dispatch: All event handling flows through DbtHookHost, ensuring consistent error handling and logging
  2. Async Support: Hook handlers can perform async work without blocking Revit's UI thread
  3. Automatic Lifecycle: Hooks are attached/detached during app startup/shutdown
  4. Settings Integration: View-activated tasks integrate with the settings system for enable/disable control
  5. Warning System: Hooks can set warnings when they fail, disabling auto-update until the user resolves them

Hook Architecture

Component Overview

flowchart TB
    subgraph Revit["Revit Application"]
        Events["UIControlledApplication Events"]
    end
    
    subgraph Core["DBTools.Core"]
        HookHost["DbtHookHost"]
        Registry["DbtToolRegistry"]
        Executor["ISafeExecutor"]
    end
    
    subgraph Handlers["Hook Handlers"]
        VAH["ViewActivatedHookHandler"]
        CRI["IContextualRibbonInjector"]
    end
    
    subgraph Tasks["View Activated Tasks"]
        ET["ElevationTagsViewActivatedTask"]
        FT["FoundationTagsViewActivatedTask"]
        JGT["JoistGirderViewActivatedTask"]
    end
    
    Events -->|ViewActivated| HookHost
    Events -->|SelectionChanged| CRI
    HookHost --> Executor
    Executor --> VAH
    Executor --> CRI
    VAH --> ET
    VAH --> FT
    VAH --> JGT
    Registry -.->|"GetHookImplementations<T>()"| HookHost

Source: src/DBTools.Core/Tools/DbtHookHost.cs:28-55

Registration Flow

sequenceDiagram
    participant Boot as DbtServiceBootstrapper
    participant Module as ToolModule
    participant Registry as DbtToolRegistry
    participant Host as DbtHookHost
    participant DI as IServiceProvider
    
    Boot->>Module: RegisterHooks(registry, manifest)
    Module->>Registry: RegisterHook<THook, TImpl>()
    Note over Registry: Stores Type mapping
    
    Boot->>DI: BuildServiceProvider()
    Boot->>Host: Attach(application)
    Host->>Registry: GetHookImplementations<T>()
    Host->>DI: GetService(implType)
    Host->>Host: Subscribe to Revit events

The DbtHookHost

DbtHookHost is the central coordinator that:

  1. Resolves handlers from the DI container using types registered in DbtToolRegistry
  2. Subscribes to Revit events (e.g., ViewActivated)
  3. Dispatches to handlers wrapped in ISafeExecutor for error handling
  4. Manages lifecycle via Attach() and Detach() methods
public sealed class DbtHookHost
{
    private static readonly HashSet<Type> _supportedHookInterfaces = new(new[]
    {
        typeof(IViewActivatedHookHandler),
        typeof(IContextualRibbonInjector)
    });
    
    public void Attach(UIControlledApplication application)
    {
        var viewHandlers = ResolveHandlers<IViewActivatedHookHandler>();
        var contextualInjectors = ResolveHandlers<IContextualRibbonInjector>();
        
        if (viewHandlers.Count > 0)
            application.ViewActivated += OnViewActivated;
            
        InitializeContextualInjectorsNonBlocking(application, contextualInjectors);
    }
}

Source: src/DBTools.Core/Tools/DbtHookHost.cs:57-79

Available Hook Types

IViewActivatedHookHandler

Responds to Revit's ViewActivated event, which fires whenever the user switches views. This is the primary hook for auto-update functionality.

public interface IViewActivatedHookHandler
{
    Task OnViewActivatedAsync(
        UIControlledApplication application,
        ViewActivatedEventArgs args,
        CancellationToken ct);
}

Source: src/DBTools.Core/Tools/DbtHookHost.cs:14-17

Use Cases:

  • Auto-updating elevation tags when entering floor plans
  • Refreshing parameter values on view change
  • Updating cached view-specific data

Important Considerations:

  • Handler runs on the Revit UI thread context
  • Must not block; use async patterns
  • Errors are caught by ISafeExecutor and logged

IContextualRibbonInjector

Manages dynamic ribbon UI that appears based on application state (typically selection). Unlike view-activated hooks, contextual injectors manage their own event subscriptions.

public interface IContextualRibbonInjector
{
    Task InitializeAsync(UIControlledApplication application, CancellationToken ct);
    Task ShutdownAsync(UIControlledApplication application, CancellationToken ct);
}

Source: src/DBTools.Core/Tools/DbtHookHost.cs:19-23

Use Cases:

  • Adding "Edit" buttons to contextual tabs when tool-specific elements are selected
  • Showing/hiding ribbon controls based on selection state
  • Injecting panels into Revit's contextual "Modify" tabs

Lifecycle:

  1. InitializeAsync called during DbtHookHost.Attach()
  2. Injector subscribes to SelectionChanged or other events internally
  3. ShutdownAsync called during DbtHookHost.Detach()

ExternalEvent Rule:

  • Create ExternalEvent instances only from standard Revit API execution contexts (for example, inside InitializeAsync/RefreshAsync work that runs through ModelessQueuedCallGate).
  • Do not call ExternalEvent.Create(...) directly from Autodesk ribbon command callbacks.

Source: src/Tools/Structural/SGT/Shell/Commands/ContextualRibbonInjector.cs:59
Source: src/Tools/Structural/SGT/Shell/Commands/ContextualRibbonInjector.cs:106
Source: src/Tools/Structural/SGT/Shell/Commands/ContextualRibbonInjector.cs:424

High-Frequency Hook Logging Policy

For high-frequency event hooks (SelectionChanged, ViewActivated), suppress routine start/completion executor noise and keep diagnostics focused on state changes and failures:

  • Set SafeExecuteOptions.LogStart = false
  • Set SafeExecuteOptions.LogCompletion = false
  • Set SafeExecuteOptions.WorkPerformed = false for no-op refresh passes
  • Keep NotifyKind = None for background hook refreshes

Use tool-level logs for meaningful state transitions (for example, contextual eligibility changed), and keep deep per-probe diagnostics at Trace so Debug logs stay readable during long Revit sessions.

Source: src/DBTools.Core/Execution/SafeExecutor.cs:460
Source: src/DBTools.Core/Tools/DbtHookHost.cs:152
Source: src/DBTools.Core/Revit/UI/ContextualRibbonInjectorBase.cs:293
Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:152

The IViewActivatedTask System

For the common pattern of "do something when a view activates," DBTools provides a higher-level abstraction: IViewActivatedTask. The single ViewActivatedHookHandler dispatches to all registered tasks.

IViewActivatedTask Interface

public interface IViewActivatedTask
{
    string TaskId { get; }
    string DisplayName { get; }
    IReadOnlyCollection<string> WarningIdsToSetOnFailure { get; }
    Task<bool> ShouldRunAsync(UIApplication uiapp, ViewActivatedEventArgs args, CancellationToken ct);
    Task ExecuteAsync(UIApplication uiapp, ViewActivatedEventArgs args, CancellationToken ct);
}

Source: src/DBTools.Core/Tools/IViewActivatedTask.cs:9-16

ViewActivatedTaskBase

A base class that provides settings integration and common checks:

public abstract class ViewActivatedTaskBase<TSettings> : IViewActivatedTask
    where TSettings : class, IAutoUpdateSettings, new()
{
    public async Task<bool> ShouldRunAsync(...)
    {
        var settings = GetSettings(_settingsProvider);
        
        // Built-in checks
        if (!settings.AutoUpdateEnabled) return false;
        if (await _warnings.IsWarningActiveAsync(...)) return false;
        
        // Delegate to subclass
        return await ShouldRunInternalAsync(uiapp, args, settings, ct);
    }
    
    protected abstract TSettings GetSettings(ISettingsProvider settingsProvider);
    protected virtual Task<bool> ShouldRunInternalAsync(...) => Task.FromResult(true);
    public abstract Task ExecuteAsync(...);
}

Source: src/DBTools.Core/Tools/ViewActivatedTaskBase.cs:15-85

Execution Flow

sequenceDiagram
    participant Revit
    participant Host as DbtHookHost
    participant Exec as ISafeExecutor
    participant Handler as ViewActivatedHookHandler
    participant Task as IViewActivatedTask
    participant Settings as ISettingsProvider
    
    Revit->>Host: ViewActivated event
    Host->>Exec: RunAsync(handler logic)
    Exec->>Handler: OnViewActivatedAsync()
    
    loop For each registered task
        Handler->>Task: ShouldRunAsync()
        Task->>Settings: Get<TSettings>()
        alt AutoUpdateEnabled && no active warning
            Task-->>Handler: true
            Handler->>Exec: RunAsync(task.Execute)
            Exec->>Task: ExecuteAsync()
            alt Success
                Task-->>Exec: Complete
            else Failure
                Exec->>Settings: SetWarning(id, true)
            end
        else Disabled or warning active
            Task-->>Handler: false (skip)
        end
    end

Registering Hooks

In ToolModule.RegisterHooks()

Override RegisterHooks in your DbtToolModule to register hook handlers:

public sealed class MyToolModule : DbtToolModule
{
    public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
    {
        // Register the handler as a service
        services.AddSingleton<MyContextualInjector>();
    }
    
    public override void RegisterHooks(DbtToolRegistry registry, DbtToolManifest manifest)
    {
        Guard.NotNull(registry, nameof(registry));
        Guard.NotNull(manifest, nameof(manifest));
        
        registry.RegisterHook<IContextualRibbonInjector, MyContextualInjector>();
    }
}

Source: src/Tools/Structural/SGT/SgtToolModule.cs:20-25

Registering View Activated Tasks

View-activated tasks use a different pattern. Instead of registering directly as hooks, they're registered as IViewActivatedTask services that the single ViewActivatedHookHandler collects:

public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
{
    var warningIds = GetWarningIdsFromManifest(manifest);
    
    services.AddSingleton<IViewActivatedTask>(sp => new ElevationTagsViewActivatedTask(
        sp.GetRequiredService<ISettingsProvider>(),
        sp.GetRequiredService<IDbtLoggingHost>(),
        sp.GetRequiredService<ILogger<ElevationTagsViewActivatedTask>>(),
        warningIds));
}

Source: src/Tools/Common/ElevationTags/ElevationTagsToolModule.cs:25-35

Registration in AppHookModule

The core AppHookModule registers the single ViewActivatedHookHandler that dispatches to all tasks:

public sealed class AppHookModule : DbtToolModule
{
    public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
    {
        services.AddSingleton<ViewActivatedHookHandler>();
    }

    public override void RegisterHooks(DbtToolRegistry registry, DbtToolManifest manifest)
    {
        registry.RegisterHook<IViewActivatedHookHandler, ViewActivatedHookHandler>();
    }
}

Source: src/DBTools.App/Features/Hooks/AppHookModule.cs:7-22

Implementing Hook Handlers

Step 1: Define Your Handler Class

public sealed class MyViewActivatedTask : ViewActivatedTaskBase<MyToolSettings>
{
    public MyViewActivatedTask(
        ISettingsProvider settings,
        ILogger<MyViewActivatedTask> logger,
        IReadOnlyCollection<string> warningIds)
        : base(settings, logger, "mytool.updater", "My Tool Auto-Update", warningIds)
    {
    }
    
    protected override MyToolSettings GetSettings(ISettingsProvider provider)
        => provider.Get<MyToolSettings>();
}

Step 2: Implement ShouldRunInternalAsync (Optional)

Filter when your task should run based on view type, document state, etc.:

protected override Task<bool> ShouldRunInternalAsync(
    UIApplication uiapp,
    ViewActivatedEventArgs args,
    MyToolSettings settings,
    CancellationToken ct)
{
    var view = uiapp.ActiveUIDocument?.Document?.ActiveView;
    if (view == null) return Task.FromResult(false);
    
    // Only run in floor plans
    return Task.FromResult(
        view.ViewType == ViewType.FloorPlan || 
        view.ViewType == ViewType.CeilingPlan);
}

Source: src/Tools/Common/ElevationTags/Hooks/ElevationTagsViewActivatedTask.cs:40-52

Step 3: Implement ExecuteAsync

Perform the actual work:

public override Task ExecuteAsync(
    UIApplication uiapp,
    ViewActivatedEventArgs args,
    CancellationToken ct)
{
    var doc = uiapp.ActiveUIDocument?.Document 
        ?? throw new InvalidOperationException("No active document.");
    var view = doc.ActiveView 
        ?? throw new InvalidOperationException("No active view.");
    
    var settings = GetSettings(SettingsProvider);
    var gate = new ModalInlineCallGate(uiapp);
    var tx = new CallGateTransactionRunner(gate);
    
    var updater = new MyAutoUpdater(tx, doc, view, Logger, settings);
    updater.Run();
    
    return Task.CompletedTask;
}

Source: src/Tools/Common/ElevationTags/Hooks/ElevationTagsViewActivatedTask.cs:54-65

Step 4: Register in Your ToolModule

public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
{
    var warningIds = new[] { "mytool.auto_update.warning" };
    
    services.AddSingleton<IViewActivatedTask>(sp => new MyViewActivatedTask(
        sp.GetRequiredService<ISettingsProvider>(),
        sp.GetRequiredService<ILogger<MyViewActivatedTask>>(),
        warningIds));
}

Implementing Contextual Ribbon Injectors

For tools that need dynamic ribbon UI (like SGT's "Edit" button on selected girts):

Step 1: Implement IContextualRibbonInjector

public sealed class MyContextualInjector : IContextualRibbonInjector
{
    private readonly ILogger<MyContextualInjector> _logger;
    private readonly ISafeExecutor _executor;
    private bool _subscribed;
    
    public Task InitializeAsync(UIControlledApplication application, CancellationToken ct)
    {
        if (_subscribed) return Task.CompletedTask;
        
        // Subscribe to selection changes
        // Use ModelessQueuedCallGate for safe async access
        return _gate.RunAsync(app =>
        {
            app.SelectionChanged += OnSelectionChanged;
            _subscribed = true;
        }, ct);
    }
    
    public Task ShutdownAsync(UIControlledApplication application, CancellationToken ct)
    {
        if (!_subscribed) return Task.CompletedTask;
        
        return _gate.RunAsync(app =>
        {
            app.SelectionChanged -= OnSelectionChanged;
            _subscribed = false;
            RemoveInjectedUI();
        }, ct);
    }
}

Source: src/Tools/Structural/SGT/Shell/Commands/ContextualRibbonInjector.cs:59

Step 2: Handle Selection Changes

private async void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
    if (sender is not UIApplication uiapp) return;
    
    try
    {
        await _executor.RunAsync(
            () => RefreshAsync(uiapp, CancellationToken.None),
            _logger,
            notifier: null,
            opts: new SafeExecutor.SafeExecuteOptions
            {
                Name = "My Contextual Selection Hook",
                SuppressCompletionBanner = true,
                NotifyKind = SafeExecutor.NotifyKind.None,
                WorkPerformed = false
            },
            ct: CancellationToken.None);
    }
    catch (OperationCanceledException) { }
    catch (Exception) { /* Logged by SafeExecutor */ }
}

Source: src/Tools/Structural/SGT/Shell/Commands/ContextualRibbonInjector.cs:113

Step 3: Inject UI into Contextual Tabs

private void EnsureInjected(UIApplication uiapp)
{
    var ribbon = ComponentManager.Ribbon;
    if (ribbon == null) return;
    
    var targetTab = TryGetContextualTab(ribbon);
    if (targetTab == null) return;
    
    var panel = EnsurePanel(targetTab);
    var btn = BuildButton();
    
    btn.CommandHandler = new RibbonButtonCommand(OnButtonClick);
    panel.Source.Items.Add(btn);
}

Source: src/Tools/Structural/SGT/Shell/Commands/ContextualRibbonInjector.cs:221

Hook Execution Order

Registration Order

Hooks are resolved in the order they were registered:

  1. AppHookModule registers ViewActivatedHookHandler (core)
  2. Tool modules register their IViewActivatedTask implementations
  3. Tool modules register IContextualRibbonInjector implementations

Dispatch Order

flowchart LR
    subgraph Startup["App Startup"]
        A1[Attach Hook Host]
        A2[Initialize Contextual Injectors]
        A3[Subscribe ViewActivated]
    end
    
    subgraph Runtime["Runtime Events"]
        R1[ViewActivated fires]
        R2[For each handler in order]
        R3[For each task in order]
    end
    
    subgraph Shutdown["App Shutdown"]
        S1[Detach Hook Host]
        S2[Shutdown Contextual Injectors]
        S3[Unsubscribe events]
    end
    
    A1 --> A2 --> A3
    A3 -.->|"User navigates"| R1
    R1 --> R2 --> R3
    R3 -.->|"App closes"| S1
    S1 --> S2 --> S3

Priority Considerations

Currently, hooks execute in registration order without explicit priority. If you need guaranteed ordering:

  1. Register in a specific order in DbtServiceBootstrapper
  2. Or coordinate within a single handler that manages multiple sub-tasks

Common Patterns

Auto-Update on View Change

The most common pattern for tools like Elevation Tags:

// 1. Define settings with IAutoUpdateSettings
public class MySettings : IAutoUpdateSettings
{
    public bool AutoUpdateEnabled { get; set; } = true;
    public bool HasWarning { get; set; }
}

// 2. Create task extending ViewActivatedTaskBase
public class MyAutoUpdateTask : ViewActivatedTaskBase<MySettings>
{
    protected override Task<bool> ShouldRunInternalAsync(...)
    {
        // Filter by view type
        return Task.FromResult(view.ViewType == ViewType.FloorPlan);
    }
    
    public override Task ExecuteAsync(...)
    {
        // Perform update logic
    }
}

// 3. Register in ToolModule
services.AddSingleton<IViewActivatedTask>(...);

Contextual Ribbon Button

For tools that need UI when specific elements are selected:

// 1. Implement IContextualRibbonInjector
public class MyInjector : IContextualRibbonInjector
{
    // Track subscribed state
    private bool _subscribed;
    
    // Track injected buttons for visibility control
    private readonly Dictionary<string, RibbonButton> _buttons = new();
    
    // Evaluate selection and show/hide button
    private void OnSelectionChanged(...)
    {
        bool hasMyElements = EvaluateSelection(uiapp);
        ToggleButtons(visible: hasMyElements);
    }
}

// 2. Register hook
registry.RegisterHook<IContextualRibbonInjector, MyInjector>();

Warning Integration

Feature warnings are for actionable prerequisite/configuration failures (e.g., missing shared parameters, invalid settings). They should not be activated for transient/runtime exceptions.

// Example: set warning only for actionable prerequisite failure
var eval = MyPrereqEvaluator.Evaluate(uiapp, doc, tx, logger);
if (eval.Kind == PrereqKind.Invalid)
{
    foreach (var warningId in WarningIdsToSetOnFailure)
        await warnings.SetWarningAsync(warningId, true, context: null, ct);
    return;
}

// Runtime exceptions: show a banner once + disable for this document session (do NOT set warnings)
try { updater.Run(); }
catch (Exception ex)
{
    RecordRuntimeFailure(doc);
    ShowRuntimeFailureBannerOnce(doc, ex.GetBaseException().Message, logger);
}

The warning IDs must be defined in the tool's settings pack and match entries in manifest.yml.

Creating New Hook Types

To add a new hook type (advanced):

Step 1: Define the Interface

// In DBTools.Core/Tools/
public interface IDocumentClosingHookHandler
{
    Task OnDocumentClosingAsync(
        UIControlledApplication application,
        DocumentClosingEventArgs args,
        CancellationToken ct);
}

Step 2: Register as Supported in DbtHookHost

private static readonly HashSet<Type> _supportedHookInterfaces = new(new[]
{
    typeof(IViewActivatedHookHandler),
    typeof(IContextualRibbonInjector),
    typeof(IDocumentClosingHookHandler)  // Add new type
});

Step 3: Add Resolution and Subscription

public void Attach(UIControlledApplication application)
{
    // ... existing code ...
    
    var closingHandlers = ResolveHandlers<IDocumentClosingHookHandler>();
    if (closingHandlers.Count > 0)
        application.DocumentClosing += OnDocumentClosing;
    
    _closingHandlers = closingHandlers;
}

private async void OnDocumentClosing(object? sender, DocumentClosingEventArgs args)
{
    if (_closingHandlers == null || _closingHandlers.Count == 0) return;
    
    try
    {
        await _executor.RunAsync(
            async () =>
            {
                foreach (var handler in _closingHandlers)
                    await handler.OnDocumentClosingAsync(_application!, args, CancellationToken.None);
            },
            _logger,
            notifier: null,
            opts: new SafeExecutor.SafeExecuteOptions { /* ... */ },
            ct: CancellationToken.None);
    }
    catch { /* Suppress to protect Revit */ }
}

Step 4: Add Detach Logic

public void Detach(UIControlledApplication application)
{
    // ... existing code ...
    
    try { _application.DocumentClosing -= OnDocumentClosing; }
    catch (Exception ex) { _logger.LogWarning(ex, "Failed to detach document-closing hook."); }
    
    _closingHandlers = null;
}

Performance Considerations

Don't Block the UI Thread

Hooks run in response to Revit events. Blocking causes UI freezes:

// BAD - blocks UI
public Task ExecuteAsync(...)
{
    Thread.Sleep(5000);  // Never do this!
    return Task.CompletedTask;
}

// GOOD - async work
public async Task ExecuteAsync(...)
{
    await SomeAsyncOperation();
}

Use SafeExecutor Options Wisely

For frequent hooks like view activation, suppress banners:

new SafeExecutor.SafeExecuteOptions
{
    SuppressCompletionBanner = true,
    NoBannerOnSuccess = true,
    NotifyKind = SafeExecutor.NotifyKind.Error,  // Only show errors
    WorkPerformed = false  // Don't log "no work" for frequent events
}

Filter Early with ShouldRunAsync

Avoid expensive work when the hook shouldn't run:

protected override Task<bool> ShouldRunInternalAsync(...)
{
    // Quick checks first
    var view = uiapp.ActiveUIDocument?.Document?.ActiveView;
    if (view == null) return Task.FromResult(false);
    if (view.ViewType != ViewType.FloorPlan) return Task.FromResult(false);
    
    // Only then check more expensive conditions
    return Task.FromResult(true);
}

Batch Operations in Transactions

When modifying elements, batch changes in a single transaction:

public override Task ExecuteAsync(...)
{
    using var tx = new Transaction(doc, "Auto Update");
    tx.Start();
    
    foreach (var element in elementsToUpdate)
        UpdateElement(element);
    
    tx.Commit();
    return Task.CompletedTask;
}

Real Examples

ElevationTags Auto-Update

The Elevation Tags tool uses a view-activated task to update tag values when entering floor plans:

public sealed class ElevationTagsViewActivatedTask : ViewActivatedTaskBase<ElevationTagsSettings>
{
    public ElevationTagsViewActivatedTask(
        ISettingsProvider settings,
        IDbtLoggingHost loggingHost,
        ILogger<ElevationTagsViewActivatedTask> logger,
        IReadOnlyCollection<string> warningIds)
        : base(settings, logger, "structural.elevation_tags.updater", "Elevation Tags", warningIds)
    {
        _loggingHost = loggingHost;
    }
    
    protected override Task<bool> ShouldRunInternalAsync(...)
    {
        var view = uiapp.ActiveUIDocument?.Document?.ActiveView;
        return Task.FromResult(
            view?.ViewType == ViewType.FloorPlan || 
            view?.ViewType == ViewType.CeilingPlan);
    }
    
    public override Task ExecuteAsync(...)
    {
        var updater = new ElevationTagsAutoUpdater(tx, doc, view, logger, settings);
        updater.Run();
        return Task.CompletedTask;
    }
}

Source: src/Tools/Common/ElevationTags/Hooks/ElevationTagsViewActivatedTask.cs:20-66

SGT Contextual Ribbon

The Super Girt Tool injects an "Edit" button into the contextual "Modify | Structural Framing" tab:

public sealed class ContextualRibbonInjector : IContextualRibbonInjector
{
    public Task InitializeAsync(UIControlledApplication application, CancellationToken ct)
    {
        return _gate.RunAsync(app =>
        {
            EnsureEditEventCreated(); // ExternalEvent.Create in API-safe context
            app.SelectionChanged += OnSelectionChanged;
            _subscribed = true;
            EnsureInjected(app);
            EvaluateAndToggle(app);
        }, ct);
    }
    
    private void EvaluateAndToggle(UIApplication uiapp)
    {
        var (hasGirts, systemIds) = EvaluateSelection(uiapp);
        UpdateSystemIds(systemIds);
        ToggleButtons(hasGirts, hasGirts);
    }
    
    private void EnsureInjected(UIApplication uiapp)
    {
        var targetTab = TryGetContextualModifyStructuralTab(ribbon);
        var panel = EnsurePanel(targetTab);
        var btn = BuildButton();
        panel.Source.Items.Add(btn);
    }
}

Source: src/Tools/Structural/SGT/Shell/Commands/ContextualRibbonInjector.cs:27

AppHookModule Registration

The core hook module that wires up view-activated handling:

public sealed class AppHookModule : DbtToolModule
{
    public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
    {
        services.AddSingleton<ViewActivatedHookHandler>();
    }

    public override void RegisterHooks(DbtToolRegistry registry, DbtToolManifest manifest)
    {
        registry.RegisterHook<IViewActivatedHookHandler, ViewActivatedHookHandler>();
    }
}

Source: src/DBTools.App/Features/Hooks/AppHookModule.cs:7-22

Debugging Hooks

Enable Debug Logging

Hook handlers log at debug level. Enable in your development configuration:

{
  "Serilog": {
    "MinimumLevel": {
      "Override": {
        "DBTools.App.Features.Hooks": "Debug"
      }
    }
  }
}

Common Issues

Symptom Possible Cause Solution
Hook never fires Not registered in RegisterHooks Verify RegisterHook<>() call
Handler not resolved Service not registered Add AddSingleton<>() in RegisterServices
UI thread deadlock Sync-waiting on async in hook Use async void for event handlers, never .Wait()
Hook runs but no effect ShouldRunAsync returning false Check settings (AutoUpdateEnabled, HasWarning)
Exception in hook Various Check logs; ISafeExecutor logs all errors

See Also