Gum Tool ViewModel Conventions
Base Classes
ViewModel(Gum/Mvvm/ViewModel.cs) — base for all view models. ProvidesGet<T>()/Set()property storage,NotifyPropertyChanged, and[DependsOn]propagation.DialogViewModel(Tools/Gum.Presentation/Dialogs/DialogViewModel.cs) — extendsViewModelfor dialogs. AddsAffirmativeCommand/NegativeCommand,RequestCloseevent, andAffirmativeText/NegativeText. Lives in the headlessGum.Presentationassembly (ADR-0005). Any dialog VM deriving from it is safe to relocate there:DialogViewResolverfalls 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. Seegum-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:boolvisibility flags,FontStyle, display strings, enabled/disabled flags. XAML binds directly (aboolvisibility flag through a stockBooleanToVisibilityConverter). 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 aFrameworkElement).
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
Stylewith aDataTriggerkeyed 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().