Table of Contents

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:

  1. Lives in its own folder under src/Tools/
  2. Declares metadata via manifest.yml
  3. Implements a DbtToolModule subclass for DI registration
  4. Provides one or more DbtToolCommand implementations
  5. 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:

  1. Ensure you can build the solution: bash build.sh
  2. Understand the target Revit versions (net48 for 2024, net8.0-windows for 2025+)
  3. Decide on a tool category: Common, Structural, Testing, or new category
  4. 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/ and src/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-68 and src/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 iconBaseKey in 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.yml with unique id and valid ribbonTools
  • [ ] Created <ToolName>ToolModule.cs extending DbtToolModule
  • [ ] Implemented command(s) extending DbtToolCommand
  • [ ] Added icon(s) in Assets/ folder
  • [ ] Created .csproj file 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

Cross-References