ASP.NET Core でSPAではなくVue.jsを利用してみる（３）

クライアント側にVue.jsをSPA（シングルページアプリケーション）ではない方法で利用することについて記述していきます。

全体
　・[Vue.jsとblumaの設定](https://qiita.com/nosa67/items/35021f5df322eadc23c2)
　・[cssをblumaに変更](https://qiita.com/nosa67/items/2f5dbfe7ccc0822b57b1)
　・バリデーションの変更（本稿）
　・[Vue.jsでのコンポーネントの利用](https://qiita.com/nosa67/items/0c13f3db64d528803148)

## -バリデーションの変更-

### 環境
「ASp.Net Core でPostgreSQLを利用してIdentityで認証を使えるようにする」で作成した環境にVue.jsを組み込み、cssフレームワークをblumaに変更します。
- VisualStudio2019 Ver.16.2.3
- ASP.NET Core 2.2
- PostgreSQL 9.6 インストール済み、接続用のアカウント作成済み
- EntityFramework
- Vue.js 2.6.10
- bluma 0.7.5

### バリデーションをどうするか
　前回までの作業でページから「Bootstrap」と「JQuery」を外して「bluma」を利用するまでを作りました。その結果、もともとJQueryで作られていたと思われるバリデーション機能が動作しなくなっています。
　クライアントでHTML5の機能で実行してもいいかと思っていたのですが、画面から操作している分には問題が無くとも「POST」を直接実行することがあるかもしれないので、サーバーと同じバリデーションが実行できるようにHTML5用のデータアノテーションを追加することにしました。

### データアノテーション（バリデーション用アトリビュート）の作成
　プロジェクト配下に「Authorization」を作成し、そこに以下のクラスを作ります。
　ちなみに既存の「Required」と区別するために「RequiredH5」としています。

```cs:RequireH5Attribute.cs

using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace WebApplication1.Attributes
{
    /// <summary>
    /// RequiredバリデーションのHTML5対応のアトリビュート
    /// </summary>
    public class RequiredH5Attribute : ValidationAttribute, IClientModelValidator
    {
        string _errorMessage = "この項目は入力必須です。";

        /// <summary>
        /// コンストラクタ（引数無）
        /// </summary>
        public RequiredH5Attribute() {
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="errorMessageAccessor">エラーメッセージへのアクセサ</param>
        public RequiredH5Attribute(Func<string> errorMessageAccessor) : base(errorMessageAccessor)
        {
            _errorMessage = errorMessageAccessor();
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="errorMessage">エラーメッセージ</param>
        public RequiredH5Attribute(string errorMessage) : base(errorMessage)
        {
            _errorMessage = errorMessage;
        }

        /// <summary>
        /// バリデーション（サーバーサイド）
        /// </summary>
        /// <param name="value">値</param>
        /// <param name="validationContext">バリデーションコンテキスト</param>
        /// <returns></returns>
        protected override ValidationResult IsValid(
            object value, ValidationContext validationContext)
        {
            if (value.ToString().Trim().Length > 0)
            {
                // 入力の桁数（トリムしたあと）が1以上なら正常
                return ValidationResult.Success;
            }
            {
                // 入力の桁数（トリムしたあと）が0以下なら異常
                return new ValidationResult(_errorMessage);
            }
        }

        /// <summary>
        /// クライアントでのバリデーション用の操作
        /// </summary>
        /// <param name="context">クライアントのバリデーションコンテキスト</param>
        public void AddValidation(ClientModelValidationContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            // タグに「required="required"」と「required-err-msg="<エラーメッセジ>"」を設定する
            MergeAttribute(context.Attributes, "required", "required");
            MergeAttribute(context.Attributes, "required-err-msg", _errorMessage);
        }

        /// <summary>
        /// アトリビュートの結合
        /// </summary>
        /// <param name="attributes">アトリビュート</param>
        /// <param name="key">追加するキー</param>
        /// <param name="value">追加する値</param>
        /// <returns></returns>
        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }

            attributes.Add(key, value);
            return true;
        }
    }
}

```

　クライアントの操作をするには以前はアダプターを作ってサービスに追加するような作業が必要だった様ですが、今は「IClientModelValidator」を継承して「AddValidation」を実装するといいようです。
　コンストラクタはデフォルトのメッセージを表示する引数無のコンストラクタと、エラーメッセージを引数とするコンストラクタを作っています。
　サーバー側のバリデーションは「IsValid」をオーバーライドして作成します。
　クライアント側のバリデーションというか、属性の追加は「AddValidation」で行います。「required」属性に「required」を設定し、「required-err-msg」にエラーメッセージを設定しています。「required」はHTML5の属性ですが、「required-err-msg」は独自の属性です。HTML5では残念ながらエラーメッセージは属性として設定できず、後述するjavascriptの「setCustomValidity」メソッドで設定する必要があります。

### クライアントのバリデーションメッセージを変更する
　HTML5のバリデーションは、制約自身は属性で追加できますが、メッセージの変更にはJavaScriptの利用が必要になります。
　「wwwroot/js/vaidationH5.js」を以下のように作成します。

```js:vaidationH5.js
// クライアント側で「input」タグのバリデーションメッセージを変更するために
// 「invalid」イベントでメッセージを設定するイベントハンドラーを設定
window.onload = function () {

    // inputタグのエレメントをすべて取得
    var inputList = document.getElementsByTagName('input');

    for (var i = 0; i < inputList.length; i ++) {

        // 全てのinputエレメントの「invalid」イベントにバリデーションチェックエラーの場合のエラーメッセージを設定
        inputList[i].addEventListener('invalid', function (e) {
            if (e.target.validity.valueMissing) {
                // Requiredの場合
                e.target.setCustomValidity(e.target.getAttribute("required-err-msg"));
            }
            else {
                e.target.setCustomValidity("");
            }
            // elseの前にエラー情報に対するエラーメッセージを追加する予定

        }, false);
    }

    // 全てのバリデーションサマリーのエレメントを取得
    var summaryErrorElements = document.getElementsByClassName('validation-summary-valid');

    for (i = 0; i < summaryErrorElements.length; i++) {

        // サマリー内はリストになっていて、「display=none」しかなければサマリーは非表示、以外のものが有ればサマリーを表示。
        var summaryElements = summaryErrorElements[i].getElementsByTagName('li');

        var existFlag = false;

        for (var j = 0; j < summaryElements.length; j ++) {
            if (summaryElements[j].style.display !== 'none') {
                summaryErrorElements[i].style.display = 'block';
                existFlag = true;
            }
        }
        if (existFlag === false) summaryErrorElements[i].style.display = 'none';
    }
};
```

　フォーム読み込み完了時に「input」タグのすべてに対して、「invalid」イベントのハンドラーを設定しています。現在Requiredのみ記述していますが、残りのバリデーションの対応は同じようにできるはずです。ちなみにバリデーションの条件（validityのメンバ）は以下のようになっているようです。

- valueMissing => 未入力（属性：required）
- typeMismatch => typeの不一致（type属性に依存）
- patternMismatch => 入力パターンの不一致（属性：pattern）
- tooLong => 文字列の最大長を超えている（属性：maxlength ）
- tooShort => 文字列の最小長より小さい（属性：minlength ）
- rangeOverflow => 数値や時刻等の最大値を超えている（属性：max ）
- rangeUnderflow => 数値や時刻等の最小値より小さい（属性：min ）
- step => 数値や時刻等の値の刻み単位が一致しない（属性：step）

　「asp-validation-summary」の対応もこの中で行っていて、サマリーが有れば表示しなければ非表示にしています。ただ、この処理はもう少し修正が必要かもしれません。

　最終的にできたものはいずれGitHubに公開するつもりです。

この処理を追加するのに「Pages/Shared/_Layout.cshtml」の最後にスクリプトを追加しておきます。

```html:_Layout.cshtml

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebApplication1</title>

    <environment include="Development">
        <link rel="stylesheet" href="~/css/bulma.css">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.1.2/css/bulma.css">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js" integrity="sha256-ufGElb3TnOtzl5E4c/qQnZFGP+FYEZj5kbSEdJNrw0A=" crossorigin="anonymous"></script>
    </environment>
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar has-background-info">

            <div class="navbar-brand">
                <div class="navbar-item title">
                    WebApplication1
                </div>

            </div>

            <div class="navbar-menu is-active is-tab">
                <div class="navbar-start">
                    <a class="navbar-item has-text-light" asp-area="" asp-page="/Index">Home</a>
                    <a class="navbar-item has-text-light" asp-area="" asp-page="/Privacy">Privacy</a>
                </div>
                <div class="navbar-end">
                    @if (SignInManager.IsSignedIn(User))
                    {
                        <div class="navbar-item">
                            Hello @User.Identity.Name!
                        </div>
                        <div class="navbar-item">
                            <form asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
                                <button type="submit" class="button is-primary is-outlined">Logout</button>
                            </form>
                        </div>
                    }
                </div>
            </div>
        </nav>
    </header>

    <content class="container is-centered">
        @RenderBody()
    </content>

    <footer class="footer">
        <div class="container">
            <div class="<content></content> has-text-centered">
                &copy; 2019 - WebApplication1 -
            </div>
        </div>
    </footer>

    <script src="~/js/vaidationH5.js" asp-append-version="true"></script>   @*　<== ここに追加　*@

    @RenderSection("Scripts", required: false)
</body>
</html>

```

　これで「RequiredH5」データアノテーションが完成です。

### ログイン画面のバリデーション
　ログイン画面のデータモデル（Areas/Identity/Account/Login.cshtml.cs）に作成した「RequiredH5」データバリデーションを設定します。

```cs:Login.cshtml.cs

// (class InputModelの部分を抜粋)

        public class InputModel
        {
            [RequiredH5(errorMessage: "メッセージ変更")]
            public string UserID { get; set; }

            [RequiredH5]
            [DataType(DataType.Password)]
            public string Password { get; set; }

            [Display(Name = "Remember me?")]
            public bool RememberMe { get; set; }
        }

```

これで実行すると、以下のようにエラーメッセージが表示されるようになります。

![クライアントバリデーション01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482873/95309ce3-3838-5285-1485-6f964be875af.png)
![クライアントバリデーション02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482873/803e76d1-90d0-4019-505d-98f40297c36f.png)

　以上でバリデーションの対応は終わりです。（2019/10/15：[「ASP.Net Core でjQuery無しでHTML5のバリデーションを利用する」](https://qiita.com/nosa67/items/d5abaa06993b1c58e93b)にもう少し書き込んでいます。サンプルソースはGitHubに公開しています。）

　これでようやくVue.jsについての作業に入ります。最初に記述したようにSPA（Single Page Application）にするつもりはなく、基本的にRazor Pageを利用したページを作成するのにVue.jsをクライアント側のコンポーネントの作成に利用したいのです。JQueryはよくできたライブラリだとは思いますが、動作の重さやセレクタの独自感が合わないんです。
　この計画に挫折したらこの先の記述は無くなっちゃいますけど。