# BormState (Class) `Bit.Borm.BormState` - implements `IDisposable` Runtime state for a form instance: holds the data, validation results, touched/dirty tracking, wizard position and submission state. The UI layer binds to this and reacts to BormState.StateChanged. ## Property - **AllMessages** : `IEnumerable` - All current validation messages across the form. - **CurrentStep** : `BormStep` - **CurrentStepIndex** : `int` - Index of the active wizard step (ignored for non-wizard forms). - **CurrentStepNumber** : `int` - The 1-based position of the current step among the visible ones — what a stepper should show, so numbering stays contiguous when a conditional step is skipped. - **Data** : `IBormDataAccessor` - The data store (typed model or dictionary). - **Definition** : `BormDefinition` - The schema being rendered. - **DirtyFields** : `IReadOnlyCollection` - Every field path whose value has changed since the form was created or last reset. - **Disabled** : `bool` = `false` - Disables every control at once — while a save is in flight, or while the record is locked by someone else. Distinct from BormState.ReadOnly on purpose: a read-only form can still be read, tabbed through and copied from, which is what a review screen wants; a disabled one is skipped by keyboard navigation entirely, which is only right when the form is genuinely inoperable. - **ExternalValidator** : `BormExternalValidator` - Optional external validator merged with built-in rules (set by integrations). - **FurthestStepIndex** : `int` - The furthest step the user has reached. Steps up to it have been validated on the way past, so they can be returned to freely — including forwards. Without this, going back to step 1 of 4 makes steps 2 and 3 unreachable except by pressing Next through them again, which is the single most common complaint about hand-rolled wizards: the user came back to fix one answer and is now made to walk the whole form. - **HasErrors** : `bool` - True if there is at least one error-severity message. Written as a loop rather than a LINQ query on purpose: every render of every button reads it through IsValid, and a nested SelectMany allocates two iterators and a closure each time to answer a question that is almost always "no, and here is the first one". - **HasValidated** : `bool` - True once a full validation pass has run at least once, so BormState.IsValid reflects a real verdict rather than the absence of one. Reset by BormState.Reset. - **IdPrefix** : `string` = `"borm_"` - What every DOM id this form renders starts with — borm_ unless changed. Ids are derived from field paths, so two forms on one page binding the same schema render the same ids, and a label, an aria-describedby or an error-summary link in one of them points into the other. Give each a prefix of its own (for example through BormView.IdPrefix) to keep them apart. Set it before the form first renders. - **IsFirstStep** : `bool` - **IsFormDirty** : `bool` - **IsLastStep** : `bool` - **IsSubmitSuccessful** : `bool` - True when the last submit passed validation and the handler it dispatched to returned without throwing — React Hook Form's isSubmitSuccessful. This is what a "Saved." confirmation binds to. BormState.IsValid cannot do the job: it goes back to true the moment the user corrects the last error, long before anything has been saved, and it says nothing at all about whether the save then failed. Reset by the next submit attempt and by BormState.Reset. - **IsSubmitted** : `bool` - True once the form has been submitted at least once. - **IsSubmitting** : `bool` - True while the consumer's submit handler is running. Used to block double submits. - **IsValid** : `bool` - True when no error-severity message is currently recorded. This reports what validation has found, not what it would find: a form that has never been validated has no messages and so reports valid, even with a required field left blank. Check BormState.HasValidated before treating it as a verdict — binding a submit button's disabled to IsValid alone would leave the button enabled on an empty form and then disable it the moment the user's first mistake is caught, which is exactly backwards. Prefer letting the user submit and reporting what is wrong. - **IsValidating** : `bool` - True while an async submit/validation is in flight. - **ReadOnly** : `bool` = `false` - Makes every field read-only at once — the "review before you submit" or "view an existing record" mode, without needing a second schema. - **RevalidationTrigger** : `BormValidationTrigger` = `BormValidationTrigger.OnChange` - When a field revalidates once the form has been submitted at least once, or the field already shows an error. Defaults to BormValidationTrigger.OnChange so a corrected field clears its error immediately. - **Services** : `IServiceProvider` - Optional service provider for validators that need DI. - **SingleErrorPerField** : `bool` = `false` - When true, a field shows only its first error at a time instead of every rule it currently breaks — React Hook Form's criteriaMode: "firstError", which is its default. This is about what the user reads, not about what runs: every rule is still evaluated, so BormState.IsValid and the submit decision are unchanged. A password that is too short, missing a digit and missing a symbol otherwise stacks three complaints under one box, which reads as three problems rather than one field to go and fix. Warnings are unaffected — they sit alongside the error rather than competing with it. Off by default, because showing everything at once is what this library has always done and telling the user only half of what is wrong is a choice, not an improvement. - **SubmitCount** : `int` - Number of times submission has been attempted. - **TouchedFields** : `IReadOnlyCollection` - Every field path the user has interacted with. - **ValidatingFields** : `IReadOnlyCollection` - Every field path with an asynchronous rule currently in flight. - **ValidationTrigger** : `BormValidationTrigger` = `BormValidationTrigger.OnChange` - When a field revalidates before the form has been submitted. Defaults to BormValidationTrigger.OnChange. - **VisibleSteps** : `IReadOnlyList` - The steps that are currently visible, in schema order. ## Method - **AcceptChanges**() : `void` - Re-captures the current values as the baseline for BormState.Reset and clears dirty tracking — call this after a successful save so the form is no longer reported as dirty. - **AddArrayItem**(BormFieldDefinition arrayField, string arrayPath) : `int` - Appends a new item to an array field and returns its index. - **ArrayCount**(string arrayPath) : `int` - **Batch**(Action action) : `void` - Runs action with change notifications held back, raising a single one at the end. Nesting is safe; only the outermost batch notifies. An exception still releases the batch, so a failure part-way through cannot leave the form permanently silent. - **CanAddArrayItem**(BormFieldDefinition arrayField, string arrayPath) : `bool` - Whether another item may be added, given BormFieldDefinition.MaxItems. - **CanRemoveArrayItem**(BormFieldDefinition arrayField, string arrayPath) : `bool` - Whether an item may be removed, given BormFieldDefinition.MinItems. - **ClearArrayItems**(string arrayPath) : `void` - Empties an array field, discarding every row's messages and touched/dirty state along with it. Removing rows one at a time costs a re-render and a re-index per row, and the caller has to walk backwards to avoid the indices moving underneath them. - **ClearMessages**() : `void` - **ClearMessages**(string path) : `void` - Clears the messages for a single field and everything nested beneath it. - **Dispose**() : `void` - Cancels any in-flight validation and detaches listeners. - **DuplicateArrayItem**(BormFieldDefinition arrayField, string arrayPath, int index) : `int` - Inserts a copy of the item at index directly after it and returns the new index, or -1 if the row could not be copied. This is the "add another one like that" a repeater of anything non-trivial always ends up needing. Values are copied field by field through the schema rather than by cloning the object, so the new row is genuinely independent — a shallow copy would leave the two rows sharing the same nested objects and editing one would silently edit the other. - **EnsureOptionsAsync**(BormFieldDefinition field, string path) : `ValueTask` - Runs a field's options provider unless its results are already cached. Renderers call this on first render and after a dependency changes; it is a no-op for fields with static options. - **FocusAsync**(string path) : `ValueTask` - Moves focus to the control at path. Returns false when nothing is rendering it — a field on another wizard step, one a condition has hidden, or a control with no focusable element of its own. A path no control renders by itself — an object, or one row of a repeater — sends focus to the first control inside it that will take it. A message about "the address" or "line 2" as a whole is fixed somewhere in there, and the error summary's link to it, and focus-the-first-error after a failed submit, otherwise went nowhere at all. - **GetFieldState**(string path) : `BormFieldState` - Everything known about one field in a single read: whether the user has been there, whether the value has moved from its baseline, and what is currently wrong with it. The pieces are all separately available (BormState.IsTouched, BormState.IsDirty, BormState.MessagesFor); this is the aggregate React Hook Form spells getFieldState, and it exists because a custom renderer asking all three questions per render otherwise reads the state three times and has to keep the answers in step itself. - **GetValue**(string path) : `object` - **GoToStep**(int index) : `void` - Jumps to a step by index. Hidden steps and out-of-range indices are ignored. Jumping forward past a step the user has not reached is allowed here — the caller has asked for it explicitly — but it skips that step's validation, which is why the stepper only offers steps up to BormState.FurthestStepIndex. - **GoToStep**(string id) : `void` - Jumps to a step by its BormStep.Id. - **InsertArrayItem**(BormFieldDefinition arrayField, string arrayPath, int index) : `int` - Inserts a new item at index (clamped into range) and returns the index it landed at. Messages below the insertion point are re-indexed so they stay with their item. - **InvalidateOptions**(string path) : `void` - Drops cached options for a field so the next render reloads them. - **IsDirty**(string path) : `bool` - **IsDisabled**(BormFieldDefinition field, string path = null) : `bool` - Whether a field is currently disabled given the data. Read-only fields are deliberately *not* disabled — a disabled control is skipped by keyboard navigation and unreadable to screen readers, so read-only is rendered with the readonly attribute instead. See BormState.IsReadOnly. - **IsLoadingOptions**(string path) : `bool` - True while a field's BormFieldDefinition.OptionsProvider is running. - **IsReadOnly**(BormFieldDefinition field, string path = null) : `bool` - Whether a field is read-only: in its own right, because a condition says so, or because the whole form is. Pass path so a condition written on an array item's template is evaluated against that row, exactly as BormState.IsVisible and BormState.IsDisabled do. - **IsRequired**(BormFieldDefinition field, string path = null) : `bool` - Whether a field is required right now, taking BormFieldDefinition.RequiredWhen into account. - **IsStepReachable**(int index) : `bool` - Whether a step may be jumped to directly — it has been reached at least once. - **IsStepVisible**(BormStep step) : `bool` - Whether a wizard step is currently visible. - **IsTouched**(string path) : `bool` - **IsValidatingField**(string path) : `bool` - Whether an asynchronous rule is currently running for path — what a control showing "checking that username…" binds to. React Hook Form spells the set of these validatingFields; TanStack Form spells it field.state.meta.isValidating. Distinct from BormState.IsValidating, which is about the form: a remote uniqueness check on one box should put a spinner beside that box, not declare the whole form busy and grey out its submit button. Only fields that actually have an async rule are ever reported, so a form of ordinary synchronous rules never flickers. - **IsVisible**(BormFieldDefinition field, string path = null) : `bool` - Whether a field is currently visible given the data. Pass path so a condition written on an array item's template is evaluated against that row; omitting it evaluates against the root, which is all a top-level field ever needs. - **MarkAllTouched**() : `void` - Marks every field touched, so validation messages become visible everywhere. Called on submit so a user who submits an untouched form still sees what is missing. - **MarkTouched**(string path) : `void` - **MessagesFor**(string path) : `IReadOnlyList` - Messages for a specific field path. - **MoveArrayItem**(string arrayPath, int from, int to) : `void` - Moves an array item from one index to another. - **NextStepAsync**() : `ValueTask` - Advances to the next visible step after validating the current one. - **NormalizeAll**() : `bool` - Runs every declared normalizer across the form — including one per row of a repeater. Called before a submit validates, so a field the user never visited is still tidied. - **NormalizeField**(BormFieldDefinition field, string path) : `bool` - Runs the field's BormFieldDefinition.Normalize over the value at path, writing the result back when it differs. Returns true if anything changed. The write goes through the ordinary path, so tidying a value runs the same sweeps typing one does — a total that reads it recomputes, a condition that watches it re-evaluates, an autosave hears about it. It is written quietly: normalising is the form tidying up after the user, not the user visiting a field. - **OptionsError**(string path) : `Exception` - The exception a field's options provider last failed with, or null when it has not failed. A lookup that goes over the network fails for ordinary reasons, and losing the whole form to it would be out of proportion — so the failure is recorded here, announced through BormState.OptionsLoadFailed, and shown in place of the choices. - **OptionsFor**(BormFieldDefinition field, string path) : `IReadOnlyList` - The options to render for a field: those loaded by its BormFieldDefinition.OptionsProvider if it has one, otherwise the static BormFieldDefinition.Options from the schema. - **OrderedMessages**() : `IReadOnlyList` - All current messages in the order their fields appear in the schema — the order an error summary should list them so "fix the first one" walks the user down the page. - **PreviousStep**() : `void` - Moves to the previous visible step (no validation). - **RegisterFocusTarget**(string path, Func> focus) : `IDisposable` - Registers a way to move focus to the control rendering path. Input components call this so the state can honour "take me to the first error" without knowing anything about the DOM. The callback reports whether focus actually landed. Dispose the returned token to unregister. - **RegisterSubmitAttempt**() : `int` - Records a submit attempt (so validation messages become visible) and returns the count. - **RemoveArrayItem**(string arrayPath, int index) : `void` - Removes an item from an array field. - **Reset**() : `void` - Restores the values captured when the state was created, and clears validation, touched/dirty tracking, the submit count and the wizard position. Values are restored, not deep-cloned: array elements that already existed when the state was created are put back by reference, so edits made *inside* one of those objects are not rolled back. Items added after construction are removed. - **Reset**(IEnumerable> values) : `void` - Rebases the form onto values: they are written, become the new baseline for BormState.Reset and BormState.IsFormDirty, and everything the previous session accumulated — messages, touched and dirty flags, the submit count, the wizard position — is cleared. This is React Hook Form's reset(values), and it is what an edit form needs after a save round-trip returns the stored record: BormState.Reset would put back the values the form was constructed with, which are now the wrong ones. Only the paths named are written; anything else keeps the value it currently holds and is baselined at that value. Pass every field to replace the form's contents wholesale. - **ResetField**(string path) : `void` - Puts one field — and anything nested beneath it — back to the value it started with, clearing its messages and its touched/dirty state. This is the "undo just this answer" that a long form needs and that BormState.Reset is far too blunt for; React Hook Form spells it resetField. A path the form has no baseline for — a field inside a row added since construction — is emptied, because "the value it started with" is nothing. - **SetServerError**(string path, string message) : `void` - Adds a message for a path (used by server-side validation results). - **SetServerErrors**(IEnumerable messages) : `void` - Replaces the form's messages with those returned by a server round-trip, so a failed POST can surface its errors on the right fields in one call. - **SetValue**(string path, object value) : `void` - Sets a value, marks the field dirty and touched, and notifies listeners. - **SetValueQuietly**(string path, object value) : `void` - Sets a value without marking the field touched — for programmatic updates (computed values, prefill) that should not make the user's untouched field start showing errors. - **SetValues**(IEnumerable> values, bool markTouched = false) : `void` - Writes several values as one change: listeners are notified once at the end rather than once per field. Prefilling a form from a saved record otherwise costs one re-render per value, and any intermediate state — half the answers applied — is briefly visible to a computed field or a condition watching from the outside. - **ShouldValidate**(string path, BormValidationTrigger trigger) : `bool` - Whether a change (or blur) on path should trigger validation, given the configured triggers. A field that already shows an error always revalidates eagerly so the error disappears as soon as it is fixed. - **Snapshot**() : `IReadOnlyDictionary` - Every value the schema binds to, as a flat path/value map — the shape BormState.Reset takes back, so the pair is a complete save-and-restore for a draft: stash a snapshot while the user is halfway through, hand it back when they return. Live array elements are walked, so a repeater contributes one entry per row per field (Lines[0].Product, Lines[1].Product, …) rather than one opaque list. Presentational fields and object containers hold no value of their own and are skipped, exactly as they are when the form captures its baseline. Values are the objects the model holds, not copies: this is a map of what is there, not a deep clone of it. Serialise it if it has to outlive the objects. - **SubmitAsync**(Func onValid = null, Func onInvalid = null) : `Task` - Runs the full submit cycle: marks every field touched, validates, and invokes onValid or onInvalid. Concurrent calls are ignored while a submit is in flight, which is what stops a double-click from posting twice. - **SwapArrayItems**(string arrayPath, int first, int second) : `void` - Exchanges two array items, taking their messages and touched/dirty state with them. Distinct from BormState.MoveArrayItem, which shuffles everything between the two positions along by one; a swap leaves every other row exactly where it was, which is what a drag-and-drop reorder of two rows means and what React Hook Form's useFieldArray spells swap. - **Text**(string key, params object[] args) : `string` - Resolves a piece of the library's own text (see BormMessageKeys) through the registered IBormMessageProvider, falling back to the English defaults. The built-in components render everything through this, so a single provider translates the whole form — buttons, placeholders and repeater labels included, not only the validation messages. - **ValidateAsync**(bool includeAsync = true) : `ValueTask` - Validates the entire form and stores the results. - **ValidateDependentsAsync**(string changedPath, bool includeAsync = false) : `ValueTask` - Revalidates every field that declared changedPath in its BormFieldDefinition.RevalidateOn list — the other half of a cross-field rule. A rule that reads two values lives on one of them, so only one of the two changes ever runs it. The confirm-password case is the whole story: the user mistypes the confirmation and is told the two do not match, then fixes the password to agree with what they typed — and the message under the confirmation box is now wrong, and stays wrong, because nothing revalidated a field the user did not touch. React Hook Form's deps and TanStack Form's onChangeListenTo exist for exactly this. A dependent that has nothing to say is left alone: a field the user has never visited on a form that has never been submitted must not start showing errors because a different field changed. The point is to correct a verdict already on screen, not to bring forward one that is not. - **ValidateFieldAsync**(string path, bool includeAsync = true) : `ValueTask` - Validates the single field at path, resolving its definition from the schema — React Hook Form's trigger(name). Returns false when the path names no field in the schema, so a caller checking one answer from outside the form does not have to go and find the definition first. - **ValidateFieldAsync**(BormFieldDefinition field, string path, bool includeAsync = true) : `ValueTask` - Validates a single field and refreshes only its messages. - **ValidateStepAsync**(bool includeAsync = true) : `ValueTask` - Validates the fields of the current wizard step. ## Event - **FieldChanged** : `Action` - Raised when a specific field value changes, with its path. - **OptionsLoadFailed** : `Action` - Raised with the field path and the exception when an options provider throws. Subscribe to log it; the form itself carries on with an empty list and retries on the next BormState.InvalidateOptions. - **StateChanged** : `Action` - Raised whenever state changes and the UI should re-render.