Design Proposal: ValidationStateChangedEventArgs IsValid Property
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
# Design Proposal: ValidationStateChangedEventArgs IsValid Property
## Summary
Add an `IsValid` property to `ValidationStateChangedEventArgs` and an `IsFormValid` property to `EditContext` so consumers can determine the current validation outcome directly from the event payload, without re-querying `GetValidationMessages()` and without triggering additional validation work. [#13413](https://github.com/dotnet/aspnetcore/issues/13413)
## Motivation and Goals
- `EditContext.OnValidationStateChanged` previously only signaled that the validation state had changed; subscribers had no way to know whether the form was valid or invalid from the event arguments.
- Consumers worked around this by calling `EditContext.GetValidationMessages()` and counting results, which is repetitive, easy to get wrong, and (when paired with `Validate()`) can cause re-validation and event feedback loops.
- Goal: expose the form's current valid/invalid state on the event args so handlers (e.g., a Submit button enable/disable logic) can react in a single step.
- Secondary goal: surface the same state through an `IsFormValid` property on `EditContext` for callers that want to read it on demand.
## In Scope
- Add a `bool IsValid { get; }` property to `ValidationStateChangedEventArgs`.
- Add a constructor `ValidationStateChangedEventArgs(bool isValid)` that sets the new property.
- Update `EditContext.NotifyValidationStateChanged()` to compute validity from `GetValidationMessages().Any()` (short-circuiting via lazy enumeration) and raise the event with a shared `_validArgs` / `_invalidArgs` instance.
- Add `bool IsFormValid` to `EditContext`, backed by the cached validity flag updated inside `NotifyValidationStateChanged`.
## Out of Scope
- Adding a separate `ValidationStateChanged` event to `EditForm`.
- Changing when `OnValidationStateChanged` is raised (e.g., suppressing duplicate raises when state is unchanged).
- Changing `EditContext.Validate()` / `ValidateAsync()` semantics or signature.
- Adding per-field validity (the property reflects the whole form, not individual fields).
## Risks and Unknowns
- Behavior change risk: any subscriber that branched on `args == ValidationStateChangedEventArgs.Empty` is unaffected (the sentinel still exists and yields `IsValid == true` via the parameterless constructor default).
- Performance: `ComputeIsValid` uses `GetValidationMessages().Any()`, so invalid forms short-circuit in O(1) and only fully valid forms pay the full traversal.
- Public API surface additions (`IsValid`, new constructor, `IsFormValid`) are additive and binary-compatible with existing consumers.
- Async validation path: when `OnValidationRequested` populates a `ValidationMessageStore`, the `OnValidationStateChanged` raised by `ValidateAsync` carries the resulting validity on `IsValid`.
## API Surface
- `Microsoft.AspNetCore.Components.Forms.ValidationStateChangedEventArgs.IsValid` (new)
```csharp
public bool IsValid { get; }
// Usage: read directly from the event payload, no message-store query needed
editContext.OnValidationStateChanged += (_, args) =>
{
canSubmit = args.IsValid;
};
```
- `Microsoft.AspNetCore.Components.Forms.ValidationStateChangedEventArgs.ValidationStateChangedEventArgs(bool isValid)` (new constructor)
```csharp
public ValidationStateChangedEventArgs(bool isValid);
// Usage: construct a state-carrying instance (e.g. for custom EditContext implementations)
var args = new ValidationStateChangedEventArgs(isValid: false);
```
- `Microsoft.AspNetCore.Components.Forms.ValidationStateChangedEventArgs.ValidationStateChangedEventArgs()` (retained parameterless constructor)
```csharp
public ValidationStateChangedEventArgs();
// Usage: backward-compatible; yields an instance whose IsValid defaults to true
var args = new ValidationStateChangedEventArgs(); // args.IsValid == true
```
- `Microsoft.AspNetCore.Components.Forms.ValidationStateChangedEventArgs.Empty` (retained static field)
```csharp
public static new readonly ValidationStateChangedEventArgs Empty;
// Usage: identity check remains valid; Empty.IsValid == true
if (args == ValidationStateChangedEventArgs.Empty) { /* unchanged behavior */ }
```
- `Microsoft.AspNetCore.Components.Forms.EditContext.IsFormValid` (new)
```csharp
public bool IsFormValid { get; }
// Usage: read form-wide validity on demand, without subscribing to the event
if (editContext.IsFormValid)
{
// safe to submit
}
```
- `Microsoft.AspNetCore.Components.Forms.EditContext.NotifyValidationStateChanged` (behavior change)
```csharp
public void NotifyValidationStateChanged();
// Usage: still invoked by validators, but now raises the event with an args
// instance that carries the computed validity (shared _validArgs / _invalidArgs)
editContext.NotifyValidationStateChanged();
// subscribers receive args.IsValid == true | false
```
## Edge Case Scenarios
- A `ValidationMessageStore.Add(field, "Error")` followed by `NotifyValidationStateChanged` raises the event with `IsValid == false` and flips `IsFormValid` to `false`.
- A subsequent `store.Clear()` followed by `NotifyValidationStateChanged` raises the event with `IsValid == true` and flips `IsFormValid` back to `true`.
- Calling `NotifyValidationStateChanged` twice in a row with no message-store changes still raises the event both times (preserving prior behavior) and `IsFormValid` remains `true`.
- During `await EditContext.ValidateAsync()`, a handler that adds a message inside `OnValidationRequested` causes the subsequently raised `OnValidationStateChanged` to carry `IsValid == false` and `IsFormValid == false`.
- `IsValid` is computed via `!GetValidationMessages().Any()`, so a form with many messages short-circuits on the first message (O(1)) and only fully valid forms traverse all messages.
- `IsValid` always reflects the whole form, not individual fields; per-field validity is not exposed.
Examples
```csharp
// Before — workaround that re-queries the message store on every change
editContext.OnValidationStateChanged += (_, _) =>
{
canSubmit = !editContext.GetValidationMessages().Any();
};
// After — read validity directly from the event arguments
editContext.OnValidationStateChanged += (_, args) =>
{
canSubmit = args.IsValid;
};
```
## Alternative Designs
### Option A: Compute validity in subscriber
- Subscriber calls `GetValidationMessages().Any()` in handler
- Rejected: Repetitive, error-prone, can cause feedback loops
### Option B: Add separate ValidityChanged event
- New event: `EditContext.ValidityChanged`
- Rejected: Two events for same concept, API duplication
- Chosen: Extend existing args instead
### Option C: Add IsFormValid only (no IsValid on args)
- Only add `EditContext.IsFormValid`
- Rejected: Doesn't solve the core problem (subscribers still need to query)
- Chosen: Both API points for flexibility
### Option D: Add IsValid to args (CHOSEN)
- Add `IsValid` to event args + `IsFormValid` to EditContext
- Pros: Solves feedback loop, backward compatible, performance-aware
- Accepted: Best balance of simplicity and power
## Breaking Changes
**None.** This is a purely additive change:
### Preserved APIs (Unchanged Behavior)
- `ValidationStateChangedEventArgs.Empty` field (identity check works)
- Parameterless constructor (yields `IsValid == true`)
- `OnValidationStateChanged` event signature (same delegate)
- `NotifyValidationStateChanged()` method signature (same)
### New APIs (Additive)
- `ValidationStateChangedEventArgs.IsValid` property
- `ValidationStateChangedEventArgs(bool isValid)` constructor
- `EditContext.IsFormValid` property
### Binary Compatibility
- 100% binary-compatible
- Existing compiled code works unchanged
- Source-level changes are additive (new APIs available)
### **Expand Examples**
```csharp
// Example 1: Submit button enable/disable
Submit
// Example 2: Validation summary visibility
@if (!editContext.IsFormValid)
{
}
// Example 3: Custom validation handler
editContext.OnValidationStateChanged += (_, args) =>
{
logger.LogInformation("Form valid: {IsValid}", args.IsValid);
canSubmit = args.IsValid;
};
Contributor guide
Assessment
This issue has not been assessed yet.