FormVM¶
When To Use It¶
Use FormVM<TM> when the user edits a working copy, then either approves the
changes or denies them and reverts to the last snapshot. This is the right
primitive for editors, settings forms, CRUD dialogs, and other explicit
edit-save-cancel workflows.
Shape And Ownership¶
FormVM owns a live Model, a Snapshot, dirty tracking, validation state,
and approve/deny commands. It is not a leaf container and it is not a subclass
of ComponentVM; consumers typically compose it inside another VM that exposes
the host-specific editor surface.
Important members:
ModelplusSetModel(...)SnapshotIsDirtyErrors,IsValid,FieldError(...)ApproveCommand,ApproveAsync(),ApproveErrorsDenyCommand
Lifecycle And Messaging¶
Construction captures the initial snapshot. After that:
- real model mutations recompute
IsDirty - validation re-runs on construct, set-model, and deny
- deny restores from the snapshot
- approve persists, advances the snapshot, and publishes
OnApproved - fire-and-forget approve failures surface on
ApproveErrors
Strict mode gates approve on IsValid && IsDirty.
Cross-Language Surface¶
| Concept | C# | Python | TypeScript | Swift |
|---|---|---|---|---|
| Type | FormVM<TM> |
FormVM[TM] |
FormVM<TM> |
FormVM<Model> |
| Builder | FormVMBuilder<TM> |
FormVMBuilder[TM] |
FormVM.builder<TM>() |
builder surface |
| Mutator | SetModel(...) |
set_model(...) |
setModel(...) |
setModel(...) |
| Awaitable approve | ApproveAsync() |
approve_async() |
approveAsync() |
approveAsync() |
Example¶
The Notes Workspace editor is the best concrete reference:
- C#: NoteFormVM.cs
- Python: note_form_vm.py
- TypeScript: noteFormVM.ts
- Swift: NoteFormVM.swift
All four compose a strict inner FormVM rather than subclassing it, then layer
editor-specific commands and notifications around that core workflow.
_form = new FormVM<NoteModel>(
initial: note,
persister: PersistAsync,
hub: Hub,
strict: true,
validators: new Dictionary<string, Func<NoteModel, string?>>
{
[nameof(Title)] = note => string.IsNullOrWhiteSpace(note.Title) ? TitleRequired : null
});
form = FormVM(
initial=note,
persister=self._persist,
hub=self._hub,
strict=True,
validators={"title": lambda m: _TITLE_REQUIRED if not m.title.strip() else None},
)
this.#form = new FormVM<NoteModel>({
initial: note,
persister: (m) => this.#persistAsync(m),
hub: this._hub,
strict: true,
validators: {
title: (m) => m.title.trim().length === 0 ? TITLE_REQUIRED : null,
},
});
let form = FormVM<NoteModel>(
initial: note,
persister: { [weak self] n in
guard let self else { return }
try await self._repo.saveNote(n)
},
hub: hub,
strict: true,
validators: [
"title": { model in
model.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? Self.titleRequired
: nil
}
]
)
Common Pitfalls¶
- Treating
FormVMas a drop-inComponentVMsubclass. It is a distinct workflow primitive. - Relying on shallow copy semantics for nested mutable models. Inject a custom snapshotter where the default is not appropriate.
- Ignoring
ApproveErrorson fire-and-forget command paths. - Re-implementing save/cancel/dirty plumbing in every editor instead of composing the primitive once.