Claude Cursor GitHub Copilot Skill

collect-user-input

Build forms, validate data, and react to user input in Blazor. USE FOR adding forms, search boxes, filter panels, inline editing, data-entry UI, file uploads, validation (annotations or custom), handling form submissions, and binding input controls. Covers EditForm, built-in inpu

LLM Mart · 0 points · 19 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dotnet-skills-plugins_dotnet-blazor_skills_collect-user-input-98f8485.zip · 4 KB
Part of dotnet/skills — 119 skills

Install

skills CLI npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-blazor/skills/collect-user-input
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
Git git clone https://github.com/dotnet/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Collect User Input

Step 1 — Read the Project's AGENTS.md

Check AGENTS.md for Interactivity Mode and Interactivity Scope. This determines which form patterns apply:

Mode Form mechanism
None (Static SSR) EditForm with FormName + [SupplyParameterFromForm]. No @bind, no @onchange.
Server EditForm with @bind-Value. Full interactivity — real-time validation, dynamic UI.
WebAssembly Same as Server, but validators needing server data must call APIs.
Auto Same as WebAssembly — code must work in both browser and server.
Scope Impact
Global All forms are interactive. FormName only needed when explicitly opting a page to static SSR.
Per-page Forms in static pages use FormName + [SupplyParameterFromForm]. Forms in @rendermode pages use @bind-Value.

EditForm Setup

EditForm requires either Model or EditContext — never both.

Model-based (default)

<EditForm Model="Employee" OnValidSubmit="HandleSubmit" FormName="employee">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <label>
        Name: <InputText @bind-Value="Employee!.Name" />
        <ValidationMessage For="() => Employee!.Name" />
    </label>

    <button type="submit">Save</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private EmployeeModel? Employee { get; set; }

    protected override void OnInitialized() => Employee ??= new();

    private async Task HandleSubmit()
    {
        // Save Employee
    }
}

This single pattern works in both SSR and interactive modes:

  • In SSR: FormName identifies the form, [SupplyParameterFromForm] binds POST data, ??= initializes on GET.
  • In interactive: @bind-Value provides two-way binding, [SupplyParameterFromForm] is ignored, FormName is harmless.

EditContext-based (advanced)

Use when you need programmatic field tracking, dynamic validation rules, or manual EditContext.Validate() calls:

private EditContext? editContext;
private EmployeeModel model = new();

protected override void OnInitialized()
{
    editContext = new EditContext(model);
}
<EditForm EditContext="editContext" OnValidSubmit="HandleSubmit" FormName="employee">

Submit Handlers

Handler Fires when Use when
OnValidSubmit Validation passes Standard forms with DataAnnotationsValidator
OnInvalidSubmit Validation fails Need custom handling for invalid state
OnSubmit Always — validation is manual Using EditContext.Validate() yourself

OnSubmit cannot combine with OnValidSubmit/OnInvalidSubmit.

Built-in Input Components

Component Binds to Notes
InputText string Renders <input type="text">
InputTextArea string Renders <textarea>
InputNumber<T> int, double, decimal Renders <input type="number">
InputDate<T> DateTime, DateOnly, DateTimeOffset Renders <input type="date">
InputCheckbox bool Renders <input type="checkbox">
InputSelect<T> string, enums, numeric types Renders <select>
InputRadioGroup<T> string, enums, numeric types Wraps InputRadio<T> children
InputFile IBrowserFile File upload — interactive modes only

All input components use @bind-Value for binding. Always wrap text in a <label> or use id/for attributes for accessibility.

InputSelect with enum values

<InputSelect @bind-Value="Model!.Status">
    <option value="">-- Select --</option>
    @foreach (var value in Enum.GetValues<OrderStatus>())
    {
        <option value="@value">@value</option>
    }
</InputSelect>

InputRadioGroup

<InputRadioGroup @bind-Value="Model!.Priority">
    @foreach (var p in Enum.GetValues<Priority>())
    {
        <label>
            <InputRadio Value="p" /> @p
        </label>
    }
</InputRadioGroup>

Validation

Data annotations

Define validation rules on the model:

public class EmployeeModel
{
    [Required, StringLength(100)]
    public string? Name { get; set; }

    [Required, EmailAddress]
    public string? Email { get; set; }

    [Range(18, 99)]
    public int Age { get; set; }

    [Required]
    public string? Department { get; set; }
}

Add <DataAnnotationsValidator /> inside EditForm — without it, annotation attributes are silently ignored.

Display errors with:

  • <ValidationSummary /> — all errors in a list
  • <ValidationMessage For="() => Model!.FieldName" /> — per-field inline errors

Custom validator component

For server-round-trip validation (uniqueness checks, business rules):

public class CustomValidator : ComponentBase
{
    [CascadingParameter]
    private EditContext? EditContext { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        messageStore = new ValidationMessageStore(EditContext!);
        EditContext!.OnValidationRequested += (s, e) => messageStore.Clear();
        EditContext!.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier);
    }

    public void DisplayErrors(Dictionary<string, List<string>> errors)
    {
        foreach (var (field, messages) in errors)
        {
            foreach (var message in messages)
            {
                messageStore!.Add(EditContext!.Field(field), message);
            }
        }
        EditContext!.NotifyValidationStateChanged();
    }

    public void ClearErrors()
    {
        messageStore?.Clear();
        EditContext?.NotifyValidationStateChanged();
    }
}

Usage in a form:

<EditForm Model="Model" OnValidSubmit="HandleSubmit" FormName="register">
    <DataAnnotationsValidator />
    <CustomValidator @ref="customValidator" />
    <ValidationSummary />
    @* inputs *@
</EditForm>

@code {
    private CustomValidator? customValidator;

    private async Task HandleSubmit()
    {
        var errors = await RegistrationService.ValidateAsync(Model!);
        if (errors.Count > 0)
        {
            customValidator!.DisplayErrors(errors);
            return;
        }
        // proceed
    }
}

React to Input Changes (Interactive Only)

@bind:after

Run logic after a bound value changes:

<InputText @bind-Value="Model!.ZipCode" @bind:after="OnZipCodeChanged" />

@code {
    private async Task OnZipCodeChanged()
    {
        // Fetch city/state based on new zip code
        var location = await LocationService.LookupAsync(Model!.ZipCode);
        Model.City = location?.City;
        Model.State = location?.State;
    }
}

@oninput for real-time filtering

<input type="text" @oninput="OnSearchInput" placeholder="Search..." />

@code {
    private string searchTerm = "";
    private List<Item> filteredItems = new();

    private void OnSearchInput(ChangeEventArgs e)
    {
        searchTerm = e.Value?.ToString() ?? "";
        filteredItems = allItems.Where(i =>
            i.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList();
    }
}

SSR-Specific Patterns

These apply when the form renders in Static SSR (mode = None, or per-page without @rendermode).

SupplyParameterFromForm

Binds POST data to a property on form submission:

[SupplyParameterFromForm]
private ContactModel? Contact { get; set; }

protected override void OnInitialized() => Contact ??= new();

Critical: The ??= in OnInitialized is required. On GET the property is null — ??= creates the model. On POST the framework populates it — ??= preserves the posted values.

FormName — multiple forms on one page

Each form needs a unique FormName:

<EditForm Model="Search" OnSubmit="DoSearch" FormName="search">...</EditForm>
<EditForm Model="Contact" OnValidSubmit="SaveContact" FormName="contact">...</EditForm>

Match [SupplyParameterFromForm] to its form:

[SupplyParameterFromForm(FormName = "search")]
private SearchModel? Search { get; set; }

[SupplyParameterFromForm(FormName = "contact")]
private ContactModel? Contact { get; set; }

Enhanced navigation for forms

Add Enhance for SPA-like form submissions without full page reload:

<EditForm Model="Model" OnValidSubmit="Save" FormName="quick" Enhance>

Enhanced forms submit via fetch, patch the DOM, and preserve scroll position. The page stays interactive-feeling even in SSR.

Plain HTML forms

When using raw <form> instead of EditForm in SSR, add the antiforgery token manually:

<form method="post" @onsubmit="Submit" @formname="raw-form">
    <AntiforgeryToken />
    <input name="Model.Name" value="@Model?.Name" />
    <button type="submit">Send</button>
</form>

EditForm includes the antiforgery token automatically.

File Upload

InputFile works in interactive modes only — not in Static SSR.

<InputFile OnChange="OnFileSelected" accept=".pdf,.jpg,.png" />

@code {
    private IBrowserFile? selectedFile;

    private async Task OnFileSelected(InputFileChangeEventArgs e)
    {
        selectedFile = e.File;

        // Read stream with size limit
        await using var stream = selectedFile.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
        // Process stream — save to disk, upload to storage, etc.
    }
}

Stream size limits:

  • Server: Default ~30 KB SignalR message size. Call OpenReadStream(maxAllowedSize) to increase. Large files stream over the circuit.
  • WebAssembly: File is read in the browser. No SignalR limit, but memory constrained.

For multiple files:

<InputFile OnChange="OnFilesSelected" multiple />

@code {
    private async Task OnFilesSelected(InputFileChangeEventArgs e)
    {
        foreach (var file in e.GetMultipleFiles(maxAllowedFiles: 10))
        {
            await using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
            // Process each file
        }
    }
}

Prevent Double Submission

Disable the submit button while processing:

<button type="submit" disabled="@isSubmitting">
    @(isSubmitting ? "Saving..." : "Save")
</button>

@code {
    private bool isSubmitting;

    private async Task HandleSubmit()
    {
        isSubmitting = true;
        try
        {
            await SaveService.SaveAsync(Model!);
        }
        finally
        {
            isSubmitting = false;
        }
    }
}

Custom Validation CSS

Replace the default valid/invalid CSS classes:

public class BootstrapFieldCssClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
        return editContext.IsModified(fieldIdentifier)
            ? (isValid ? "is-valid" : "is-invalid")
            : "";
    }
}

Apply to the form:

protected override void OnInitialized()
{
    editContext = new EditContext(model);
    editContext.SetFieldCssClassProvider(new BootstrapFieldCssClassProvider());
}

Don'ts

  • Don't use @bind or @oninput in Static SSR forms — they require interactivity. Use [SupplyParameterFromForm] and FormName.
  • Don't forget Model ??= new() in OnInitialized — the model is null on GET, populated on POST.
  • Don't use OnSubmit together with OnValidSubmit/OnInvalidSubmit — they're mutually exclusive.
  • Don't omit <DataAnnotationsValidator /> — validation attributes are silently ignored without it.
  • Don't omit FormName in SSR when a page has multiple forms — both forms will fire on any submission.
  • Don't use InputFile in Static SSR — it requires an interactive render mode.
  • Don't use both Model and EditContext on an EditForm — pick one.
  • Don't forget <AntiforgeryToken /> in plain <form> elements — the server rejects the POST without it.
Files (skills)
  • SKILL.md 12.5 KB
    ---
    license: MIT
    name: collect-user-input
    description: Build forms, validate data, and react to user input in Blazor. USE FOR adding forms, search boxes, filter panels, inline editing, data-entry UI, file uploads, validation (annotations or custom), handling form submissions, and binding input controls. Covers EditForm, built-in input components, DataAnnotationsValidator, custom validation, SSR form patterns (SupplyParameterFromForm, FormName, AntiforgeryToken, Enhance), and @bind for simple interactive controls. DO NOT USE for project scaffolding (see create-blazor-project) or prerendering issues (see support-prerendering).
    ---
    
    # Collect User Input
    
    ## Step 1 — Read the Project's AGENTS.md
    
    Check `AGENTS.md` for **Interactivity Mode** and **Interactivity Scope**. This determines which form patterns apply:
    
    | Mode | Form mechanism |
    |------|---------------|
    | None (Static SSR) | `EditForm` with `FormName` + `[SupplyParameterFromForm]`. No `@bind`, no `@onchange`. |
    | Server | `EditForm` with `@bind-Value`. Full interactivity — real-time validation, dynamic UI. |
    | WebAssembly | Same as Server, but validators needing server data must call APIs. |
    | Auto | Same as WebAssembly — code must work in both browser and server. |
    
    | Scope | Impact |
    |-------|--------|
    | Global | All forms are interactive. `FormName` only needed when explicitly opting a page to static SSR. |
    | Per-page | Forms in static pages use `FormName` + `[SupplyParameterFromForm]`. Forms in `@rendermode` pages use `@bind-Value`. |
    
    ## EditForm Setup
    
    `EditForm` requires **either** `Model` or `EditContext` — never both.
    
    ### Model-based (default)
    
    ```razor
    <EditForm Model="Employee" OnValidSubmit="HandleSubmit" FormName="employee">
        <DataAnnotationsValidator />
        <ValidationSummary />
    
        <label>
            Name: <InputText @bind-Value="Employee!.Name" />
            <ValidationMessage For="() => Employee!.Name" />
        </label>
    
        <button type="submit">Save</button>
    </EditForm>
    
    @code {
        [SupplyParameterFromForm]
        private EmployeeModel? Employee { get; set; }
    
        protected override void OnInitialized() => Employee ??= new();
    
        private async Task HandleSubmit()
        {
            // Save Employee
        }
    }
    ```
    
    This single pattern works in **both** SSR and interactive modes:
    - In SSR: `FormName` identifies the form, `[SupplyParameterFromForm]` binds POST data, `??=` initializes on GET.
    - In interactive: `@bind-Value` provides two-way binding, `[SupplyParameterFromForm]` is ignored, `FormName` is harmless.
    
    ### EditContext-based (advanced)
    
    Use when you need programmatic field tracking, dynamic validation rules, or manual `EditContext.Validate()` calls:
    
    ```csharp
    private EditContext? editContext;
    private EmployeeModel model = new();
    
    protected override void OnInitialized()
    {
        editContext = new EditContext(model);
    }
    ```
    
    ```razor
    <EditForm EditContext="editContext" OnValidSubmit="HandleSubmit" FormName="employee">
    ```
    
    ## Submit Handlers
    
    | Handler | Fires when | Use when |
    |---------|-----------|----------|
    | `OnValidSubmit` | Validation passes | Standard forms with `DataAnnotationsValidator` |
    | `OnInvalidSubmit` | Validation fails | Need custom handling for invalid state |
    | `OnSubmit` | Always — validation is manual | Using `EditContext.Validate()` yourself |
    
    `OnSubmit` cannot combine with `OnValidSubmit`/`OnInvalidSubmit`.
    
    ## Built-in Input Components
    
    | Component | Binds to | Notes |
    |-----------|----------|-------|
    | `InputText` | `string` | Renders `<input type="text">` |
    | `InputTextArea` | `string` | Renders `<textarea>` |
    | `InputNumber<T>` | `int`, `double`, `decimal` | Renders `<input type="number">` |
    | `InputDate<T>` | `DateTime`, `DateOnly`, `DateTimeOffset` | Renders `<input type="date">` |
    | `InputCheckbox` | `bool` | Renders `<input type="checkbox">` |
    | `InputSelect<T>` | `string`, enums, numeric types | Renders `<select>` |
    | `InputRadioGroup<T>` | `string`, enums, numeric types | Wraps `InputRadio<T>` children |
    | `InputFile` | `IBrowserFile` | File upload — interactive modes only |
    
    All input components use `@bind-Value` for binding. Always wrap text in a `<label>` or use `id`/`for` attributes for accessibility.
    
    ### InputSelect with enum values
    
    ```razor
    <InputSelect @bind-Value="Model!.Status">
        <option value="">-- Select --</option>
        @foreach (var value in Enum.GetValues<OrderStatus>())
        {
            <option value="@value">@value</option>
        }
    </InputSelect>
    ```
    
    ### InputRadioGroup
    
    ```razor
    <InputRadioGroup @bind-Value="Model!.Priority">
        @foreach (var p in Enum.GetValues<Priority>())
        {
            <label>
                <InputRadio Value="p" /> @p
            </label>
        }
    </InputRadioGroup>
    ```
    
    ## Validation
    
    ### Data annotations
    
    Define validation rules on the model:
    
    ```csharp
    public class EmployeeModel
    {
        [Required, StringLength(100)]
        public string? Name { get; set; }
    
        [Required, EmailAddress]
        public string? Email { get; set; }
    
        [Range(18, 99)]
        public int Age { get; set; }
    
        [Required]
        public string? Department { get; set; }
    }
    ```
    
    Add `<DataAnnotationsValidator />` inside `EditForm` — without it, annotation attributes are silently ignored.
    
    Display errors with:
    - `<ValidationSummary />` — all errors in a list
    - `<ValidationMessage For="() => Model!.FieldName" />` — per-field inline errors
    
    ### Custom validator component
    
    For server-round-trip validation (uniqueness checks, business rules):
    
    ```csharp
    public class CustomValidator : ComponentBase
    {
        [CascadingParameter]
        private EditContext? EditContext { get; set; }
    
        private ValidationMessageStore? messageStore;
    
        protected override void OnInitialized()
        {
            messageStore = new ValidationMessageStore(EditContext!);
            EditContext!.OnValidationRequested += (s, e) => messageStore.Clear();
            EditContext!.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier);
        }
    
        public void DisplayErrors(Dictionary<string, List<string>> errors)
        {
            foreach (var (field, messages) in errors)
            {
                foreach (var message in messages)
                {
                    messageStore!.Add(EditContext!.Field(field), message);
                }
            }
            EditContext!.NotifyValidationStateChanged();
        }
    
        public void ClearErrors()
        {
            messageStore?.Clear();
            EditContext?.NotifyValidationStateChanged();
        }
    }
    ```
    
    Usage in a form:
    
    ```razor
    <EditForm Model="Model" OnValidSubmit="HandleSubmit" FormName="register">
        <DataAnnotationsValidator />
        <CustomValidator @ref="customValidator" />
        <ValidationSummary />
        @* inputs *@
    </EditForm>
    
    @code {
        private CustomValidator? customValidator;
    
        private async Task HandleSubmit()
        {
            var errors = await RegistrationService.ValidateAsync(Model!);
            if (errors.Count > 0)
            {
                customValidator!.DisplayErrors(errors);
                return;
            }
            // proceed
        }
    }
    ```
    
    ## React to Input Changes (Interactive Only)
    
    ### @bind:after
    
    Run logic after a bound value changes:
    
    ```razor
    <InputText @bind-Value="Model!.ZipCode" @bind:after="OnZipCodeChanged" />
    
    @code {
        private async Task OnZipCodeChanged()
        {
            // Fetch city/state based on new zip code
            var location = await LocationService.LookupAsync(Model!.ZipCode);
            Model.City = location?.City;
            Model.State = location?.State;
        }
    }
    ```
    
    ### @oninput for real-time filtering
    
    ```razor
    <input type="text" @oninput="OnSearchInput" placeholder="Search..." />
    
    @code {
        private string searchTerm = "";
        private List<Item> filteredItems = new();
    
        private void OnSearchInput(ChangeEventArgs e)
        {
            searchTerm = e.Value?.ToString() ?? "";
            filteredItems = allItems.Where(i =>
                i.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList();
        }
    }
    ```
    
    ## SSR-Specific Patterns
    
    These apply when the form renders in Static SSR (mode = None, or per-page without `@rendermode`).
    
    ### SupplyParameterFromForm
    
    Binds POST data to a property on form submission:
    
    ```csharp
    [SupplyParameterFromForm]
    private ContactModel? Contact { get; set; }
    
    protected override void OnInitialized() => Contact ??= new();
    ```
    
    **Critical:** The `??=` in `OnInitialized` is required. On GET the property is null — `??=` creates the model. On POST the framework populates it — `??=` preserves the posted values.
    
    ### FormName — multiple forms on one page
    
    Each form needs a unique `FormName`:
    
    ```razor
    <EditForm Model="Search" OnSubmit="DoSearch" FormName="search">...</EditForm>
    <EditForm Model="Contact" OnValidSubmit="SaveContact" FormName="contact">...</EditForm>
    ```
    
    Match `[SupplyParameterFromForm]` to its form:
    
    ```csharp
    [SupplyParameterFromForm(FormName = "search")]
    private SearchModel? Search { get; set; }
    
    [SupplyParameterFromForm(FormName = "contact")]
    private ContactModel? Contact { get; set; }
    ```
    
    ### Enhanced navigation for forms
    
    Add `Enhance` for SPA-like form submissions without full page reload:
    
    ```razor
    <EditForm Model="Model" OnValidSubmit="Save" FormName="quick" Enhance>
    ```
    
    Enhanced forms submit via `fetch`, patch the DOM, and preserve scroll position. The page stays interactive-feeling even in SSR.
    
    ### Plain HTML forms
    
    When using raw `<form>` instead of `EditForm` in SSR, add the antiforgery token manually:
    
    ```razor
    <form method="post" @onsubmit="Submit" @formname="raw-form">
        <AntiforgeryToken />
        <input name="Model.Name" value="@Model?.Name" />
        <button type="submit">Send</button>
    </form>
    ```
    
    `EditForm` includes the antiforgery token automatically.
    
    ## File Upload
    
    `InputFile` works in **interactive modes only** — not in Static SSR.
    
    ```razor
    <InputFile OnChange="OnFileSelected" accept=".pdf,.jpg,.png" />
    
    @code {
        private IBrowserFile? selectedFile;
    
        private async Task OnFileSelected(InputFileChangeEventArgs e)
        {
            selectedFile = e.File;
    
            // Read stream with size limit
            await using var stream = selectedFile.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
            // Process stream — save to disk, upload to storage, etc.
        }
    }
    ```
    
    Stream size limits:
    - **Server:** Default ~30 KB SignalR message size. Call `OpenReadStream(maxAllowedSize)` to increase. Large files stream over the circuit.
    - **WebAssembly:** File is read in the browser. No SignalR limit, but memory constrained.
    
    For multiple files:
    
    ```razor
    <InputFile OnChange="OnFilesSelected" multiple />
    
    @code {
        private async Task OnFilesSelected(InputFileChangeEventArgs e)
        {
            foreach (var file in e.GetMultipleFiles(maxAllowedFiles: 10))
            {
                await using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
                // Process each file
            }
        }
    }
    ```
    
    ## Prevent Double Submission
    
    Disable the submit button while processing:
    
    ```razor
    <button type="submit" disabled="@isSubmitting">
        @(isSubmitting ? "Saving..." : "Save")
    </button>
    
    @code {
        private bool isSubmitting;
    
        private async Task HandleSubmit()
        {
            isSubmitting = true;
            try
            {
                await SaveService.SaveAsync(Model!);
            }
            finally
            {
                isSubmitting = false;
            }
        }
    }
    ```
    
    ## Custom Validation CSS
    
    Replace the default `valid`/`invalid` CSS classes:
    
    ```csharp
    public class BootstrapFieldCssClassProvider : FieldCssClassProvider
    {
        public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
            return editContext.IsModified(fieldIdentifier)
                ? (isValid ? "is-valid" : "is-invalid")
                : "";
        }
    }
    ```
    
    Apply to the form:
    
    ```csharp
    protected override void OnInitialized()
    {
        editContext = new EditContext(model);
        editContext.SetFieldCssClassProvider(new BootstrapFieldCssClassProvider());
    }
    ```
    
    ## Don'ts
    
    - Don't use `@bind` or `@oninput` in Static SSR forms — they require interactivity. Use `[SupplyParameterFromForm]` and `FormName`.
    - Don't forget `Model ??= new()` in `OnInitialized` — the model is null on GET, populated on POST.
    - Don't use `OnSubmit` together with `OnValidSubmit`/`OnInvalidSubmit` — they're mutually exclusive.
    - Don't omit `<DataAnnotationsValidator />` — validation attributes are silently ignored without it.
    - Don't omit `FormName` in SSR when a page has multiple forms — both forms will fire on any submission.
    - Don't use `InputFile` in Static SSR — it requires an interactive render mode.
    - Don't use both `Model` and `EditContext` on an `EditForm` — pick one.
    - Don't forget `<AntiforgeryToken />` in plain `<form>` elements — the server rejects the POST without it.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related