Creating New Tools in DBTools
This guide covers the complete process of creating a new tool for the DBTools Revit add-in platform. By the end, you'll understand the tool module system, manifest configuration, command implementation, and UI patterns.
Overview
DBTools uses a manifest-driven plugin architecture. Each tool:
- Lives in its own folder under
src/Tools/ - Declares metadata via
manifest.yml - Implements a
DbtToolModulesubclass for DI registration - Provides one or more
DbtToolCommandimplementations - Optionally includes WPF UI, settings, and hooks
Tool Module Lifecycle
stateDiagram-v2
[*] --> Discovered: Manifest scan at startup
Discovered --> Instantiated: Reflection creates module
Instantiated --> Registered: RegisterSettings()<br/>RegisterServices()<br/>RegisterHooks()
Registered --> Active: Ribbon composed
Active --> Active: Commands executed
Active --> [*]: OnShutdown
note right of Discovered
DbtToolManifestLoader scans
embedded manifest.yml files
end note
note right of Registered
DI container configured,
hooks attached
end note
Key architectural principle: Tool source files are file-linked into DBTools.App at build time. Individual tool .csproj files exist for IDE navigation and per-tool test projects, but the final DBTools.dll assembly contains all tool code compiled together.
Source:
src/DBTools.App/DBTools.App.csproj:61-78
Prerequisites
Before creating a new tool:
- Ensure you can build the solution:
bash build.sh - Understand the target Revit versions (net48 for 2024, net8.0-windows for 2025+)
- Decide on a tool category:
Common,Structural,Testing, or new category - Choose a unique tool ID (e.g.,
DBTools.MyTool)
Step 1: Directory Structure
Create your tool folder under the appropriate category:
src/Tools/
Common/ # General-purpose tools
GM/ # Global Mapper (complex example)
ElevationTags/ # Simple example
Structural/ # Structural engineering tools
SGT/ # Super Girt Tool (complex example)
FramingJoins/ # Simple example
Testing/ # Testing/debug tools
Minimal Tool Structure
src/Tools/<Category>/<ToolName>/
Assets/
my_tool.png # 32x32 icon for ribbon (required)
my_tool_16.png # 16x16 small icon (optional)
Features/
MyToolCommand.cs # IExternalCommand implementation
manifest.yml # Tool metadata and ribbon config
<ToolName>ToolModule.cs # DI registration
DBTools.<ToolName>.csproj # Project file (for IDE/tests)
Complex Tool Structure (with UI)
src/Tools/<Category>/<ToolName>/
Assets/
*.png # Icons
Bootstrap/
ServiceExtensions.cs # DI extension methods
Features/
Commands/
MyToolCommand.cs
Logic/
MyService.cs
IMyService.cs
Shell/
DI/
Services.cs # Detailed DI registrations
DesignTime/
MyWindowDesignTimeViewModel.cs
UI/
Views/
MyWindow.xaml
MyWindow.xaml.cs
ViewModels/
MyWindowViewModel.cs
Shared/
Models/
Contracts/
Tests/ # Separate test project folder
DBTools.<ToolName>.Tests.csproj
Properties/
DesignTimeResources.xaml # For XAML designer
manifest.yml
<ToolName>ToolModule.cs
DBTools.<ToolName>.csproj
Source: Reference structure from
src/Tools/Common/GM/andsrc/Tools/Structural/SGT/
Step 2: Project File (.csproj)
Create a project file for IDE support and test isolation. The project file is NOT used for the main build (files are linked into DBTools.App), but it enables:
- IntelliSense and navigation
- Per-tool test projects
- Design-time XAML preview
Minimal Project File
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(DBT_RevitTargetFrameworks)</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>DBTools.MyTool</RootNamespace>
</PropertyGroup>
<!-- Exclude Revit-dependent code during design-time builds -->
<ItemGroup Condition="'$(DesignTimeBuild)'=='true' or '$(DBT_IsSandboxBuild)'=='true'">
<Compile Remove="**\Revit\**\*.cs" />
</ItemGroup>
<ItemGroup Condition="'$(DBT_IsSandboxBuild)'=='true'">
<Compile Remove="Features\MyToolCommand.cs" />
<Compile Remove="MyToolToolModule.cs" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Tests\**\*.cs" />
</ItemGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='net48'">
<NoWarn>$(NoWarn);CS0618;MA0038;MA0051;MA0048;MA0016;MA0098;MA0004;CA1068;CA1707;CA1725</NoWarn>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='net8.0-windows'">
<NoWarn>$(NoWarn);CS0618;MA0038;MA0048;MA0016;MA0051;MA0098;MA0004;CA1707;CA1725;CA1068;MSB3277</NoWarn>
<Nullable>enable</Nullable>
<SupportedOSPlatformVersion>6.1</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\DBTools.Core\DBTools.Core.csproj" />
</ItemGroup>
<!-- Embed icons -->
<ItemGroup>
<EmbeddedResource Include="Assets\\*.png">
<Link>Resources\\Icons\\%(Filename)%(Extension)</Link>
</EmbeddedResource>
</ItemGroup>
<!-- Required packages -->
<ItemGroup>
<PackageReference Include="ricaun.Revit.UI.Tasks" />
</ItemGroup>
<!-- Revit API references -->
<ItemGroup Condition="'$(DesignTimeBuild)'!='true' and '$(DBT_IsSandboxBuild)'!='true' and '$(TargetFramework)'=='net48'">
<Reference Include="RevitAPI">
<HintPath>$(REVIT2024_DIR)\RevitAPI.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="RevitAPIUI">
<HintPath>$(REVIT2024_DIR)\RevitAPIUI.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup Condition="'$(DesignTimeBuild)'!='true' and '$(DBT_IsSandboxBuild)'!='true' and '$(TargetFramework)'=='net8.0-windows'">
<Reference Include="RevitAPI" Condition="'$(REVIT_NET8_DIR)'!='' and Exists('$(REVIT_NET8_DIR)\\RevitAPI.dll')">
<HintPath>$(REVIT_NET8_DIR)\RevitAPI.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="RevitAPIUI" Condition="'$(REVIT_NET8_DIR)'!='' and Exists('$(REVIT_NET8_DIR)\\RevitAPIUI.dll')">
<HintPath>$(REVIT_NET8_DIR)\RevitAPIUI.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
Source:
src/Tools/Structural/FramingJoins/DBTools.Structural.FramingJoins.csproj:1-69
For Tools with WPF UI
Add the following to your .csproj:
<PropertyGroup>
<UseWPF>true</UseWPF>
</PropertyGroup>
<!-- Additional packages for MVVM -->
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" />
</ItemGroup>
<!-- DBTools uses standard WPF windows + window-scoped theming (DBTools.Themes + DBTools.HandyControl). -->
<!-- Design-time resources -->
<ItemGroup>
<Page Update="Properties\DesignTimeResources.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
<ContainsDesignTimeResources>true</ContainsDesignTimeResources>
</Page>
</ItemGroup>
Source:
src/Tools/Common/GM/DBTools.GM.csproj:1-94
Step 3: manifest.yml (Complete Schema)
The manifest file declares your tool's metadata, ribbon configuration, and optional settings.
Complete Schema Reference
# Required: Unique tool identifier
id: DBTools.MyTool
# Required: Assembly name (without .dll) - always "DBTools" for main tools
assembly: DBTools
# Required: Fully-qualified type name of DbtToolModule subclass
moduleType: DBTools.MyTool.MyToolToolModule
# Required: Load order (lower = earlier). Use 0 unless you need ordering.
order: 0
# Optional: Sandbox windows for design-time preview
sandboxWindows:
- id: DBTools.MyTool.Main
displayName: "My Tool"
group: "Common" # or "Structural", "Testing"
windowType: "DBTools.MyTool.Shell.UI.Views.MyToolWindow"
designTimeViewModelType: "DBTools.MyTool.Shell.DesignTime.MyToolDesignTimeViewModel"
# Required: Tool configuration
tool:
# Optional: Settings configuration
settings:
configSection: Tools.MyTool # Section in settings.{YEAR}.json
# Optional: Settings packs for Settings window
settingsPacks:
- key: common.my_tool
title: "My Tool"
warnings:
- id: core.my_tool.warning
title: "My Tool Disabled"
message: "Tool is disabled due to a configuration issue."
disableTools:
- DBTools.MyToolCommand
# Required: Ribbon button definitions
ribbonTools:
- internalName: DBTools.MyToolCommand
commandType: DBTools.MyTool.Features.MyToolCommand
availabilityType: DBTools.App.Tools.Availability.DbtDocumentAvailability
runProfile: InlineUi
displayText: "My Tool"
iconBaseKey: my_tool
tooltip: "Launch My Tool"
controlKind: PushButton
order: 50
Manifest Properties Reference
| Property | Required | Description |
|---|---|---|
id |
Yes | Unique identifier (e.g., DBTools.MyTool) |
assembly |
Yes | Assembly name without .dll (always DBTools) |
moduleType |
Yes | Full type name of DbtToolModule subclass |
order |
Yes | Load order (0 for most tools) |
sandboxWindows |
No | Design-time preview windows |
tool.settings.configSection |
No | settings.{YEAR}.json section for Options pattern |
tool.settingsPacks |
No | Settings window configuration |
tool.ribbonTools |
Yes | At least one ribbon button definition |
Ribbon Tool Properties
| Property | Required | Description |
|---|---|---|
internalName |
Yes | Unique button identifier |
commandType |
Yes | Full type name of command class |
availabilityType |
No | IExternalCommandAvailability implementation |
runProfile |
No | InlineUi (default), BackgroundWork, etc. |
displayText |
Yes | Button label (use \n for line breaks) |
iconBaseKey |
Yes | Icon filename without extension |
tooltip |
Yes | Button tooltip text |
controlKind |
Yes | PushButton, SplitButtonItem, PulldownButtonItem, StackedButtonItem |
splitGroup |
No | Group name for split/pulldown/stacked buttons |
order |
Yes | Button order within panel |
groupDisplayText |
No | Header text for pulldown groups |
groupTooltip |
No | Tooltip for pulldown header |
groupIconBaseKey |
No | Icon for pulldown header |
Source:
src/DBTools.Core/Tools/DbtToolManifest.cs:1-68andsrc/DBTools.Core/Tools/DbtToolManifestLoader.cs:287-307
Example: Simple Tool
id: DBTools.Structural.FramingJoins
assembly: DBTools
moduleType: DBTools.Structural.FramingJoins.FramingJoinsToolModule
order: 0
tool:
ribbonTools:
- internalName: DBTools.AllowJoinSelected
commandType: DBTools.Structural.FramingJoins.AllowJoinSelectedCommand
availabilityType: DBTools.App.Tools.Availability.DbtStructuralFramingSelectionAvailability
runProfile: InlineUi
displayText: "Allow Join"
iconBaseKey: allow_join
tooltip: "Allow structural framing joins for selected elements"
controlKind: SplitButtonItem
splitGroup: framing_joins
order: 0
- internalName: DBTools.DisallowJoinSelected
commandType: DBTools.Structural.FramingJoins.DisallowJoinSelectedCommand
availabilityType: DBTools.App.Tools.Availability.DbtStructuralFramingSelectionAvailability
runProfile: InlineUi
displayText: "Disallow Join"
iconBaseKey: disallow_join
tooltip: "Disallow structural framing joins for selected elements"
controlKind: SplitButtonItem
splitGroup: framing_joins
order: 1
Source:
src/Tools/Structural/FramingJoins/manifest.yml:1-27
Example: Complex Tool with UI
id: DBTools.GM
assembly: DBTools
moduleType: DBTools.GM.GmToolModule
order: 0
sandboxWindows:
- id: DBTools.GM.Main
displayName: "Global Mapper"
group: "Common"
windowType: "DBTools.GM.Shell.UI.Views.GmWindow"
designTimeViewModelType: "DBTools.GM.Shell.DesignTime.GmShellDesignTimeViewModel"
- id: DBTools.GM.CommitReview
displayName: "Global Mapper - Commit Review"
group: "Common"
windowType: "DBTools.GM.Shell.Views.CommitReviewWindow"
designTimeViewModelType: "DBTools.GM.Shell.DesignTime.CommitReviewDesignTimeViewModel"
tool:
ribbonTools:
- internalName: DBTools.GM
commandType: DBTools.GM.Features.GmCommand
availabilityType: DBTools.App.Tools.Availability.DbtDocumentAvailability
runProfile: InlineUi
displayText: "Global Mapper"
iconBaseKey: gm
tooltip: "Open Global Mapper"
controlKind: PushButton
order: 30
Source:
src/Tools/Common/GM/manifest.yml:1-27
Step 4: ToolModule Class
The DbtToolModule subclass registers services with the DI container.
Minimal Implementation
For simple tools with no custom services:
using DBTools.Core.Tools;
namespace DBTools.Structural.FramingJoins;
public sealed class FramingJoinsToolModule : DbtToolModule
{
}
Source:
src/Tools/Structural/FramingJoins/FramingJoinsToolModule.cs:1-8
With Service Registration
using DBTools.Core.Tools;
using DBTools.Core.Compat;
using Microsoft.Extensions.DependencyInjection;
namespace DBTools.MyTool;
public sealed class MyToolToolModule : DbtToolModule
{
public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
{
Guard.NotNull(services, nameof(services));
Guard.NotNull(manifest, nameof(manifest));
// Register tool-specific services
services.AddScoped<IMyService, MyService>();
services.AddScoped<MyOtherService>();
}
}
Source:
src/Tools/Common/GM/GmToolModule.cs:1-19
With Hooks
using DBTools.Core.Tools;
using DBTools.Core.Compat;
using Microsoft.Extensions.DependencyInjection;
namespace DBTools.SGT;
public sealed class SgtToolModule : DbtToolModule
{
public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
{
Guard.NotNull(services, nameof(services));
Guard.NotNull(manifest, nameof(manifest));
services.AddSgtServices();
services.AddSingleton<SgtContextualRibbonInjector, SgtContextualRibbonInjector>();
}
public override void RegisterHooks(DbtToolRegistry registry, DbtToolManifest manifest)
{
Guard.NotNull(registry, nameof(registry));
Guard.NotNull(manifest, nameof(manifest));
// Register contextual ribbon hook
registry.RegisterHook<IContextualRibbonInjector, SgtContextualRibbonInjector>();
}
}
Source:
src/Tools/Structural/SGT/SgtToolModule.cs:1-27
DbtToolModule Virtual Methods
| Method | Purpose |
|---|---|
RegisterSettings |
Register Options pattern settings types |
RegisterServices |
Register services for DI container |
RegisterSettingsPacks |
Register settings pack definitions |
RegisterHooks |
Register event hooks (ViewActivated, ContextualRibbon) |
Source:
src/DBTools.Core/Tools/DbtToolModule.cs:1-50
Step 5: Command Implementation
Commands implement DbtToolCommand (not IExternalCommand directly) to get automatic error handling, logging, and run scope management.
Basic Command Structure
using System;
using System.Threading.Tasks;
using Autodesk.Revit.Attributes;
using DBTools.Core.Compat;
using DBTools.Core.Revit.Execution;
using Microsoft.Extensions.Logging;
namespace DBTools.MyTool.Features;
[Transaction(TransactionMode.Manual)]
public sealed class MyToolCommand : DbtToolCommand
{
protected override async Task RunAsync(IDbtToolContext context)
{
Guard.EnsureNotNull(context, nameof(context));
var logger = context.Logger;
var doc = context.Document;
var uidoc = context.UIDocument;
logger.LogDebug("[MyTool] Starting execution");
// Your tool logic here
await Task.CompletedTask.ConfigureAwait(false);
}
protected override SafeExecutor.SafeExecuteOptions CreateExecuteOptions(
Autodesk.Revit.UI.ExternalCommandData commandData)
{
return new SafeExecutor.SafeExecuteOptions
{
Name = "My Tool",
ShowCompletionToUser = true,
SuppressCompletionBanner = false
};
}
}
With Transaction
[Transaction(TransactionMode.Manual)]
public sealed class AllowJoinSelectedCommand : DbtToolCommand
{
protected override async Task RunAsync(IDbtToolContext context)
{
Guard.EnsureNotNull(context, nameof(context));
var uidoc = context.UIDocument;
var doc = context.Document;
var logger = context.Logger;
// Get selection
var selectedIds = uidoc.Selection.GetElementIds();
if (selectedIds == null || selectedIds.Count == 0)
{
throw new InvalidOperationException("Please select elements.");
}
// Create transaction runner
var gate = new ModalInlineCallGate(context.UIApplication);
var tx = new DBTools.Core.Revit.Transactions.CallGateTransactionRunner(gate);
int modified = 0;
await tx.RunAsync(doc, "DB Tools - Allow Join", d =>
{
foreach (var id in selectedIds)
{
var elem = doc.GetElement(id) as FamilyInstance;
if (elem == null) continue;
StructuralFramingUtils.AllowJoinAtEnd(elem, 0);
StructuralFramingUtils.AllowJoinAtEnd(elem, 1);
modified++;
}
}).ConfigureAwait(false);
logger.LogDebug("[AllowJoin] Modified {Count} elements.", modified);
// Show result
var alerts = context.Resolve<IAlertService>();
_ = alerts.Show(new AlertRequest(
"Allow Join",
new MessageBodyViewModel($"Allowed joins on {modified} element(s)."))
{
Buttons = new[] { new AlertButtonSpec("ok", "OK") { IsDefault = true } }
});
}
}
Source:
src/Tools/Structural/FramingJoins/Features/AllowJoinSelectedCommand.cs:1-89
IDbtToolContext Properties
| Property | Description |
|---|---|
UIApplication |
Revit UIApplication |
UIDocument |
Active UIDocument |
Document |
Active Document |
Logger |
ILogger for the command |
Resolve<T>() |
Resolve service from DI |
Source:
src/DBTools.Core/Revit/Execution/DbtToolCommand.cs:1-239
Step 6: UI Implementation (MVVM Pattern)
For tools with UI, follow the MVVM pattern with DBTools conventions.
Window Base Classes
| Class | Use Case |
|---|---|
DbtWindowBase |
Modal dialogs |
DbtRibbonWindowBase |
Windows with Fluent Ribbon |
Source:
src/DBTools.Core/UI/Windows/DbtWindowBase.cs:1-48
XAML Window Template
<?xml version="1.0" encoding="utf-8"?>
<core:DbtWindowBase
x:Class="DBTools.MyTool.Shell.UI.Views.MyToolWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:core="clr-namespace:DBTools.Core.UI.Windows;assembly=DBTools.Core"
xmlns:theme="clr-namespace:DBTools.Themes;assembly=DBTools.Themes"
xmlns:dt="clr-namespace:DBTools.MyTool.Shell.DesignTime"
d:DataContext="{d:DesignInstance Type=dt:MyToolDesignTimeViewModel, IsDesignTimeCreatable=True}"
Title="My Tool"
Width="800"
Height="600"
MinWidth="600"
MinHeight="400"
ResizeMode="CanResize"
mc:Ignorable="d">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/DBTools.Themes;component/Themes/App.Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<!-- Your UI content -->
</Grid>
</core:DbtWindowBase>
Source:
src/Tools/Common/GM/Shell/UI/Views/GmWindow.xaml:1-26
ViewModel Pattern
Use CommunityToolkit.Mvvm for MVVM implementation:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace DBTools.MyTool.Shell.UI.ViewModels;
public partial class MyToolWindowViewModel : ObservableObject
{
[ObservableProperty]
private string _searchText = string.Empty;
[ObservableProperty]
private bool _isBusy;
[RelayCommand]
private async Task ExecuteAsync()
{
IsBusy = true;
try
{
// Do work
}
finally
{
IsBusy = false;
}
}
}
Design-Time ViewModel
Create a design-time ViewModel for XAML preview:
using System.Windows.Input;
using DBTools.Core.UI;
namespace DBTools.MyTool.Shell.DesignTime;
/// <summary>
/// Design-time ViewModel for XAML Designer support.
/// DO NOT instantiate at runtime.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public sealed class MyToolDesignTimeViewModel
{
// Commands - use DesignTimeRelayCommand for all
public ICommand ExecuteCommand => DesignTimeRelayCommand.Instance;
public ICommand CloseCommand => DesignTimeRelayCommand.Instance;
// Properties with sample data
public string SearchText { get; set; } = "Sample search";
public bool IsBusy => false;
public MyToolDesignTimeViewModel()
{
// Initialize sample data for designer preview
}
}
Source:
src/Tools/Common/GM/Shell/DesignTime/GmShellDesignTimeViewModel.cs:1-250
Step 7: Settings (Optional)
Defining Settings
Create a settings class:
namespace DBTools.MyTool.Settings;
public sealed class MyToolSettings
{
public bool EnableFeatureX { get; set; } = true;
public int Threshold { get; set; } = 50;
}
Registering Settings
In your ToolModule:
public override void RegisterSettings(
IServiceCollection services,
IConfiguration configuration,
DbtToolManifest manifest)
{
Guard.NotNull(services, nameof(services));
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(manifest, nameof(manifest));
var section = manifest.Tool?.Settings?.ConfigSection ?? "Tools.MyTool";
services.Configure<MyToolSettings>(configuration.GetSection(section));
}
Using Settings
public sealed class MyService
{
private readonly MyToolSettings _settings;
public MyService(IOptionsMonitor<MyToolSettings> options)
{
_settings = options.CurrentValue;
}
}
settings.{YEAR}.json Structure
{
"Tools": {
"MyTool": {
"EnableFeatureX": true,
"Threshold": 75
}
}
}
Step 8: Hooks (Optional)
Available Hook Interfaces
| Interface | Purpose |
|---|---|
IViewActivatedHookHandler |
React to view changes |
IContextualRibbonInjector |
Add contextual ribbon tabs |
Source:
src/DBTools.Core/Tools/DbtHookHost.cs:14-23
ViewActivated Hook
using DBTools.Core.Tools;
using Autodesk.Revit.UI;
using Autodesk.Revit.UI.Events;
namespace DBTools.MyTool.Features.Hooks;
public sealed class MyViewActivatedHandler : IViewActivatedHookHandler
{
public Task OnViewActivatedAsync(
UIControlledApplication application,
ViewActivatedEventArgs args,
CancellationToken ct)
{
// React to view changes
return Task.CompletedTask;
}
}
Register in your ToolModule:
public override void RegisterHooks(DbtToolRegistry registry, DbtToolManifest manifest)
{
registry.RegisterHook<IViewActivatedHookHandler, MyViewActivatedHandler>();
}
public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
{
services.AddSingleton<MyViewActivatedHandler>();
}
Step 9: Availability Types
Availability types control when ribbon buttons are enabled.
Built-in Availability Types
| Type | Description |
|---|---|
DbtDocumentAvailability |
Requires an open document |
DbtActiveViewAvailability |
Requires an active view |
DbtSelectionAvailability |
Requires selected elements |
Source:
src/DBTools.App/Tools/Availability/DbtDocumentAvailability.cs:1-17
Custom Availability
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
namespace DBTools.App.Tools.Availability;
public sealed class MyCustomAvailability : IExternalCommandAvailability
{
public bool IsCommandAvailable(UIApplication applicationData, CategorySet selectedCategories)
{
var doc = applicationData?.ActiveUIDocument?.Document;
if (doc == null) return false;
// Custom logic
return selectedCategories?.Contains(Category.GetCategory(doc, BuiltInCategory.OST_Walls)) == true;
}
}
Step 10: File Linking (How It Works)
Tool source files are compiled into DBTools.dll via file linking in DBTools.App.csproj:
<!-- Tool Source Files -->
<ItemGroup Label="Tool Source Files">
<Compile Include="..\Tools\**\*.cs"
Exclude="..\Tools\**\Tests\**\*.cs;..\Tools\**\obj\**\*.cs"
Link="Tools\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<!-- Tool XAML Files -->
<ItemGroup Label="Tool XAML Files">
<Page Include="..\Tools\**\*.xaml"
Exclude="..\Tools\**\obj\**\*.xaml;..\Tools\**\Properties\DesignTimeResources.xaml"
Link="Tools\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<!-- Tool Icons -->
<ItemGroup Label="Tool Embedded Assets">
<EmbeddedResource Include="..\Tools\**\Assets\*.png"
Link="Resources\Icons\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<!-- Tool Manifests -->
<ItemGroup>
<EmbeddedResource Include="..\Tools\**\manifest.yml"
LogicalName="DBTools.ToolManifests.%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
Source:
src/DBTools.App/DBTools.App.csproj:61-78, 218-222
Step 11: Testing Setup
Test Project Structure
src/Tools/<Category>/<ToolName>/Tests/
DBTools.<ToolName>.Tests.csproj
MyToolCommandTests.cs
MyServiceTests.cs
Test Project File
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="NSubstitute" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\DBTools.App\DBTools.App.csproj" />
<ProjectReference Include="..\..\..\..\testing\TestSupport\DBTools.TestSupport.csproj" />
</ItemGroup>
</Project>
Running Tests
# Run all tests for a tool
bash invoke-revit-tests.sh --smart --tool MyTool -y 2025
# Run specific test
bash invoke-revit-tests.sh --smart -f "FullyQualifiedName~MyTestName"
Common Patterns and Best Practices
1. Service Extension Pattern
Centralize DI registration in an extension method:
// Bootstrap/MyToolServiceExtensions.cs
namespace DBTools.MyTool.Bootstrap;
public static class MyToolServiceExtensions
{
public static IServiceCollection AddMyToolServices(this IServiceCollection services)
{
Guard.NotNull(services, nameof(services));
services.AddScoped<IMyService, MyService>();
services.AddScoped<MyOtherService>();
return services;
}
}
Then in ToolModule:
services.AddMyToolServices();
Source:
src/Tools/Common/GM/Bootstrap/GmServiceExtensions.cs:1-16
2. Error Handling
Never swallow exceptions. Use ISafeExecutor for top-level error handling (provided by DbtToolCommand):
// DON'T do this:
try { DoWork(); } catch { } // Silent failure
// DO this:
// Exceptions propagate to DbtToolCommand's ISafeExecutor
DoWork(); // Let exceptions propagate
3. Logging
Use the injected logger, not Console.WriteLine:
var logger = context.Logger;
logger.LogDebug("[MyTool] Processing {Count} elements", count);
logger.LogWarning("[MyTool] Skipped invalid element {Id}", elementId);
logger.LogError(ex, "[MyTool] Failed to process");
4. Transactions
Always name transactions descriptively:
await tx.RunAsync(doc, "DB Tools - My Tool Operation", d =>
{
// Modifications here
}).ConfigureAwait(false);
5. Icon Naming
- Main icon:
my_tool.png(32x32) - Small icon:
my_tool_16.png(16x16, optional) - Match the
iconBaseKeyin manifest (without extension)
6. Namespace Conventions
DBTools.<ToolName> # Root namespace
DBTools.<ToolName>.Bootstrap # DI extensions
DBTools.<ToolName>.Features # Commands and feature logic
DBTools.<ToolName>.Features.Commands # Revit commands
DBTools.<ToolName>.Features.Logic # Business logic
DBTools.<ToolName>.Shell # Application shell
DBTools.<ToolName>.Shell.DI # Detailed DI registrations
DBTools.<ToolName>.Shell.UI # Views and ViewModels
DBTools.<ToolName>.Shell.DesignTime # Design-time ViewModels
DBTools.<ToolName>.Shared # Shared types/contracts
Checklist: New Tool Completion
- [ ] Created directory structure under
src/Tools/<Category>/<ToolName>/ - [ ] Added
manifest.ymlwith uniqueidand validribbonTools - [ ] Created
<ToolName>ToolModule.csextendingDbtToolModule - [ ] Implemented command(s) extending
DbtToolCommand - [ ] Added icon(s) in
Assets/folder - [ ] Created
.csprojfile for IDE support - [ ] Build passes:
bash build.sh - [ ] Tool appears in Revit ribbon
- [ ] Command executes successfully
- [ ] (Optional) Created test project
- [ ] (Optional) Added UI with design-time preview