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)
　・[バリデーションの変更](https://qiita.com/nosa67/items/eebe6371bb231606f3c9)
　・Vue.jsでのコンポーネントの利用（本稿）

## -Vue.jsでのコンポーネントの利用-

### 環境
「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

### Vue.jsを使う理由
　すいません、タイトルに偽りありでした。ようやくVue.jsの記述にたどり着きました。本当はRazorPageでクライアントコンポーネントにVue.jsが使いたかっただけなのですが。
　事の発端はJQuery使いにくい、クライアント動作の為のHTMLはサーバーサイドで作っているのでソースが分かれてしまうのがいや、RazorPageの利点を生かしたままクライアント側のコンポーネントを再利用可能な状態で整理して作りたいという思いから、どうせやるなら.Net Core、それならLinuxで動かそう、それならDBもMySQLかPostgresだよねー、JQueryやめるならBootstrapも変えちゃえと思ってたらこんなことに...。

### 作るもの
　そんなに大きなものは作れないので、ボタンを押すとページをロックしてページが変わるか一定時間ロック状態にするボタンを作ります。

### 下ごしらえ
　とりあえず、画面ロックのJavaScriptとして、「wwwroot/js/utilities.js」を作り「_Layout.cshtml」のどこかで作ったスクリプトを読み込ませておきます。

```js:utilities.js
// スクリーンロック
function lockScreen(message, timeout) {

    // ロック用のdivを生成
    var lockElement = document.createElement('div');
    lockElement.id = "screenLock";
    var textElement = document.createElement('div');
    textElement.innerText = message;
    lockElement.appendChild(textElement);

    // ロック用のスタイル
    lockElement.style.display = 'table';
    lockElement.style.height = '100%';
    lockElement.style.left = '0px';
    lockElement.style.position = 'fixed';
    lockElement.style.top = '0px';
    lockElement.style.width = '100%';
    lockElement.style.zIndex = '9999';
    lockElement.style.opacity = '0.9';
    lockElement.style.background = "white";

    textElement.style.display = 'table-cell';
    textElement.style.textAlign = 'center';
    textElement.style.verticalAlign = 'middle';
    textElement.style.margin = 'auto';
    textElement.style.fontSize = 'x-large';

    // ロックの追加
    var objBody = document.getElementsByTagName("body").item(0);
    objBody.appendChild(lockElement);

    if (timeout) {
        // タイムアウトが設定されていれば、そのmsec後にロック解除
        setTimeout(function () {
            // ロック画面の削除
            unlockScreen();
        }, timeout);
    }
    else {
        // タイムアウトが設定されていなければ、30秒後に解除
        setTimeout(function () {
            unlockScreen();
        }, 30000);
    }
}

// スクリーンロック解除
function unlockScreen() {
    var lockElement = document.getElementById('screenLock');
    var dom_obj_parent = lockElement.parentNode;
    dom_obj_parent.removeChild(lockElement);
}

```

　見ての通り、画面全体を覆う「div」を作っているだけです。画面が切り替わる場合は解除不要ですがページを更新しない場合は「unlockScreen」を呼び出してロックを解除した方がいいです。（タイムアウトするはずですが、それはちっと...）


### コンポーネントの作成
　「wwwroot/vue/compornents」フォルダを作成し、その下に「button-with-lock.js」を以下のように作成します。

```js:button-with-lock.js
Vue.component('button-with-lock', {
    props: {
        label: String,
        lockmessage: String
    },
    template: '<button type="submit" class="button is-primary is-outlined" v-on:click="lockOnClick">{{label}}</button>',
    methods: {
        lockOnClick: function () {
            lockScreen(this.lockmessage);
        }
    }
});
```

　最初の「button-with-lock」はコンポーネント名で、使う場合にはタグ名となります。
　「props」はタグ内の属性として外部から設定できるものです。
　「template」は、挿入されるHTMLにvue独自の味付けをしたものです。ここでは「v-on:click」でonclickイベントで後述の「methods」の「lockOnClick」を実行させます。また、propで設定した「label」に入っている文字列をボタンのテキストとなるように「{{label}}」（二つの中かっこでくくる）と、その内容に置き換わります。
　最後の「methosds」はスクリプトを記述しています。ここでは下ごしらえで作成した「lockScreen」に引数として「prop」に設定されている「lockmessage」を渡しています。

　なお、ES6のみを対象とする場合、「template」ではテンプレート文字列(「`」でくくり、改行も利用できる文字列)のほうが書きやすいです。IE11もサポートしたいので文字列にしていますので、改行を使いたい場合は行末に「\」を入れる必要があります。


### コンポーネントンの利用
　作成したコンポーネントをログイン画面で利用してみます。
　「Areas/identity/Pages/Account/Login.schtml」の記述を以下のようにします。

```html:Login.schtml
@page
@model LoginModel

@{
    ViewData["Title"] = "Log in";
}
<script src="~/vue/compornents/button-with-lock.js" asp-append-version="true"></script>

@*vueのターゲットになるようにidを設定する*@
<div class="section" id="vueTarget">

    （フォームの前半は省略...）

                <div class="field has-text-centered">
                    @*ボタンをコンポーネントに置き換える*@
                    @*<button type="submit" class="button is-primary is-outlined">ログイン</button>*@
                    <button-with-lock label="ログイン" lockmessage="ログイン中..."></button-with-lock>
                </div>
            </div>
        </div>
    </form>
</div>

<script>
    //vueを利用する
    var vm = new Vue({
    el: '#vueTarget',
    });
</script>

```
　最初に作ったコンポーネントを読み込んでいます。（多分後のvueインスタンス作成）直前でも大丈夫）
　全体の「div」の「id」を「vueTarget」としました。これはvueを使う目印になります。（名前は何でもいいです）
　最後のほうのログインボタンを作ったコンポーネントで置き換えています。このように「props」に設定している値を属性として指定できます。
　最後にvueインスタンスを作成すると、コンポーネントが実装されます。この時「el」で対象となる「div」の「id」を「vueTarget」指定したことで、この「div」内が対象となります。（つまり作ったコンポーネント「button-with-lock」が設定した「div」の範囲外なら何もしないということです。）
　これでデバッグを実行すると、ボタンをクリックするとロックされます。（見やすいように、サーバーの「login」の実装に数秒の待ち時間を入れたほうがわかりやすいです）

### RazorPageのモデルと関連付ける
　上記のボタンはそれなりに使えると思います。
　ただ、実際には作ったコンポーネントにRazorPageのモデルから値を連携させたい場面があるので、ここではとりあえずロックメッセージをモデルから取得するように変更します。（実際にはすることはないと思う）

　まずはビューモデルである「Areas/identity/Pages/Account/Login.cshtml.cs」にロックメッセージのプロパティを追加します。

```cs:Login.schtml.cs

前の方は省略...

        [BindProperty]
        public InputModel Input { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public string ReturnUrl { get; set; }

        // ログイン中のメッセージを追加
        public string TestLockMessage { get; set; } = "ろぐいんちゅうでっせー";

この後ろも省略
```

　ログイン中のメッセージとして「TestLockMessage」を追加しています。

　次にモデルをvueインスタンスの作成時に「data」で与えるのですが、この時ひと工夫が必要でした。（結構調べるのに手間取った）。
　ビューモデルを@Json.Serialize(Model)でシリアライズして渡そうとしたのですが、単純なビューモデルではなく「PageModel」を継承しているためにこのクラスのメンバが大きいうえに循環参照していたりしてエラーになってしまいます。循環参照は無視させると、時間がかかりすぎて帰ってきませんでした。何をしようとしているのやら...。
　調べていくと「Json.Serialize」の第2引数で与える「JsonSerializerSettings」の「ContractResolver」のメソッド「GetSerializableMembers」をオーバーライドしてシリアライズする対象を選別すればいいことがわかりました。「Utility」フォルダを作って「JsonConverterExceptPageModel.cs」を以下のように作成しました。

```cs:JsonConverterExceptPageModel.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApplication1.Utility
{
    public static class JsonConverterExceptPageModel
    {
        static DefaultContractResolver resolver = new DefaultContractResolver();

        static public Microsoft.AspNetCore.Html.IHtmlContent Serialize(IJsonHelper jsonHelper, object value)
        {
            JsonSerializerSettings settings = new JsonSerializerSettings();
            NotInhelitContractResolver resolver = new NotInhelitContractResolver();
            settings.ContractResolver = resolver;

            return  jsonHelper.Serialize(value, settings);
        }

    }

    public class NotInhelitContractResolver : DefaultContractResolver
    {
        protected override List<MemberInfo> GetSerializableMembers(Type objectType)
        {
            Type pageModel = typeof(PageModel);

            List<MemberInfo> list = new List<MemberInfo>();
            foreach (var member in base.GetSerializableMembers(objectType))
            {
                if (member.DeclaringType != pageModel) list.Add(member);
            }

            return list;
        }
    }
}
```

　「GetSerializableMembers」のところで「PageModel」のメンバを除外するようにしています。使う環境によってはもう少し工夫がいるかもしれません。

　で、「Areas/identity/Pages/Account/Login.schtml」」を以下のように変更します。

```html:Login.schtml
@page
@model LoginModel
@using WebApplication1.Utility　　　@*これを追加した*@

中略...

                <div class="field has-text-centered">
                    @*ボタンをコンポーネントに置き換える*@
                    @*<button type="submit" class="button is-primary is-outlined">ログイン</button>*@
                    <button-with-lock label="ログイン" v-bind:lockmessage="items.TestLockMessage"></button-with-lock>
                </div>
            </div>
        </div>
    </form>
</div>

<script>
    //vueｗ利用する
    var vm = new Vue({
    el: '#vueTarget',
    data: {　　@*これを追加した*@
        items: @JsonConverterExceptPageModel.Serialize(Json, Model)
    }
    });
</script>
```

　作成したシリアライズ用のクラスを利用する為に、最初に「@using WebApplication1.Utility」を追加。
　ログインボタン「lockmessage」プロパティーは「vue」の「data」の「item.TestLockMessage」を参照させています。参照させる場合はこのように、「v-bind:prop名」の形になります。

　これで実行すればロックメッセージが関西弁に変わります。

　これで当初の目的であったRazorPageの利点も生かしながらクライアントサイドのコンポーネントをvue.jsで記述することができるようになったので、ひとまずこのシリーズは終わりかな。
　まあ、これを使って使えそうなコンポーネントができればどこかで公開するかもしません。

　C#.Net coreもvueも本格的には使っていないので、つたない部分があるかと思います。
　もっとすっきり記述する方法もあるかと思いますので、こうした方がいいのではとかありましたらご指摘ください。