Blazor forms, EditForm, input components, DataAnnotations, FluentValidation, and custom validation
From dotnet-blazornpx claudepluginhub markus41/claude --plugin dotnet-blazorThis skill is limited to using the following tools:
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
@rendermode InteractiveServer
<EditForm Model="@_model" OnValidSubmit="HandleSubmit" FormName="create-item">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label class="form-label">Name</label>
<InputText @bind-Value="_model.Name" class="form-control" />
<ValidationMessage For="@(() => _model.Name)" />
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<InputText @bind-Value="_model.Email" class="form-control" type="email" />
<ValidationMessage For="@(() => _model.Email)" />
</div>
<div class="mb-3">
<label class="form-label">Category</label>
<InputSelect @bind-Value="_model.CategoryId" class="form-select">
<option value="">Select...</option>
@foreach (var cat in _categories)
{
<option value="@cat.Id">@cat.Name</option>
}
</InputSelect>
</div>
<div class="mb-3">
<label class="form-label">Accept Terms</label>
<InputCheckbox @bind-Value="_model.AcceptTerms" />
</div>
<button type="submit" class="btn btn-primary" disabled="@_submitting">
@(_submitting ? "Saving..." : "Submit")
</button>
</EditForm>
public sealed class CreateItemModel
{
[Required(ErrorMessage = "Name is required")]
[StringLength(200, MinimumLength = 2)]
public string Name { get; set; } = "";
[Required, EmailAddress]
public string Email { get; set; } = "";
[Range(1, int.MaxValue, ErrorMessage = "Select a category")]
public int CategoryId { get; set; }
[Range(typeof(bool), "true", "true", ErrorMessage = "Must accept terms")]
public bool AcceptTerms { get; set; }
}
// Install: Blazored.FluentValidation
public sealed class CreateItemValidator : AbstractValidator<CreateItemModel>
{
public CreateItemValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(200);
RuleFor(x => x.Email)
.NotEmpty().EmailAddress();
RuleFor(x => x.CategoryId)
.GreaterThan(0).WithMessage("Select a category");
}
}
@using Blazored.FluentValidation
<EditForm Model="@_model" OnValidSubmit="HandleSubmit">
<FluentValidationValidator />
@* ... inputs ... *@
</EditForm>
For static SSR pages, use [SupplyParameterFromForm]:
@page "/items/create"
<EditForm Model="@Model" OnValidSubmit="HandleSubmit" FormName="create-item" method="post">
<AntiforgeryToken />
<DataAnnotationsValidator />
@* inputs *@
</EditForm>
@code {
[SupplyParameterFromForm]
private CreateItemModel Model { get; set; } = new();
private async Task HandleSubmit()
{
await ItemService.CreateAsync(Model);
Navigation.NavigateTo("/items");
}
}
| Component | Binds to | HTML |
|---|---|---|
InputText | string | <input type="text"> |
InputTextArea | string | <textarea> |
InputNumber<T> | int, decimal, etc. | <input type="number"> |
InputDate<T> | DateTime, DateOnly | <input type="date"> |
InputCheckbox | bool | <input type="checkbox"> |
InputSelect<T> | enum, int, string | <select> |
InputRadio<T> | enum, string | <input type="radio"> |
InputFile | IBrowserFile | <input type="file"> |
<InputFile OnChange="HandleFileSelected" accept=".pdf,.docx" multiple />
@code {
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
foreach (var file in e.GetMultipleFiles(maxAllowedFiles: 5))
{
if (file.Size > 10 * 1024 * 1024) continue; // 10MB limit
using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
// Process stream...
}
}
}
@* Two-way binding - updates on element blur by default *@
<input @bind="inputValue" />
<input @bind="InputValue" />
@code {
private string? inputValue;
private string? InputValue { get; set; }
}
@* Update on every keystroke instead of blur *@
<input @bind="searchText" @bind:event="oninput" />
@code {
private string? searchText;
}
@* CORRECT: Use @bind:get/@bind:set for two-way binding with custom logic *@
<input @bind:get="inputValue" @bind:set="OnInput" />
@code {
private string? inputValue;
private void OnInput(string? value)
{
var newValue = value ?? string.Empty;
inputValue = newValue.Length > 4 ? "Long!" : newValue;
}
}
Important: Do NOT use value="@x" @oninput="handler" for two-way binding - Blazor won't sync the value back. Always use @bind:get/@bind:set.
<input @bind="searchText" @bind:after="PerformSearch" />
@code {
private string? searchText;
private async Task PerformSearch()
{
// Runs after searchText is updated
results = await SearchService.SearchAsync(searchText);
}
}
<input @bind="startDate" @bind:format="yyyy-MM-dd" />
@code {
private DateTime startDate = new(2020, 1, 1);
}
@* Parent *@
<YearSelector @bind-Year="selectedYear" />
@code {
private int selectedYear = 2024;
}
@* Child: YearSelector.razor *@
<input @bind:get="Year" @bind:set="YearChanged" />
@code {
[Parameter] public int Year { get; set; }
[Parameter] public EventCallback<int> YearChanged { get; set; }
@* Convention: parameter + "Changed" suffix *@
}
<select @bind="SelectedCities" multiple>
<option value="bal">Baltimore</option>
<option value="la">Los Angeles</option>
<option value="sea">Seattle</option>
</select>
@code {
public string[] SelectedCities { get; set; } = Array.Empty<string>();
}