双方向バインディング
双方向バインディングは、値を DOM へ書き出し、利用者の編集を自分の状態へ読み戻す、1つの装飾です。.Bind は Razor の @bind にあたります。必要なものが、すべて目に見える引数として並ぶ書き方になっています。ジェネレーターはこれを、属性のフレームと、Blazor 自身の CreateBinder
を運ぶイベントのフレームへ変換します。生成されたコードが実行時に式ツリーをコンパイルすることはなく、リフレクションを使うこともありません。enum をバインドしたときだけ、フレームワーク自身の変換器の中で1つだけリフレクションによる参照が起きます。
要素をバインドする#
値を運ぶ属性、変化を知らせるイベント、そして今の値を読むラムダを指定します。この3つのうち、マークアップに現れるのは最初の1つだけです。
protected override View Body => Div[ Input.Type("text").Bind("value", "oninput", () => _name), P[$"Hello, {_name}"]];
<div> <input type="text" value="Ada"> <p>Hello, Ada</p> </div>
このラムダは両方向に読まれます。本体は属性の値になり、ゲッターだけのこの形では、同じ本体が、ジェネレーターの書く代入の左辺にもなります。そのため対象は代入できるものでなければなりません。フィールド、セッターを持つプロパティ、あるいはそのどちらかを通る経路(_form.Name、
Model.Items[0].Title、_dict["k"])です。() => _name.ToUpper() のような計算した式は
BCF3018 を報告します。そう書きたいときは、下のセッターを明示する形を使います。
キー入力のたびにバインドするなら "oninput"、要素からフォーカスが外れたときにバインドするなら
"onchange" を使います。通常使うのはこの2つですが、組み合わせを縛る一覧はありません。制約は次の3つの規則だけです。
- どちらの名前も、空でないコンパイル時定数であること(BCF3011)
- イベントの名前が
onで始まること(BCF3019) - どちらも、同じ要素の別の装飾で既にバインドされていないこと(BCF3010)
名前を HTML と照合する検査はありません。
名前を2つとも書く理由#
Razor は属性をマークアップから推論します。.razor のファイルからリテラルの type="checkbox" を読み取り、value ではなく checked をバインドします。この API には、読み取れるリテラルがありません。タグは文字列で、type は式です。Input.Type(kind) は通常の C# の呼び出しで、その値は実行するまで決まらないこともあります。そのため推論を照合する対象がありません。
value を既定にすると、もっとも避けたい失敗が起こります。チェックボックスが間違った属性にバインドされ、それが黙って起き、知らせる診断も出ない、という失敗です。そのためこの API 全体の規則は 確かめられることだけを推論する です。要素の側は確かめられないので推論せず、代わりに短い文字列を2つ書きます。
この間違いのうち、確かめ られる 半分は検出します。on で始まらないイベントの名前は BCF3019 を報告します。そのため2つの引数を取り違えても、何もしない属性が増えるのではなく、コンパイル時に止まります。
.Bind のコンポーネント側は名前を推論します。同じ規則が、そちらでは推論を許すからです。コンポーネントのパラメーターをバインドするを見てください。
チェックボックスは checked をバインドする#
チェックボックスは、bool を checked 属性へ、onchange でバインドします。同じ装飾で、最初の引数だけが違い、出力に出る属性も違います。
protected override View Body => Label[ Input.Type("checkbox").Bind("checked", "onchange", () => _agreed), " I agree"];
<label><input type="checkbox" checked> I agree</label>
bool は HTML の真偽値属性の形で、.Attr が取るのと同じものです。true なら値の空な属性として出力し、false なら属性ごと出しません。
セッターを明示して正規化する#
4つ目の引数を渡すと、生成される代入の代わりに自分のセッターが使われます。検証や正規化、編集のたびに実行したい処理は、ここに置きます。
private string _name = ""; protected override View Body => Input.Type("text").Bind("value", "oninput", () => _name, v => _name = v.Trim());
これは Razor が @bind:get / @bind:set と @bind:after に分けているものを、まとめて引き受けます。セッターが書き込みそのものなので、後で実行したかった処理も同じラムダに入ります。ただし書き込みは自分で行います。セッターを渡した時点で、代わりに代入する処理はなくなります。メソッドグループ(SetName)も使え、ラムダの本体はブロックでもよく、Task を返せば async
の形も使えます。
Input.Type("text").Bind("value", "oninput", () => _query, async v => { _query = v; await SearchAsync(v); });
代入できるゲッターが必要なのは、ゲッターだけの形のときだけです。セッターがあれば、ゲッターは読まれるだけなので、どんな式でもかまいません。ただしゲッター自身は、どちらの形でも、その場に書いた式本体のラムダである必要があります(BCF3017)。その本体が、属性の値とバインダーの両方へ写されるからです。
正規化するセッターは、ずれを生みます。要素は入力した文字を表示し、フィールドは切り詰めた値を持ちます。通常の差分計算は何も書きません。前回のレンダリングからレンダーツリーが変わっていないからです。value か checked のバインドなら、.Bind はその属性を DOM の再同期に登録してこのずれを埋め、要素の表示は正規化した値に直ります。設定は不要です。
登録される名前はこの2つだけです。Blazor のクライアントが返してくるのは、フォーム要素自身の
value、チェックボックスなら checked で、それ以外は返ってきません。ほかの属性へのバインドは何も登録しません。カスタム要素での .Bind("hue", "onhuechange", () => _hue, Normalize) が、よくある形です。セッターは変わらず実行され、新しい値も次のレンダリングで通常の差分計算によって
DOM へ届きます。行われないのは、上に書いた修復だけです。この修復は、正規化してもレンダーツリーが変わらず、要素が入力した文字を表示し続ける場合のためにあります。
テキスト入力を空にしたとき、セッターが受け取るのは "" で、null ではありません。そのため引数の型は null 許容でない string です。セッターから自分の状態へ書き込むのは許されています。Body
のほかの場所で書き込めば BCF3001 になります。セッターだけが例外なのは、.OnClick のラムダと同じ遅延したハンドラーで、ツリーを組み立てている最中には実行されないためです。
数値、日付、enum#
カルチャーを書けば、どの型でもバインドできます。カルチャーは最後の引数で、省略はできません。
private int _age; protected override View Body => Input.Type("number").Bind("value", "oninput", () => _age, CultureInfo.InvariantCulture);
このカルチャーが、出ていく値を書式化し、戻ってきた値を解析します。通るのは Blazor 自身の
BindConverter です。数値、日付、時刻、Guid、enum、そしてそれらの null 許容形が、どれも動きます。変換がこのライブラリのものではなく、フレームワークのものだからです。
既定値ではなく引数にしているのは、既定値にすれば、見えないところでカルチャーが選ばれるからです。
Razor は要素のリテラルな type から選びます。この API はそのリテラルを読みません。属性の名前を推論しないのと同じ理由です。そのため選択は、呼び出し側へ移してあります。
number と date には不変カルチャーを書く#
<input type="number"> と <input type="date"> は、利用者のロケールではなく固定の書式で定義されています。ここで CultureInfo.CurrentCulture を使うと、小数点にコンマを使うロケールで、要素の受け取れない値になります。
これは診断しません。 検査するには type を読む必要がありますが、ここでの type は式です。リテラルを書いたときだけ発火する規則は、同じ誤りをある書き方では捕まえ、別の書き方では見逃します。この2つには CultureInfo.InvariantCulture を書いてください。利用者が文章として読む値には現在のカルチャーを使います。
解析できない値は元に戻る#
入力された文字を変換器が読めない場合、セッターは呼ばれません。フィールドと要素は、どちらも前の値に戻ります。これは Blazor の挙動で、.Bind でも、上に書いた DOM の再同期を通じて同じ結果になります。
ここでイベントをどちらにするかは、意識して選ぶ必要があります。"oninput" では巻き戻しがキー入力のたびに実行されるので、int のバインドに入力した小数点は残りません。4. が拒否され、. がそのまま取り去られます。数値の入力では、多くの場合これは望ましくありません。
// フォーカスが外れたときに巻き戻すので、入力途中の数値が消えません。 Input.Type("number").Bind("value", "onchange", () => _amount, CultureInfo.InvariantCulture);
"oninput" が正しいのは、途中の値がどれも意味を持つときです。range のスライダーや、入力できる文字をすべて受け付ける text の欄がこれにあたります。
欄を空にした場合は事情が異なり、これは拒否ではありません。Blazor は空文字列をその型の既定値として読むので、int のバインドを空にすると前の値が残るのではなく 0 になります。値が本当に任意なら
int? をバインドしてください。そちらは null を受け取ります。
日付には書式が必要#
date の入力は yyyy-MM-dd を要求します。Razor と違い、この API はそれを type から補えません。カルチャーの1つ手前の引数として書きます。
private DateOnly _due = new(2026, 8, 14); protected override View Body => Input.Type("date").Bind( "value", "oninput", () => _due, "yyyy-MM-dd", CultureInfo.InvariantCulture);
書式を受け取るのは DateTime / DateTimeOffset / DateOnly / TimeOnly とその null 許容形だけで、ほかの型は受け取りません。フレームワークが書式付きの変換器を宣言しているのが、その8つだからです。int に書けば BCF3031 です。数値を書式化したい場合は、ゲッターで書式化し、セッターを明示して解析してください。
トリムして配布する場合#
値型を1つでもバインドすると、Blazor の BindConverter が丸ごと保持されます。自分がバインドしない型の変換器も残ります。トリム済みの self-contained な publish で実測しました。
Microsoft.AspNetCore.Components.dll のおよそ 10 KB にあたります。string と bool だけをバインドするアプリに、このコストはかかりません。バインドの個数ではなく、一度きりのコストです。
変換を自分で書く#
明示する書き方も、これまでどおり使えます。変換ではなく検証をしたいときは、そちらが向いています。
private decimal _amount; protected override View Body => Input.Type("text").Bind( "value", "onchange", () => _amount.ToString(CultureInfo.InvariantCulture), v => _amount = decimal.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var d) ? d : _amount);
ここでは Parse ではなく TryParse を使ってください。セッターから投げられた例外は、フレームワークが値を拒否する経路ではありません。描画そのものが失敗します。
コンポーネントのパラメーターをバインドする#
コンポーネントでは、名前を書く代わりに導きます。導いた名前を確かめられるからです。.Bind はラムダでパラメーターを選び、ジェネレーターが {Name}Changed を、コンポーネントが宣言していれば {Name}Expression も足します。
using Microsoft.AspNetCore.Components.Forms; private readonly NameModel _model = new(); protected override View Body => Component<InputText>().Bind(c => c.Value, () => _model.Name);
この1回の呼び出しが、Value、ValueChanged、ValueExpression を渡します。導いた名前はどれもコンポーネントの型から探すので、{Name}Changed が無かったり綴りが違ったりすれば、何もバインドしないのではなく BCF3020 を報告します。要素側との非対称が、行き当たりばったりではなく規則だと言えるのは、この検査があるからです。
対象を参照渡しではなくゲッターのラムダとして書くのは、{Name}Expression のためです。EditForm
の下にあるコンポーネントは、その式から FieldIdentifier を解決します。この識別子が入力をモデルのプロパティに結び付けるので、検証のメッセージが正しいフィールドに届きます。ほかの書き方では、これを渡せません。
周りの EditForm も同じ書き方です。EditForm.ChildContent は RenderFragment<EditContext> で、角括弧はコンテキストを捨てて、そこへ内容を渡します。下の内容には、ほかに必要なものがありません。
EditContext を読む内容だけ、.Template で指定します。
protected override View Body => Component<EditForm>() .Param(form => form.Model, _model)[ Component<NameFields>().Param(fields => fields.Value, _model)];
バインドした入力は、角括弧の中に直接置いても、独立したコンポーネントに置いてもかまいません。上のバインドと、それが渡す ValueExpression は、どちらでも変わりません。EditContext を読む書き方と、.Param でキャッシュしたデリゲートを渡すほうがよい場面については、ジェネリックなフラグメントのパラメーターを見てください。
静的SSRでは — ページにインタラクティブなレンダーモードが無い場合は — EditForm.Model を保持するプロパティ自身に [SupplyParameterFromForm] が必要です。単なるフィールドでは足りません。
POST されたフォームから復元されるのは [SupplyParameterFromForm] を持つプロパティだけで、それ以外は
POST 前の値のまま、多くの場合 null です。バインドする入力を別のコンポーネントへ切り出してもこの問題は避けられません。InputText / InputTextArea は自分自身の ValueExpression から
POST するフィールド名を導くので、子コンポーネント側のパラメーターは、保持元のプロパティが期待するのとは違う名前で POST してしまいます。動く形は
samples/Guestbook/Components/Pages/GuestbookPage.cs を見てください。
セッターを明示する形と async のセッターは、こちらでも使えます。意味は要素のときと同じです。
TValue は、カルチャーと書式のどちらも取りません。値が向かう先は DOM ではなくパラメーターです。途中で書式化や解析が入らないので、引数に書き添える選択がそもそもありません。
Component<T>() が、同じプロジェクトで宣言した .razor のコンポーネントを指定できないことは覚えておいてください(BCF3012)。
InputText のようなフレームワークのコンポーネントと、手書きの C# のコンポーネントは、いつでも解決します。
何が検査されるか#
.Bind を読む診断は6つあり、どれもリファレンスに項があります。
- BCF3017 はゲッターの形を見る
- BCF3018 は、ゲッターだけの形で代入できない対象を見る
- BCF3019 は
onの無いイベント名を見る - BCF3020 は、対応する変更コールバックの無いコンポーネントを見る
- BCF3024 は、
.Classと並んだclassのバインドを見る - BCF3031 は、値の型に変換器の無い書式を見る
1つの要素が .Bind を複数持つことはできます。そのうち2つが属性の名前かイベントの名前を共有すれば BCF3010 で、どの装飾を2つ重ねても出るのと同じ重複です。
DOM の再同期の対象は value と checked だけです。利用者が入力した文字の上に、正規化した値を置き直す修復のことです。ブラウザーがイベントと一緒に返してくるのが、その2つだけだからです。
次に読むもの#
- これが組み立てられている、一方向の
.Attrと.Onは要素と装飾へ。 .Paramとコンポーネント周りの残りはコンポーネントと再利用へ。