# Gum Tool Dialogs

> Gum Dialog Systems Reference

- Skill: `vchelaru/gum-tool-dialogs` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/gum-tool-dialogs`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/gum-tool-dialogs/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/gum-tool-dialogs

---


# Gum Dialog Systems Reference

## Two Separate Systems

Gum has **two independent dialog systems**. Knowing which one is involved is critical before making changes.

### 1. DialogService System (MVVM, most dialogs)

Used by: message popups, yes/no confirmations, text input, choice selection, plugin management, import dialogs.

**Flow**: `DialogService` creates a `DialogWindow`, sets its `DataContext` to a view model. The `Dialog` control inside uses a `DialogTemplateSelector` to resolve the view model type to a `UserControl` view. After the view loads, `Dialog.OnContentChanged` binds attached properties from the view up to the `Dialog` control via deferred dispatch (`DispatcherPriority.Loaded`).

**Key attached properties on views** (set in XAML on the UserControl):
- `Dialog.DialogTitle` — window title
- `Dialog.Actions` — custom button area (replaces default OK/Cancel)
- `Dialog.AuxiliaryActions` — extra buttons on the left side (e.g. Browse)
- `Dialog.ScrollContent` — `true` (default) enables outer ScrollViewer; `false` disables it so the view can manage its own scrolling (used by ImportFromGumxView)

**View resolution**: `DialogViewResolver` maps view model types to views by naming convention (`FooViewModel` -> `FooView`) or by `[Dialog(typeof(VM))]` attribute. Scans assemblies lazily and caches results. It always scans the VM's own assembly first, then - if unresolved - falls back to `IDialogViewAssemblyProvider` (default: every assembly currently loaded in the process) to find a VM whose View lives elsewhere (e.g. a `DialogViewModel` relocated into the headless `Gum.Presentation` assembly, paired with a View that stays in the Gum tool assembly or a dynamically-loaded plugin). **The fallback only works via `[Dialog(typeof(VM))]`** - naming-convention matching pairs a VM+View found within the *same* scanned assembly, so it can never bridge a cross-assembly pairing. Attribute the View before moving its VM out of the tool assembly. Plugin dialog views live in the WPF head (`Gum/PluginViews/<Plugin>/`) so the plugin assemblies stay WPF-free. The Avalonia head has no resolver: it maps each view model to its view in `Tool/Gum.Avalonia/Dialogs/DialogViewRegistry.cs` and binds the window title to the view model's `Title`.

**Window sizing**: `DialogWindow` starts with `SizeToContent="WidthAndHeight"`. After content loads, `Dialog.OnContentChanged` switches to `SizeToContent.Manual` and clears the view's fixed Width/Height (sets to NaN), allowing the window to be resizable. `DialogService.CreateDialogWindow` sets `MaxHeight` to the owner window's `ActualHeight`. `IDialogService` has no size option: a dialog that must open at a fixed size (the import dialog, whose tree would otherwise grow the window) sets `Width`/`Height` on its view, which becomes the window's starting size in both heads.

### 2. DeleteOptionsWindow (standalone, code-behind)

Used by: delete confirmation only (`DeleteLogic.ShowDeleteDialog`).

**Why it exists separately**: The delete dialog needs runtime UI composition — plugins inject checkboxes and options into a `StackPanel` (e.g., "Delete associated files?", "Remove child instances?"). This cannot be done through the MVVM template system.

**Never give an injected control an explicit `Width`.** WPF centers an element that has a fixed size inside space it would otherwise stretch into, so a fixed-width checkbox renders indented relative to its neighbours, and a label longer than that width is clipped rather than wrapped. Being standalone, this window gets none of `Dialog.OnContentChanged`'s fixed-size clearing described above. Set `HorizontalAlignment.Left` instead.

**A checkbox label only wraps with an explicit `MaxWidth` on its own `TextBlock`.** The themed `CheckBox` template (`Themes/Frb.Styles.Defaults.xaml`) measures its `ContentPresenter` in an `Auto` column, which supplies no width to wrap against, so `TextWrapping` alone is a no-op that widens the window instead. This applies to every checkbox in the tool, not just this dialog.

**Flow**: `DeleteLogic` (headless, `Tools/Gum.Presentation/Managers/DeleteLogic.cs`) calls the head's `IDeleteDialogService`. Plugins contribute options in one of two ways:

- **Neutral (use this):** handle `PluginBase.DeleteOptionsShow(DeleteOptionsDialogViewModel, Array)` and add a `DeleteOptionCheckboxViewModel` to `dialog.CheckBoxes` or a pick-one `DeleteOptionChoiceViewModel` to `dialog.Choices`; read the user's choice back in `DeleteOptionsConfirmed`. Dispatch goes through `IDeletePluginNotifier.ShowDeleteOptions`/`ConfirmDeleteOptions`. `DeleteObjectPlugin` and the State Animation plugin use this.
- **WPF-only (legacy):** `WpfPluginBase.DeleteOptionsWindowShow`/`DeleteConfirmed` hand the plugin the `DeleteOptionsWindow` to add controls to `MainStackPanel`. Only CodeOutputPlugin still does this.

The WPF `DeleteDialogService` creates the `DeleteOptionsWindow`, fires the WPF event (concrete `PluginManager.ShowDeleteDialog()`), then the neutral one, renders the neutral options into `MainStackPanel` (`DeleteOptionCheckboxExtensions.ToCheckBox`/`ToGroupBox`, bound two-way), and calls `ShowDialog()`. The Avalonia head's `AvaloniaDeleteDialogService` shows the `DeleteOptionsDialogViewModel` through `IDialogService` (`DeleteOptionsDialogView`), so under Avalonia this dialog **is** part of the MVVM system. `DeleteDialogService` depends on the concrete `PluginManager` for the WPF-typed pair, which lives only on the concrete class.

**Not managed by DialogService** — no view model, no template selection, no attached property binding. Changes to `DialogWindow.xaml` or `Dialog.cs` have **zero effect** on this window.

## Key Files

| File | System | Purpose |
|------|--------|---------|
| `Gum/Services/Dialogs/DialogService.cs` | MVVM | Creates and shows DialogWindow instances |
| `Gum/Services/Dialogs/DialogWindow.xaml` | MVVM | Window chrome, layout template with ScrollViewer + button footer |
| `Gum/Services/Dialogs/Dialog.cs` | MVVM | ContentControl with attached properties and template selector |
| `Gum/Services/Dialogs/DialogViewResolver.cs` | MVVM | Maps view model types to view types; falls back to scanning its own (tool) assembly for a relocated VM's `[Dialog]`-attributed View |
| `Gum/Services/Dialogs/DialogViewModel.cs` | MVVM | Base class with affirm/negative commands and RequestClose event |
| `Gum/Gui/Windows/DeleteOptionsWindow.xaml` | Standalone | Delete confirmation window layout |
| `Gum/Gui/Windows/DeleteOptionsWindow.xaml.cs` | Standalone | Code-behind with plugin-accessible StackPanel |
| `Gum/Services/Dialogs/DeleteDialogService.cs` | Standalone | Creates and shows DeleteOptionsWindow; calls the concrete `PluginManager` |
| `Tools/Gum.Presentation/Managers/DeleteLogic.cs` | Standalone | Orchestrates the delete flow via `IDeleteDialogService` |

## Avalonia head

The Avalonia head (`Tool/Gum.Avalonia`) has its own synchronous `IDialogService`
(`Dialogs/AvaloniaDialogService.cs`, a nested dispatcher loop per dialog). It does not scan
assemblies: `Dialogs/DialogViewRegistry.cs` maps each `DialogViewModel` type to a C# view factory
(`Register<TViewModel>(() => new SomeView())`; a registration covers subclasses, so every
`GetUserStringDialogBaseViewModel` shares one view). `DialogWindow` supplies the OK/Cancel row from
the VM; a view sets `DialogWindow.SetDialogTitle(this, "...")` and
`DialogWindow.SetAuxiliaryActions(this, control)` where the WPF view used `Dialog.DialogTitle` and
`Dialog.AuxiliaryActions`. `Tests/Gum.Avalonia.Tests/DialogViewRegistryTests` fails when a concrete
`DialogViewModel` in `Gum.Presentation` has no registered view and no named owner in its
`OwnedElsewhere` list, so **adding a dialog VM means registering its Avalonia view in the same PR**.

**Menu actions run after the menu closes.** Every Avalonia menu item (main menu, context menus,
the Variables tab, the Animations tab, the Standards palette) invokes its action through
`MenuItemActions.InvokeAfterClose` (`Tool/Gum.Avalonia/Shell/MenuItemActions.cs`). The dialog
service is synchronous (a nested dispatcher loop), so an action invoked inside the click handler
opened its dialog while the menu was still open, and the menu's light-dismiss swallowed the first
click into the dialog. A new menu site must use the same helper; `MenuBuilderTests` pins it.

## Common Pitfalls

**Wrong system**: The most common mistake is modifying `DialogWindow.xaml` or `Dialog.cs` expecting it to affect the delete dialog. Always verify which system shows the dialog you're fixing.

**File copy prompt**: The "copy or reference?" dialog shown when a SourceFile/Font path outside the project folder is assigned lives in `SetVariableLogic.AskIfShouldCopy` (`Gum/Plugins/InternalPlugins/VariableGrid/SetVariableLogic.cs`), triggered via `ReactIfChangedMemberIsSourceFile` — not in the drag-drop layer.

**ScrollViewer behavior**: The `Dialog` template wraps content in a ScrollViewer. With `Auto` scrolling, child controls get infinite available height during WPF measure — so internal scroll viewers (like a TreeView) won't scroll. Set `Dialog.ScrollContent="False"` on views that need bounded height for internal scrolling.

**Deferred binding**: `Dialog.OnContentChanged` binds attached properties at `DispatcherPriority.Loaded`, not immediately. Code that reads these values before the dispatch fires will see defaults.

## Testing Dialog Interactions (mocking IDialogService)

`IDialogService` exposes one message primitive: `ShowMessage(message, title?, MessageDialogStyle?)`, returning a `MessageDialogResult` enum (`Affirmative` / `Negative` / `Canceled`; `Negative == 0` is the unmocked Moq default → a `Mock<IDialogService>` returns "No" unless you set it up). The friendly helpers — `ShowYesNoMessage`, `ShowChoices`, etc. — are **extension methods** in `IDialogServiceExtensions` (`Gum.Presentation`), so a Moq mock can only `Setup`/`Verify` the underlying `ShowMessage`. A Yes/No prompt arrives as `ShowMessage(msg, title, MessageDialogStyle.YesNo)`.

Gotcha: `MessageDialogStyle` is a **class**, and `.Ok` / `.YesNo` / `.OkCancel` each `new` up a fresh instance with no equality override — you cannot match a specific style by reference or equality. Distinguish a styled prompt (Yes/No, OK/Cancel) from a plain informational popup by **`style != null` vs `style == null`** — a bare `ShowMessage(msg)` passes `null` — or by inspecting `AffirmativeText`.

