双方向バインディング

双方向バインディングは、値を 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.NameModel.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 をバインドする#

チェックボックスは、boolchecked 属性へ、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)。その本体が、属性の値とバインダーの両方へ写されるからです。

正規化するセッターは、ずれを生みます。要素は入力した文字を表示し、フィールドは切り詰めた値を持ちます。通常の差分計算は何も書きません。前回のレンダリングからレンダーツリーが変わっていないからです。valuechecked のバインドなら、.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 はそのリテラルを読みません。属性の名前を推論しないのと同じ理由です。そのため選択は、呼び出し側へ移してあります。

numberdate には不変カルチャーを書く#

<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 にあたります。stringbool だけをバインドするアプリに、このコストはかかりません。バインドの個数ではなく、一度きりのコストです。

変換を自分で書く#

明示する書き方も、これまでどおり使えます。変換ではなく検証をしたいときは、そちらが向いています。

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回の呼び出しが、ValueValueChangedValueExpression を渡します。導いた名前はどれもコンポーネントの型から探すので、{Name}Changed が無かったり綴りが違ったりすれば、何もバインドしないのではなく BCF3020 を報告します。要素側との非対称が、行き当たりばったりではなく規則だと言えるのは、この検査があるからです。

対象を参照渡しではなくゲッターのラムダとして書くのは、{Name}Expression のためです。EditForm の下にあるコンポーネントは、その式から FieldIdentifier を解決します。この識別子が入力をモデルのプロパティに結び付けるので、検証のメッセージが正しいフィールドに届きます。ほかの書き方では、これを渡せません。

周りの EditForm も同じ書き方です。EditForm.ChildContentRenderFragment<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 は、ゲッターだけの形で代入できない対象を見る
  • BCF3019on の無いイベント名を見る
  • BCF3020 は、対応する変更コールバックの無いコンポーネントを見る
  • BCF3024 は、.Class と並んだ class のバインドを見る
  • BCF3031 は、値の型に変換器の無い書式を見る

1つの要素が .Bind を複数持つことはできます。そのうち2つが属性の名前かイベントの名前を共有すれば BCF3010 で、どの装飾を2つ重ねても出るのと同じ重複です。 DOM の再同期の対象は valuechecked だけです。利用者が入力した文字の上に、正規化した値を置き直す修復のことです。ブラウザーがイベントと一緒に返してくるのが、その2つだけだからです。

次に読むもの#

An unhandled error has occurred. Reload 🗙