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でした。