Table of Contents

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:

  1. Interactive Mode - Launch and preview tool windows during development
  2. 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

  1. Parameterless Constructor - Must be instantiable without DI
  2. No Revit API Calls - All data must be mocked
  3. Same Interface as Runtime ViewModel - XAML bindings must resolve
  4. 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.dll resource 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:

  1. Load the tool assembly
  2. Resolve window type and ViewModel type
  3. Instantiate both with parameterless constructors
  4. Set DataContext on window
  5. Run WindowGhostValidator (layout passes)
  6. Validate tab interactions (cycle through tabs)
  7. Validate DataGrid row expansion
  8. 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: net48 validator
  • 2025+: net8.0-windows validator

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

  1. Run sandbox interactively to see actual UI rendering
  2. Check sandbox logs in %LOCALAPPDATA%\DBTools\Logs\Sandbox-*.log
  3. Enable verbose output by running from command line
  4. 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


See Also