Collect User Input
.NET (Microsoft)OfficialPublished by .NET (Microsoft)
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive guide for managing user input in Blazor, covering both Static SSR and interactive rendering modes. It details the use of EditForm, built-in input components, and validation strategies including DataAnnotations and custom validation logic. The guide also addresses specific patterns for handling form submissions, file uploads, and real-time input reactions, ensuring developers can build robust data-entry interfaces across different Blazor hosting models.
What it does
- Explains EditForm setup for both model-based and EditContext-based forms.
- Details validation techniques using DataAnnotations and custom validation components.
- Provides patterns for handling form submissions in Static SSR and interactive modes.
- Covers built-in input components like InputText, InputSelect, and InputFile.
- Outlines strategies for real-time input reaction and preventing double submissions.
- Explains SSR-specific requirements like FormName, SupplyParameterFromForm, and AntiforgeryToken.
When to use it
- When building data-entry forms, search boxes, or filter panels in Blazor.
- When implementing server-side or client-side form validation.
- When handling file uploads within interactive Blazor components.
- When you need to manage form state and submission logic across different rendering modes.
Advantages
- Provides clear distinctions between Static SSR and interactive form patterns.
- Includes reusable patterns for custom validation and CSS class providers.
- Offers guidance on handling complex scenarios like multiple forms on one page.
- Addresses critical security and state management requirements like antiforgery tokens.
Usage examples
- “How do I create a basic contact form in a Blazor SSR page?”
- The guide explains using EditForm with a Model, the [SupplyParameterFromForm] attribute, and the required FormName to handle POST requests in static pages.
- “How can I validate a field based on a server-side check?”
- It provides a CustomValidator component implementation that uses a ValidationMessageStore to inject errors into the EditContext after a service call.
- “How do I handle file uploads in Blazor?”
- It demonstrates using the InputFile component with OnChange handlers, including how to stream files and set size limits.
- “How do I prevent a user from clicking submit multiple times?”
- It shows a pattern using a boolean flag to disable the submit button during the asynchronous save operation.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/dotnet/collect-user-input/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip collect-user-input.zip -d .agents/skills
Documentation by .NET (Microsoft)
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:
FormNameidentifies the form,[SupplyParameterFromForm]binds POST data,??=initializes on GET.In interactive:
@bind-Valueprovides two-way binding,[SupplyParameterFromForm]is ignored,FormNameis 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
@bindor@oninputin Static SSR forms — they require interactivity. Use[SupplyParameterFromForm]andFormName.Don't forget
Model ??= new()inOnInitialized— the model is null on GET, populated on POST.Don't use
OnSubmittogether withOnValidSubmit/OnInvalidSubmit— they're mutually exclusive.Don't omit
<DataAnnotationsValidator />— validation attributes are silently ignored without it.Don't omit
FormNamein SSR when a page has multiple forms — both forms will fire on any submission.Don't use
InputFilein Static SSR — it requires an interactive render mode.Don't use both
ModelandEditContexton anEditForm— pick one.Don't forget
<AntiforgeryToken />in plain<form>elements — the server rejects the POST without it.
Files
- ATTRIBUTION.mdAdded by AutoGineer849 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md12.5 KiB
LICENSE
The MIT License (MIT) Copyright (c) .NET Foundation and Contributors All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- .NET (Microsoft)
- Publisher
- .NET (Microsoft) (Official)
- Source
- dotnet/skills · plugins/dotnet-blazor/skills/collect-user-input · commit 96c3d68
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by .NET (Microsoft).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download collect-user-input.zip