DBTools.Sandbox
The DBTools.Sandbox project is a standalone WPF application that enables development, testing, and validation of DBTools UI windows without running Revit. It provides both an interactive gallery for browsing tool windows and a headless validation mode used during CI builds.
Source:
src/DBTools.Sandbox/DBTools.Sandbox.csproj:1-54
Overview
Purpose
Sandbox exists to solve a fundamental Revit development challenge: tool UI windows normally require Revit to be running, which makes iterative UI development slow and makes CI validation difficult. Sandbox provides:
- Interactive Development: Launch tool windows with design-time ViewModels to preview UI without Revit
- Build-Time Validation: Catch XAML errors, missing resources, and binding failures during CI
- Screenshot Capture: Generate UI screenshots for documentation or visual regression testing
Key Concepts
- Design-Time ViewModels: Each sandbox-enabled window must provide a ViewModel that works without Revit services
- Sandbox Mode: A global flag (
SandboxMode.IsActive) that windows check to avoid calling Revit-dependent code - Dist-Driven Discovery: Sandbox loads windows from the built
dist/payload, validating the actual shipped assemblies
Project Structure
src/DBTools.Sandbox/
├── App.xaml.cs # Application entry, mode switching
├── MainWindow.xaml.cs # Interactive gallery window
├── SandboxAppRuntime.cs # IAppRuntime implementation for sandbox
├── SandboxDiagnostics.cs # Logging utilities
├── SandboxSettingsProvider.cs # Settings stub for sandbox mode
├── HeadlessUiSuppression.cs # Suppresses OS dialogs in CI
└── Validation/
├── DistValidator.cs # Main validation orchestrator
├── ToolWindowValidator.cs # Tool window instantiation/layout
├── ManifestValidator.cs # Manifest schema validation
├── WindowGhostValidator.cs # WPF layout pass validation
├── MergeValidator.cs # ILRepack/embedded payload checks
├── BindingErrorListener.cs # WPF binding error capture
├── SandboxValidateOptions.cs # CLI argument parsing
├── SandboxModeActivator.cs # Cross-assembly mode activation
├── SandboxScreenshotHandler.cs # Screenshot capture mode
├── DistDirLocator.cs # Dist directory resolution
├── DistAssemblyResolver.cs # Assembly loading for dist payload
├── RevitDirLocator.cs # Revit installation discovery
├── DistValidationReflection.cs # Reflection helpers
├── AssemblyMetadataInspector.cs # PE metadata inspection
└── XamlExceptionDiagnostics.cs # XAML error formatting
Operating Modes
Interactive Mode (Default)
Launch the sandbox gallery to browse and open tool windows:
# From .artifacts/sandbox/Release/net8.0-windows/
DBTools.Sandbox.exe
Source:
src/DBTools.Sandbox/App.xaml.cs:109-137
The interactive mode:
- Initializes
SandboxAppRuntimefor service resolution - Loads
DBTools.dllfrom the dist payload - Discovers all
sandboxWindowsfrom tool manifests - Displays a grouped gallery of launchable windows
Headless Validation Mode
Used by the build system to validate dist output:
DBTools.Sandbox.exe --headless --dist-dir "path/to/dist/Release/2026"
Source:
src/DBTools.Sandbox/App.xaml.cs:45-57
Headless mode performs comprehensive validation without showing any UI:
- Theme resource validation
- Core window instantiation
- Tool manifest validation
- Tool window XAML validation
- WPF binding error detection
Screenshot Mode
Capture window screenshots for documentation:
# List available tools
DBTools.Sandbox.exe --screenshot --list
# Capture specific tool
DBTools.Sandbox.exe --screenshot --tool-id DBTools.GM.Main --output gm.png
Source:
src/DBTools.Sandbox/Validation/SandboxScreenshotHandler.cs:24-49
Key Components
SandboxAppRuntime
Minimal IAppRuntime implementation that provides essential services without Revit:
internal sealed class SandboxAppRuntime : IAppRuntime, IDisposable
{
// Provides: ILogger, ISettingsProvider, ILoggerFactory, IAlertService
public T Resolve<T>() where T : notnull { ... }
}
Source:
src/DBTools.Sandbox/SandboxAppRuntime.cs:17-90
Services provided:
IDbtLoggingHost- Real logging to%APPDATA%/DBTools/Logs/dbtools-SANDBOX-*.logISettingsProvider- Stub implementation returning defaultsILoggerFactory- Creates category-scoped loggersIAlertService- Real alert service for error display
SandboxMode
Global flag that windows check to avoid Revit-dependent code paths:
public static class SandboxMode
{
public static bool IsActive => _isActive;
public static void Activate() { _isActive = true; }
}
Source:
src/DBTools.Core/Compat/SandboxMode.cs:1-24
Windows should check this before calling Revit services:
if (!SandboxMode.IsActive)
{
// Call Revit-dependent code
}
DbtSandboxCatalog
Discovers sandbox windows from tool manifests:
public static IReadOnlyList<DbtSandboxWindowSpec> Discover(Assembly rootAssembly)
{
var entries = DbtToolManifestLoader.LoadEntries(rootAssembly);
// Extract sandboxWindows from each manifest
}
Source:
src/DBTools.Core/Tools/DbtSandboxCatalog.cs:8-60
Validates that each sandbox window entry has:
id- Unique identifierdisplayName- Human-readable namegroup- Category for gallery groupingwindowType- Fully-qualified Window class namedesignTimeViewModelType- ViewModel for sandbox mode
Tool Manifest Integration
Tools register sandbox windows via manifest.yml:
id: DBTools.GM
assembly: DBTools
moduleType: DBTools.GM.GmToolModule
sandboxWindows:
- id: DBTools.GM.Main
displayName: "Global Mapper"
group: "Common"
windowType: "DBTools.GM.Shell.UI.Views.GmWindow"
designTimeViewModelType: "DBTools.GM.Shell.DesignTime.GmShellDesignTimeViewModel"
Source:
src/Tools/Common/GM/manifest.yml:1-15
Schema Fields
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique window identifier (e.g., DBTools.GM.Main) |
displayName |
Yes | Gallery display name |
group |
Yes | Gallery category grouping |
windowType |
Yes | Fully-qualified Window type name |
designTimeViewModelType |
Yes | Design-time ViewModel type |
assembly |
No | Assembly name (defaults to manifest's assembly) |
Source:
src/DBTools.Core/Tools/DbtToolManifestLoader.cs:121-153
Validation System
The validation system runs during BuildAll to catch UI issues before deployment.
DistValidator (Orchestrator)
Main validation entry point that coordinates all validators:
internal static class DistValidator
{
public static int Run(SandboxValidateOptions options, ILogger? logger = null)
{
// 1. Validate dist layout
// 2. Install assembly resolver
// 3. Validate theme resources
// 4. Validate core windows
// 5. Validate manifests (optional)
// 6. Validate tool windows (optional)
}
}
Source:
src/DBTools.Sandbox/Validation/DistValidator.cs:15-118
ToolWindowValidator
Instantiates each sandbox window with its design-time ViewModel:
internal static class ToolWindowValidator
{
public static void ValidateOrThrow(Assembly dbtoolsAssembly, ILogger? logger = null)
{
var specs = DiscoverSpecs(dbtoolsAssembly);
foreach (var spec in specs)
{
// 1. Load window type from assembly
// 2. Create window instance
// 3. Create design-time ViewModel
// 4. Set DataContext
// 5. Run WindowGhostValidator
// 6. Validate tab interactions
// 7. Validate row expansion
}
}
}
Source:
src/DBTools.Sandbox/Validation/ToolWindowValidator.cs:13-173
Additional validations performed:
- Tab cycling - Switches through all tabs to catch transition errors
- DataGrid row expansion - Toggles row details visibility
- Preview mode switching - Tests mode transitions (e.g., SGT preview modes)
Source:
src/DBTools.Sandbox/Validation/ToolWindowValidator.cs:201-368
WindowGhostValidator
Forces WPF layout passes to catch XAML errors:
internal static class WindowGhostValidator
{
public static void Validate(Window window)
{
// Pass 1: Fixed 800x600 size
ValidateLayoutPass(window, new Size(800, 600), "fixed size");
// Pass 2: Infinite size (catches DesiredSize calculation errors)
ValidateLayoutPass(window, new Size(PositiveInfinity, PositiveInfinity), "infinite size");
}
}
Source:
src/DBTools.Sandbox/Validation/WindowGhostValidator.cs:9-129
BindingErrorListener
Captures WPF binding errors that normally fail silently:
internal sealed class BindingErrorListener : TraceListener
{
// Hooks into PresentationTraceSources.DataBindingSource
// Collects "System.Windows.Data Error:" messages
// ThrowIfErrors() fails validation if any binding errors occurred
}
Source:
src/DBTools.Sandbox/Validation/BindingErrorListener.cs:13-118
This catches issues like:
- Missing
StaticResourcereferences - Broken binding paths
- Type conversion failures
MergeValidator
Validates assembly merge/embedding is correct:
For net48 (ILRepack):
- Verifies expected types exist inside
DBTools.dll - Ensures merged assemblies don't exist as separate files
For net8 (Embedded Payload):
- Verifies
DBTools.EmbeddedAssemblies.DBTools.Core.dllresource exists - Checks resource isn't corrupted (minimum size validation)
Source:
src/DBTools.Sandbox/Validation/MergeValidator.cs:17-222
CLI Arguments
| Argument | Description |
|---|---|
--headless |
Run validation only, no UI |
--screenshot |
Run screenshot capture mode |
--dist-dir <path> |
Path to dist directory or year folder |
--revit-dir <path> |
Override Revit installation path |
--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 |
--list |
List available tools (screenshot mode) |
--tool-id <id> |
Tool to capture (screenshot mode) |
--output <path> |
Screenshot output path |
Source:
src/DBTools.Sandbox/Validation/SandboxValidateOptions.cs:7-113
Build Integration
The sandbox validator is invoked automatically during BuildAll:
bash build.sh BuildAll
# Internally runs: DBTools.Sandbox.exe --headless --dist-dir ...
The build system also handles orphaned processes:
static void KillOrphanedSandboxValidatorProcesses()
{
// Kills any lingering DBTools.Sandbox processes from previous builds
}
Source:
build/BuildTargets.cs:1292
Creating a Sandbox-Enabled Window
1. Create Design-Time ViewModel
public class MyWindowDesignTimeViewModel : INotifyPropertyChanged
{
public MyWindowDesignTimeViewModel()
{
// Initialize with sample data
Items = new ObservableCollection<ItemModel>
{
new ItemModel { Name = "Sample 1" },
new ItemModel { Name = "Sample 2" }
};
}
public ObservableCollection<ItemModel> Items { get; }
}
2. Add Parameterless Constructor to Window
public partial class MyWindow : Window
{
// Required for sandbox mode
public MyWindow()
{
InitializeComponent();
if (SandboxMode.IsActive)
{
// Skip Revit-dependent initialization
return;
}
// Normal initialization...
}
}
3. Register in manifest.yml
sandboxWindows:
- id: MyTool.Main
displayName: "My Tool Window"
group: "Common"
windowType: "MyNamespace.UI.Views.MyWindow"
designTimeViewModelType: "MyNamespace.DesignTime.MyWindowDesignTimeViewModel"
4. Test Locally
# Build
bash build.sh BuildAll
# Launch interactive sandbox
.artifacts/sandbox/Release/net8.0-windows/DBTools.Sandbox.exe
Multi-Framework Support
Sandbox targets both net48 and net8.0-windows to validate both Revit 2024 (net48) and Revit 2025+ (net8) distributions:
<TargetFrameworks>$(DBT_RevitTargetFrameworks)</TargetFrameworks>
Source:
src/DBTools.Sandbox/DBTools.Sandbox.csproj:3
The validator automatically selects the compatible year folder:
- net48 sandbox validates Revit 2024 (
year <= 2024) - net8 sandbox validates Revit 2025+ (
year >= 2025)
Source:
src/DBTools.Sandbox/Validation/DistDirLocator.cs:75-80
Headless UI Suppression
In CI environments, the sandbox suppresses all OS-level dialogs:
- SetErrorMode - Suppresses Win32 critical error dialogs
- WerSetFlags - Suppresses Windows Error Reporting UI
- Trace listeners - Disables
Debug.Assertpopups
Source:
src/DBTools.Sandbox/HeadlessUiSuppression.cs:17-120
This prevents CI builds from hanging on modal dialogs.
Troubleshooting
"Dist payload not found"
The sandbox requires a built dist payload:
bash build.sh BuildAll
Window crashes in sandbox but works in Revit
Check for Revit-dependent code not guarded by SandboxMode.IsActive:
// Wrong - crashes in sandbox
var doc = AppRuntime.Resolve<IRevitService>().Document;
// Right - sandbox-safe
if (!SandboxMode.IsActive)
{
var doc = AppRuntime.Resolve<IRevitService>().Document;
}
Binding errors in validation
The BindingErrorListener will report silent WPF binding failures. Check:
- Missing
StaticResourcekeys in XAML - Incorrect binding paths
- Missing value converters
"Type not found" during validation
Ensure the window and ViewModel types:
- Have fully-qualified names in the manifest
- Are public classes
- Have public parameterless constructors
Related Documentation
- Architecture: Sandbox Validator - Detailed validation architecture
- DBTools.App - How App discovers tools via manifests
- Tool Manifest Schema - Complete manifest documentation
See Also
DbtSandboxWindowSpec- Window specification modelDbtSandboxCatalog- Window discovery from manifestsDbtToolManifestLoader- Manifest parsing and validation