Blazor [tips]: bindとonchangeの併用は不可
1. はじめに
Blazorを調査しているときに、データバインディングについて理解していない段階で はまった ことを思い出したのでTipsとして書いておきます。
「inputタグに"bind属性"と"onchange属性"を両方つけると、(おそらく)想定した動きをしないよねー」という話です。
2. サンプル
具体的には以下のコードです。
# Pages/index.razor # @page "/" <input type="text" bind="@inputValue" onchange="@(e => inputValueChanged(e.Value.ToString()) )"/> <br /> → inputValue: <div>@inputValue</div> → changedValue: <div>@changedValue</div> @functions { private string inputValue { get; set; } = ""; private string changedValue { get; set; } = ""; void inputValueChanged(string newValue) { changedValue = newValue; } }
実行画面
以下が実行画面です。
テキストボックスに「はろー」と入力して、フォーカスをテキストボックスから外します。
そうすると以下の画面のようになります。
実装コード的には以下を想定したのですが、、、
inputValue: はろー changedValue: はろー
以下のようになりました。
inputValue: はろー changedValue:
結論としては「onchangeが効いていない」という事です。
記事タイトルの通り「bindとonchangeの併用は不可」って事ですね。
onchange="@(e => inputValueChanged(e.Value.ToString()) )"
3. なんで bindとonchangeは併用 できない?
ということで、先程のページ(index.razor)をビルドしたアセンブリ(dll)をILSpyで覗いてみます。
(補足:ここではindex.razorはhello_bind_appというBlazorプロジェクトに実装しました)
// hello_bind_app.Pages.Index using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.RenderTree; protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "input"); builder.AddAttribute(1, "type", "text"); builder.AddAttribute(2, "onchange", EventCallback.Factory.Create(this, delegate(UIChangeEventArgs e) { inputValueChanged(e.Value.ToString()); })); builder.AddAttribute(3, "value", BindMethods.GetValue(inputValue)); builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, delegate(string __value) { inputValue = __value; }, inputValue)); builder.CloseElement(); builder.AddMarkupContent(5, "\r\n<br>\r\n→ inputValue: "); builder.OpenElement(6, "div"); builder.AddContent(7, inputValue); builder.CloseElement(); builder.AddMarkupContent(8, "\r\n→ changedValue: "); builder.OpenElement(9, "div"); builder.AddContent(10, changedValue); builder.CloseElement(); }
Blazorのページレンダリング(RenderTreeの構築)の仕組みとして、ページクラスの BuildRenderTree() メソッドがその役割を担います。
ここに .razor で定義した「HTML / CSS / インラインで記述したrazorコード」が、コードとして展開されています。
onchangeイベントに対するハンドリング処理は以下のように、良い感じに構築されています。
builder.AddAttribute(2, "onchange", EventCallback.Factory.Create(this, delegate(UIChangeEventArgs e) { inputValueChanged(e.Value.ToString()); }));
が、その直後に同じinputタグに対して再度 onchange 属性がaddAttribute()されています。
これは「bind="@inputValue"」によるtwo-wayデータバインディングを構築する実装に該当します。
builder.AddAttribute(3, "value", BindMethods.GetValue(inputValue)); builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, delegate(string __value) { inputValue = __value; }, inputValue));
同じinputタグの onchange属性 に対して2回AddAttribute()が行われ、結果として後勝ちとなり「onchange="@(e => inputValueChanged(e.Value.ToString()) )」は無かったものとなってしまいます。
という事で、「なんで bindとonchangeは併用 できない?」の根拠がすっきりしました。
valueとonchangeの併用はおk
bindではなく、valueによるone-wayバインディングではonchangeがハンドルされないので、以下の onchange は有効に働きます。
# Pages/index.razor # ...省略... <input type="text" value="@inputValue" onchange="@(e => inputValueChanged(e.Value.ToString()) )"/> ...省略...
ビルド後のアセンブリ実装は以下。
// hello_bind_app.Pages.Index using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.RenderTree; protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "input"); builder.AddAttribute(1, "type", "text"); builder.AddAttribute(2, "value", inputValue); builder.AddAttribute(3, "onchange", EventCallback.Factory.Create(this, delegate(UIChangeEventArgs e) { inputValueChanged(e.Value.ToString()); })); builder.CloseElement(); builder.AddMarkupContent(4, "\r\n<br>\r\n→ inputValue: "); builder.OpenElement(5, "div"); builder.AddContent(6, inputValue); builder.CloseElement(); builder.AddMarkupContent(7, "\r\n→ changedValue: "); builder.OpenElement(8, "div"); builder.AddContent(9, changedValue); builder.CloseElement(); }
まあ、このサンプルではbindによる動作が落ちることになるので、これが正解実装というわけではありません。
以下のような画面になる。changedValueには反映するが、two-wayではなくなったのでinputValueには反映しない。
3. ということは bindとoninputの併用はおk
bindはonchangeに展開されるので、bindとoninputの併用は問題ありません。
# Pages/index.razor # ...省略... <input type="text" bind="@inputValue" oninput="@(e => inputValueChanged(e.Value.ToString()) )"/> ...省略...
oninputは入力ごとのイベントが発生するので、値の反映のタイミングは異なりますが、以下の画面のようにbindとoninputは両方が有効に動作していることが確認できます。
4. まとめ
ということで、
「bindはコンパイル後に onchange を含む実装に展開されるから、Developer側のonchange定義が無効になっちゃうよ」
そんなTipsでした。