Table of Contents

Theme System

DBTools implements a comprehensive WPF theming system that provides a cohesive dark theme across all windows and controls. The system is designed to work within Revit's add-in environment while avoiding conflicts with other add-ins.

Architecture Overview

+-----------------------------------------------------------------------------------+
|                            DBTools Theme Stack                                     |
+-----------------------------------------------------------------------------------+
|                                                                                    |
|  +-------------------+     +--------------------+     +------------------------+   |
|  | DBTools.Themes    |     | Vendored Libraries |     | DBTools.Core           |   |
|  | (Resources)       |     | (UI Frameworks)    |     | (Theme Loading)        |   |
|  +-------------------+     +--------------------+     +------------------------+   |
|          |                         |                            |                  |
|          v                         v                            v                  |
|  +----------------+     +---------------------+     +------------------------+    |
|  | App.Theme.xaml | --> | DBTools.HandyControl| --> | DbtWindowInitHelper    |    |
|  | (Root Dict)    |     | (vendored)          |    | DbtThemeValidator      |    |
|  +----------------+     +---------------------+     +------------------------+    |
|          |              +---------------------+                                    |
|          v                                                                        |
|  +---------------------------+                                                    |
|  | Merged Dictionaries       |                                                    |
|  | - App.Tokens.xaml         |                                                    |
|  | - App.Brushes.xaml        |                                                    |
|  | - App.Controls.Base.xaml  |                                                    |
|  | - App.DataGrid.xaml       |                                                    |
|  | - App.Menus.xaml          |                                                    |
|  | - App.Components.xaml     |                                                    |
|  +---------------------------+                                                    |
+-----------------------------------------------------------------------------------+

Source: src/DBTools.Themes/Themes/App.Theme.xaml:1-19

Project Structure

The DBTools.Themes project contains all theme resources and references vendored UI libraries:

DBTools.Themes/
+-- DBTools.Themes.csproj       # Project configuration with vendored refs
+-- BrushKeys.cs                # ComponentResourceKey definitions
+-- Assets/
|   +-- (linked) db_tools_icon.png  # Window icon (source: src/Assets/Icons/db_tools_icon.png)
+-- Themes/
    +-- App.Theme.xaml          # Root theme dictionary
    +-- App.Tokens.xaml         # Spacing, sizing, typography tokens
    +-- App.Brushes.xaml        # Color/brush definitions
    +-- App.Converters.xaml     # Value converters
    +-- App.Controls.Base.xaml  # Basic control styles
    +-- App.DataGrid.xaml       # DataGrid-specific styles
    +-- App.Menus.xaml          # Menu/ContextMenu styles
    +-- App.Components.xaml     # Composite component styles

Source: src/DBTools.Themes/DBTools.Themes.csproj:1-63

Vendored UI Libraries

DBTools uses renamed/vendored copies of third-party UI libraries to avoid assembly conflicts with other Revit add-ins (especially pyRevit):

Library Vendored Name Purpose
HandyControl DBTools.HandyControl Base theme framework

Source: src/DBTools.Themes/DBTools.Themes.csproj:54-59

Why Vendored Libraries?

WPF theme assemblies cannot be ILRepack-merged or embedded as resources because they require Assembly.Location for pack:// URI resolution. The vendoring process:

  1. Clones upstream repos to vendor/
  2. Renames assembly names and updates pack URIs
  3. Builds renamed assemblies to .artifacts/vendor/
  4. Deploys alongside DBTools.dll

Source: build-vendored-deps.sh:1-446

Theme Loading Mechanism

Window-Scoped Theming

DBTools applies themes at the window level (not application level) because Revit's Application.Resources are shared across all add-ins. This prevents theme pollution between add-ins.

public static void EnsureWindowScopedTheme(Window window)
{
    WpfUiThread.EnsurePackUriSupport();

    if (!ResourceDictionaryHelper.HasSource(window.Resources, ThemeSource))
    {
        window.Resources.MergedDictionaries.Add(new ResourceDictionary
        {
            Source = new Uri(ThemeSource, UriKind.Absolute)
        });
    }

    DbtThemeValidator.FreezeFreezablesOrThrow(window.Resources);
}

Source: src/DBTools.Core/UI/Windows/DbtWindowInitHelper.cs:91-108

Theme Initialization Flow

  1. Window Constructor: DbtWindowInitHelper.Initialize() is called
  2. Pack URI Support: WpfUiThread.EnsurePackUriSupport() ensures pack:// scheme is registered
  3. Dictionary Merge: App.Theme.xaml is merged into Window.Resources
  4. Resource Freeze: All freezable resources are frozen to prevent runtime leaks
  5. Validation: Theme contract is validated (dictionary order, completeness)

Source: src/DBTools.Core/UI/Windows/DbtWindowInitHelper.cs:44-78

Theme Opt-Out

Windows can opt out of DBTools theming via IThemeOptOut:

public interface IThemeOptOut
{
    bool UseDefaultTheme { get; set; }
}

Source: src/DBTools.Core/UI/Windows/DbtWindowBase.cs:13-21

ResourceDictionary Structure

Root Theme Dictionary

App.Theme.xaml is the root dictionary that merges all theme components:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <ResourceDictionary.MergedDictionaries>
        <!-- HandyControl Foundation -->
        <ResourceDictionary Source="pack://application:,,,/DBTools.HandyControl;component/Themes/SkinDark.xaml"/>
        <ResourceDictionary Source="pack://application:,,,/DBTools.HandyControl;component/Themes/Theme.xaml"/>

        <!-- DBTools Custom Themes -->
        <ResourceDictionary Source="App.Tokens.xaml"/>
        <ResourceDictionary Source="App.Brushes.xaml"/>
        <ResourceDictionary Source="App.Converters.xaml"/>
        <ResourceDictionary Source="App.Controls.Base.xaml"/>
        <ResourceDictionary Source="App.DataGrid.xaml"/>
        <ResourceDictionary Source="App.Menus.xaml"/>
        <ResourceDictionary Source="App.Components.xaml"/>
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Source: src/DBTools.Themes/Themes/App.Theme.xaml:1-19

Dictionary Order Contract

The merged dictionary order is locked and validated at runtime:

private static readonly IReadOnlyList<string> ExpectedMergedDictionarySources = new[]
{
    "pack://application:,,,/DBTools.HandyControl;component/Themes/SkinDark.xaml",
    "pack://application:,,,/DBTools.HandyControl;component/Themes/Theme.xaml",
    "pack://application:,,,/DBTools.Themes;component/Themes/App.Tokens.xaml",
    "pack://application:,,,/DBTools.Themes;component/Themes/App.Brushes.xaml",
    // ... additional dictionaries
};

Source: src/DBTools.Core/UI/Theming/DbtThemeValidator.cs:20-33

Design Tokens

Spacing Tokens

Standard spacing values for consistent layouts:

<Thickness x:Key="Spacing4">4</Thickness>
<Thickness x:Key="Spacing8">8</Thickness>
<Thickness x:Key="Spacing12">12</Thickness>
<Thickness x:Key="Spacing16">16</Thickness>
<Thickness x:Key="Spacing32">32</Thickness>

<!-- Scalar variants for non-Thickness contexts -->
<sys:Double x:Key="Spacing8.Value">8</sys:Double>

Source: src/DBTools.Themes/Themes/App.Tokens.xaml:5-18

Typography Tokens

<sys:Double x:Key="FontSize.Caption">11</sys:Double>
<sys:Double x:Key="FontSize.Body.Small">12</sys:Double>
<sys:Double x:Key="FontSize.Body">13</sys:Double>
<sys:Double x:Key="FontSize.Subtitle">14</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:66-72

Corner Radius Tokens

<CornerRadius x:Key="Radius4">4</CornerRadius>
<CornerRadius x:Key="Radius6">6</CornerRadius>
<CornerRadius x:Key="Radius8">8</CornerRadius>
<CornerRadius x:Key="Radius12">12</CornerRadius>

Source: src/DBTools.Themes/Themes/App.Tokens.xaml:43-46

Color System

BrushKeys (ComponentResourceKeys)

All brushes are defined using ComponentResourceKey to enable reliable cross-template resolution:

public static class BrushKeys
{
    public static ComponentResourceKey Primary => new(typeof(BrushKeys), "Brush.Primary");
    public static ComponentResourceKey PrimaryLight => new(typeof(BrushKeys), "Brush.PrimaryLight");
    public static ComponentResourceKey Paper => new(typeof(BrushKeys), "Brush.Paper");
    public static ComponentResourceKey Surface => new(typeof(BrushKeys), "Brush.Surface");
    public static ComponentResourceKey Body => new(typeof(BrushKeys), "Brush.Body");
    // ... 130+ additional keys
}

Source: src/DBTools.Themes/BrushKeys.cs:1-139

Brand Colors

DBTools uses a blue/gold brand palette:

<!-- Core Brand Colors -->
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Primary}" Color="#FF1946B9"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.PrimaryLight}" Color="#FF3D6AD4"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Secondary}" Color="#FFFEC425"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.SecondaryLight}" Color="#FFFFD54F"/>

Source: src/DBTools.Themes/Themes/App.Brushes.xaml:14-19

Surface Colors

Dark theme surfaces with subtle differentiation:

<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Paper}" Color="#FF181820"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Surface}" Color="#FF222228"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.SurfaceAlt}" Color="#FF1C1C22"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.CardSurface}" Color="#FF1E1E24"/>

Source: src/DBTools.Themes/Themes/App.Brushes.xaml:24-30

Text Colors (WCAG Compliant)

Text colors maintain minimum 4.5:1 contrast ratio for accessibility:

<!-- WCAG AA compliant - min 4.5:1 contrast ratio on dark backgrounds -->
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Body}" Color="#FFE6E6E6"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.BodyLight}" Color="#FFBDBDBD"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.TextMuted}" Color="#FF999999"/>

Source: src/DBTools.Themes/Themes/App.Brushes.xaml:35-40

Status Colors

Semantic colors for success, warning, error, and info states:

<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Success}" Color="#FF4CAF50"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Warning}" Color="#FFFFA000"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Error}" Color="#FFCF6679"/>
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.Info}" Color="#FF1946B9"/>

Source: src/DBTools.Themes/Themes/App.Brushes.xaml:84-95

Window Base Classes

DbtWindowBase

Standard modal window with theme and progress overlay support:

public class DbtWindowBase : Window, IWindowWithOwnerProvider, IThemeOptOut
{
    public DbtWindowBase()
    {
        DbtWindowInitHelper.Initialize(this, nameof(DbtWindowBase));
    }
}

Source: src/DBTools.Core/UI/Windows/DbtWindowBase.cs:27-32

The XAML style applies default theme properties and progress overlay:

<Style TargetType="{x:Type core:DbtWindowBase}" BasedOn="{StaticResource {x:Type Window}}">
    <Setter Property="Icon" Value="pack://application:,,,/DBTools.Themes;component/Assets/db_tools_icon.png"/>
    <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.Paper}}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static theme:BrushKeys.Body}}"/>
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <Grid>
                    <ContentPresenter/>
                    <Border Panel.ZIndex="1000" ...>
                        <progress:ProgressOverlayControl .../>
                    </Border>
                </Grid>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

Source: src/DBTools.Themes/Themes/App.Controls.Base.xaml:13-33

Control Styles

Standard Controls

Implicit styles override WPF defaults with theme colors:

<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
    <Setter Property="MinWidth" Value="88"/>
    <Setter Property="MinHeight" Value="36"/>
    <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.Surface}}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static theme:BrushKeys.Body}}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource {x:Static theme:BrushKeys.Divider}}"/>
    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.SecondaryHover}}"/>
        </Trigger>
    </Style.Triggers>
</Style>

Source: src/DBTools.Themes/Themes/App.Controls.Base.xaml:50-69

DataGrid Styles

Custom DataGrid with accent stripe and enhanced selection:

<Style TargetType="DataGridRow" BasedOn="{StaticResource {x:Type DataGridRow}}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="DataGridRow">
                <Border x:Name="DGR_Border" ...>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="3"/>  <!-- Accent stripe -->
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <Border x:Name="AccentStripe" Grid.Column="0"
                                Background="Transparent"/>
                        <SelectiveScrollingGrid Grid.Column="1">
                            <!-- Row content -->
                        </SelectiveScrollingGrid>
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter TargetName="AccentStripe" Property="Background"
                                Value="{DynamicResource {x:Static theme:BrushKeys.DataGridAccentStripe}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Source: src/DBTools.Themes/Themes/App.DataGrid.xaml:24-101

Full custom templates avoid Revit add-in styling conflicts:

<Style x:Key="ContextMenu" TargetType="ContextMenu">
    <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.Surface}}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource {x:Static theme:BrushKeys.Divider}}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ContextMenu">
                <Border Background="{TemplateBinding Background}"
                        CornerRadius="4" SnapsToDevicePixels="True">
                    <Border.Effect>
                        <DropShadowEffect BlurRadius="8" ShadowDepth="2" Opacity="0.35"/>
                    </Border.Effect>
                    <ScrollViewer>
                        <ItemsPresenter/>
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Source: src/DBTools.Themes/Themes/App.Menus.xaml:9-42

Component Styles

Cards

Elevated card surfaces with shadows:

<Style x:Key="Card" TargetType="Border">
    <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.CardSurface}}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource {x:Static theme:BrushKeys.CardBorder}}"/>
    <Setter Property="CornerRadius" Value="{DynamicResource Radius8}"/>
    <Setter Property="Padding" Value="{DynamicResource Card.Padding}"/>
</Style>

<Style x:Key="Card.Elevated" TargetType="Border" BasedOn="{StaticResource Card}">
    <Setter Property="Effect">
        <Setter.Value>
            <DropShadowEffect BlurRadius="18" ShadowDepth="4" Opacity="0.35"/>
        </Setter.Value>
    </Setter>
</Style>

Source: src/DBTools.Themes/Themes/App.Components.xaml:205-225

Toolbar Chips

Toggle buttons styled as filter chips:

<Style x:Key="ToolbarChip" TargetType="ToggleButton">
    <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.ToolbarChipBackground}}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ToggleButton">
                <Border x:Name="Chip" CornerRadius="{DynamicResource Radius6}" ...>
                    <ContentPresenter/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter TargetName="Chip" Property="Background"
                                Value="{DynamicResource {x:Static theme:BrushKeys.ToolbarChipSelected}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Source: src/DBTools.Themes/Themes/App.Components.xaml:29-91

Theme Validation

Startup Validation

DbtThemeValidator.ValidateOrThrow() performs comprehensive theme validation:

  1. Incremental Merge Test: Merges each dictionary one-by-one to pinpoint failures
  2. Freezable Freeze: Freezes all brushes to prevent runtime leaks
  3. Contract Assertion: Validates merged dictionary order matches expected
  4. Ghost Validation: Creates invisible control tree to force template evaluation
public static void ValidateOrThrow()
{
    WpfUiThread.EnsurePackUriSupport();

    // Incremental merge to pinpoint first broken dictionary
    var scratch = new ResourceDictionary();
    foreach (var source in ExpectedMergedDictionarySources)
    {
        scratch.MergedDictionaries.Add(new ResourceDictionary
        {
            Source = new Uri(source, UriKind.Absolute)
        });
    }

    FreezeFreezablesOrThrow(scratch);
    AssertMergedDictionariesContract(themeRoot);
    GhostValidate(themeRoot);
}

Source: src/DBTools.Core/UI/Theming/DbtThemeValidator.cs:52-92

Ghost Validation

Creates an invisible visual tree to force template instantiation and catch deferred failures:

private static void GhostValidate(ResourceDictionary themeRoot)
{
    var root = new Grid { Width = 800, Height = 600 };
    root.Resources.MergedDictionaries.Add(themeRoot);

    root.Children.Add(BuildProbeContent(root));

    root.Measure(new Size(root.Width, root.Height));
    root.Arrange(new Rect(0, 0, root.Width, root.Height));
    root.UpdateLayout();
}

Source: src/DBTools.Core/UI/Theming/DbtThemeValidator.cs:229-248

Using the Theme

In XAML

Reference brushes via DynamicResource and BrushKeys:

<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>

In Code

Use SetResourceReference for dynamic binding:

window.SetResourceReference(Window.BackgroundProperty, BrushKeys.Paper);
window.SetResourceReference(Window.ForegroundProperty, BrushKeys.Body);

Source: src/DBTools.Core/UI/Windows/DbtWindowInitHelper.cs:151-155

Extending the Theme

Adding New Brushes

  1. Add ComponentResourceKey property in BrushKeys.cs:
public static ComponentResourceKey MyNewBrush => new(typeof(BrushKeys), "Brush.MyNew");
  1. Add brush definition in App.Brushes.xaml:
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.MyNewBrush}" Color="#FF123456"/>

Adding New Styles

  1. Create new keyed style in appropriate dictionary:
<Style x:Key="MyCustomButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
    <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.Primary}}"/>
</Style>
  1. For implicit styles, omit the x:Key:
<Style TargetType="MyCustomControl">
    <Setter Property="Background" Value="{DynamicResource {x:Static theme:BrushKeys.Surface}}"/>
</Style>

XAML Resource File Types

Some XAML files reference DBTools.Core types and cannot be compiled as BAML:

<!-- These files are raw resources, not compiled BAML -->
<Page Remove="Themes\App.Converters.xaml"/>
<Page Remove="Themes\App.Controls.Base.xaml"/>
<Page Remove="Themes\App.Components.xaml"/>

<Resource Include="Themes\App.Converters.xaml"/>
<Resource Include="Themes\App.Controls.Base.xaml"/>
<Resource Include="Themes\App.Components.xaml"/>

Source: src/DBTools.Themes/DBTools.Themes.csproj:38-46


Files Reviewed

File Purpose
src/DBTools.Themes/DBTools.Themes.csproj Project configuration
src/DBTools.Themes/BrushKeys.cs ComponentResourceKey definitions
src/DBTools.Themes/Themes/App.Theme.xaml Root theme dictionary
src/DBTools.Themes/Themes/App.Tokens.xaml Design tokens
src/DBTools.Themes/Themes/App.Brushes.xaml Color definitions
src/DBTools.Themes/Themes/App.Controls.Base.xaml Base control styles
src/DBTools.Themes/Themes/App.DataGrid.xaml DataGrid styles
src/DBTools.Themes/Themes/App.Menus.xaml Menu styles
src/DBTools.Themes/Themes/App.Components.xaml Component styles
src/DBTools.Themes/Themes/App.Converters.xaml Value converters
src/DBTools.Core/UI/Theming/DbtThemeValidator.cs Theme validation
src/DBTools.Core/UI/Windows/DbtWindowBase.cs Window base class
src/DBTools.Core/UI/Windows/DbtWindowInitHelper.cs Theme loading logic
src/DBTools.Core/UI/Windows/ResourceDictionaryHelper.cs Dictionary utilities
build-vendored-deps.sh Vendored library build script