Frontend Intermediate 22 min

Advanced Blazor Forms with Validation

Build robust forms in Blazor. Master FluentValidation integration, custom input components, and complex state management.

By Victor Robin Updated:

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-12

Why 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

  1. Form Model: A dedicated DTO for the form, separate from the database entity.
  2. FluentValidation: Integrating server-side validation rules into the UI.
  3. 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.

[FluentValidation Documentation] — Jeremy Skinner , 2024-03-15
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.

[ASP.NET Core Blazor input components] — Microsoft , 2024-11-12
@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.

[ASP.NET Core Blazor forms validation] — Microsoft , 2024-11-12
<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 EditContext and 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.

Further Reading

[FluentValidation Documentation] — Jeremy Skinner , 2024 [ASP.NET Core Blazor forms and validation] — Microsoft , 2024 [Blazored.FluentValidation] — GitHub Community , 2024