Blazor で、行が動的に増減する入力フォームを実装してみる


[Blazor Advent Calendar](https://qiita.com/advent-calendar/2019/blazor) の Day 12 に投稿いただいた、@sugimomoto さんの記事「[Blazor で API Explorer を作って色々悩んだお話 ＃GyutanKaigi2019](http://kageura.hatenadiary.jp/entry/blazorsmaregi)」にて、**「(Blazor での) 動的な入力フォームの INPUT維持・取得」で困ってる**とのこと。

自分、ちょうど今時期、まとまった時間が取りにくく、@sugimomoto さんの公開されているソースコードにも目をとおせていません。
そのため、的ハズレなことを書いているかもしれませんが、せめて何かしら参考になるかも、と思いまして、「Blazor で、行が動的に増減する入力フォームを実装してみる」というテーマで記事投下したいと思います。

# 作るもの

こんなの作ります。

![movie-002.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/71ee58c9-6e49-cc84-b466-e73d57a88bc3.gif)

なるべくシンプルにして本質のみを示そうと思いまして、編集結果 (オブジェクトの状態) は、ブラウザの開発者コンソールに表示するようにしてみました。

# C# コードブロック

まずは、モデルというかコントローラというか何というか、とにかく、レンダリングじゃない、C# コードブロックだけから見てみましょう。

この部分にはとくに面白い・変わったところはなく、下記のとおりです。

```csharp
... これは App.razor ファイル ...

@code {

  // このデモのために用意した、"人物クラス"
  private class Person
  {
    public Guid Id; // 固有の ID
    public string Name; // 名前
    public int Age; // 年齢
  }
  
  // 人物 x n人のリスト
  private List<Person> People = new List<Person>();
  
  // [人物を追加] ボタンが押されときに呼び出されます。
  private void OnClickAddPerson()
  {
    this.People.Add(new Person { Id = Guid.NewGuid() });
  }
  
  // [削除] ボタンが押されときに呼び出されます。
  private void OnClickRemove(Person person)
  {
    this.People.Remove(person);
  }
  
  // [開発者コンソールに表示] ボタンが押されたときに呼び出されます。
  private void OnClickDump()
  {
    foreach (var person in this.People)
    {
      Console.WriteLine($"{person.Id} | {person.Name} | {person.Age}");
    }
  }
}
```

- "追加" ボタンが押されるたびに、人物オブジェクトを new してリストに追加する、および、
- "削除" ボタンが押されたら、引数に指定された人物オブジェクトをリストから除く

というだけですね。
そして、動作確認のために、開発者コンソールに現在のリストの内容を表示するようになっています。

たぶん、C# のコードが読める方なら、労せず読解できることでしょう。

強いて言えば、数が増減するオブジェクトである人物クラスに、**要素(オブジェクト)を一意に識別できるユニークキー (ここでは `Id` プロパティ) を用意している**点がポイントでしょうか。

このユニークキーとして、今回の人物クラスでは `Guid` 型を使いましたが、他の要素と重複しない値を持つようにすれば、`int` とか `string` など、他の型でも構いません。

# UI にバインドする

さて、じゃぁ次に、この C# コードブロックで実装している人物リストを、UI にどうバインドしたらいいの? という話になりますね。

実は UI へのバインド時も、さほど特別な仕掛けは不要で、愚直に、**foreach でリストの要素を列挙しつつ、iteration 変数をそのまま (Blazor の標準機構で) バインドで OK、** です。

```html
... これは App.razor ファイル ...

@foreach (var person in this.People)
{
  <div @key="person.Id">
    <input type="text" placeholder="名前" @bind="person.Name" />
    <input type="number" placeholder="年齢" @bind="person.Age" />
    <button @onclick="() => OnClickRemove(person)">削除</button>
  </div>
}
```

これだけで、はい、冒頭の GIF アニメーションのとおりの、動的に数が増減するオブジェクトに対する入力フォームができあがりです。

なお、`@key` ディレクティブにより、列挙されるオブジェクトを一意に識別できる値を指定しています。

この指定があるおかげで、オブジェクトが増減したときに、DOM のどの部分を更新すればよいかが Blazor にわかる仕掛けです。

`@key` ディレクティブについて詳しくは、ASP.NET ブログの下記記事が参考になるかと思います。

- [ASP.NET Core and Blazor updates in .NET Core 3.0 Preview 6](https://devblogs.microsoft.com/aspnet/asp-net-core-and-blazor-updates-in-net-core-3-0-preview-6/)

なお、「動的」な INPUT 要素とそのバインドでも、今回作成したデモンストレーションアプリのように、「追加」「削除」で要素数が増減するのではない場合は、`@key` ディレクティブで行とオブジェクトとの関連付けを強固にできなくても、（行の追加や撤去の再レンダリングがないでしょうから）普通に動作することができます。

## 補足 - 削除機能

あと、@sugimoto さんの命題にはなかったかと思いますが、「削除」の機能もこのサンプルには取り付けています。
ということで「削除」ボタンが押されたときのイベントハンドラの記述を見てみるとですね、ここだけラムダ式で記述することとなってるんですね (下記に再掲)。

```html
@foreach (var person in this.People)
{
  ...
  <button @onclick="() => OnClickRemove(person)">削除</button>
```

というのも、削除対象のオブジェクトをイベントハンドラの引数に渡すために、このようなラムダ式を書く必要があるのです。

ですが、他の JavaScript ベースの SPA フレームワークなどと比べると、書き方がちょっとキモいかもですね。
まぁ、Blazor ではそういうものとして認識しておきましょうか。

# 実践レベルにはもう少し必要

今回は「動的に増減するオブジェクトに対する入力フォーム」というテーマで、極力、テーマの本質と関係ないコードを排除しました。

ですが、現実の場面においては、入力内容の検証や、単純な `input type="text"` 的な入力要素だけではない `select` などの入力要素の使用などが必要になるはずです。

そのためには `<EditForm>` コンポーネントの使用などなど、さらなる実装が必要となるはずです。
本投稿ではあくまでも本質のみに焦点を当てるため、これ以上のことは割愛いたします。


なお、 [@piyo_esq](https://mobile.twitter.com/piyo_esq) さんも "BlazorFidlle" に実装例を公開しておられまして (下記 URL)、**どちらかといいますと、@sugimomoto さんのお困りごとに対する回答としては、こちらのほうが合ってる**かもしれません。

- [https://blazorfiddle.com/s/jwn1olci](https://blazorfiddle.com/s/jwn1olci)

# おしまい

以上、こんなんで、参考になりますかね...?

今回作成したデモンストレーションアプリのコードは、下記 gist に貼っておきます。

- [https://gist.github.com/jsakamoto/b2affa91d6a12d27a4a4eee2243fab84](https://gist.github.com/jsakamoto/b2affa91d6a12d27a4a4eee2243fab84)


`App.razor` しか貼ってませんが、`Startup.cs` とか他のコードは、プロジェクトテンプレートから新規作成したそのままで OK です。

「いや、問題はそこじゃないw」とか「この投稿もここが違うのでは?」などなど、気になることがあればコメントくださいー。

また、ご自身のお困りごとを、「きっと誰かが素晴らしい回答をくれるはず！」と公開し、多くの人と問題を共有、ネタを投下してくださった @sugimomoto さんに感謝します!

Happy Coding! :)