Forms, dirty guard, undo
FormViewModel lives in Core and does not reference MAUI. Field(name, value) creates a FormField<T>. Bind(field, propertyName, notifyCanExecute) wires the public property and CanExecute — do not write a manual PropertyChanged wrapper. Bind FormField.Error / HasError. When IDialogs is registered, leaving a dirty form confirms “Discard changes?”. Tests set DirtyNavigation = DirtyNavigationMode.SilentBlock. SubmitAsync(work) calls MarkClean() on success. Use [MustMatch(nameof(Password))] or MustMatch(password, confirm).
public sealed class ProductEditViewModel : FormViewModel
{
private readonly FormField<string> _name;
public ProductEditViewModel()
{
_name = Field("Name", "");
Bind(_name, nameof(Name), () => SaveCommand.NotifyCanExecuteChanged());
}
public string Name
{
get => _name.Value ?? "";
set => _name.Value = value;
}
private Task<Outcome> SaveAsync(CancellationToken ct) =>
SubmitAsync(token => _catalog.SaveAsync(Name, token), cancellationToken: ct);
}