5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Blazor - 配列を添え字でインデックスアクセスで @bind すると、値変更時にクラッシュするよ

Last updated at Posted at 2024-12-09

お題

例えば次のような C# クラスを用意しておいて、

Item.cs
public class Item
{
    public string? Name { get; init; }
    public bool Selected { get; set; }
}

このクラスを使用して、Razor コンポーネント内で以下のようにコレクションを用意し、

App.razor
@code {
  private Item[] _items =
  [
    new() { Name = "りんご" },
    new() { Name = "みかん" },
    new() { Name = "いちご" },
  ];
}

このコレクションをチェックボックスで表示し、

好きな食べ物をチェックしてもらうと、対応する Item オブジェクトの Selected プロパティが true に設定されるようにしてみましょう。

for ループで実装すると...

このお題を、以下のように実装したとします。

App.razor
<p>以下から好きなものを選んでください。</p>

@for (var i = 0; i < _items.Length; i++)
{
    <div @key="_items[i]">
        <label>
            <input type="checkbox" @bind="_items[i].Selected" />
            @(_items[i].Name)
        </label>
    </div>
}
@code {
    ...

早速実行してみますと、無事、チェックボックスの一覧がページ上に表示されます。そこで、いずれかのチェックボックスをクリックしてチェックを On にしようとしてみると...

おおっと、System.IndexOutOfRangeException: Index was outside the bounds of the array. という例外が発生してしまいました。

例外発生の原因

この例外が発生してしまう原因は、この @bind 構文を使った .razor ファイルが、どのような C# コードに変換されているのかを知るとわかります。.razor ファイルがどのような C# コードに変換されるのかを知る手段については下記リンク先に関連記事があります。

上記リンク先の記事に倣って、今回の例で、C# に変換されたあとのコードを確認すると、要旨としては以下のようになっています。

.razorから変換されたC#コードの要旨
for (var i = 0; i < _items.Length; i++)
{
  ...
  builder.OpenElement(3, "input");
  ...
  builder.AddAttribute(6, "onchange", CreateBinder(..., value => _items[i].Selected = value, ...));
  ...
}

注目箇所は、チェックが変更されたときの "onchange" イベントハンドラの指定で渡されているコールバック関数です (以下に抜粋)。

value => _items[i].Selected = value

つまり、ページ上でユーザーが input type="checkbox" 要素のチェックを On/Off 切り替えようとすると、上記コールバック関数が、そのときの最新のチェック状態 (truefalse か) を引数に、呼び出されるわけです。この動作によって、ユーザー入力の結果がモデル側に書き戻されるのですね。

しかしです。上記コールバック関数が呼び出されるとき、変数 i の値はどうなっているでしょうか?

今一度、C# の for ループにおけるループカウンタ変数の振る舞いについて再確認します。以下のような for ループを含む C# コードにおいて、for ループを抜けた時点で、ループカウンタ i の値はどうなっているでしょうか。

for (var i = 0; i < 3; i++)
{
    // ここでは、i は、0, 1, 2 と変化
}
// Q: ではここに到達したとき、i はいくつ?

答えは、4 です。つまり、for ループがまわるたびに i++ によって i の値が 1 つずつ加算されつつ、ついに条件 i < 3 を満たさなくなったとき、つまり i が 4 になったときに for ループから抜けるので、それで for ループを抜け終わった以後は、ループカウンタ変数 i は 4 になっている、ということです。

さぁ、これでおわかりでしょうか。先のチェックボックスの場合、いったんレンダリングは終わってますから、変数 i の値は 4 になっているわけです。すなわち、チェックボックスの On/Off が変更されるときのコールバック関数は、それが呼び出されたときには、実態としては以下のようになってしまっています。

value => _items[4 /*forループは既に抜けているので。*/].Selected = value

しかしながら、フィールド変数 _items の要素数は 3 ですから、それで、配列インデックスが、実際に収納されている要素数を超えているという IndexOutOfRangeException 例外が発生してしまったのです。

解決方法 - foreach 使いましょう

この不具合を解消するには、for ループで添え字で配列要素にアクセスするのではなく、foreach を使って、配列要素を直接に列挙しましょう。

App.razor
...
@foreach (var item in _items)
{
    <div @key="item">
        <label>
            <input type="checkbox" @bind="item.Selected" />
            @(item.Name)
        </label>
    </div>
}
...

これで解決です!

少なくとも自分の体験の限りにおいては、C# プログラミングにおいては for ループでコレクションの要素に添え字でインデックスアクセスする必要に遭遇することが、まず滅多にないと思います。Blazor でコレクションを繰り返し要素としてレンダリングする際は、シンプルに foreach でいきましょう。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?