Table of Contents

Settings Packs

Settings Packs are the mechanism for tools to expose user-configurable options in the DBTools Settings window. This guide covers the complete settings lifecycle: from manifest declaration to runtime access and persistence.


Overview

What Are Settings Packs?

A Settings Pack bundles together:

  1. Settings Model - A typed class holding the configuration values
  2. Settings Pack Context - A ViewModel that manages UI state and data binding
  3. Settings Pack View - A WPF UserControl rendered in the Settings window
  4. Warning Definitions - Optional conditions that can disable tool functionality

Settings Packs appear as collapsible cards in the Settings window, organized by ribbon panel (Common, Structural, Testing).

Why Use Settings Packs?

  • Centralized configuration: Users manage all tool settings in one place
  • Automatic persistence: Settings are saved to settings.{YEAR}.json with atomic writes
  • Change notification: IOptionsMonitor<T> provides live reload without restart
  • Feature warnings: Link settings validation to tool availability
  • Consistent UX: All tools share the same settings interface pattern

Apply Timing

The Settings window should describe when saved values take effect, not show a blanket restart warning.

  • Ribbon visibility and similar shell-level flags can apply immediately after save.
  • Tool settings typically apply on the next command run or next view-activation hook.
  • RequiresRestart should be reserved for an explicitly documented pack-specific limitation, not shown globally.

Source: src/DBTools.Core/Settings/Features/DbtSettingsApplyBehavior.cs:1 Source: src/DBTools.Core/Settings/Shell/SettingsWindowViewModel.cs:1


Architecture

The Options Pattern in DBTools

DBTools uses Microsoft.Extensions.Options with a custom persistence layer:

┌─────────────────────┐     ┌──────────────────────┐
│  settings.{YEAR}.json │────>│  IConfiguration      │
│  (user data)        │     │  (read from file)    │
└─────────────────────┘     └──────────┬───────────┘
                                       │
                            ┌──────────▼───────────┐
                            │  IOptionsMonitor<T>  │
                            │  (live reload)       │
                            └──────────┬───────────┘
                                       │
         ┌─────────────────────────────┼─────────────────────────────┐
         │                             │                             │
┌────────▼────────┐         ┌──────────▼──────────┐       ┌──────────▼──────────┐
│ Tool Command    │         │ ViewActivatedTask   │       │ Settings Window     │
│ (read current)  │         │ (check auto-update) │       │ (edit & save)       │
└─────────────────┘         └─────────────────────┘       └──────────┬──────────┘
                                                                     │
                                                          ┌──────────▼──────────┐
                                                          │  IOptionsWriter     │
                                                          │  (atomic persist)   │
                                                          └──────────┬──────────┘
                                                                     │
                                                          ┌──────────▼──────────┐
                                                          │  settings.{YEAR}.json │
                                                          │  (updated)          │
                                                          └─────────────────────┘

Key Interfaces

Interface Purpose
ISettingsProvider Read typed settings: Get<T>() and SaveAsync<T>()
IOptionsMonitor<T> Live-reloading settings with CurrentValue
IOptionsWriter Atomic write to settings file sections
IDbtSettingsPackDefinition Metadata about a settings pack (title, key, view factory)
IDbtSettingsPackContext<T> ViewModel interface for settings UI
IDbtSettingsWarningDefinition Warning linked to settings validation

Source: src/DBTools.Core/Settings/ISettingsProvider.cs:3-8


Defining Settings in manifest.yml

Settings packs are declared in the tool's manifest.yml under tool.settingsPacks:

Schema

tool:
  settings:
    configSection: "<section-name>"  # Required: JSON key in settings.{YEAR}.json
  settingsPacks:
    - key: "<unique-pack-key>"       # Required: Globally unique identifier
      title: "<display-title>"       # Required: Shown in Settings window
      warnings:                      # Optional: Associated warnings
        - id: "<warning-id>"         # Required: Unique warning identifier
          title: "<warning-title>"   # Required: Warning card title
          message: "<warning-msg>"   # Required: Explanation shown to user
          disableTools:              # Optional: Tools disabled when warning active
            - "<tool-internal-name>"
            - "<another-tool>"

Complete Example: FoundationTags

id: DBTools.Structural.FoundationTags
assembly: DBTools
moduleType: DBTools.Structural.FoundationTags.FoundationTagsToolModule
order: 0
tool:
  settings:
    configSection: Tools.FoundationTags
  settingsPacks:
    - key: structural.foundation_tags
      title: "Combined Foundation Tags"
      warnings:
        - id: core.structural.combined_tags
          title: "Combined Foundation Tags Disabled"
          message: "Combined foundation tag updates are disabled due to a warning. Clear the warning to re-enable."
          disableTools:
            - DBTools.UpdateCombinedFoundationTags
            - DBTools.MoveCombinedFoundationTags
            - DBTools.OrganizeFoundationTypes
  ribbonTools:
    # ... ribbon tool definitions

Source: src/Tools/Structural/FoundationTags/manifest.yml:1-18

Key Naming Conventions

Property Convention Example
configSection {Category}.{ToolName} Tools.FoundationTags
key {category}.{feature} structural.foundation_tags
warnings[].id core.{category}.{feature} core.structural.combined_tags

Settings Model Classes

Basic Settings Model

A settings model is a POCO class with public properties:

namespace DBTools.Structural.JoistGirderWeight.Settings;

public sealed class JoistGirderWeightSettings : IAutoUpdateSettings
{
    public bool AutoUpdateEnabled { get; set; } = true;
    public bool HasWarning { get; set; }
}

Source: src/Tools/Structural/JoistGirderWeight/Settings/JoistGirderWeightSettings.cs:5-10

IAutoUpdateSettings Interface

For tools with auto-update on view activation, implement IAutoUpdateSettings:

namespace DBTools.Core.Settings.Models;

public interface IAutoUpdateSettings
{
    bool AutoUpdateEnabled { get; set; }
    bool HasWarning { get; set; }
}

Source: src/DBTools.Core/Settings/Models/IAutoUpdateSettings.cs:3-7

Complex Settings Model

Settings can include collections, nested objects, and custom defaults:

namespace DBTools.Structural.FoundationTags.Settings;

public sealed class FoundationTagsSettings : IAutoUpdateSettings
{
    public bool AutoUpdateEnabled { get; set; } = true;
    public bool HasWarning { get; set; }
    
    /// <summary>
    /// Regex patterns to match tag family names.
    /// </summary>
    public List<string> TagFamilyPatterns { get; set; } = new()
    {
        "^Combined Foundation Tag",
        "^DB Foundation Tag"
    };
    
    /// <summary>
    /// Regex patterns to match pier family names.
    /// </summary>
    public List<string> PierFamilyPatterns { get; set; } = new()
    {
        "^Foundation Pier"
    };
    
    /// <summary>
    /// Regex patterns to match footing family names.
    /// </summary>
    public List<string> FootingFamilyPatterns { get; set; } = new()
    {
        "^Footing-Rectangular$",
        "^Pile Cap-Rectangular$"
    };
}

Source: src/Tools/Structural/FoundationTags/Settings/FoundationTagsSettings.cs:1-50

JSON Representation

Settings are stored in settings.{YEAR}.json under the configSection key:

{
  "Tools.FoundationTags": {
    "AutoUpdateEnabled": true,
    "HasWarning": false,
    "TagFamilyPatterns": [
      "^Combined Foundation Tag",
      "^DB Foundation Tag"
    ],
    "PierFamilyPatterns": [
      "^Foundation Pier"
    ],
    "FootingFamilyPatterns": [
      "^Footing-Rectangular$",
      "^Pile Cap-Rectangular$"
    ]
  }
}

Registering Settings

Settings registration happens in two phases within your DbtToolModule:

Phase 1: RegisterSettings()

Bind the settings model to configuration using the Options pattern:

public override void RegisterSettings(
    IServiceCollection services, 
    IConfiguration configuration, 
    DbtToolManifest manifest)
{
    Guard.NotNull(services, nameof(services));
    Guard.NotNull(configuration, nameof(configuration));
    Guard.NotNull(manifest, nameof(manifest));
    
    var configSection = manifest.GetRequiredConfigSection();
    services.AddOptions<FoundationTagsSettings>(configuration, configSection);
}

Source: src/Tools/Structural/FoundationTags/FoundationTagsToolModule.cs:16-23

This registration:

  1. Reads the configSection from manifest (Tools.FoundationTags)
  2. Binds IOptions<FoundationTagsSettings> to that section
  3. Enables IOptionsMonitor<T> for live configuration reload

Phase 2: RegisterSettingsPacks()

Register the full settings pack definition with UI factory:

public override void RegisterSettingsPacks(IServiceCollection services, DbtToolManifest manifest)
{
    Guard.NotNull(services, nameof(services));
    Guard.NotNull(manifest, nameof(manifest));
    
    var configSection = manifest.GetRequiredConfigSection();
    var pack = manifest.GetSingleSettingsPack();
    var warning = pack.GetSingleWarning(manifest.Id);
    var panel = DbtToolPanelResolver.GetSettingsPanelName(manifest.Id);
    
    services.AddSingleton<IDbtSettingsPackDefinition>(_ =>
    {
        // Define warnings
        var warningDefinitions = new[]
        {
            new DbtSettingsWarningDefinition<FoundationTagsSettings, FoundationTagsSettingsPackContext>(
                warning.Id,
                pack.Key,
                warning.Title,
                warning.Message,
                settings => settings.Get<FoundationTagsSettings>(),
                (settings, options, ct) => settings.SaveAsync(configSection, options, ct),
                options => options.HasWarning,
                (options, active) =>
                {
                    options.HasWarning = active;
                    if (active) options.AutoUpdateEnabled = false;
                },
                context => context.HasWarning,
                (context, active) =>
                {
                    context.HasWarning = active;
                    if (active) context.AutoUpdateEnabled = false;
                },
                warning.DisableTools?.ToArray() ?? Array.Empty<string>())
        };

        // Create pack definition
        return new DbtSettingsPackDefinition<FoundationTagsSettings, FoundationTagsSettingsPackContext>(
            pack.Key,
            pack.Title,
            panel,
            () => new FoundationTagsSettingsPackContext(),          // Context factory
            ctx => new FoundationTagsSettingsPackView { DataContext = ctx },  // View factory
            () => new FoundationTagsSettings(),                      // Defaults factory
            settings => settings.Get<FoundationTagsSettings>(),     // Options getter
            (settings, options, ct) => settings.SaveAsync(configSection, options, ct),  // Options saver
            warningDefinitions);
    });
}

Source: src/Tools/Structural/FoundationTags/FoundationTagsToolModule.cs:36-81

Using AutoUpdateSettingsPackContext (Simpler Pattern)

For simple auto-update settings without custom UI, use the built-in context:

public override void RegisterSettingsPacks(IServiceCollection services, DbtToolManifest manifest)
{
    var configSection = manifest.GetRequiredConfigSection();
    var pack = manifest.GetSingleSettingsPack();
    var warning = pack.GetSingleWarning(manifest.Id);
    var panel = DbtToolPanelResolver.GetSettingsPanelName(manifest.Id);
    
    services.AddSingleton<IDbtSettingsPackDefinition>(_ =>
    {
        var warningDefinitions = new[]
        {
            new DbtSettingsWarningDefinition<JoistGirderWeightSettings, AutoUpdateSettingsPackContext<JoistGirderWeightSettings>>(
                warning.Id, pack.Key, warning.Title, warning.Message,
                settings => settings.Get<JoistGirderWeightSettings>(),
                (settings, options, ct) => settings.SaveAsync(configSection, options, ct),
                options => options.HasWarning,
                (options, active) => { options.HasWarning = active; if (active) options.AutoUpdateEnabled = false; },
                context => context.HasWarning,
                (context, active) => { context.HasWarning = active; if (active) context.AutoUpdateEnabled = false; },
                warning.DisableTools?.ToArray() ?? Array.Empty<string>())
        };

        return new DbtSettingsPackDefinition<JoistGirderWeightSettings, AutoUpdateSettingsPackContext<JoistGirderWeightSettings>>(
            pack.Key, pack.Title, panel,
            () => new AutoUpdateSettingsPackContext<JoistGirderWeightSettings>(
                "Auto Update Joist Girder Weights",
                "Automatically calculate joist girder weights when opening views.",
                "Enable automatic joist girder weight updates when opening views"),
            ctx => new AutoUpdateSettingsPackView { DataContext = ctx },
            () => new JoistGirderWeightSettings(),
            settings => settings.Get<JoistGirderWeightSettings>(),
            (settings, options, ct) => settings.SaveAsync(configSection, options, ct),
            warningDefinitions);
    });
}

Source: src/Tools/Structural/JoistGirderWeight/JoistGirderWeightToolModule.cs:37-85


Settings Pack Context

The context is the ViewModel for your settings UI. It must implement IDbtSettingsPackContext<T>.

Required Interface Methods

public interface IDbtSettingsPackContext<TOptions> : IDbtSettingsPackContext
{
    void Load(TOptions options);       // Load settings into bindable properties
    TOptions BuildOptionsSnapshot();   // Create settings object from current state
}

public interface IDbtSettingsPackContext
{
    void Load(object options);
    object BuildOptionsSnapshot();
    Task<DbtSettingsValidationResult> ValidateAsync(CancellationToken ct = default);
}

Source: src/DBTools.Core/Settings/DbtToolSettingsPack.cs:8-22

Optional Context Interfaces

Interface Purpose
IDbtSettingsPackContextWithState Fire StateChanged event when properties change
IDbtSettingsPackContextWithPendingChanges Track HasPendingChanges for save button state
IDbtSettingsPackContextOwnerAware Receive parent window for file dialogs
IDbtSettingsPackContextSaveAware Reset baseline after save via MarkSaved()
IDbtSettingsPackContextValidateAware Trigger warning evaluation via WarningId
IDbtSettingsPackContextBlockSaveOnInvalid Block settings save when ValidateAsync returns invalid

Source: src/DBTools.Core/Settings/Features/DbtSettingsPackContextContracts.cs

Use IDbtSettingsPackContextBlockSaveOnInvalid for settings where invalid values should never be persisted (for example, invalid regex patterns that would be ignored at runtime).

Built-in: AutoUpdateSettingsPackContext

For standard auto-update toggle + warning, use the built-in context:

public sealed partial class AutoUpdateSettingsPackContext<TSettings> : ObservableObject,
    IDbtSettingsPackContext<TSettings>,
    IDbtSettingsPackContextWithState,
    IDbtSettingsPackContextWithPendingChanges
    where TSettings : class, IAutoUpdateSettings, new()
{
    public AutoUpdateSettingsPackContext(string title, string description, string toggleToolTip)
    {
        // Validates inputs and stores for binding
    }
    
    public string Title { get; }
    public string Description { get; }
    public string ToggleToolTip { get; }
    public bool AutoUpdateEnabled { get; set; }
    public bool HasWarning { get; set; }
    public bool ToggleEnabled => !HasWarning;
    public bool HasPendingChanges { get; }
    public event EventHandler? StateChanged;
    
    public void Load(TSettings options) { /* ... */ }
    public TSettings BuildOptionsSnapshot() { /* ... */ }
    public Task<DbtSettingsValidationResult> ValidateAsync(CancellationToken ct) { /* ... */ }
}

Source: src/DBTools.Core/Settings/AutoUpdateSettingsPackContext.cs:8-130

Custom Context Example

For tools requiring additional UI beyond the toggle, create a custom context:

public sealed partial class FoundationTagsSettingsPackContext : ObservableObject,
    IDbtSettingsPackContext<FoundationTagsSettings>,
    IDbtSettingsPackContextWithState,
    IDbtSettingsPackContextWithPendingChanges
{
    private const int MaxPatterns = 3;
    private FoundationTagsSettings _baseline = new();

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(ToggleEnabled))]
    private bool _autoUpdateEnabled = true;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(ToggleEnabled))]
    private bool _hasWarning;

    public FoundationTagsSettingsPackContext()
    {
        TagFamilyPatterns = new ObservableCollection<RegexPatternEntry>();
        PierFamilyPatterns = new ObservableCollection<RegexPatternEntry>();
        FootingFamilyPatterns = new ObservableCollection<RegexPatternEntry>();
        
        // Subscribe to collection changes
        TagFamilyPatterns.CollectionChanged += (_, _) => NotifyStateChanged();
        // ... etc
    }

    public ObservableCollection<RegexPatternEntry> TagFamilyPatterns { get; }
    public ObservableCollection<RegexPatternEntry> PierFamilyPatterns { get; }
    public ObservableCollection<RegexPatternEntry> FootingFamilyPatterns { get; }

    public void Load(FoundationTagsSettings options)
    {
        AutoUpdateEnabled = options.AutoUpdateEnabled;
        HasWarning = options.HasWarning;
        LoadPatternCollection(TagFamilyPatterns, options.TagFamilyPatterns, "^Combined Foundation Tag");
        // ... load other collections
        _baseline = CloneOptions(options);
        NotifyStateChanged();
    }

    public FoundationTagsSettings BuildOptionsSnapshot()
    {
        return new FoundationTagsSettings
        {
            AutoUpdateEnabled = HasWarning ? false : AutoUpdateEnabled,
            HasWarning = HasWarning,
            TagFamilyPatterns = TagFamilyPatterns.Select(p => p.Pattern).Where(p => !string.IsNullOrWhiteSpace(p)).ToList(),
            // ... etc
        };
    }

    public Task<DbtSettingsValidationResult> ValidateAsync(CancellationToken ct = default)
    {
        var errors = new List<string>();
        ValidatePatternGroup(errors, TagFamilyPatterns, "tag family");
        // ... validate other groups
        
        return errors.Count == 0
            ? Task.FromResult(DbtSettingsValidationResult.Success)
            : Task.FromResult(DbtSettingsValidationResult.FromErrors(errors.ToArray()));
    }

    [RelayCommand]
    private void AddTagPattern() => AddPattern(TagFamilyPatterns, nameof(CanAddTagPattern), nameof(CanRemoveTagPattern));

    [RelayCommand]
    private void ClearWarning()
    {
        HasWarning = false;
        NotifyStateChanged();
    }
}

Source: src/Tools/Structural/FoundationTags/Settings/FoundationTagsSettingsPackContext.cs:18-272


Accessing Settings at Runtime

In Commands (One-time Read)

Use ISettingsProvider.Get<T>() for a snapshot of current settings:

public class MyCommand : DbtToolCommand
{
    protected override Result ExecuteCore(ExternalCommandData commandData, ref string message, ElementSet elements)
    {
        var settings = AppRuntime.Settings.Get<MyToolSettings>();
        
        if (!settings.SomeFeatureEnabled)
        {
            message = "Feature is disabled in settings.";
            return Result.Cancelled;
        }
        
        // Use settings.SomeValue, settings.SomeList, etc.
    }
}

In Services (Live Reload)

Inject IOptionsMonitor<T> for settings that update without restart:

public class MyService
{
    private readonly IOptionsMonitor<MyToolSettings> _options;
    
    public MyService(IOptionsMonitor<MyToolSettings> options)
    {
        _options = options;
    }
    
    public void DoWork()
    {
        // Always reflects current settings.{YEAR}.json values
        var current = _options.CurrentValue;
        
        if (current.AutoUpdateEnabled && !current.HasWarning)
        {
            // Perform auto-update logic
        }
    }
}

In ViewActivatedTask (Auto-update Pattern)

The ViewActivatedTaskBase<T> abstracts settings access for view-activated tools:

public sealed class CombinedTagsViewActivatedTask : ViewActivatedTaskBase<FoundationTagsSettings>
{
    public CombinedTagsViewActivatedTask(
        ISettingsProvider settingsProvider,
        ILogger<CombinedTagsViewActivatedTask> logger,
        string[] warningIds)
        : base(settingsProvider, logger, warningIds)
    {
    }

    protected override FoundationTagsSettings GetSettings(ISettingsProvider settingsProvider)
        => settingsProvider.Get<FoundationTagsSettings>();

    protected override void ExecuteTask(Document document, View view)
    {
        // Called only when AutoUpdateEnabled && !HasWarning
    }
}

Source: src/DBTools.Core/Tools/ViewActivatedTaskBase.cs:16-80


Persisting Settings

Automatic Save (Settings Window)

When the user clicks Save in the Settings window, SettingsViewModel.Save() handles persistence:

// Inside SettingsViewModel.Save()
foreach (var pack in _packLookup.Values)
{
    var snapshot = pack.Pack.BuildOptionsSnapshot();
    await pack.Definition.SaveOptionsAsync(_settings, snapshot, CancellationToken.None);
    _publisher.PublishPackChanged(pack.Key, pack.Definition.OptionsType, snapshot);
}

Source: src/DBTools.Core/Settings/ViewModels/SettingsViewModel.cs:259-292

Manual Save (Programmatic)

Save settings directly using ISettingsProvider:

var settings = AppRuntime.Settings.Get<MyToolSettings>();
settings.SomeValue = newValue;
await AppRuntime.Settings.SaveAsync("Tools.MyTool", settings);

IOptionsWriter (Low-level)

For direct section writes, use IOptionsWriter:

public interface IOptionsWriter
{
    /// <summary>
    /// Save a typed settings object under a named section in the settings file.
    /// The write must be atomic (write temp + replace).
    /// </summary>
    Task SaveSectionAsync<T>(string section, T data, CancellationToken ct = default) where T : class;
}

Source: src/DBTools.Core/Settings/Contracts/IOptionsWriter.cs:1-10

The OptionsWriter implementation ensures atomic writes:

  1. Write to settings.{YEAR}.json.tmp
  2. Use File.Replace() for atomic swap
  3. Retry on IOException (file in use)

Source: src/DBTools.Core/Settings/OptionsWriter.cs:27-110


Settings UI Integration

View Location

Settings pack views are rendered in the Settings window under their assigned panel. The panel is determined by DbtToolPanelResolver.GetSettingsPanelName():

Tool ID Pattern Panel
DBTools.Settings DB Tools Settings
DBTools.Structural.* or DBTools.SGT DB Tools Structural
DBTools.VTC or DBTools.Testing.* DB Tools Testing
Everything else DB Tools Common

Source: src/DBTools.Core/Tools/DbtToolPanelResolver.cs:27-44

Built-in AutoUpdateSettingsPackView

For simple toggle + description, use the provided view:

<UserControl x:Class="DBTools.Core.Settings.Views.AutoUpdateSettingsPackView">
    <Border Style="{DynamicResource FeatureCard}">
        <Grid>
            <ToggleButton
                IsChecked="{Binding AutoUpdateEnabled, Mode=TwoWay}"
                IsEnabled="{Binding ToggleEnabled}"
                Style="{DynamicResource SwitchToggleButton}"
                ToolTip="{Binding ToggleToolTip}" />
            <TextBlock Text="{Binding Title}" />
            <TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
        </Grid>
    </Border>
</UserControl>

Source: src/DBTools.Core/Settings/Views/AutoUpdateSettingsPackView.xaml:1-54

Custom Views

For complex settings, create a custom view in your tool's Settings/ folder:

<UserControl x:Class="DBTools.Structural.FoundationTags.Settings.FoundationTagsSettingsPackView">
    <StackPanel>
        <!-- Auto-update toggle -->
        <CheckBox IsChecked="{Binding AutoUpdateEnabled}" 
                  IsEnabled="{Binding ToggleEnabled}" />
        
        <!-- Tag pattern list -->
        <ItemsControl ItemsSource="{Binding TagFamilyPatterns}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Pattern, UpdateSourceTrigger=PropertyChanged}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        
        <!-- Add/Remove buttons -->
        <Button Command="{Binding AddTagPatternCommand}" 
                IsEnabled="{Binding CanAddTagPattern}" />
    </StackPanel>
</UserControl>

Warning Integration

How Warnings Work

Warnings are conditions that disable tool functionality:

  1. Warning activates: Validation fails (e.g., invalid path)
  2. Tools disabled: Ribbon buttons grayed out, commands rejected
  3. User notified: Warning card appears in Settings window
  4. User fixes: Corrects settings, clicks "Clear Warning"
  5. Tools re-enabled: Ribbon buttons active again

Warning Definition

In RegisterSettingsPacks(), create a DbtSettingsWarningDefinition:

new DbtSettingsWarningDefinition<TSettings, TContext>(
    id: "core.structural.combined_tags",       // Unique warning ID
    packKey: "structural.foundation_tags",     // Parent pack key
    title: "Combined Foundation Tags Disabled",
    message: "Combined foundation tag updates are disabled due to a warning...",
    optionsGetter: settings => settings.Get<TSettings>(),
    optionsSaver: (settings, options, ct) => settings.SaveAsync(section, options, ct),
    optionsFlagGetter: options => options.HasWarning,
    optionsFlagSetter: (options, active) => { options.HasWarning = active; if (active) options.AutoUpdateEnabled = false; },
    contextFlagGetter: context => context.HasWarning,
    contextFlagSetter: (context, active) => { context.HasWarning = active; if (active) context.AutoUpdateEnabled = false; },
    toolInternalNamesToDisable: new[] { "DBTools.MyCommand" })

Source: src/DBTools.Core/Settings/DbtSettingsWarningDefinition.cs:22-99

Warning Service

IDbtSettingsWarningService manages warning state across the application:

public interface IDbtSettingsWarningService
{
    IReadOnlyCollection<IDbtSettingsWarningDefinition> Definitions { get; }
    Task<bool> IsWarningActiveAsync(string warningId, CancellationToken ct = default);
    Task SetWarningAsync(string warningId, bool active, object? context = null, CancellationToken ct = default);
    Task PublishWarningStateAsync(CancellationToken ct = default);
}

Source: src/DBTools.Core/Settings/DbtSettingsWarningService.cs:11-18

Validation-Triggered Warnings

Implement IDbtSettingsPackContextValidateAware to automatically trigger warnings on validation:

public sealed partial class LibrarySettingsPackContext : ObservableObject,
    IDbtSettingsPackContext<MasterLibrarySettings>,
    IDbtSettingsPackContextValidateAware  // <-- Key interface
{
    public string WarningId => _warningId;  // From constructor

    public async Task<DbtSettingsValidationResult> ValidateAsync(CancellationToken ct = default)
    {
        var options = BuildOptionsSnapshot();
        var invalidEntries = options.Files.Where(f => !IsValid(f)).ToList();
        
        HasWarnings = invalidEntries.Count > 0;
        
        if (HasWarnings)
            return new DbtSettingsValidationResult(false, new[] { "Invalid library path" });
        
        return DbtSettingsValidationResult.Success;
    }
}

Source: src/Tools/Common/TDV/Settings/LibrarySettingsPackContext.cs:19-169

Startup Evaluation (Validate-Aware Packs)

Validate-aware settings packs are evaluated during addin startup (after ribbon composition and before the initial warning publish) so ribbon tools are disabled immediately when underlying configuration is invalid, without requiring the user to open Settings.

The startup evaluator:

  • Creates pack contexts without constructing WPF views
  • Loads options from ISettingsProvider
  • Runs ValidateAsync only for IDbtSettingsPackContextValidateAware contexts
  • Activates warnings on invalid results
  • Does not activate warnings on validation exceptions (exceptions are treated as runtime errors and should surface via normal error flows/logs)
  • Does not proactively clear warnings on startup

Source: src/DBTools.App/Addin/AddinEntry.cs:324 Source: src/DBTools.Core/Settings/Features/DbtSettingsStartupWarningEvaluator.cs:36


Real Examples

Example 1: JoistGirderWeight (Auto-Update + Regex Matching)

Use case: Toggle auto-calculation and configure regex matching for family/load resolution.

Settings model:

public sealed class JoistGirderWeightSettings : IAutoUpdateSettings
{
    public bool AutoUpdateEnabled { get; set; } = true;
    public bool HasWarning { get; set; }
    public List<string> FamilyNamePatterns { get; set; } = new()
    {
        "^(?:(?:[BV]?G)|Vulcraft) Joist Girder$"
    };
    public List<string> LoadParameterPatterns { get; set; } = new()
    {
        "^(?:Panel Point Load|Panel Load - User Defined|Panel Load|Point Load - User Defined|Point Load|Total Load - User Defined|Total Load|Joist Load|BG Joist Girder)$"
    };
}

Source: src/Tools/Structural/JoistGirderWeight/Settings/JoistGirderWeightSettings.cs

Registration: Uses custom JoistGirderWeightSettingsPackContext and JoistGirderWeightSettingsPackView with +/- regex row controls (max 3) and save-time validation.

Manifest:

settingsPacks:
  - key: structural.joist_girder_weight
    title: "Joist Girder Weights"
    warnings:
      - id: core.structural.joist_girder
        title: "Joist Girder Weights Disabled"
        disableTools:
          - DBTools.UpdateJoistGirderWeights

Source: src/Tools/Structural/JoistGirderWeight/manifest.yml:8-16

Example 2: FoundationTags (Custom Patterns)

Use case: Configure regex patterns for family matching.

Settings model: Includes List<string> for patterns.

Source: src/Tools/Structural/FoundationTags/Settings/FoundationTagsSettings.cs:1-50

Context: Custom FoundationTagsSettingsPackContext with pattern editing commands.

Source: src/Tools/Structural/FoundationTags/Settings/FoundationTagsSettingsPackContext.cs:18-272

View: Custom FoundationTagsSettingsPackView.xaml with pattern lists.

Example 3: TDV Library (Complex Validation)

Use case: Configure library file paths with validation warnings.

Settings model:

public sealed class MasterLibrarySettings
{
    public IList<LibraryFileEntry> Files { get; set; } = new List<LibraryFileEntry>();
    public bool HasWarnings { get; set; }
}

Source: src/Tools/Common/TDV/Settings/MasterLibrarySettings.cs:1-7

Context: LibrarySettingsPackContext with file picker, tree view, and validation.

Source: src/Tools/Common/TDV/Settings/LibrarySettingsPackContext.cs:19-352

Key features:

  • IDbtSettingsPackContextOwnerAware for file dialogs
  • IDbtSettingsPackContextSaveAware for baseline reset
  • IDbtSettingsPackContextValidateAware for automatic warning activation

Best Practices

Do

  • Use IAutoUpdateSettings for tools with view-activated auto-update
  • Clone settings in context to detect HasPendingChanges
  • Fire StateChanged when any bindable property changes
  • Implement validation that returns meaningful error messages
  • Use existing patterns - check JoistGirderWeight for simple, FoundationTags for complex

Don't

  • Don't save directly from context - let SettingsViewModel.Save() handle persistence
  • Don't bypass warnings - respect HasWarning in command availability
  • Don't block UI thread - use async for file I/O in validation
  • Don't ignore CancellationToken - validation can be cancelled

Testing Settings

  1. Build artifacts tests verify settings structure
  2. Integration tests can mock ISettingsProvider
  3. Manual testing in Revit validates UI binding


Summary

Settings Packs provide a unified way to expose tool configuration:

  1. Declare in manifest.yml with settingsPacks
  2. Model settings as a typed class (optionally implementing IAutoUpdateSettings)
  3. Register via RegisterSettings() and RegisterSettingsPacks() in your tool module
  4. Access at runtime with ISettingsProvider.Get<T>() or IOptionsMonitor<T>
  5. Persist automatically through the Settings window or manually via IOptionsWriter
  6. Warn users when validation fails, disabling affected tools until resolved

The system provides live-reload, atomic persistence, and consistent UX across all DBTools tools.