Skip to main content
skill CX Codex

collect-user-input — Claude Code skill for Blazor forms and validation

A skill that walks the agent through Blazor forms, input components and validation. Useful when you add a form, a search box or file upload to a page.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add dotnet/skills --skill collect-user-input -a claude-code
Open on GitHub

What collect-user-input does

The skill covers user input in Blazor: forms, search boxes, filter panels, inline editing, file uploads. It documents EditForm, the built-in input components (InputText, InputSelect, InputFile and the rest), DataAnnotationsValidator and @bind-Value.

Step one tells the agent to read the project's AGENTS.md and figure out the interactivity mode. That decides the form mechanism: static SSR needs FormName plus [SupplyParameterFromForm], with no @bind or @onchange; interactive modes use @bind-Value and validate as you type. The same EditForm pattern works in both, so switching modes does not force a rewrite of the markup.

There is a section on a custom validator component for checks that need the server, like uniqueness or business rules. The skill also states what it is not for: project scaffolding and prerendering issues belong to other skills. If AGENTS.md is missing, the mode has to be worked out by hand, and picking the wrong form pattern is easy.

What it helps with

  • add a form with field validation
  • build a search box or filter panel
  • set up inline editing for a record
  • accept a file upload through a form
  • check a value's uniqueness on the server

Example prompts for the agent

  • Add an employee registration form with DataAnnotationsValidator
  • Build a filter panel with InputSelect bound via @bind-Value
  • I need file upload in a form — show me how to wire up InputFile

How to install collect-user-input

1Skills CLI (Vercel)

npx skills add dotnet/skills --skill collect-user-input -a claude-code

2Manually: put the files in place

~/.claude/skills/collect-user-input/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/collect-user-input/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about collect-user-input

How do I install collect-user-input in Claude?

Run npx skills add dotnet/skills --skill collect-user-input -a claude-code. The skill lands in ~/.claude/skills/collect-user-input/SKILL.md.

What does collect-user-input do?

It guides the agent through building forms and input controls in Blazor: EditForm, built-in components, validation via annotations or a custom validator, and submit handling.

How is it different from create-blazor-project?

collect-user-input only handles input and validation. Project scaffolding and prerendering questions live in separate skills.

Source text

SKILL.md · MIT
---
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.

© dotnet · MIT · dotnet/skills

Similar skills

Everything for Frontend and UI →

From the same set101

convert-blazor-server-to-webapp Guides a pre-.NET 8 Blazor Server app to the Blazor Web App model. Use it when replacing the old host setup while retaining interactive server rendering. CX Codex +1 3,000installs extension-points This skill maps how MSBuild discovers hooks and imports: CustomBefore/CustomAfter, wildcard directories, NuGet auto-imports. Useful when assets go missing or get replaced. CX Codex +1 3,000installs item-management Guides AI agents through MSBuild ItemGroup semantics and item expressions. It covers Include, Remove, Update operations, item batching, and metadata transforms. CX Codex +1 3,000installs plan-ui-change Splits a complex Blazor page into focused components before the agent writes any code. Useful once a screen has three or more visual sections. CX Codex +1 3,000installs coordinate-components Helps AI agents share state across Blazor components without parent-child parameter chains. Covers cascading parameters, scoped services, and render mode boundaries. CX Codex +1 3,000installs create-blazor-project The skill walks the agent through picking a Blazor render mode and runs dotnet new blazor with the right flags. CX Codex +1 3,000installs property-patterns A skill that tracks down broken MSBuild properties using patterns from the MSBuild repo. Useful when a build picks the wrong value or fails on another OS. CX Codex +1 3,000installs author-component Creates and reviews Blazor components without JavaScript interop. Useful when setting up parameters, RenderFragment slots, and proper async lifecycles. CX Codex +1 3,100installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor