Advanced Blazor Forms with Validation
Build robust forms in Blazor. Master FluentValidation integration, custom input components, and complex state management.
When I first configured form validation for our Blazor document management system, I made what seemed like a reasonable choice: binding the EditForm directly to the EF Core entity. Within a week, we had silent data corruption because the two-way binding was mutating tracked entities before the user even pressed Save. Rebuilding around a dedicated form model with FluentValidation took effort, but it completely eliminated that class of bug. The experience taught me that forms in Blazor deserve the same architectural rigor as any backend service.
Introduction
Forms are the primary way users interact with your application. But building forms that are user-friendly, accessible, and securely validated is hard.
[ASP.NET Core Blazor forms overview] — Microsoft , 2024-11-12Why advanced form handling matters:
- User Experience: Immediate feedback on bad input prevents frustration.
- Maintainability: Separating validation rules from UI markup keeps code clean.
- Consistency: Using a standard set of “Glass” inputs ensures the entire app looks and behaves the same.
What We’ll Build
- Form Model: A dedicated DTO for the form, separate from the database entity.
- FluentValidation: Integrating server-side validation rules into the UI.
- GlassInput: A reusable text input that handles labels and error messages automatically.
Architecture Overview
flowchart LR
subgraph UI["🖥️ User Interface"]
Input["🔤 GlassInput"] --> EC["🎯 EditContext"]
Select["📋 GlassSelect"] --> EC
end
subgraph Logic["⚙️ Validation Logic"]
EC -->|FieldChanged| Validator["⚡ FluentValidation"]
Validator -->|ValidationResult| EC
end
subgraph Server["💾 Persistence"]
EC -->|ValidSubmit| Service["🏢 DocumentService"]
end
classDef primary fill:#7c3aed,color:#fff
classDef secondary fill:#06b6d4,color:#fff
classDef db fill:#f43f5e,color:#fff
classDef warning fill:#fbbf24,color:#000
class UI primary
class Logic secondary
class Server db
Section 1: The Form Model
Never bind EditForm directly to your database entity. Use a specific model.
public class DocumentFormModel
{
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public string Category { get; set; } = "General";
public DateTime? DateSpecific { get; set; }
}
public class DocumentValidator : AbstractValidator<DocumentFormModel>
{
public DocumentValidator()
{
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Title is required.")
.MaximumLength(100);
RuleFor(x => x.Category)
.Must(c => AllowedCategories.Contains(c))
.WithMessage("Invalid category.");
}
}
[Blazored.FluentValidation]
— Chris Sainty , 2024-01-20
Section 2: Reusable Input Components
Instead of repeating <div class="form-group">...</div> everywhere, build a component.
@inherits InputBase<string>
<div class="glass-field group">
<label for="@Id" class="glass-label">@Label</label>
<div class="relative">
<input @attributes="AdditionalAttributes"
id="@Id"
class="@CssClass glass-input"
value="@CurrentValue"
@onchange="EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString)" />
@if (!string.IsNullOrEmpty(CssClass) && CssClass.Contains("invalid"))
{
<ExclamationCircleIcon class="input-icon-error" />
}
</div>
<div class="glass-message">
<ValidationMessage For="@ValueExpression" />
</div>
</div>
@code {
[Parameter] public string Label { get; set; }
}
Section 3: The EditForm
Putting it all together using the Blazored.FluentValidation library.
<EditForm Model="@_model" OnValidSubmit="@HandleSubmitAsync">
<FluentValidationValidator />
<GlassPanel Title="Document Details">
<GlassInput Label="Title" @bind-Value="@_model.Title" />
<GlassTextArea Label="Description" @bind-Value="@_model.Description" />
<div class="flex justify-end mt-4">
<GlassButton Type="submit" IsLoading="@_isSaving">
Save Document
</GlassButton>
</div>
</GlassPanel>
</EditForm>
[Data Annotations vs FluentValidation in .NET]
— Code Maze , 2024-05-10
Conclusion
By componentizing your form inputs and externalizing your validation logic, you create forms that are a joy to build and a joy to use. The state management handles the complexity, leaving your markup clean and declarative.
Reflecting on this journey, the combination of FluentValidation with dedicated form models has become my default approach for every Blazor form, regardless of complexity. The upfront cost of creating a validator class and a form DTO pays for itself the moment you need to add a new validation rule or debug a submission issue. The separation of concerns makes each layer independently testable and the overall form behavior predictable.
Next Steps
- Implement multi-step wizard forms using cascading
EditContextand shared form state. - Add real-time server-side validation using the NATS notification service for uniqueness checks.
- Build accessible error summary components that integrate with screen readers.