Gum Dialog Systems Reference

SkillCommunication

Gum 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.

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 title
  • Dialog.Actions — custom button area (replaces default OK/Cancel)
  • Dialog.AuxiliaryActions — extra buttons on the left side (e.g. Browse)
  • Dialog.ScrollContenttrue (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

FileSystemPurpose
Gum/Services/Dialogs/DialogService.csMVVMCreates and shows DialogWindow instances
Gum/Services/Dialogs/DialogWindow.xamlMVVMWindow chrome, layout template with ScrollViewer + button footer
Gum/Services/Dialogs/Dialog.csMVVMContentControl with attached properties and template selector
Gum/Services/Dialogs/DialogViewResolver.csMVVMMaps 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.csMVVMBase class with affirm/negative commands and RequestClose event
Gum/Gui/Windows/DeleteOptionsWindow.xamlStandaloneDelete confirmation window layout
Gum/Gui/Windows/DeleteOptionsWindow.xaml.csStandaloneCode-behind with plugin-accessible StackPanel
Gum/Services/Dialogs/DeleteDialogService.csStandaloneCreates and shows DeleteOptionsWindow; calls the concrete PluginManager
Tools/Gum.Presentation/Managers/DeleteLogic.csStandaloneOrchestrates 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