【ViewModel】ASP.NET MVCでサーバー側と画面側でデータを受け渡す

## 前回の記事

ASP.NET MVCでページを編集・追加する

https://qiita.com/shironana/items/41c35b5ca3c2532f5ad9

## 本記事のゴール
- サーバー側のControllerから画面にデータを受け渡す。
- 画面内のフォームからサーバー側にデータを受け渡す。
- ViewModelの使い方を理解する。

## ASP.NETでのデータ受け渡し方法

ASP.NETでは、複数のデータを受け渡す際に **ViewModel** というモデルクラスを利用します。ViewModel を使用すると、Controller と View のデータのやり取りを一括で定義することができます。
また、データの型や入力規則等も併せて設定することができる優れものです。

今回はコントローラーから、ラーメン情報を画面に渡して表示し、さらに画面内のフォームからコントローラーに検索条件を受け渡す流れを作成します。

![Section 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/5894af08-1efe-fbd4-d967-53deddaded42.png)

## ControllerからViewにデータを渡す

ここでは、「コントローラーから、ラーメン情報を画面に渡して表示」(オレンジ色の線)までを実装します。

### ViewModelを作成する

1．右側のソリューションエクスプローラーに存在する「Controller」ディレクトリを右クリック
2．「追加」をクリック
3．「新しい項目」をクリック

![スクリーンショット 2023-11-29 020539.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/0ef2a0ea-7b28-42ed-1b96-fd8566773e95.png)

4．ViewModelの名前を設定します。命名は「xxxViewModel」とする慣例があります。今回は RamenViewModel とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/0222ec96-14ce-752a-f13b-e03645a318ab.png)

5．今回はラーメンの情報をリスト型で送りたいので `Data/RamenData.cs` というモデルを追加で作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/25152241-3052-029f-1729-fe03943936dd.png)


6．ViewModelとデータモデルの中身を書き換えます。

```Model/RamenViewModel.cs
using GourmetApplication.Models.Data;

namespace GourmetApplication.Models
{
    public class RamenViewModel
    {
        /// <summary>
        /// データ更新日
        /// </summary>
        public DateTime UpdateDate { get; set; }

        /// <summary>
        /// ラーメン情報
        /// </summary>
        public List<RamenData> RamenList { get; set; } = new List<RamenData>();
    }
}
```

```Model/Data/RamenData.cs
namespace GourmetApplication.Models.Data
{
    public class RamenData
    {
        /// <summary>
        /// 都道府県
        /// </summary>
        public required string Prefecture { get; set; }

        /// <summary>
        /// 名前
        /// </summary>
        public required string Name { get; set; }
    }
}
```

:::note warn
required修飾子はC#11から追加された記法です。環境によっては使用できない場合があります。意味としてはnullを許容しないというだけですので、nullのケアを行うという前提で `public string` や `public string?` でも問題ありません。
詳細は以下の公式ドキュメントをご覧ください。
https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/required

:::

### Controllerからデータを送る

前項で ViewModel を作成したのでデータを詰めていきます。
データを詰めた後、最終的に View を返却する際に、引数に ViewModel を設定します。

```Controller/RamenController.cs
using GourmetApplication.Models;
using GourmetApplication.Models.Data;
using Microsoft.AspNetCore.Mvc;

namespace GourmetApplication.Controllers
{
    public class RamenController : Controller
    {
        public IActionResult Index()
        {
            // ViewModelを作成する。
            var viewModel = new RamenViewModel()
            {
                UpdateDate = DateTime.Now,
            };

            // ラーメン情報を作成する。
            var ramenList = new List<RamenData>
            {
                new RamenData()
                {
                    Prefecture = "北海道",
                    Name = "旭川ラーメン",
                },
                new RamenData()
                {
                    Prefecture = "福島県",
                    Name = "喜多方ラーメン",
                },
                new RamenData()
                {
                    Prefecture = "栃木県",
                    Name = "佐野らーめん",
                }
            };

            // ViewModelにラーメン情報を追加する。
            viewModel.RamenList = ramenList;

            // Viewを返却する際に、ViewModelを送る。
            return View(viewModel);
        }
    }
}
```

### Viewでデータを受け取る

画面が描画される際に ViewModel が送られてくるため、View 側で受け取る設定をします。
まず、`@model RamenViewModel` と記述し、どの ViewModel と対応しているかを指定します。

ViewModel が指定できれば、あとは好きな箇所に `Model.[プロパティ名]` とすることで Controller からのデータを受け取ることができます。

```Views/Ramen/Index.cshtml
@* ViewModelを指定する *@
@model RamenViewModel
@{
    ViewData["Title"] = "ラーメン";
}

<div>各地のラーメンリストを表示する画面です。</div>

@* データ更新日を日付のみに加工して表示 *@
<div class="mt-4">データ更新日：@Model.UpdateDate.ToString("d")</div>
<table class="table">
    <thead>
        <tr>
            <th>都道府県</th>
            <th>名前</th>
        </tr>
    </thead>
    <tbody>
        @* テーブルのデータ行をラーメン情報の個数分繰り返し出力 *@
        @foreach(var ramen in Model.RamenList)
        {
            <tr>
                <td>@ramen.Prefecture</td>
                <td>@ramen.Name</td>
            </tr>
        }
    </tbody>
</table>
```

### 動作を確認する

実際に、ここまでの内容を動かしてみると、Controller で設定したラーメン情報が画面に表示されていることを確認できます。

![localhost_7296_Ramen (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/c8f2b50c-048c-4c68-d3f8-edc966cdda33.png)


## View内のフォームからControllerにデータを渡す

今度は、「画面内のフォームからコントローラーに検索条件を受け渡す」(水色の線)を実装します。

### ViewModelを修正する

先ほど作った RamenViewModel を、検索条件を持ち運びできるように修正します。
緑色になっている部分が加筆した箇所になります。
検索用の都道府県と名前を追加しました。

:::note
`#region [任意の名前]` ~ `#endregion` で囲うと Visual Studio 上でコードを折りたたむことができます。適切に使うことでコードの可読性を上げることができます。
:::

```diff_c_sharp
  using GourmetApplication.Models.Data;
  
  namespace GourmetApplication.Models
  {
      public class RamenViewModel
      {
+         #region 検索条件
+
+         /// <summary>
+         /// 検索用：都道府県
+         /// </summary>
+         public string? SearchPrefecture {  get; set; }
+
+         /// <summary>
+         /// 検索用：ラーメン名
+         /// </summary>
+         public string? SearchName { get; set; }
+
+         #endregion

+         #region 画面表示

          /// <summary>
          /// データ更新日
          /// </summary>
          public DateTime UpdateDate { get; set; }

          /// <summary>
          /// ラーメン情報
          /// </summary>
          public List<RamenData> RamenList { get; set; } = new List<RamenData>();

+         #endregion
      }
  }
```

### View内にフォームを作成する

View `(Views/Ramen/Index.cshtml)` を修正し、「検索」ボタンを押すとGET通信で同一ページに検索条件を送信するformを追加します。緑色になっている部分が加筆した箇所になります。
bootstrapの標準に従って以下のようなフォームを作成しました。(デザインなどはお好みで設定してください。)

```diff_html
  @* ViewModelを指定する *@
  @model RamenViewModel
  @{
      ViewData["Title"] = "ラーメン";
  }

  <div>各地のラーメンリストを表示する画面です。</div>

+ <form class="card my-4" action="#" method="get">
+     <div class="card-header">
+         検索条件
+     </div>
+     <div class="card-body">
+         <div class="row gy-2">
+             <div class="col-2">
+                 <label for="Prefecture" class="col-form-label">都道府県名</label>
+             </div>
+             <div class="col-10">
+                 @* 都道府県入力用 *@
+                 <input type="text" id="Prefecture" class="form-control">
+             </div>
+             <div class="col-2">
+                 <label for="Name" class="col-form-label">名前</label>
+             </div>
+             <div class="col-10">
+                 @* 名前入力用 *@
+                 <input type="text" id="Name" class="form-control">
+             </div>
+             <div class="col-12 text-end">
+                 <button class="btn btn-primary">検索</button>
+             </div>
+         </div>
+     </div>
+ </form>

  @* データ更新日を日付のみに加工して表示 *@
  <div class="mt-4">データ更新日：@Model.UpdateDate.ToString("d")</div>
  <table class="table">
    ( 中略 )
  </table>
```

### View内のフォームからデータを送る

ViewModel からデータを送るために、前項で作成したフォームを修正します。
`asp-controller` と `asp-action` を用いて送信先のコントローラーを指定します。

```diff_html
- <form class="card my-4" action="#" method="get">
+ <form class="card my-4" asp-controller="Ramen" asp-action="Index" method="get">
```

さらに、入力項目に対しては `asp-for=[ViewModelのプロパティ名]` を用いて紐づけを行います。
label と input に紐づけることで、それぞれ `for` と `name` `id` が自動的に補完されます。

```diff_html
- <label for="Prefecture" class="col-form-label">都道府県名</label>
+ <label asp-for="SearchPrefecture" class="col-form-label">都道府県名</label>

- <input type="text" id="Prefecture" class="form-control">
+ <input type="text" asp-for="SearchPrefecture" class="form-control">
```

詳細は以下の公式ドキュメントを確認してください。

https://learn.microsoft.com/ja-jp/aspnet/core/mvc/views/working-with-forms

ここまでの修正で、画面内に検索条件を入力するフォームが完成しました。

![localhost_7296_Ramen (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/ee646790-7e20-fe2c-86d5-0539dd96c215.png)

### Controllerでデータを受け取る

View 内の form に `asp-controller="Ramen" asp-action="Index"` と記載したため、RamenController の Index() の処理が実行されます。( /Ramen にアクセスした際と同じ処理が実行される。)
そこで、Index の引数に `RamenViewModel formData` と指定し、フォームから送られてくる検索条件を受け取ります。

```diff_c_sharp
  using GourmetApplication.Models;
  using GourmetApplication.Models.Data;
  using Microsoft.AspNetCore.Mvc;

  namespace GourmetApplication.Controllers
  {
      public class RamenController : Controller
      {
-         public IActionResult Index()
+         public IActionResult Index(RamenViewModel formData)
          {
              // ViewModelを作成する。
              var viewModel = new RamenViewModel()
              {
                  UpdateDate = DateTime.Now,
              };

              // ラーメン情報を作成する。
              var ramenList = new List<RamenData>
              {
                  ( 中略 )
              };

              // ViewModelにラーメン情報を追加する。
              viewModel.RamenList = ramenList;

              // Viewを返却する際に、ViewModelを送る。
              return View(viewModel);
          }
      }
  }
```

#### デバッグして値を確認する

左端（画像で赤丸がある箇所）をクリックすると、各処理に対してブレークポイントを設定できます。ブレークポイントを設定することで処理を任意の位置で一時停止させることができます。
今回は、Indexの引数に入ってくる値を確認するために、12行目のViewModelを作成している箇所に対してブレークポイントを設定します。

![スクリーンショット 2023-11-29 125520.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/471a2a0e-568f-71a6-45a0-701109248f9c.png)

ブレークポイントを設定したら実際に /Ramen にアクセスします。
初回のアクセスでも `Index()` が実行されるため処理が停止します。このとき、変数や引数にカーソルを合わせるとその時点で格納されている値を見ることができます。
引数にカーソルを合わせ、`formData` の中身のプロパティが空であることを確認します。

※基本的には `null` が入りますが、数字や真偽値、日付などの初期値が定義されている型には初期値が入ります。

![スクリーンショット 2023-11-29 130436.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/20c5855f-742b-8453-6240-fc2c6cf1fa0b.png)

ブレークポイントによる一時停止を解除するにはツールバーにある「続行」を押します。

![スクリーンショット 2023-11-29 131107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/8a371fa8-e8db-fe24-4616-390f58db2245.png)

/Ramen が表示されるので、先ほど作ったフォームに、検索条件を入力し、「検索」を押します。

![localhost_7296_Ramen (3) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/ad77c89b-ce89-d30b-9c34-52790f1c5bef.png)

すると、先ほどとは異なり、フォームに入力した「北海道」と「ラーメン」という値が取得できます。
これで、画面内フォームに入力した値を取得することができました。

![スクリーンショット 2023-11-29 132211.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/0b4d80df-982a-7eb3-22af-5d0a7e372c5d.png)

## おまけ：検索条件で絞り込みをする

本記事のゴールは既に達成しましたが、検索機能を完成させるためにコントローラーの一部修正を行います。
これで、検索条件が指定された場合に部分一致する行のみを一覧表示することができました。

```diff_c_sharp
  using GourmetApplication.Models;
  using GourmetApplication.Models.Data;
  using Microsoft.AspNetCore.Mvc;

  namespace GourmetApplication.Controllers
  {
      public class RamenController : Controller
      {
          public IActionResult Index(RamenViewModel formData)
          {
              // ViewModelを作成する。
              var viewModel = new RamenViewModel()
              {
                  UpdateDate = DateTime.Now,
              };

              // ラーメン情報を作成する。
              var ramenList = new List<RamenData>
              {
                  ( 中略 )
              };

+             // 都道府県が指定されていれば絞り込みを行う。
+             if (!string.IsNullOrWhiteSpace(formData.SearchPrefecture))
+             {
+                 ramenList = ramenList.Where(x => x.Prefecture.Contains(formData.SearchPrefecture)).ToList();
+             }
+
+             // 名前が指定されていれば絞り込みを行う。
+             if (!string.IsNullOrWhiteSpace(formData.SearchName))
+             {
+                 ramenList = ramenList.Where(x => x.Name.Contains(formData.SearchName)).ToList();
+             }

              // ViewModelにラーメン情報を追加する。
              viewModel.RamenList = ramenList;

              // Viewを返却する際に、ViewModelを送る。
              return View(viewModel);
          }
      }
  }
```

名前欄に「ラーメン」と入力すると、名前に『ラーメン』が含まれたデータが一覧表示されました。
実際に運用する際には、データベースから情報を取得したり、検索条件を増やしたり、都道府県をドロップダウンで表示するなど、やるべきことは多々ありますが、ひとまずデータの受け渡しという点については実装が完了となります。

![localhost_7296_Ramen_SearchPrefecture=&SearchName=%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/7d9beb7a-773c-f99b-1d53-29a792bd090f.png)

## 次回の記事

ASP.NET MVCにデータベースを接続する

https://qiita.com/shironana/items/18d38933324a3e4276ee