Leveraging DBTools Modularity
DBTools is designed as a modular Revit add-in platform where tools share common infrastructure while maintaining clear boundaries. This guide explains how to leverage the shared services from DBTools.Core and DBTools.Themes to build tools that integrate seamlessly with the platform.
Philosophy
DBTools follows these modularity principles:
- Shared Infrastructure, Independent Tools - Core services are centralized; tools are self-contained
- Dependency Injection First - All services are resolved through DI, enabling testing and flexibility
- Contract-Driven - Interfaces define contracts; implementations can vary per context
- Error Handling at Boundaries - All entrypoints run inside
ISafeExecutor - Window-Scoped Theming - Each window owns its theme resources to avoid cross-add-in conflicts
Source:
src/DBTools.App/Bootstrap/DbtServiceBootstrapper.cs:69-130
Two-Tier DI Architecture
DBTools uses a two-tier dependency injection model:
| Tier | Lifetime | Purpose | Examples |
|---|---|---|---|
| Singleton | Application lifetime | Infrastructure services | ISafeExecutor, IAlertService, ISettingsProvider, DbtToolRegistry |
| Scoped | Per-command lifetime | Revit context services | ITransactionRunner, IRevitCallGate, IRevitRunScope |
Source:
src/DBTools.App/Bootstrap/DbtServiceBootstrapper.cs:77-79
Shared Services from DBTools.Core
ISafeExecutor
The central error handling service. All tool entrypoints must execute within ISafeExecutor.
public interface ISafeExecutor
{
Task RunAsync(Func<Task> action, ILogger logger, IErrorNotifier? notifier = null,
CancellationToken ct = default);
Task RunAsync(Func<Task> action, ILogger logger, IErrorNotifier? notifier,
SafeExecutor.SafeExecuteOptions? opts, CancellationToken ct = default);
}
Source:
src/DBTools.Core/Execution/ISafeExecutor.cs:6-16
What it provides:
- Correlation IDs for log tracing
- Execution timing
- Exception logging with full stack traces and inner exceptions
- User notification via banners
- Automatic debug mode activation on fatal errors
- Lifecycle hooks (
OnSuccessAsync,OnCancelAsync,OnErrorAsync)
Source:
src/DBTools.Core/Execution/SafeExecutor.cs:12-467
Usage in a command:
// Commands automatically use ISafeExecutor via DbtToolCommand base class
public sealed class GmCommand : DbtToolCommand
{
protected override async Task RunAsync(IDbtToolContext context)
{
var exec = context.Resolve<ISafeExecutor>();
// For nested async operations that need error handling:
await exec.RunAsync(async () =>
{
// Your work here
}, context.Logger, context.ErrorNotifier);
}
}
Source:
src/Tools/Common/GM/Features/GmCommand.cs:32-93
ITransactionRunner
Unified API for executing Revit model modifications with automatic transaction management.
public interface ITransactionRunner
{
Task RunAsync(string name, Action action, CancellationToken ct = default);
Task<T> RunAsync<T>(string name, Func<T> action, CancellationToken ct = default);
Task RunAsync(string name, Action<Document> action, CancellationToken ct = default);
Task<T> RunAsync<T>(string name, Func<Document, T> action, CancellationToken ct = default);
Task RunAsync(string name, Action<UIApplication, Document> action, CancellationToken ct = default);
Task<T> RunAsync<T>(string name, Func<UIApplication, Document, T> action, CancellationToken ct = default);
// Overloads with explicit Document parameter
Task RunAsync(Document doc, string name, Action<Document> action, CancellationToken ct = default);
Task<T> RunAsync<T>(Document doc, string name, Func<Document, T> action, CancellationToken ct = default);
}
Source:
src/DBTools.Core/Revit/Transactions/ITransactionRunner.cs:6-53
Key behaviors:
- Auto-selects transaction type:
SubTransactionifdoc.IsModifiable, otherwise newTransaction - Cross-document protection
- Failure handling with
SilentFailuresPreprocessor
Usage example:
public class FoundationTypeOrganizer
{
private readonly ITransactionRunner _tx;
public FoundationTypeOrganizer(ITransactionRunner tx, Document doc, ILogger logger)
{
_tx = tx;
// ...
}
public async Task OrganizeAsync()
{
await _tx.RunAsync("Organize Foundation Types", doc =>
{
// Modify document - transaction is automatic
});
}
}
Source:
src/Tools/Structural/OrganizeFoundationTypes/Features/FoundationTypeOrganizer.cs:19-27
ITransactionGroupService
Manages transaction groups for operations requiring multiple transactions to appear as a single undo item.
public interface ITransactionGroupService
{
bool IsActive { get; }
Task BeginAsync(string name, CancellationToken ct = default);
Task FinalizeAsync(bool commit, CancellationToken ct = default);
Task RunAsync(string name, bool commit, Func<Task> work, CancellationToken ct = default);
}
Source:
src/DBTools.Core/Revit/Transactions/ITransactionGroupService.cs:3-14
Usage for batch operations:
public class TdvService
{
private readonly ITransactionRunner _tx;
private readonly ITransactionGroupService _group;
public TdvService(IRevitCallGate gate, ITransactionRunner tx,
ITransactionGroupService group, ILogger<TdvService> logger)
{
_tx = tx;
_group = group;
// ...
}
public async Task ApplyMultipleChangesAsync()
{
await _group.RunAsync("Batch Apply", commit: true, async () =>
{
// Multiple transactions appear as one undo item
await _tx.RunAsync("Step 1", doc => { /* ... */ });
await _tx.RunAsync("Step 2", doc => { /* ... */ });
await _tx.RunAsync("Step 3", doc => { /* ... */ });
});
}
}
Source:
src/Tools/Common/TDV/Revit/Services/TdvService.cs:20-23
IAlertService
Service for showing alert dialogs with various body types.
public interface IAlertService
{
AlertResult Show(AlertRequest request);
bool Confirm(string message, string title = "DB Tools");
T? SelectSingle<T>(IEnumerable<T> items, Func<T, string> displayFunc,
string windowTitle, string header, string? message = null,
AlertWindowOptions? options = null) where T : class;
IReadOnlyList<T> SelectMultiple<T>(IEnumerable<T> items, Func<T, string> displayFunc,
string windowTitle, string header, string? message = null,
AlertWindowOptions? options = null) where T : class;
}
Source:
src/DBTools.Core/UI/Alerts/Services/IAlertService.cs:5-26
Usage examples:
// Simple confirmation
var alerts = context.Resolve<IAlertService>();
if (alerts.Confirm("Delete 15 elements?", "Confirm Delete"))
{
// Proceed
}
// Single selection
var selectedLevel = alerts.SelectSingle(
levels,
level => level.Name,
windowTitle: "Select Level",
header: "Target Level",
message: "Choose the level to place elements on");
// Multiple selection
var selectedTypes = alerts.SelectMultiple(
familyTypes,
t => t.Name,
windowTitle: "Select Types",
header: "Types to Process");
Source:
src/DBTools.Core/UI/Alerts/Services/AlertService.cs:51-183
ILogger
DBTools uses Microsoft.Extensions.Logging with Serilog as the provider.
// Resolve from DI
var logger = context.Resolve<ILogger<MyService>>();
// Or use the context's logger
var logger = context.Logger;
// Structured logging
logger.LogInformation("Processing {Count} elements in {Document}",
elements.Count, doc.Title);
logger.LogWarning("[GM] Failed to compute DocumentKey: {Message}", ex.Message);
logger.LogError(ex, "[{Command}] execution failed.", GetType().Name);
Source:
src/DBTools.App/Bootstrap/DbtServiceBootstrapper.cs:381-386
Important: Always use structured logging with named placeholders, not string interpolation.
IRevitRunScope
Represents a single Revit run scope (command, modeless session, or test). Provides access to the current Revit context.
public interface IRevitRunScope
{
UIApplication UIApplication { get; }
IRevitCallGate CallGate { get; }
ITransactionRunner TransactionRunner { get; }
ITransactionGroupService TransactionGroupService { get; }
UIDocument GetActiveUiDocument();
Document GetActiveDocument();
Document GetLockedDocument(Document expectedDocument, string? context = null);
}
Source:
src/DBTools.Core/Revit/Execution/IRevitRunScope.cs:11-20
Run Scope Profiles:
| Profile | Use Case | Description |
|---|---|---|
InlineUi |
Modal commands, RevitTest | Executes directly on Revit UI thread |
QueuedModeless |
Long-running/modeless tools | Uses ExternalEvent/RevitTask |
Source:
src/DBTools.Core/Revit/Execution/IRevitRunScope.cs:22-36
ISettingsProvider / IOptionsMonitor
Type-safe settings access with persistence and hot-reload support.
public interface ISettingsProvider
{
TSettings Get<TSettings>() where TSettings : class, new();
Task SaveAsync<TSettings>(string section, TSettings settings,
CancellationToken ct = default) where TSettings : class;
}
Source:
src/DBTools.Core/Settings/ISettingsProvider.cs:1-8
Usage:
// Via ISettingsProvider (singleton)
var settings = context.Resolve<ISettingsProvider>();
var gmSettings = settings.Get<GmSettings>();
// Via IOptionsMonitor (for reactive updates)
var monitor = context.Resolve<IOptionsMonitor<GmSettings>>();
var currentSettings = monitor.CurrentValue;
Source:
src/DBTools.App/Bootstrap/DiAppRuntime.cs:78
Shared UI from DBTools.Themes
BrushKeys
All theme brushes are defined using ComponentResourceKey for reliable cross-template resolution:
public static class BrushKeys
{
// Brand colors
public static ComponentResourceKey Primary => new(typeof(BrushKeys), "Brush.Primary");
public static ComponentResourceKey Secondary => new(typeof(BrushKeys), "Brush.Secondary");
// Surface colors
public static ComponentResourceKey Paper => new(typeof(BrushKeys), "Brush.Paper");
public static ComponentResourceKey Surface => new(typeof(BrushKeys), "Brush.Surface");
public static ComponentResourceKey CardSurface => new(typeof(BrushKeys), "Brush.CardSurface");
// Text colors (WCAG AA compliant)
public static ComponentResourceKey Body => new(typeof(BrushKeys), "Brush.Body");
public static ComponentResourceKey TextSecondary => new(typeof(BrushKeys), "Brush.TextSecondary");
public static ComponentResourceKey TextMuted => new(typeof(BrushKeys), "Brush.TextMuted");
// Status colors
public static ComponentResourceKey Success => new(typeof(BrushKeys), "Brush.Success");
public static ComponentResourceKey Warning => new(typeof(BrushKeys), "Brush.Warning");
public static ComponentResourceKey Error => new(typeof(BrushKeys), "Brush.Error");
// DataGrid specific
public static ComponentResourceKey DataGridRowSelected => new(typeof(BrushKeys), "Brush.DataGrid.RowSelected");
public static ComponentResourceKey DataGridAccentStripe => new(typeof(BrushKeys), "Brush.DataGrid.AccentStripe");
// ... 130+ additional keys
}
Source:
src/DBTools.Themes/BrushKeys.cs:9-139
Usage in XAML:
<Window xmlns:theme="clr-namespace:DBTools.Themes;assembly=DBTools.Themes">
<Border Background="{DynamicResource {x:Static theme:BrushKeys.Surface}}">
<TextBlock Text="Hello"
Foreground="{DynamicResource {x:Static theme:BrushKeys.Body}}"/>
</Border>
</Window>
Design Tokens
Standard spacing, sizing, and typography values for consistent layouts:
<!-- Spacing -->
<Thickness x:Key="Spacing4">4</Thickness>
<Thickness x:Key="Spacing8">8</Thickness>
<Thickness x:Key="Spacing16">16</Thickness>
<!-- Padding -->
<Thickness x:Key="Pad8">8</Thickness>
<Thickness x:Key="Card.Padding">16,12</Thickness>
<!-- Corner radius -->
<CornerRadius x:Key="Radius4">4</CornerRadius>
<CornerRadius x:Key="Radius8">8</CornerRadius>
<!-- Typography -->
<sys:Double x:Key="FontSize.Body">13</sys:Double>
<sys:Double x:Key="FontSize.Title">16</sys:Double>
<sys:Double x:Key="FontSize.Header">20</sys:Double>
Source:
src/DBTools.Themes/Themes/App.Tokens.xaml:5-72
Window Base Classes
DbtWindowBase
Standard modal window with automatic theme loading and progress overlay:
public class DbtWindowBase : Window, IWindowWithOwnerProvider, IThemeOptOut
{
public DbtWindowBase()
{
DbtWindowInitHelper.Initialize(this, nameof(DbtWindowBase));
}
public IWindowOwnerProvider? OwnerProvider { get; set; }
public bool UseDefaultTheme { get; set; }
}
Source:
src/DBTools.Core/UI/Windows/DbtWindowBase.cs:27-47
DbtRibbonWindowBase
Ribbon window for tools with tab-based navigation:
public class DbtRibbonWindowBase : RibbonWindow, IWindowWithOwnerProvider, IThemeOptOut
{
public DbtRibbonWindowBase()
{
DbtWindowInitHelper.Initialize(this, nameof(DbtRibbonWindowBase));
}
}
Source:
src/DBTools.Core/UI/Windows/DbtRibbonWindowBase.cs:15-35
What window bases provide:
- Window-scoped theme loading (avoids cross-add-in conflicts)
- Revit window ownership binding
- Progress overlay integration
- UI error protection
- DBTools icon
Source:
src/DBTools.Core/UI/Windows/DbtWindowInitHelper.cs:44-78
DI Patterns
Creating a Tool Module
Every tool implements DbtToolModule to register its services:
public sealed class GmToolModule : DbtToolModule
{
public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
{
Guard.NotNull(services, nameof(services));
Guard.NotNull(manifest, nameof(manifest));
services.AddGmServices(); // Extension method for cleaner registration
}
}
Source:
src/Tools/Common/GM/GmToolModule.cs:10-18
DbtToolModule Base Class
public abstract class DbtToolModule
{
/// <summary>Registers tool settings types (Options pattern).</summary>
public virtual void RegisterSettings(IServiceCollection services,
IConfiguration configuration, DbtToolManifest manifest) { }
/// <summary>Registers services required by this tool module.</summary>
public virtual void RegisterServices(IServiceCollection services,
DbtToolManifest manifest) { }
/// <summary>Registers settings pack definitions for the Settings UI.</summary>
public virtual void RegisterSettingsPacks(IServiceCollection services,
DbtToolManifest manifest) { }
/// <summary>Registers hook handlers with the tool registry.</summary>
public virtual void RegisterHooks(DbtToolRegistry registry,
DbtToolManifest manifest) { }
}
Source:
src/DBTools.Core/Tools/DbtToolModule.cs:9-49
Service Registration Patterns
public static class GmServiceCollectionExtensions
{
public static IServiceCollection AddGm(this IServiceCollection services)
{
// === Pure/shared logic (scoped) ===
services.AddScoped<IProjectLifecycleService, ProjectLifecycleService>();
services.AddScoped<INamingSimilarityService, NamingSimilarityService>();
// === Services with complex initialization ===
services.AddScoped<IGmMappingService>(sp =>
{
var logger = sp.GetRequiredService<ILogger<GmMappingService>>();
var usage = sp.GetRequiredService<IUsageIndexService>();
var tx = sp.GetRequiredService<ITransactionRunner>();
// ... resolve other dependencies
return new GmMappingService(logger, usage, tx, writers);
});
// === Revit infrastructure (scoped) ===
services.AddScoped<ICategoryService, CategoryService>();
services.AddScoped<IElementQuery, ElementQueryService>();
// === UI services ===
services.AddScoped<ICommitReviewWindowService, CommitReviewWindowService>();
return services;
}
}
Source:
src/Tools/Common/GM/Shell/DI/Services.cs:28-108
Resolving Services in Commands
protected override async Task RunAsync(IDbtToolContext context)
{
// Single service
var alerts = context.Resolve<IAlertService>();
// Multiple services (tuple pattern)
var (tx, groups, mapping, planner, lifecycle) = context.Resolve<
ITransactionRunner,
ITransactionGroupService,
IGmMappingService,
IGmPlanningService,
IProjectLifecycleService>();
}
Source:
src/Tools/Common/GM/Features/GmCommand.cs:59-91
Best Practices
1. Always Use ISafeExecutor at Entrypoints
// DO: Inherit from DbtToolCommand (uses ISafeExecutor internally)
public sealed class MyCommand : DbtToolCommand
{
protected override async Task RunAsync(IDbtToolContext context)
{
// ISafeExecutor wraps this automatically
}
}
// DON'T: Create IExternalCommand without error handling
public class BadCommand : IExternalCommand
{
public Result Execute(...)
{
// No error handling - will crash Revit
}
}
Source:
src/DBTools.Core/Revit/Execution/DbtToolCommand.cs:35-126
2. Prefer Scoped Services for Revit-Dependent Code
// DO: Scoped registration for services that need Revit context
services.AddScoped<ICategoryService, CategoryService>();
services.AddScoped<IElementQuery, ElementQueryService>();
// DON'T: Singleton for services that capture Document references
services.AddSingleton<IBadService, ServiceThatHoldsDocument>(); // Memory leak!
3. Use Transaction Services, Not Raw Transactions
// DO: Use ITransactionRunner
await _tx.RunAsync("Create Wall", doc =>
{
Wall.Create(doc, curve, levelId, false);
});
// DON'T: Create raw Transaction objects
using var tx = new Transaction(doc, "Create Wall");
tx.Start();
Wall.Create(doc, curve, levelId, false);
tx.Commit(); // Manual error handling required
4. Reference Theme Brushes via BrushKeys
<!-- DO: Use BrushKeys for reliable resolution -->
<Border Background="{DynamicResource {x:Static theme:BrushKeys.Surface}}"/>
<!-- DON'T: Use string keys (fragile) -->
<Border Background="{DynamicResource Brush.Surface}"/>
5. Inherit from Window Base Classes
// DO: Inherit from DbtWindowBase
public partial class MyToolWindow : DbtWindowBase
{
// Theme, progress overlay, error protection all included
}
// DON'T: Use raw Window
public partial class MyToolWindow : Window
{
// No theme, no error protection, no Revit ownership
}
Anti-Patterns to Avoid
1. Service Locator Pattern
// DON'T: Use AppRuntime.Resolve outside of command constructors
public class BadService
{
public void DoWork()
{
var alerts = AppRuntime.Resolve<IAlertService>(); // Hidden dependency
}
}
// DO: Inject via constructor
public class GoodService
{
private readonly IAlertService _alerts;
public GoodService(IAlertService alerts)
{
_alerts = alerts;
}
}
2. Silent Failures
// DON'T: Swallow exceptions
try { DoWork(); }
catch { /* silent failure */ }
// DON'T: Return defaults pretending success
catch (Exception) { return new List<Element>(); }
// DO: Let ISafeExecutor handle errors
await executor.RunAsync(async () =>
{
DoWork(); // Exception propagates to error handling
}, logger, notifier);
3. Capturing Document References
// DON'T: Hold Document references in singleton services
public class BadService
{
private readonly Document _doc; // Will become stale!
public BadService(Document doc) => _doc = doc;
}
// DO: Accept Document per-call
public class GoodService
{
public void DoWork(Document doc)
{
// Fresh reference each call
}
}
4. Raw Console/MessageBox Output
// DON'T: Use Console or MessageBox
Console.WriteLine("Debug info");
MessageBox.Show("Error occurred");
// DO: Use logging and IAlertService
_logger.LogDebug("Debug info");
_alerts.Show(new AlertRequest("Error", new MessageBodyViewModel("Error occurred")));
5. Bypassing Theme System
// DON'T: Use hard-coded colors
<Border Background="#222228"/>
// DO: Use theme brushes
<Border Background="{DynamicResource {x:Static theme:BrushKeys.Surface}}"/>
Related Documentation
- DBTools.Core Project - Complete Core API reference
- Theme System - Detailed theming documentation
- Architecture Overview - System-wide architecture
Quick Reference Table
| Need | Service | Lifetime |
|---|---|---|
| Error handling at entrypoints | ISafeExecutor |
Singleton |
| Model modifications | ITransactionRunner |
Scoped |
| Batch undo operations | ITransactionGroupService |
Scoped |
| User dialogs | IAlertService |
Singleton |
| Logging | ILogger<T> |
Singleton |
| Revit context | IRevitRunScope |
Scoped |
| Thread-safe API access | IRevitCallGate |
Scoped |
| Settings access | ISettingsProvider |
Singleton |
| Reactive settings | IOptionsMonitor<T> |
Singleton |