[ASP.NET]Remote モデル検証に複数フィールドを引数に使う

備忘録です。
[asp.net-mvc – Data annotations – Remote validation](https://www.codeday.top/2017/10/02/45974.html)を参考にしました。

ViewModelクラスに属性をつけて値の検証で、フィールド同士を比較するため、Remote属性を使用する方法です。
Remote属性は基本的にDB上の値と比較するなど、クライアント・サーバ間で検証するものですが、
複数フィールドを検証に使う方法がわからなかったのでこちらを使いました。

[参考](https://www.codeday.top/2017/10/02/45974.html)のようにRemote属性を使用すれば、複数フィールドを引数として扱えます。

```csharp
[Remote("IsEmailAvailable", HttpMethod="Post", AdditionalFields="ID", ErrorMessage = "Email already exists. Please enter a different email address.")]  
public string Email { get; set; }
```

これ以降は補足です。

## 環境

* C#
* .NET Framework 4.6
* ASP.NET 5.2.3.0

## 用意
MVCのテンプレートを使用している場合は気にしなくていいので読み飛ばしてください。

Remote属性は[jQuery Validation Plugin](https://jqueryvalidation.org/)を使用しているので、以下のスクリプトが必要となります。

* jquery-{version}.js
* jquery.validate.js
* jquery.validate.min.js
* jquery.validate.unobtrusive.js
* jquery.validate.unobtrusive.min.js

スクリプトを直接呼び出すか、バンドルすれば動くはずです。
※ 空のテンプレートからMVCを参照追加して作成して確認してます。

```csharp:BundleConfig.cs

bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
    "~/Scripts/jquery-{version}.js",
    "~/Scripts/jquery.unobtrusive-ajax.js",
    "~/Scripts/jquery.validate*"));
```

## 実装

ある倉庫にある`在庫数`と`確認した数`が不一致の場合登録するページを想定します。
`在庫数 != 確認した数`になっている場合のみ登録出来る仕様です。
`在庫数 == 確認した数`の場合、エラーメッセージを出します。

### ViewModel

実装は以下になります。

```csharp:DiscrepancyOfQuantityViewModels.cs

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace WebApplication1.Models
{
    public class DiscrepancyOfQuantityViewModels
    {
        [HiddenInput] //表示のみ
        [Display(Name="在庫数")]
        public string inventoryQuantity { get; set; }

        [Display(Name="確認した数")]
        [Required(ErrorMessage = "数量を入力してください")] // Null不許可
        [RegularExpression(@"[0-9]+", ErrorMessage = "数を入力してください")] //0~9のみ
        [Remote(action: "VerifyItemsCount", controller: "DiscrepancyOfQuantity", 
            AdditionalFields = nameof(inventoryQuantity), ErrorMessage = "在庫数と同じ数です")] // 今回説明している属性
        public string countedQuantity { get; set; }
    }
}
```

```csharp:
[Remote(action: "VerifyItemsCount", controller: "DiscrepancyOfQuantity", AdditionalFields = nameof(inventoryQuantity), ErrorMessage = "在庫数と同じ数です")]
```

Action: コントローラーのアクション名
Controller: コントローラー名
additionalFields: 対象のフィールドの他にメッセージに追加するフィールドを設定
ErrorMessage: 表示するエラーメッセージ

上記のRemote属性を設定することでコントローラーで実装する検証処理が実行されます。
クライアント・サーバ間はXHRを使用して通信が行われます。
通信が発生するタイミングは初回フォーカスアウト時、検証失敗後は入力のたびに通信が発生します。


### コントローラー

GET,PUTの処理、検証の処理を実装します。

```csharp:DiscrepancyOfQuantityController.cs
using System.Web.Mvc;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
    public class DiscrepancyOfQuantityController : Controller
    {
        // GET: DiscrepancyOfQuantity
        public ActionResult Index()
        {
            // DBとかからデータ取得するなりなんなり
            var data = InventoryModel.GetData();
            return View(data);
        }

        [HttpPost]
        public ActionResult Index(DiscrepancyOfQuantityViewModels data)
        {
            // データを登録
            if (!InventoryModel.AddData(data))
            {
                // Todo エラーページに遷移させるなど変更する
                return View(data);
            }

            // Todo 成功時にどこかにリダイレクトするなど変更する
            return View(data);
        }

        // 検証
        public ActionResult VerifyItemsCount(string countedQuantity, string inventoryQuantity)
        {
            var cq = int.Parse(countedQuantity);
            var iq = int.Parse(inventoryQuantity);

            // 確認した数 != 在庫数
            var ret = cq != iq;

            return Json(ret, JsonRequestBehavior.AllowGet);
        }
    }
}
```

`VerifyItemsCount`は`DiscrepancyOfQuantityViewModels`のRemote属性のアクション名と一致する必要があります。
また、引数もフィールド名と一致する必要があります。

実際に動作させて通信を確認するとルーティングの設定方針はわかります。
例として確認した数に1111を入力後フォーカスアウトした時、以下の通信が発生します。
`/DiscrepancyOfQuantity/VerifyItemsCount?countedQuantity=1111&inventoryQuantity=1000`
応答はJsonファイルでFalseが返ってきます。

一応HTMLも乗せておきます。
コントローラーからビューの自動生成で作成しています。

```html:Index.cshtml
@model WebApplication1.Models.DiscrepancyOfQuantityViewModels

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/jqueryval")
    
    @using (Html.BeginForm()) 
    {
        @Html.AntiForgeryToken()
        
        <div class="form-horizontal">
            <h4>DiscrepancyOfQuantityViewModels</h4>
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            <div class="form-group">
                @Html.LabelFor(model => model.inventoryQuantity, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.inventoryQuantity, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.inventoryQuantity, "", new { @class = "text-danger" })
                </div>
            </div>
    
            <div class="form-group">
                @Html.LabelFor(model => model.countedQuantity, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.countedQuantity, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.countedQuantity, "", new { @class = "text-danger" })
                </div>
            </div>
    
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Create" class="btn btn-default" />
                </div>
            </div>
        </div>
    }
    
    <div>
        @Html.ActionLink("Back to List", "Index")
    </div>
</body>
</html>

```

## 引数をさらに増やす

`,`で区切ると使うフィールドを増やせます。

```csharp:ViewModel
[Remote(action: "VerifyItemsCount2", controller: "DiscrepancyOfQuantity",
    AdditionalFields = nameof(inventoryQuantity) + "," + nameof(inventoryQuantity2) + "," + nameof(inventoryQuantity3),
    ErrorMessage = "エラー")] // 今回説明している属性
public string countedQuantity2 { get; set; }
```

```csharp:Controller
public ActionResult VerifyItemsCount2(string countedQuantity2, string inventoryQuantity, string inventoryQuantity2, string inventoryQuantity3)
{
    // ToDo 処理を実装
    return Json(true, JsonRequestBehavior.AllowGet);
}
```

実装していて気づいたのですが、オーバーロードしているアクションメソッドはきちんとルーティングしてくれないみたいですね。
`VerifyItemsCount2`になっているのは2をつけるまで動かなくて名前の付け直しが面倒になりました。
きちんと仕様を確認したいところです。