| name | gum-tool-viewmodels |
| description | Gum tool ViewModel conventions. Triggers: ViewModels, XAML views, data binding, DependsOn, visibility properties. |
Gum Tool ViewModel Conventions
Base Classes
ViewModel (Gum/Mvvm/ViewModel.cs) — base for all view models. Provides Get<T>()/Set() property storage, NotifyPropertyChanged, and [DependsOn] propagation.
DialogViewModel (Gum/Services/Dialogs/DialogViewModel.cs) — extends ViewModel for dialogs. Adds AffirmativeCommand/NegativeCommand, RequestClose event, and AffirmativeText/NegativeText.
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 VM — Visibility, 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().