Gum Tool ViewModel Conventions

SkillAI & models

Gum tool ViewModel conventions. Triggers: ViewModels, XAML views, data binding, DependsOn, visibility properties.

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 Tool ViewModel Conventions skill

What this skill tells your AI

The instructions your AI receives, as published by vchelaru/gum in .claude/skills/gum-tool-viewmodels/SKILL.md and read by ahel’s review.

Base Classes

  • ViewModel (Gum/Mvvm/ViewModel.cs) — base for all view models. Provides Get<T>()/Set() property storage, NotifyPropertyChanged, and [DependsOn] propagation.
  • DialogViewModel (Tools/Gum.Presentation/Dialogs/DialogViewModel.cs) — extends ViewModel for dialogs. Adds AffirmativeCommand/NegativeCommand, RequestClose event, and AffirmativeText/NegativeText. Lives in the headless Gum.Presentation assembly (ADR-0005). Any dialog VM deriving from it is safe to relocate there: DialogViewResolver falls back to scanning other loaded assemblies (IDialogViewAssemblyProvider) when the VM's own assembly has no View — but that fallback only pairs via [Dialog(typeof(VM))] on the View, not naming convention, so attribute the View before (or alongside) moving its VM. See gum-tool-dialogs.

Property Patterns

Stored properties use Get<T>()/Set():

public string Name
{
    get => Get<string>() ?? string.Empty;
    set => Set(value);
}

Derived properties must use [DependsOn] so changes to the source property automatically raise PropertyChanged for the derived property. Without this, the UI will not update.

[DependsOn(nameof(SourceType))]
public bool IsLocalFile => SourceType == SourceType.LocalFile;

Multiple dependencies are expressed with multiple attributes:

[DependsOn(nameof(IsPreviewLoaded))]
[DependsOn(nameof(IsLoading))]
public bool CanImport => IsPreviewLoaded && !IsLoading;

View Logic on the VM, Not in XAML

The boundary is logic vs. theming:

  • Logic — what to show, when, in what state — lives on the VM as [DependsOn] computed properties, in framework-neutral types (ADR-0004). Examples: bool visibility flags, FontStyle, display strings, enabled/disabled flags. XAML binds directly (a bool visibility flag through a stock BooleanToVisibilityConverter). Unit-testable, and the VM stays eligible for the headless assembly.
  • Theming — which brush, which font size — stays in XAML so {DynamicResource ...} can repaint on a runtime theme switch. Not testable, by necessity (DynamicResource only resolves through a FrameworkElement).

Do not use IValueConverter or DataTrigger for logic. The one exception is themed brushes (see below).

Visibility example — expose a bool, not WPF Visibility (ADR-0004), so the VM can move into the headless assembly:

[DependsOn(nameof(ErrorMessage))]
public bool IsErrorMessageVisible => !string.IsNullOrEmpty(ErrorMessage);

XAML binds through the stock BooleanToVisibilityConverter (a pure type-adapter, not logic):

<TextBlock Visibility="{Binding IsErrorMessageVisible, Converter={StaticResource BoolToVisibilityConverter}}" />

Never expose System.Windows types from a VMVisibility, Color, Brush, WriteableBitmap. They pin the VM to the WPF assembly and defeat the compiler-enforced logic↔view boundary. Resolve display state in neutral types instead: bool for visibility, Gum's color type (RenderingLibrary has one) for colors, a color in place of a Brush, byte[]/a Gum image for pixels. Framework types live only in the view/converter layer. See Direction/decisions/0004-viewmodels-expose-neutral-presentation-state.md.

VMs in GumCommon or runtime projects must stay UI-agnostic — this rule is for tool code only.

Exception: themed brushes

Light/dark theming uses brushes defined in Gum/Themes/Frb.Brushes.{Light,Dark}.xaml (e.g. Frb.Brushes.Error). These must be resolved with {DynamicResource ...} so a runtime theme switch repaints — that only works from a FrameworkElement, not from a VM-side Brush property. The right pattern:

  • Keep the logical state on the VM (IsOrphaned, IsInvalid, etc., still [DependsOn]-driven and unit-testable).
  • Apply the brush via a Style with a DataTrigger keyed off that VM bool, e.g. <Setter Property="Foreground" Value="{DynamicResource Frb.Brushes.Error}" />.

This is the only situation where DataTrigger is preferred over a VM-side property.

Common Pitfalls

Missing [DependsOn]: If a getter computes from another property but lacks the attribute, the UI will show stale values. The ViewModel constructor scans for [DependsOn] via reflection at construction time — it only works if the attribute is present.

Two-way derived properties: Properties like IsLocalFile that both read from and write to a backing property need [DependsOn] for the read direction. The write direction (setter updating SourceType) works normally through Set().

Signals

GitHub stars
620
Forks
80
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
gum-tool-viewmodels
Source
github.com/vchelaru/gum