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