Sandbox Integration Guide
This guide covers integrating DBTools tool windows with the Sandbox system for design-time previews and automated UI validation.
Overview
The Sandbox is a standalone WPF application that validates tool UI without requiring Revit. It serves two critical purposes:
- Interactive Mode - Launch and preview tool windows during development
- Headless Mode - Automated validation in CI pipelines (ValidateDist target)
Why Validation Matters
UI defects that slip past development often surface only when loaded in Revit:
- XAML binding errors that fail silently
- Theme resource resolution failures (ghost buttons, missing styles)
- ILRepack merge issues causing type resolution failures
- Design-time ViewModel instantiation crashes
The Sandbox catches these issues before deployment by:
- Loading the actual dist artifacts (post-merge assemblies)
- Instantiating windows with design-time ViewModels
- Forcing full layout passes to surface XAML errors
- Monitoring WPF binding errors that normally fail silently
Source:
src/DBTools.Sandbox/Validation/DistValidator.cs:73-118
Sandbox Architecture
flowchart TB
subgraph Build["Build Pipeline"]
NUKE[NUKE Build]
BuildSandbox[BuildSandbox Target]
ValidateDist[ValidateDist Target]
end
subgraph Sandbox["DBTools.Sandbox.exe"]
App[App.xaml.cs]
DistValidator[DistValidator]
ToolWindowValidator[ToolWindowValidator]
ManifestValidator[ManifestValidator]
MergeValidator[MergeValidator]
BindingErrorListener[BindingErrorListener]
WindowGhostValidator[WindowGhostValidator]
end
subgraph Dist["Dist Artifacts"]
DBToolsDll[DBTools.dll]
Manifests[Embedded Manifests]
ToolAssemblies[Tool Types]
end
subgraph Core["DBTools.Core"]
SandboxMode[SandboxMode]
DbtSandboxCatalog[DbtSandboxCatalog]
DbtToolManifestLoader[DbtToolManifestLoader]
end
NUKE --> BuildSandbox
BuildSandbox --> ValidateDist
ValidateDist --> |--headless| App
App --> |Run| DistValidator
DistValidator --> MergeValidator
DistValidator --> ManifestValidator
DistValidator --> ToolWindowValidator
ToolWindowValidator --> DbtSandboxCatalog
DbtSandboxCatalog --> Manifests
DistValidator --> BindingErrorListener
ToolWindowValidator --> WindowGhostValidator
DistValidator --> |Activate| SandboxMode
DBToolsDll --> ToolAssemblies
ToolAssemblies --> |Instantiate| WindowGhostValidator
Key Components
| Component | Purpose |
|---|---|
DistValidator |
Orchestrates full validation pipeline |
ToolWindowValidator |
Instantiates and validates each tool window |
ManifestValidator |
Validates manifest.yml entries and command types |
MergeValidator |
Verifies ILRepack merge (net48) or embedded payload (net8) |
WindowGhostValidator |
Forces layout passes to surface XAML errors |
BindingErrorListener |
Captures silent WPF binding failures |
SandboxMode |
Signals to tools they're running without Revit |
DbtSandboxCatalog |
Discovers sandbox window specs from manifests |
Registering Sandbox Windows
Tools register their windows for sandbox validation in manifest.yml:
manifest.yml Schema
id: DBTools.MyTool
assembly: DBTools
moduleType: DBTools.MyTool.MyToolModule
order: 0
sandboxWindows:
- id: DBTools.MyTool.Main
displayName: "My Tool"
group: "Common"
windowType: "DBTools.MyTool.UI.Views.MyToolWindow"
designTimeViewModelType: "DBTools.MyTool.DesignTime.MyToolDesignTimeViewModel"
Required Fields
| Field | Description |
|---|---|
id |
Unique identifier (must be globally unique across all tools) |
displayName |
Human-readable name shown in sandbox launcher |
group |
Category for grouping (e.g., "Common", "Structural") |
windowType |
Fully-qualified Window class name |
designTimeViewModelType |
Fully-qualified design-time ViewModel class name |
Optional Fields
| Field | Description |
|---|---|
assembly |
Assembly name override (defaults to manifest's assembly) |
Source:
src/DBTools.Core/Tools/DbtSandboxCatalog.cs:10-59
Example: GM Manifest
# src/Tools/Common/GM/manifest.yml
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"
Source:
src/Tools/Common/GM/manifest.yml:1-15
Example: SGT Manifest
# src/Tools/Structural/SGT/manifest.yml
id: DBTools.SGT
assembly: DBTools
moduleType: DBTools.SGT.SgtToolModule
order: 0
sandboxWindows:
- id: DBTools.SGT.Main
displayName: "Super Girt Tool"
group: "Structural"
windowType: "DBTools.SGT.Shell.UI.Views.SgtWindow"
designTimeViewModelType: "DBTools.SGT.Shell.DesignTime.SgtWindowDesignTimeViewModel"
Source:
src/Tools/Structural/SGT/manifest.yml:1-10
Design-Time ViewModels
Design-time ViewModels provide mock data for sandbox/designer rendering without Revit dependencies.
Requirements
- Parameterless Constructor - Must be instantiable without DI
- No Revit API Calls - All data must be mocked
- Same Interface as Runtime ViewModel - XAML bindings must resolve
- Sample Data - Populate collections with representative test data
Pattern
namespace DBTools.MyTool.DesignTime;
/// <summary>
/// Design-time ViewModel for MyToolWindow XAML Designer support.
/// DO NOT instantiate at runtime.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class MyToolDesignTimeViewModel
{
// Commands - use DesignTimeRelayCommand for all commands
public ICommand ExecuteCommand => DesignTimeRelayCommand.Instance;
public ICommand CloseCommand => DesignTimeRelayCommand.Instance;
// Properties - provide reasonable defaults
public bool IsBusy => false;
public string Status => "Ready";
// Collections - populate with sample data
public ObservableCollection<ItemRow> Items { get; } = new();
public MyToolDesignTimeViewModel()
{
// Add sample data for designer preview
Items.Add(new ItemRow { Name = "Sample Item 1", Value = 42 });
Items.Add(new ItemRow { Name = "Sample Item 2", Value = 100 });
}
}
Source:
src/Tools/Common/GM/Shell/DesignTime/GmShellDesignTimeViewModel.cs:19-246
Using DesignTimeRelayCommand
All commands should use the shared DesignTimeRelayCommand.Instance:
public ICommand ScanCommand => DesignTimeRelayCommand.Instance;
public ICommand ApplyCommand => DesignTimeRelayCommand.Instance;
This provides a no-op command that satisfies bindings without runtime behavior.
Analyzer Suppressions
Design-time ViewModels typically need these suppressions:
#pragma warning disable CA1822 // Instance members for XAML binding
#pragma warning disable MA0041 // Design-time uses instance bindings
#pragma warning disable CA1861 // Array allocations acceptable in design-time
SandboxMode Detection
Tools detect sandbox mode to skip Revit-dependent initialization:
Checking SandboxMode
using DBTools.Core.Compat;
public partial class MyToolWindow : DbtRibbonWindowBase
{
public MyToolWindow()
{
InitializeComponent();
if (DesignerProperties.GetIsInDesignMode(this))
return;
// Skip Revit initialization in sandbox
if (SandboxMode.IsActive)
{
DataContext = new MyToolDesignTimeViewModel();
return;
}
// Normal runtime initialization with DI
}
}
Source:
src/Tools/Common/GM/Shell/UI/Views/GmWindow.xaml.cs:37-48
SandboxMode API
namespace DBTools.Core.Compat;
public static class SandboxMode
{
/// <summary>
/// Returns true if running in the sandbox executable (no Revit API available).
/// </summary>
public static bool IsActive { get; }
/// <summary>
/// Called once at sandbox app startup to enable sandbox mode.
/// </summary>
public static void Activate();
}
Source:
src/DBTools.Core/Compat/SandboxMode.cs:1-23
Common Uses
| Location | Purpose |
|---|---|
| Window constructors | Use design-time ViewModel |
| Preview renderers | Skip GPU/DirectX initialization |
| Service calls | Return mock data |
| Logging initialization | Use null logger |
Validation Checks
DistValidator Pipeline
The DistValidator runs these checks in sequence:
flowchart LR
A[Dist Layout] --> B[Assembly Resolver]
B --> C[Embedded Resolver]
C --> D[Merge/Payload Validation]
D --> E[Addin Entrypoint]
E --> F[SandboxMode Activation]
F --> G[Theme Validation]
G --> H[Core Windows]
H --> I[Manifest Validation]
I --> J[Tool Windows]
J --> K[Binding Error Check]
Source:
src/DBTools.Sandbox/Validation/DistValidator.cs:73-117
1. Dist Layout Validation
Verifies required files exist:
var required = new[]
{
"DBTools.Loader.dll",
"DBTools.dll",
"DBTools.Themes.dll",
"DBTools.HandyControl.dll"
};
2. Merge/Embedded Payload Validation
Net48 (ILRepack):
- Verifies types from merged assemblies exist inside DBTools.dll
- Checks that merged assemblies don't exist as separate files
- Validates internalization exclusions (required public types)
Net8 (Embedded Payload):
- Verifies
DBTools.EmbeddedAssemblies.DBTools.Core.dllresource exists - Checks resource is loadable and has reasonable size (>1KB)
Source:
src/DBTools.Sandbox/Validation/MergeValidator.cs:17-221
3. Manifest Validation
For each manifest.yml:
- Validates moduleType derives from
DbtToolModule - Validates ribbon command types implement
IExternalCommand - Validates availability types implement
IExternalCommandAvailability
Source:
src/DBTools.Sandbox/Validation/ManifestValidator.cs:10-178
4. Tool Window Validation
For each sandbox window spec:
- Load the tool assembly
- Resolve window type and ViewModel type
- Instantiate both with parameterless constructors
- Set DataContext on window
- Run WindowGhostValidator (layout passes)
- Validate tab interactions (cycle through tabs)
- Validate DataGrid row expansion
- Validate preview mode switching
Source:
src/DBTools.Sandbox/Validation/ToolWindowValidator.cs:13-370
5. WindowGhostValidator
Forces layout passes to surface XAML errors:
// Pass 1: Fixed size (800x600) - typical window size
ValidateLayoutPass(window, new Size(800, 600), "fixed size");
// Pass 2: Infinite size - catches controls that crash on "desired" size
ValidateLayoutPass(window, new Size(double.PositiveInfinity, double.PositiveInfinity), "infinite size");
Source:
src/DBTools.Sandbox/Validation/WindowGhostValidator.cs:9-130
6. BindingErrorListener
Captures WPF data binding errors that normally fail silently:
using var bindingListener = BindingErrorListener.Install();
// ... validation code ...
bindingListener.ThrowIfErrors("full validation");
Source:
src/DBTools.Sandbox/Validation/BindingErrorListener.cs:8-119
Running Sandbox Locally
Interactive Mode
Launch the sandbox for manual window exploration:
# Build dist first
bash build.sh BuildAll
# Run sandbox (auto-detects dist)
.artifacts/sandbox/Release/net8.0-windows/DBTools.Sandbox.exe
# Or specify dist directory
DBTools.Sandbox.exe --dist-dir "C:/path/to/dist/Release/2026"
The sandbox launcher shows all registered windows grouped by category.
Headless Mode (Manual)
Run validation without UI:
# Validate with all checks
DBTools.Sandbox.exe --headless --dist-dir "path/to/dist/Release/2026"
# Skip tool validation (faster)
DBTools.Sandbox.exe --headless --dist-dir "path/to/dist/Release/2026" --skip-validate-tools
# Skip manifest validation
DBTools.Sandbox.exe --headless --dist-dir "path/to/dist/Release/2026" --skip-validate-manifests
Command-Line Options
| Option | Description |
|---|---|
--headless |
Run validation without UI (exit code 0=pass, 1=fail) |
--dist-dir <path> |
Path to dist folder (defaults to build output) |
--revit-dir <path> |
Path to Revit installation (for RevitAPI resolution) |
--validate-manifests |
Enable manifest validation (default: true) |
--skip-validate-manifests |
Disable manifest validation |
--validate-tools |
Enable tool window validation (default: true) |
--skip-validate-tools |
Disable tool window validation |
--screenshot |
Screenshot mode for documentation |
--list |
List available tools (screenshot mode) |
--tool-id <id> |
Specific tool to screenshot |
--output <path> |
Screenshot output path |
Source:
src/DBTools.Sandbox/Validation/SandboxValidateOptions.cs:7-113
Headless Mode (CI)
The sandbox runs in CI as part of the ValidateDist NUKE target.
Build Pipeline Integration
# Full build with validation
bash build.sh BuildAll
# Build without validation (faster local dev)
bash build.sh BuildOnly
# Run validation separately after build
bash build.sh ValidateDist
ValidateDist Target
The NUKE build invokes the sandbox for each Revit year:
Target ValidateDist => _ => _
.Description("Validate dist outputs via DBTools.Sandbox headless runner")
.DependsOn(BuildSandbox)
.After(PromoteToDist)
.OnlyWhenDynamic(() => Configuration == "Release")
.Executes(() =>
{
foreach (var year in RevitYears)
{
var distDir = ArtifactsDir / "dist" / Configuration / year;
var validatorExe = ArtifactsDir / "sandbox" / Configuration / tfm / "DBTools.Sandbox.exe";
var args = new[]
{
"--headless",
"--dist-dir", distDir,
"--validate-manifests",
"--validate-tools"
};
// Execute with 3-minute timeout
Process.Start(validatorExe, args);
}
});
Source:
build/BuildTargets.cs:1150-1258
TFM Selection
The validator executable matches the Revit year's target framework:
- 2024:
net48validator - 2025+:
net8.0-windowsvalidator
Troubleshooting
Common Failures
"Sandbox window type not found"
Cause: WindowType in manifest doesn't match actual class name
Fix: Verify fully-qualified type name in manifest.yml matches code:
windowType: "DBTools.MyTool.UI.Views.MyToolWindow" # Exact match required
"Design-time ViewModel must have parameterless constructor"
Cause: ViewModel constructor requires DI parameters
Fix: Create a dedicated design-time ViewModel:
public MyToolDesignTimeViewModel() // No parameters
{
// Initialize with mock data
}
"WPF binding errors detected"
Cause: XAML bindings referencing properties that don't exist on design-time ViewModel
Fix: Ensure design-time ViewModel exposes all bound properties:
// XAML: {Binding Items}
public ObservableCollection<Item> Items { get; } = new();
// XAML: {Binding SelectedItem}
public Item? SelectedItem { get; set; }
"ILRepack merge validation failed"
Cause: Assembly merge configuration changed, expected types missing from DBTools.dll
Fix: Review ILRepack configuration in src/DBTools.App/DBTools.App.csproj:
<ILRepackOutput>$(ArtifactsDir)dist\...</ILRepackOutput>
<ILRepackInputAssemblies>...</ILRepackInputAssemblies>
"Window layout validation failed (infinite size)"
Cause: Control doesn't handle infinite available size during measure pass
Fix: Add size constraints or check for infinite values:
protected override Size MeasureOverride(Size availableSize)
{
var width = double.IsInfinity(availableSize.Width) ? 800 : availableSize.Width;
var height = double.IsInfinity(availableSize.Height) ? 600 : availableSize.Height;
// ...
}
"SandboxMode activation failed"
Cause: DBTools.Core not loaded correctly from dist
Fix: Ensure dist build completed successfully:
bash build.sh Clean
bash build.sh BuildAll
Debug Tips
- Run sandbox interactively to see actual UI rendering
- Check sandbox logs in
%LOCALAPPDATA%\DBTools\Logs\Sandbox-*.log - Enable verbose output by running from command line
- Test single window by removing others from manifest temporarily
Real Examples
GM: Multiple Windows
GM registers multiple windows for different features:
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"
The design-time ViewModel populates extensive sample data:
public GmShellDesignTimeViewModel()
{
// Families tab with expanded row
var expandedFamily = new MatchingRow(12345, "W10x33 (Expanded)", hasDeepScan: true)
{
IsExpanded = true,
CanExpandDetails = true
};
expandedFamily.ChildTypeMatches.Add(new MatchingRow(100, "W10x33", hasDeepScan: false));
Families.FamilyMatches.Add(expandedFamily);
// Duplicates tab
Duplicates.DuplicateGroups.Add(new DuplicateGroupRow(...));
// Materials, Line Styles, etc.
}
Source:
src/Tools/Common/GM/Shell/DesignTime/GmShellDesignTimeViewModel.cs:89-246
SGT: Window with SandboxMode Check
SGT window uses SandboxMode in constructor:
public SgtWindow()
{
InitializeComponent();
if (DesignerProperties.GetIsInDesignMode(this))
return;
if (SandboxMode.IsActive)
{
DataContext = new SgtWindowDesignTimeViewModel();
return;
}
}
Source:
src/Tools/Structural/SGT/Shell/UI/Views/SgtWindow.xaml.cs:43-47
The design-time ViewModel provides preview data including mock wall geometry:
public WallPreviewData PreviewData { get; } = SgtDesignTimePreviewDataFactory.CreateMockWallPreviewData();
public bool HasPreviewData => true;
public bool Is3DAvailable => false; // GPU not available in sandbox
public string Unavailable3DReason => "3D preview requires GPU (not available in sandbox/designer mode)";
Source:
src/Tools/Structural/SGT/Shell/DesignTime/SgtWindowDesignTimeViewModel.cs:51-71