Table of Contents

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:

  1. Interactive Development: Launch tool windows with design-time ViewModels to preview UI without Revit
  2. Build-Time Validation: Catch XAML errors, missing resources, and binding failures during CI
  3. 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:

  1. Initializes SandboxAppRuntime for service resolution
  2. Loads DBTools.dll from the dist payload
  3. Discovers all sandboxWindows from tool manifests
  4. 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-*.log
  • ISettingsProvider - Stub implementation returning defaults
  • ILoggerFactory - Creates category-scoped loggers
  • IAlertService - 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 identifier
  • displayName - Human-readable name
  • group - Category for gallery grouping
  • windowType - Fully-qualified Window class name
  • designTimeViewModelType - 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 StaticResource references
  • 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.dll resource 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:

  1. SetErrorMode - Suppresses Win32 critical error dialogs
  2. WerSetFlags - Suppresses Windows Error Reporting UI
  3. Trace listeners - Disables Debug.Assert popups

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

See Also

  • DbtSandboxWindowSpec - Window specification model
  • DbtSandboxCatalog - Window discovery from manifests
  • DbtToolManifestLoader - Manifest parsing and validation