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:
- Clones upstream repos to
vendor/ - Renames assembly names and updates pack URIs
- Builds renamed assemblies to
.artifacts/vendor/ - 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
- Window Constructor:
DbtWindowInitHelper.Initialize()is called - Pack URI Support:
WpfUiThread.EnsurePackUriSupport()ensures pack:// scheme is registered - Dictionary Merge:
App.Theme.xamlis merged intoWindow.Resources - Resource Freeze: All freezable resources are frozen to prevent runtime leaks
- 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
Menu Styles
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:
- Incremental Merge Test: Merges each dictionary one-by-one to pinpoint failures
- Freezable Freeze: Freezes all brushes to prevent runtime leaks
- Contract Assertion: Validates merged dictionary order matches expected
- 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
- Add
ComponentResourceKeyproperty inBrushKeys.cs:
public static ComponentResourceKey MyNewBrush => new(typeof(BrushKeys), "Brush.MyNew");
- Add brush definition in
App.Brushes.xaml:
<SolidColorBrush x:Key="{x:Static theme:BrushKeys.MyNewBrush}" Color="#FF123456"/>
Adding New Styles
- 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>
- 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
Related Documentation
- Architecture Overview - High-level system architecture
- Project References - Project dependency graph
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 |