collect-user-input について
静的 SSR とインタラクティブなレンダリングモードでの入力処理を扱う、Blazor 開発者向けのフォームパターンスキルです。フォーム設定、バインド、検証、アップロード、送信を説明します。
主な機能
- 01SSR とインタラクティブフォームのパターン
- 02EditForm と送信ハンドラー
- 03組み込み入力コンポーネントの指針
- 04データ注釈とカスタム検証
- 05インタラクティブ入力とファイルアップロード
- 06フォームのセキュリティと送信対策
活用シーン
- 01SSR とインタラクティブモードに対応するフォーム作成
- 02検証と送信処理の選択
- 031 ページに複数の SSR フォームを配置
- 04インタラクティブな絞り込みやファイルアップロードの追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill collect-user-inputインストール後、Claude Code で/collect-user-inputを入力して読み込まれたか確認します。
詳細
ユーザー入力を収集する
ステップ 1 — プロジェクトの AGENTS.md を読む
AGENTS.md で Interactivity Mode と Interactivity Scope を確認します。これにより、適用するフォームパターンが決まります。
| モード | フォームの仕組み |
|---|---|
| None(静的 SSR) | FormName と [SupplyParameterFromForm] を指定した EditForm。@bind と @onchange は使用しません。 |
| Server | @bind-Value を使う EditForm。完全なインタラクティブ性に対応し、リアルタイム検証と動的 UI を利用できます。 |
| WebAssembly | Server と同じですが、サーバーのデータを必要とするバリデーターは API を呼び出す必要があります。 |
| Auto | WebAssembly と同じです。コードはブラウザーとサーバーの両方で動作する必要があります。 |
| スコープ | 影響 |
|---|---|
| Global | すべてのフォームがインタラクティブです。静的 SSR に明示的に切り替えるページでのみ FormName が必要です。 |
| Per-page | 静的ページのフォームでは FormName と [SupplyParameterFromForm] を使用します。@rendermode ページのフォームでは @bind-Value を使用します。 |
EditForm の設定
EditForm には Model または EditContext のどちらか一方が必要です。両方を指定してはいけません。
Model ベース(デフォルト)
<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
}
}
このパターンだけで、SSR モードとインタラクティブモードの両方に対応できます。
- SSR では、
FormNameがフォームを識別し、[SupplyParameterFromForm]が POST データをバインドし、??=が GET 時に初期化します。 - インタラクティブモードでは、
@bind-Valueが双方向バインディングを行い、[SupplyParameterFromForm]は無視され、FormNameは問題なく使用できます。
EditContext ベース(高度)
プログラムによるフィールド追跡、動的な検証ルール、または手動での EditContext.Validate() の呼び出しが必要な場合に使用します。
private EditContext? editContext;
private EmployeeModel model = new();
protected override void OnInitialized()
{
editContext = new EditContext(model);
}
<EditForm EditContext="editContext" OnValidSubmit="HandleSubmit" FormName="employee">
送信ハンドラー
| ハンドラー | 実行されるタイミング | 用途 |
|---|---|---|
OnValidSubmit | 検証に合格したとき | DataAnnotationsValidator を使う標準的なフォーム |
OnInvalidSubmit | 検証に失敗したとき | 無効な状態を独自に処理する必要がある場合 |
OnSubmit | 常に実行 — 検証は手動 | EditContext.Validate() を自分で使用する場合 |
OnSubmit は OnValidSubmit/OnInvalidSubmit と組み合わせて使用できません。
組み込みの入力コンポーネント
| コンポーネント | バインド先 | 備考 |
|---|---|---|
InputText | string | <input type="text"> をレンダリングします |
InputTextArea | string | <textarea> をレンダリングします |
InputNumber<T> | int、double、decimal | <input type="number"> をレンダリングします |
InputDate<T> | DateTime、DateOnly、DateTimeOffset | <input type="date"> をレンダリングします |
InputCheckbox | bool | <input type="checkbox"> をレンダリングします |
InputSelect<T> | string、列挙型、数値型 | <select> をレンダリングします |
InputRadioGroup<T> | string、列挙型、数値型 | 子要素の InputRadio<T> をラップします |
InputFile | IBrowserFile | ファイルアップロード — インタラクティブモードのみ |
すべての入力コンポーネントでは、バインディングに @bind-Value を使用します。アクセシビリティのため、テキストは必ず <label> で囲むか、id/for 属性を使用してください。
列挙型の値を使った InputSelect
<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>
検証
データ注釈
モデルに検証ルールを定義します。
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; }
}
EditForm 内に <DataAnnotationsValidator /> を追加します。これがないと、注釈属性は警告なしに無視されます。
エラーの表示方法:
<ValidationSummary />— すべてのエラーをリストで表示します<ValidationMessage For="() => Model!.FieldName" />— フィールドごとのインラインエラーを表示します
カスタムバリデーターコンポーネント
サーバーとのラウンドトリップを伴う検証(重複チェック、ビジネスルールなど)に使用します。
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();
}
}
フォームでの使用例:
<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
}
}
入力変更への対応(インタラクティブモードのみ)
@bind:after
バインドされた値が変更された後に処理を実行します。
<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
<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 固有のパターン
これらはフォームが静的 SSR でレンダリングされる場合(モード = None、または @rendermode のないページ)に適用されます。
SupplyParameterFromForm
フォーム送信時に POST データをプロパティにバインドします。
[SupplyParameterFromForm]
private ContactModel? Contact { get; set; }
protected override void OnInitialized() => Contact ??= new();
重要: OnInitialized 内の ??= は必須です。GET 時にはプロパティが null のため、??= がモデルを作成します。POST 時にはフレームワークが値を設定するため、??= によって送信された値が維持されます。
FormName — 1 つのページに複数のフォームを配置する場合
各フォームには一意の FormName が必要です:
<EditForm Model="Search" OnSubmit="DoSearch" FormName="search">...</EditForm>
<EditForm Model="Contact" OnValidSubmit="SaveContact" FormName="contact">...</EditForm>
[SupplyParameterFromForm] をフォームに対応付けます:
[SupplyParameterFromForm(FormName = "search")]
private SearchModel? Search { get; set; }
[SupplyParameterFromForm(FormName = "contact")]
private ContactModel? Contact { get; set; }
フォームの拡張ナビゲーション
ページ全体を再読み込みせず、SPA のようにフォームを送信するには Enhance を追加します:
<EditForm Model="Model" OnValidSubmit="Save" FormName="quick" Enhance>
拡張フォームは fetch で送信し、DOM を更新してスクロール位置を維持します。SSR でも、ページはインタラクティブに操作できるように感じられます。
プレーンな HTML フォーム
SSR で EditForm の代わりに生の <form> を使う場合は、偽造防止トークンを手動で追加します:
<form method="post" @onsubmit="Submit" @formname="raw-form">
<AntiforgeryToken />
<input name="Model.Name" value="@Model?.Name" />
<button type="submit">Send</button>
</form>
EditForm は偽造防止トークンを自動的に含めます。
ファイルのアップロード
InputFile は インタラクティブモードでのみ 動作し、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.
}
}
ストリームのサイズ制限:
- サーバー: SignalR メッセージサイズのデフォルトは約 30 KB です。
OpenReadStream(maxAllowedSize)を呼び出すと上限を増やせます。大きなファイルはサーキット経由でストリーミングされます。 - WebAssembly: ファイルはブラウザーで読み込まれます。SignalR の制限はありませんが、メモリに制約されます。
複数のファイルを扱う場合:
<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
}
}
}
二重送信を防ぐ
処理中は送信ボタンを無効にします:
<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;
}
}
}
カスタム検証 CSS
デフォルトの valid/invalid CSS クラスを置き換えます:
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")
: "";
}
}
フォームに適用します:
protected override void OnInitialized()
{
editContext = new EditContext(model);
editContext.SetFieldCssClassProvider(new BootstrapFieldCssClassProvider());
}
やってはいけないこと
- Static SSR フォームでは
@bindや@oninputを使わないでください。これらにはインタラクティブ性が必要です。代わりに[SupplyParameterFromForm]とFormNameを使用してください。 OnInitializedでModel ??= new()を忘れないでください。GET ではモデルは null で、POST では値が設定されます。OnSubmitとOnValidSubmit/OnInvalidSubmitを併用しないでください。これらは相互に排他的です。<DataAnnotationsValidator />を省略しないでください。これがないと、検証属性は無視されます。- 1 つのページに複数のフォームがある場合、SSR で
FormNameを省略しないでください。送信時に両方のフォームが実行されます。 - Static SSR で
InputFileを使わないでください。インタラクティブなレンダーモードが必要です。 EditFormでModelとEditContextを併用しないでください。どちらか一方を選んでください。- プレーンな
<form>要素で<AntiforgeryToken />を忘れないでください。これがないとサーバーは POST を拒否します。