Gum Dialog Systems Reference
SkillCommunicationGum dialog/popup systems. Triggers: DialogService, DialogWindow, DeleteOptionsWindow, dialog scrolling/layout, adding new dialog types, ShowMessage/ShowYesNoMessage, mocking IDialogService in unit tests.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Gum Dialog Systems Reference skill
What this skill tells your AI
The instructions your AI receives, as published by vchelaru/gum in .claude/skills/gum-tool-dialogs/SKILL.md and read by ahel’s review.
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 titleDialog.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;falsedisables 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 aDeleteOptionCheckboxViewModeltodialog.CheckBoxesor a pick-oneDeleteOptionChoiceViewModeltodialog.Choices; read the user's choice back inDeleteOptionsConfirmed. Dispatch goes throughIDeletePluginNotifier.ShowDeleteOptions/ConfirmDeleteOptions.DeleteObjectPluginand the State Animation plugin use this. - WPF-only (legacy):
WpfPluginBase.DeleteOptionsWindowShow/DeleteConfirmedhand the plugin theDeleteOptionsWindowto add controls toMainStackPanel. 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.
Signals
- GitHub stars
- 620
- Forks
- 80
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
gum-tool-dialogs- Source
- github.com/vchelaru/gum