ASP.Net Core+RazorPage+Vue（ラジオボタンとチェックボタンの変更）

**** 2020.5.21 タイトル変えました ****

## 今回の内容
　以前作った入力用のコンポーネントのラジオボタンとチェックボタンの修正と拡張をしました。
　具体的にはプロパティーなどで不要なものを削除してソースを少しでも小さくする。（どうしても最初は不要なコードが多くなりますねー）
　次にvue特有の属性値「v-bind」をタグヘルパーでは利用しない。別に使用しても問題ないのですが、なんとなくこの「v-xxx」についてはhtmlやvueのソース、javaスクリプト内のみで利用する方がいいかなという程度です。配列やオブジェクトをタグヘルパーで設定する場合、そこで「v-bind」を利用すると何も考えずにJSONのデータが利用できるのですが、これをやめるとvueのコード内でいったんJSONを変換する必要があり、ソースは増えてしまうのですが、タグヘルパーで「v-bind:list」とかに設定するのが気持ち悪いのでやめました。
　あとは未選択の許容とその際に入力必須をできる用にしました。未選択を許容した場合、ラジオボタンは選択されているものを再度クリックすると未選択状態となります。

## 前提
　ASP.NetCore RazorPage+Vue+blumaの環境を利用します。
　以前に書いた「[Vue.jsを利用してみる（１）](https://qiita.com/nosa67/items/49ac036a15a4f7deed8b)」と「[Vue.jsを利用してみる（１）](https://qiita.com/nosa67/items/49ac036a15a4f7deed8b)」を参照しての環境を構築します。

### 必須バリデーション
　必須バリデーションアトリビュートを作ります。

#### 基本クラス（AnnotationBase）
　バリデーションの基本クラスです。今後各種入力で使う為の共通の処理などを入れておくために作成しています。
　今のところエラーメッセージとhtmlの属性の追加処理を作成しています。

```cs:AnnotationBase.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace RazorPageVue.VueAnnotations
{
    /// <summary>
    /// バリデーション属性用の基本クラス
    /// </summary>
    public class AnnotationBase : ValidationAttribute
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        protected AnnotationBase() { }

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

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

        /// <summary>
        /// アトリビュートの設定
        /// </summary>
        /// <param name="attributes">アトリビュート</param>
        /// <param name="key">追加するキー</param>
        /// <param name="value">追加する値</param>
        protected void MergeAttribute(IDictionary<string, string> attributes, string key, string value, bool setForce = false)
        {

            if (attributes.ContainsKey(key))
            {
                // 属性が既に設定されている場合

                if (string.IsNullOrWhiteSpace(attributes[key]))
                {
                    // 値が未設定の場合は値の未設定する
                    attributes[key] = value;
                }
                else
                {
                    // 値が設定されており、設定しようとする値と異なる場合は例外を発生させる                    
                    if (attributes[key] != value)
                    {
                        //throw new ValidationH5Error("タグの属性[" + key + "]に設定されている値[" + attributes[key] + "]が想定[" + value + "]と異なります。");
                        attributes.Remove("type");
                        attributes[key] = value;
                    }
                }
            }
            else
            {
                // 属性が未設定の場合は、新たな属性を設定する
                attributes.Add(key, value);
            }
        }
    }
}
```

#### 必須バリデーションクラス（RequiredAttribute）
　必須バリデーションです。これによりレーザーページでモデルクラスのプロパティに[Required]を付けると必須入力となります。実際の処理はhtml出力時に「required='required'」と「required-err-msg='設定したエラーメッセージ'」が追加されます。エラーメッセージは未設定なら「required-err-msg」は追加されません。
　サーバーサイドの処理は記述していますが、基本的にはこの後に記述するvueの中でえらーチェックするので実行されることはないはずです。（ダイレクトにpostを実行された場合の対処です）

```cs:RequiredAttribute.cs
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPageVue.VueAnnotations
{
    /// <summary>
    /// 入力必須属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class RequiredAttribute : AnnotationBase, IClientModelValidator
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public RequiredAttribute(): base()
        {
        }

        /// <summary>
        /// バリデーション（サーバーサイド）
        /// </summary>
        /// <param name="value">値</param>
        /// <param name="validationContext">バリデーションコンテキスト</param>
        /// <returns></returns>
        protected override ValidationResult IsValid(
            object value, ValidationContext validationContext)
        {
            if (value == null)
            {
                return new ValidationResult(GetErrorMessage(validationContext.DisplayName));
            }
            else
            {
                if (value.GetType() == typeof(string))
                {
                    if (string.IsNullOrWhiteSpace((string)value))
                    {
                        return new ValidationResult(GetErrorMessage(validationContext.DisplayName));
                    }
                    else
                    {
                        return ValidationResult.Success;
                    }
                }
                else
                {
                    return ValidationResult.Success;
                }
            }
        }

        /// <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");
            if (!string.IsNullOrWhiteSpace(ErrorMessage)) MergeAttribute(context.Attributes, "required-err-msg", ErrorMessage);
        }

        /// <summary>
        /// サーバーバリデーション時のエラーメッセージ取得
        /// </summary>
        /// <param name="displayName">表示名称（DisplayNameアトリビュートで変更できる）</param>
        /// <returns>必須エラーメッセージ</returns>
        string GetErrorMessage(string displayName)
        {
            if (string.IsNullOrEmpty(ErrorMessage))
            {
                return displayName + "は入力必須です。";
            }
            else
            {
                return ErrorMessage;
            }
        }

    }
}
```

### ラジオボタン（改造）
　必須バリデーションの対応とかしました。

#### VueRadioButtonsタグヘルパー（改造）
　以前書いたものを改造しました。
　一番大きなところは「Process」メソッドで、バリデーションに対応できるようにしています。この部分はinputタグ用のGeneratorを流用しています。
　Enumの場合のリストの作成で、null許容型の判定が増えてます。

```cs:VueRadioButtonsTagHelper.cs
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;

namespace RazorPageVue.VueTagHelpers
{
    /// <summary>
    /// Vueで作ったVueRadioButtonコンポーネントをRazorで利用する為のタグヘルパー
    /// 以下のタグ属性を利用できるようにする
    /// asp-for：ラジオボタンの選択値
    /// asp-items: ラジオボタンのリスト（KeyValuePair＜string, string＞のリスト）enumの選択の場合は設定不要
    /// selectedclass:選択されているラジオボタンのCSSクラス（ボタンとテキストをくくったspanタグ）
    /// notselectedclass:未選択のラジオボタンのCSSクラス（ボタンとテキストをくくったspanタグ）
    /// [例]
    /// 　<vue-radio-buttons asp-for='@Model.SelectedList' asp-items='@Model.KeyValueList' selectedclass="has-background-primary"></radio-buttons>
    ///   ModelのSelectedListは選択されたチェックボタンのリストの初期値を設定し、選択した結果リストが設定される
    ///   Model.eyValueListはチェックボタンのリストをList＜KeyValuePair＜string,string＞＞型で設定する。（keyは表示する文字列、valueは設定される値）
    ///   ただし、SelectedListがEnum型の配列の場合、リストを空にしておくと自動的にEnumのリストが設定される。Enumのリストを加工したい場合のみ上記のリストを設定する
    /// </summary>
    [HtmlTargetElement("vue-radio-buttons", TagStructure = TagStructure.NormalOrSelfClosing)]
    public class VueRadioButtonsTagHelper : TagHelper
    {
        /// <summary>
        /// asp-for属性が入るプロパティ
        /// </summary>
        public ModelExpression AspFor { get; set; }

        /// <summary>
        /// タグ属性「name」を受け取るプロパティ。（未設定で「asp-for」が設定されている場合はその変数を示す値が設定される）
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// ラジオボタンのリスト(asp-items)
        /// kKeyValuePairのリストで、keyが表示される文字列で、valueが設定される値になる
        /// </summary>
        public List<KeyValuePair<string, string>> AspItems { get; set; }

        /// <summary>
        /// バリデーターの処理を実施させる為に設定するテキストボックス用のジェネレータ作成用
        /// （コンストラクタのデータインジェクションで設定）
        /// </summary>
        protected IHtmlGenerator Generator { get; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="htmlHelper"></param>
        public VueRadioButtonsTagHelper(IHtmlHelper htmlHelper, IHtmlGenerator generator)
        {
            Generator = generator;
        }

        /// <summary>
        /// htmlヘルパーを利用するために関連付けるビューのコンテキスト
        /// </summary>
        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }

        /// <summary>
        /// タグの調整処理
        /// </summary>
        /// <param name="context"></param>
        /// <param name="output"></param>
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            // 引数のコンテキストと出力がnullならエラー
            if (context == null) throw new ArgumentNullException(nameof(context));
            if (output == null) throw new ArgumentNullException(nameof(output));

            // asp-forがEnum型でリストが未設定の場合はEnumのリストを作成する(Nullableの場合も対処している)
            if (AspFor.Metadata.ModelType.IsEnum)
            {
                if (AspItems == null)
                {
                    AspItems = new List<KeyValuePair<string, string>>();

                    foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType))
                    {
                        AspItems.Add(new KeyValuePair<string, string>(enumItemName, enumItemName));
                    }
                }
            }
            else if (AspFor.Metadata.ModelType.IsGenericType && AspFor.Metadata.ModelType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                if (AspItems == null)
                {
                    AspItems = new List<KeyValuePair<string, string>>();

                    foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType.GenericTypeArguments[0]))
                    {
                        AspItems.Add(new KeyValuePair<string, string>(enumItemName, enumItemName));
                    }
                }
            }

            // 選択リストをJSONに変換して設定(Vueに配列を渡すにはv-bindを利用する必要がある)
            var serializer = new DataContractJsonSerializer(typeof(List<KeyValuePair<string, string>>));
            using (MemoryStream ms = new MemoryStream())
            {
                serializer.WriteObject(ms, AspItems);
                output.Attributes.SetAttribute("list", Encoding.UTF8.GetString(ms.ToArray()));
            }


            // テキストボックスのタグビルダーを作成し、バリデーションなどの属性で設定されているものをタグに取り込む
            // この時、すでに設定されているタグの属性は更新できないので注意
            output.MergeAttributes(GenerateRadioButton());
        }

        /// <summary>
        /// タグビルダーを作成
        /// </summary>
        /// <returns>標準のinputタグを利用したタグビルダー</returns>
        private TagBuilder GenerateRadioButton()
        {

            // このタグで設定する属性リストを作成（NameがあってAspForが無い場合にNameが直接書かれていればそれを設定している）
            IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            if (string.IsNullOrEmpty(AspFor.Name) &&
                string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix))
            {
                if (!string.IsNullOrEmpty(Name))
                {
                    htmlAttributes.Add("name", Name);
                }
            }
            else
            {
                htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            }

            // inputの基本的なタグビルダーを生成して返す
            return Generator.GenerateTextBox(
                ViewContext,
                AspFor.ModelExplorer,
                AspFor.Name,
                AspFor.ModelExplorer.Model,
                null,
                htmlAttributes);
        }
    }
}

```

#### VueRadioButtonsコンポーネント（改造）
　vueのコンポーネントです。この後の事を考えてvue-というプレフィックスを付けました。
　必須バリデーションの対応をしています。その関係で、選択されている項目をもう一度クリックすると完全に選択外になるようにしました。
　idやnameを以前はhiddenidやhiddennameとしていましたが、意味がないので普通にしました。最初に書いたように、タグヘルパーで「v-bind:」の記述をやめましたので、選択リストはdataのselectListにJSONを変換して取り込んでいます。v-bindを使うとプロパティの型と等を自動的に判定して行ってくれるようです。
　クリック処理でいろいろ行っているのですが、キーボード操作でも同じ様に動いてくれていますので、キーボードのイベントは記述してません。おかげで助かった。

```js:vueRadioButtons.js
//======================================================================
// ラジオボタンタグ
// ラジオボタンをグループで設定し、選択されている一つのデータを返す
// VueRadioButtonsTagHelperと連携して利用する
// 使用方法は「VueRadioButtonsTagHelper」を参照
//======================================================================
Vue.component('vue-radio-buttons', {
    props:
    {
        id: String,                 // このコントロールのid。Razor Pageとasp-forで関連付けされている
        name: String,               // このコントロールのname。Razor Pageとasp-forで関連付けされている
        list: String,               // 選択リスト(key,value)のペアのリスト（JSONデータ）値がenum型の場合は列挙の配列で未設定ならすべての列挙
        value: String,              // 選択されている初期値（key）または列挙
        selectedclass: String,      // 選択されているアイテムのcssクラス
        notselectedclass: String,   // 選択されていないアイテムのcssクラス
        required: Boolean,          // 必須フラグ
        requiredErrMsg: String      // 必須エラーメッセージ

    },
    data: function () {
        return {
            selectList: JSON.parse(this.list),      // 選択リスト
            selectedValue: this.value,              // 選択値
            groupName: "items_" + this.name         // 選択ラジオボタンのグループ名

        };
    },
    mounted: function () {
        this.checkRequired();       // マウント時に必須チェックを行う
    },
    methods: {
        //------------------------------------------------------------
        // 選択リストのid作成
        //------------------------------------------------------------
        itemid: function (index) {
            return this.id + '_' + index;
        },
        //------------------------------------------------------------
        // 選択操作。選択アイテムをクリックした時に実行
        //------------------------------------------------------------
        selectAction: function (e) {
            
            if (this.selectedValue === e.target.getAttribute("value")) {
                // 同じものを選択した場合は[cannoselect]が有効な場合は未選択状態にする（valueにnullが可能な場合のみ）
                if (!this.required) {
                    this.selectedValue = "";
                }
            }
            else {
                // 選択値を変更する
                this.selectedValue = e.target.getAttribute("value");
            }

            this.checkRequired();
        },
        //------------------------------------------------------------
        // その値が選択されているかどうか（自身が選択状態かどうかの判定用）
        //------------------------------------------------------------
        isSelected: function (val) {
            return val === this.selectedValue;
        },
        //------------------------------------------------------------
        // 必須チェック
        //------------------------------------------------------------
        checkRequired: function () {
            if (this.required && this.selectedValue === "") {

                // 変更エラーメッセージを取得
                if (this.requiredErrMsg) {
                    // 変更エラーメッセージがある場合は変更エラーメッセージを設定
                    for (let i = 0; i < this.$refs.Items.length; i++) {

                        this.$refs.Items[i].setCustomValidity(this.requiredErrMsg);
                    }
                }
                else {
                    // 変更エラーメッセージが無ければデフォルトのエラーメッセージを設定
                    for (let i = 0; i < this.$refs.Items.length; i++) {

                        this.$refs.Items[i].setCustomValidity("どれか一つ入力してください。");
                    }
                }
            }
            else {
                // エラーが無いのでカスタムエラーを削除
                for (let i = 0; i < this.$refs.Items.length; i++) {

                    this.$refs.Items[i].setCustomValidity("");
                }
            }
        }
    },
    template: '<div>\
                    <input :id=id :name=name type="hidden" :value=selectedValue>\
                    <span v-for="(item, index) in selectList" :key="item.value">\
                        <span style="display:inline-block" :class="[isSelected(item.value) ? selectedclass : notselectedclass ]" :value=item.value>\
                            <input type="radio" ref="Items" :id=itemid(index) :name=groupName :checked=isSelected(item.value) v-on:click=selectAction :value=item.value >\
                            <label v-on:click=selectAction :value=item.value>{{ item.key }}</label>\
                        </span>\
                    </span>\
                </div>'
    
});
```
### チェックボタン（改造）
　必須バリデーションの対応とかしました。

#### VueCheckButtonsタグヘルパー（改造）
　以前書いたものを改造しました。
　一番大きなところは「Process」メソッドで、バリデーションに対応できるようにしています。この部分はinputタグ用のGeneratorを流用しています。

```cs:VueCheckButtonsTagHelper.cs
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;

namespace RazorPageVue.VueTagHelpers
{
    /// <summary>
    /// Vueで作ったVueCheckButtonコンポーネントをRazorで利用する為のタグヘルパー
    /// 以下のタグ属性を利用できるようにする
    /// asp-for：チェックボタンの選択リスト
    /// asp-items: チェックボタンのリスト（KeyValuePair＜string, string＞のリスト）enumの選択の場合は設定不要
    /// selectedclass:選択されているラジオボタンのCSSクラス（ボタンとテキストをくくったspanタグ）
    /// notselectedclass:未選択のラジオボタンのCSSクラス（ボタンとテキストをくくったspanタグ）
    /// [例]
    /// 　<vue-check-buttons asp-for='@Model.RadioSelected' asp-items='@Model.RadioKeyValueList' selectedclass="has-background-primary"></radio-buttons>
    ///   ModelのRadioSelectedは選択されたラジオボタンの値が設定される
    ///   Model.RadioKeyValueListはラジオボタンのリストをList＜KeyValuePair＜string,string＞＞型で設定する。（keyは表示する文字列、valueは設定される値）
    ///   ただし、RadioSelectedがEnum型の場合、リストを空にしておくと自動的にEnumのリストが設定される。Enumのリストを加工したい場合のみ上記のリストを設定する
    ///   なお、Enumの場合、Nullable（null許容型）にしていないと、Required未設定でも結果が空にはならないので注意
    /// </summary>
    [HtmlTargetElement("vue-check-buttons", TagStructure = TagStructure.NormalOrSelfClosing)]
    public class VueCheckButtonsTagHelper : TagHelper
    {
        /// <summary>
        /// asp-for属性が入るプロパティ
        /// </summary>
        public ModelExpression AspFor { get; set; }

        /// <summary>
        /// タグ属性「name」を受け取るプロパティ。（未設定で「asp-for」が設定されている場合はその変数を示す値が設定される）
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// ラジオボタンのリスト(asp-items)
        /// kKeyValuePairのリストで、keyが表示される文字列で、valueが設定される値になる
        /// </summary>
        public List<KeyValuePair<string, string>> AspItems { get; set; }

        /// <summary>
        /// バリデーターの処理を実施させる為に設定するテキストボックス用のジェネレータ作成用
        /// （コンストラクタのデータインジェクションで設定）
        /// </summary>
        protected IHtmlGenerator Generator { get; }

        /// <summary>
        /// htmlヘルパーを利用するために関連付けるビューのコンテキスト
        /// </summary>
        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="htmlHelper"></param>
        public VueCheckButtonsTagHelper(IHtmlHelper htmlHelper, IHtmlGenerator generator)
        {
            Generator = generator;
        }

        /// <summary>
        /// タグの調整処理
        /// </summary>
        /// <param name="context"></param>
        /// <param name="output"></param>
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            // 引数のコンテキストと出力がnullならエラー
            if (context == null) throw new ArgumentNullException(nameof(context));
            if (output == null) throw new ArgumentNullException(nameof(output));

            //// RazorPageでPost時にモデルにバインドできるように、Vueのid,name,valueプロパティを設定
            //output.Attributes.SetAttribute("name", AspFor.Name);

            // asp-forがEnum型でリストが未設定の場合はEnumのリストを作成する
            if (AspFor.Metadata.ModelType.GenericTypeArguments[0].IsEnum)
            {
                if (AspItems == null)
                {
                    AspItems = new List<KeyValuePair<string, string>>();

                    foreach (var enumItemName in Enum.GetNames(AspFor.Metadata.ModelType.GenericTypeArguments[0]))
                    {
                        AspItems.Add(new KeyValuePair<string, string>(enumItemName, ((int)Enum.Parse(AspFor.Metadata.ModelType.GenericTypeArguments[0],enumItemName)).ToString()));
                    }
                }
            }

            // 選択リストをJSONに変換して設定
            var serializer = new DataContractJsonSerializer(typeof(List<KeyValuePair<string, string>>));
            using (MemoryStream ms = new MemoryStream())
            {
                serializer.WriteObject(ms, AspItems);
                output.Attributes.SetAttribute("list", Encoding.UTF8.GetString(ms.ToArray()));
            }

            // 選択結果リストを設定
            var jsonValueList = "";
            if (AspFor.Metadata.ModelType.GenericTypeArguments[0].IsEnum)
            {
                // Enumの場合は一旦文字列リストに変換してからJSON形式にして設定する
                var workList = new List<string>();
                foreach (var item in (System.Collections.IEnumerable)AspFor.Model) workList.Add(((int)item).ToString());

                serializer = new DataContractJsonSerializer(typeof(List<string>));
                using (MemoryStream ms = new MemoryStream())
                {
                    serializer.WriteObject(ms, workList);
                    jsonValueList = Encoding.UTF8.GetString(ms.ToArray());
                    //output.Attributes.SetAttribute("v-bind:value", Encoding.UTF8.GetString(ms.ToArray()));
                }
            }
            else
            {
                // Enum以外の場合は文字列リストをJSON形式にして設定する
                serializer = new DataContractJsonSerializer(typeof(List<string>));
                using (MemoryStream ms = new MemoryStream())
                {
                    serializer.WriteObject(ms, AspFor.Model);
                    jsonValueList = Encoding.UTF8.GetString(ms.ToArray());
                    //output.Attributes.SetAttribute("v-bind:value", Encoding.UTF8.GetString(ms.ToArray()));
                }
            }

            //base.Process(context, output);
            // テキストボックスのタグビルダーを作成し、バリデーションなどの属性で設定されているものをタグに取り込む
            // この時、すでに設定されているタグの属性は更新できないので注意
            output.MergeAttributes(GenerateRadioButton(jsonValueList));
        }

        /// <summary>
        /// タグビルダーを作成
        /// </summary>
        /// <returns>標準のinputタグを利用したタグビルダー</returns>
        private TagBuilder GenerateRadioButton(string value)
        {

            // このタグで設定する属性リストを作成（NameがあってAspForが無い場合にNameが直接書かれていればそれを設定している）
            IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            if (string.IsNullOrEmpty(AspFor.Name) &&
                string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix))
            {
                if (!string.IsNullOrEmpty(Name))
                {
                    htmlAttributes.Add("name", Name);
                }
            }
            else
            {
                htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            }

            // inputの基本的なタグビルダーを生成して返す
            return Generator.GenerateTextBox(
                ViewContext,
                AspFor.ModelExplorer,
                AspFor.Name,
                //AspFor.ModelExplorer.Model,
                value,
                null,
                htmlAttributes);
        }
    }
}
```

#### VueCheckButtonsコンポーネント（改造）
　ラジオボタンと同じように変更しています。

```js:vueCheckButtons.js
//======================================================================
// チェックボタンタグ
// チェックボタンをグループで設定し、選択されている一つのデータを返す
// VueCheckButtonsTagHelperと連携して利用する
// 使用方法は「VueCheckButtonsTagHelper」を参照
//======================================================================
Vue.component('vue-check-buttons', {
    props:
    {
        id: String,                 // このコントロールのid。Razor Pageとasp-forで関連付けされている
        name: String,               // このコントロールのname。Razor Pageとasp-forで関連付けされている
        list: String,               // 選択リスト(key,value)のペアのリスト（JSONデータ）値がenum型の場合は列挙の配列で未設定ならすべての列挙
        value: String,              // 選択されている項目のvalueの配列（JSONデータ）値が列挙の場合は列挙の配列
        selectedclass: String,      // 選択されているチェックボックスのcssクラス
        notselectedclass: String,   // 選択されていないチェックボックスのcssクラス
        required: Boolean,          // 必須フラグ
        requiredErrMsg: String      // 必須エラーメッセージ
    },
    data: function () {
        return {
            selectlist: JSON.parse(this.list),              // 選択リスト
            selectedValues: JSON.parse(this.value),         // 選択されている値のリスト
            groupName: "items_" + this.name                 // 選択チェックボックスのグループ名
        };
    },
    mounted: function () {
        this.checkRequired();       // マウント時に必須チェックを行う
    },
    methods: {
        //------------------------------------------------------------
        // 選択リストのid作成
        //------------------------------------------------------------
        itemid: function (index) {
            return this.id + '_' + index;
        },
        //------------------------------------------------------------
        // 選択操作。選択アイテムをクリックした時に実行
        //------------------------------------------------------------
        selectAction: function (e) {
            var val = e.target.getAttribute("value");
            var exitIndex = this.selectedValues.indexOf(val);
            if (exitIndex >= 0) {
                this.selectedValues.splice(exitIndex, 1);
            }
            else {
                this.selectedValues.push(val);
            }
            this.checkRequired();
        },
        //------------------------------------------------------------
        // その値が選択されているかどうか（自身が選択状態かどうかの判定用）
        //------------------------------------------------------------
        isSelected: function (val) {
            return this.selectedValues.indexOf(val) >= 0;
        },
        //------------------------------------------------------------
        // 必須チェック
        //------------------------------------------------------------
        checkRequired: function () {
            if (this.required && this.selectedValues.length === 0) {

                // 変更エラーメッセージを取得
                if (this.requiredErrMsg) {
                    // 変更エラーメッセージがある場合は変更エラーメッセージを設定
                    for (let i = 0; i < this.$refs.Items.length; i++) {

                        this.$refs.Items[i].setCustomValidity(this.requiredErrMsg);
                    }
                }
                else {
                    // 変更エラーメッセージが無ければデフォルトのエラーメッセージを設定
                    for (let i = 0; i < this.$refs.Items.length; i++) {

                        this.$refs.Items[i].setCustomValidity("どれか一つ入力してください。");
                    }
                }
            }
            else {
                // エラーが無いのでカスタムエラーを削除
                for (let i = 0; i < this.$refs.Items.length; i++) {

                    this.$refs.Items[i].setCustomValidity("");
                }
            }
        }    },
    template: '<div :id=id>\
                    <input :name=name v-for="selected in selectedValues" :key=selected type="hidden" :value=selected>\
                    <span v-for="(item, index) in selectlist" :key="item.key">\
                        <span style="display:inline-block" :class="[isSelected(item.value) ? selectedclass : notselectedclass ]" :value=item.value>\
                            <input type="checkbox" ref="Items" :id=itemid(index) :name="groupName" :checked=isSelected(item.value) v-on:click=selectAction :value=item.value >\
                            <label v-on:click=selectAction :value=item.value>{{ item.key }}</label>\
                        </span>\
                    </span>\
                </div>'

});
```

### 参考ページとテスト
　以下は利用サンプルのページとそのテスト用のtescafeです。ちょっと適当ですがご容赦ください。

#### VueRadioButton利用サンプルのページ

``` html:RadioButton.cshtml
@page
@model RadioButtonModel
@{
    ViewData["Title"] = "RadioButton Check";
}
@addTagHelper *,RazorPageVue

@*利用するVueコンポーネントをここで取り込む*@
<script src="~/js/vueUtil.js" asp-append-version="true"></script>
<script src="~/compornents/vueRadioButtons.js" asp-append-version="true"></script>

<form method="post">
    <div id="components-demo">
        <h1 class="display-4">ラジオボタンチェック</h1>

        <div class="content box">
            <label>通常のラジオボタン</label>
            <vue-radio-buttons asp-for='@Model.BindData.RadioSelected' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons>
        </div>

        <div class="content box">
            <label>入力必須のラジオボタン</label>
            <vue-radio-buttons asp-for='@Model.BindData.RadioRequiredSelected' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons>
        </div>

        <div class="content box">
            <label>入力必須(メッセージ変更)のラジオボタン</label>
            <vue-radio-buttons asp-for='@Model.BindData.RadioRequiredSelectedErrChange' asp-items='@RadioButtonModel.RadioValues' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons>
        </div>

        <div class="content box">
            <label>Enumのラジオボタン</label>
            <vue-radio-buttons asp-for='@Model.BindData.EnumRadioSelected' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons>
        </div>

        <div class="content box">
            <label>入力必須のEnumのラジオボタン</label>
            <vue-radio-buttons asp-for='@Model.BindData.EnumRequiredRadioSelected' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons>
        </div>

        <div class="content box">
            <label>入力必須(メッセージ変更)のEnumのラジオボタン</label>
            <vue-radio-buttons asp-for='@Model.BindData.EnumRequiredRadioSelectedErrChange' selectedclass="radio-item has-background-primary" notselectedclass="radio-item"></vue-radio-buttons>
        </div>

        <input id="mainsubmit" type="submit" />
    </div>
    <a id="returnLink" href="~/">戻る</a>

</form>


<script>
    // Vueコンポーネントを展開させる
    CreateVue('#components-demo');
</script>
```

``` cs:RadioButton.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPageVue.VueAnnotations;
using System.Collections.Generic;

namespace RazorPageVue.Pages
{
    public class RadioButtonModel : PageModel
    {
        /// <summary>
        /// ラジオボタンの選択リスト
        /// </summary>
        static public List<KeyValuePair<string, string>> RadioValues
        {
            get
            {
                if (_radioValues == null)
                {
                    _radioValues = new List<KeyValuePair<string, string>>();
                    _radioValues.Add(new KeyValuePair<string, string>("第1項目", "1"));
                    _radioValues.Add(new KeyValuePair<string, string>("第2項目", "2"));
                    _radioValues.Add(new KeyValuePair<string, string>("第3項目", "3"));
                    _radioValues.Add(new KeyValuePair<string, string>("第4項目", "4"));
                    _radioValues.Add(new KeyValuePair<string, string>("最終項目", "5"));
                }
                return _radioValues;
            }
        }
        static List<KeyValuePair<string, string>> _radioValues = null;

        /// <summary>
        /// 結合データ
        /// </summary>
        [BindProperty]
        public InputModel BindData { get; set; } = new InputModel();

        public class InputModel
        {
            // ラジオボタンの選択結果
            public string RadioSelected { get; set; } = "2";

            public string RadioSelectedText { 
                get 
                {
                    foreach (var item in RadioValues)
                    {
                        if (item.Value == RadioSelected) return item.Key;
                    }
                    return "";
                } 
            }

            [Required]
            public string RadioRequiredSelected { get; set; }

            [Required(ErrorMessage ="必須変更")]
            public string RadioRequiredSelectedErrChange { get; set; }

            public string RadioRequiredSelectedText
            {
                get
                {
                    foreach (var item in RadioValues)
                    {
                        if (item.Value == RadioRequiredSelected) return item.Key;
                    }
                    return "";
                }
            }

            // Enumを利用したラジオボタンの選択結果
            public SampleEnum? EnumRadioSelected { get; set; } = SampleEnum.sample2;

            // Enumを利用したラジオボタンの選択結果
            [Required]
            public SampleEnum? EnumRequiredRadioSelected { get; set; } = null;

            [Required(ErrorMessage = "必須変更")]
            public SampleEnum? EnumRequiredRadioSelectedErrChange { get; set; }
        }

        public void OnGet()
        {
        }

        /// <summary>
        /// Post処理
        /// </summary>
        /// <returns></returns>
        public IActionResult OnPost()
        {
            return RedirectToPage("RadioButtonResult", 
                new { SelectedId = BindData.RadioSelected, SelectedName = BindData.RadioSelectedText,
                    RequiredSelectedId = BindData.RadioRequiredSelected, RequiredSelectedName = BindData.RadioRequiredSelectedText,
                    SelectedEnum = BindData.EnumRadioSelected,
                    RequiredSelectedEnum = BindData.EnumRequiredRadioSelected
                });
        }
    }
}

```

#### VueRadioButton利用サンプルのページを使ったtestcafeのテスト

```js:VueRadioButton.test.ts
import "testcafe";
import {Selector} from "testcafe";
import { ClientFunction } from "testcafe";

fixture("Vue ラジオボタンテスト")
    .page("https://localhost:44357/RadioButton");

//----------------------------------------------------------------------
//  バリデーションを実施しその結果を取得する
//  【変数】
//      id: 対象のinputタグのid属性
//----------------------------------------------------------------------
const getInputValidity = ClientFunction((id) => {
    var element = document.getElementById(id) as HTMLInputElement;
    return element.checkValidity();
});

//----------------------------------------------------------------------
//  バリデーションメッセージを取得する
//  【変数】
//      id: 対象のinputタグのid属性
//----------------------------------------------------------------------
const getInputInvalidMessage = ClientFunction((id) => {
    var element = document.getElementById(id) as HTMLInputElement;
    return element.validationMessage;
});

//----------------------------------------------------------------------
//  バリデーション正常チェック
//  【変数】
//      t:  TestCafeの基本オブジェクト
//      id: 対象のinputタグのid属性
//----------------------------------------------------------------------
async function checkValid(t, id, count, message = '') {
    for (let i = 0; i < count; i++) {
        let itemid = id + '_' + i;
        await t.expect(getInputValidity(itemid)).eql(true, message + "(バリデーション正常)");
        await t.expect(getInputInvalidMessage(itemid)).eql("", message + "(エラーメッセージなし)");
    }
}

//----------------------------------------------------------------------
//  バリデーション異常チェック
//  【変数】
//      t:          TestCafeの基本オブジェクト
//      id:         対象のinputタグのid属性
//      messsage:   メッセージ
//----------------------------------------------------------------------
async function checkInvalidMessageChange(t, id, count, message = '') {
    for (let i = 0; i < count; i++) {
        let itemid = id + '_' + i;
        await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)");
        await t.expect(getInputInvalidMessage(itemid)).contains("変更", message + "（バリデーションメッセージ有）");
    }
}

//----------------------------------------------------------------------
//  バリデーション異常チェック（メッセージが変更されていないことを確認）
//  【変数】
//      t:          TestCafeの基本オブジェクト
//      id:         対象のinputタグのid属性
//      messsage:   メッセージ
//----------------------------------------------------------------------
async function checkInvalidMessageNoChange(t, id, count, message = '') {
    for (let i = 0; i < count; i++) {
        let itemid = id + '_' + i;
        await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)");
        let elementMessage = getInputInvalidMessage(itemid);
        await t.expect(elementMessage).notEql("");
        await t.expect(elementMessage).notEql(null);
        await t.expect(elementMessage).notContains("変更", message + "（バリデーションメッセージ有）");
    }
}

async function setDefatltNoErr(t) {
    await t.click(Selector("#BindData_RadioRequiredSelected_0"));
    await t.click(Selector("#BindData_RadioRequiredSelectedErrChange_0"));
    await t.click(Selector("#BindData_EnumRequiredRadioSelected_0"));
    await t.click(Selector("#BindData_EnumRequiredRadioSelectedErrChange_0"));
}

test("初期状態チェック", async (t) => {

    await t.click(Selector("#mainsubmit"));

    await checkValid(t, "BindData_RadioSelected",5);
    await checkInvalidMessageNoChange(t, "BindData_RadioRequiredSelected", 5);
    await checkInvalidMessageChange(t, "BindData_RadioRequiredSelectedErrChange", 5);
    await checkValid(t, "BindData_EnumRadioSelected", 4);
    await checkInvalidMessageNoChange(t, "BindData_EnumRequiredRadioSelected", 4);
    await checkInvalidMessageChange(t, "BindData_EnumRequiredRadioSelectedErrChange", 4);

    await setDefatltNoErr(t);

    await t.click(Selector("#mainsubmit"));

    await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("2", "【正常】[通常ラジオボタン]初期選択(値=2)");
    await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第2項目", "【正常】[通常ラジオボタン]初期選択(テキスト=第2項目)");
    await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]初期選択(値=1)");
    await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]初期選択(テキスト=第1項目)");
    await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample2", "【正常】[enumラジオボタン]初期選択(値=sample2)");
    await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]初期選択(値=sample1)");
});

test("通常ラジオボタンチェック", async (t) => {

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioSelected_0"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]第1項目選択(値 = 1)");
    await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]第1項目選択(テキスト=第1項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioSelected_1"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("", "【正常】[通常ラジオボタン]第2項目選択(値 =  )");
    await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("", "【正常】[通常ラジオボタン]第2項目選択(テキスト=第2項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioSelected_2"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("3", "【正常】[通常ラジオボタン]第3項目選択(値 = 3)");
    await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第3項目", "【正常】[通常ラジオボタン]第3項目選択(テキスト=第3項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioSelected_3"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("4", "【正常】[通常ラジオボタン]第4項目選択(値 = 4)");
    await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("第4項目", "【正常】[通常ラジオボタン]第4項目選択(テキスト=第4項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioSelected_4"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#normal-radio-result").find("span").nth(0).textContent).eql("5", "【正常】[通常ラジオボタン]第5項目選択(値 = 5)");
    await t.expect(Selector("#normal-radio-result").find("span").nth(1).textContent).eql("最終項目", "【正常】[通常ラジオボタン]第5項目選択(テキスト=第5項目)");
});

test("必須ラジオボタンチェック", async (t) => {
    await setDefatltNoErr(t);

    await t.click(Selector("#BindData_RadioRequiredSelected_0"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("1", "【正常】[通常ラジオボタン]第1項目選択(値 = 1)");
    await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第1項目", "【正常】[通常ラジオボタン]第1項目選択(テキスト=第1項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioRequiredSelected_1"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("2", "【正常】[通常ラジオボタン]第2項目選択(値 = 2)");
    await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第2項目", "【正常】[通常ラジオボタン]第2項目選択(テキスト=第2項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioRequiredSelected_2"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("3", "【正常】[通常ラジオボタン]第3項目選択(値 = 3)");
    await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第3項目", "【正常】[通常ラジオボタン]第3項目選択(テキスト=第3項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioRequiredSelected_3"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("4", "【正常】[通常ラジオボタン]第4項目選択(値 = 4)");
    await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("第4項目", "【正常】[通常ラジオボタン]第4項目選択(テキスト=第4項目)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_RadioRequiredSelected_4"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-radio-result").find("span").nth(0).textContent).eql("5", "【正常】[通常ラジオボタン]第5項目選択(値 = 5)");
    await t.expect(Selector("#required-radio-result").find("span").nth(1).textContent).eql("最終項目", "【正常】[通常ラジオボタン]第5項目選択(テキスト=第5項目)");
    await t.click(Selector("#returnLink"));
});

test("Enumラジオボタンチェック", async (t) => {
    await setDefatltNoErr(t);

    await t.click(Selector("#BindData_EnumRadioSelected_0"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]第1項目選択(値=sample1)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_EnumRadioSelected_1"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("", "【正常】[enumラジオボタン]第2項目選択(値=)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_EnumRadioSelected_2"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sample3", "【正常】[enumラジオボタン]第3項目選択(値=sample3)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_EnumRadioSelected_3"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#enum-radio-result").find("span").nth(0).textContent).eql("sampleOther", "【正常】[enumラジオボタン]第4項目選択(値=sampleOther)");
});

test("必須Enumラジオボタンチェック", async (t) => {
    await setDefatltNoErr(t);

    await t.click(Selector("#BindData_EnumRequiredRadioSelected_0"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample1", "【正常】[enumラジオボタン]第1項目選択(値=sample1)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_EnumRequiredRadioSelected_1"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample2", "【正常】[enumラジオボタン]第2項目選択(値=sample2)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_EnumRequiredRadioSelected_2"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sample3", "【正常】[enumラジオボタン]第3項目選択(値=sample3)");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_EnumRequiredRadioSelected_3"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#required-enum-radio-result").find("span").nth(0).textContent).eql("sampleOther", "【正常】[enumラジオボタン]第4項目選択(値=sampleOther)");
});
```

#### VueCheckButton利用サンプルのページ

```html:CheckButton.cshtml
@page
@model CheckButtonModel
@{
    ViewData["Title"] = "RadioButton Check";
}
@addTagHelper *,RazorPageVue

@*利用するVueコンポーネントをここで取り込む*@
<script src="~/js/vueUtil.js" asp-append-version="true"></script>
<script src="~/compornents/vueCheckButtons.js" asp-append-version="true"></script>

<form method="post">
    <div id="components-demo">
        <h1 class="display-4">ラジオボタンチェック</h1>

        <div class="content box">
            <label>通常のチェックボックス</label>
            <vue-check-buttons asp-for='@Model.BindData.NormalCheckList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons>
        </div>

        <div class="content box">
            <label>必須のチェックボックス</label>
            <vue-check-buttons asp-for='@Model.BindData.RequiredCheckList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons>
        </div>

        <div class="content box">
            <label>必須でエラーメッセージ変更のチェックボックス</label>
            <vue-check-buttons asp-for='@Model.BindData.RequiredErrChangeList' asp-items='@Model.CheckValues' selectedclass="selected-check-item" notselectedclass="check-item "></vue-check-buttons>
        </div>

        <div class="content box">
            <label>Enumのチェックボックス</label>
            <vue-check-buttons asp-for='@Model.BindData.NormalEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons>
        </div>

        <div class="content box">
            <label>必須のEnumのチェックボックス</label>
            <vue-check-buttons asp-for='@Model.BindData.RequiredEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons>
        </div>

        <div class="content box">
            <label>必須でエラーメッセージ変更のEnumのチェックボックス</label>
            <vue-check-buttons asp-for='@Model.BindData.RequiredErrChangeEnumList' selectedclass="selected-check-item" notselectedclass="check-item"></vue-check-buttons>
        </div>

        <input id="mainsubmit" type="submit" />
    </div>
    <a id="returnLink" href="~/">戻る</a>
</form>


<script>
    // Vueコンポーネントを展開させる
    CreateVue('#components-demo');
</script>
```

```cs:CheckButton.cshtml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPageVue.VueAnnotations;

namespace RazorPageVue.Pages
{
    public class CheckButtonModel : PageModel
    {
        /// <summary>
        /// ラジオボタンの選択リスト
        /// </summary>
        public List<KeyValuePair<string, string>> CheckValues { get; set; } = new List<KeyValuePair<string, string>>();

        /// <summary>
        /// 結合データ
        /// </summary>
        [BindProperty]
        public InputModel BindData { get; set; } = new InputModel();

        public class InputModel
        {
            /// <summary>
            /// 通常のチェックボックスの選択結果
            /// </summary>
            public List<string> NormalCheckList { get; set; } = new List<string>();

            /// <summary>
            /// 入力必須のチェックボックスの選択結果
            /// </summary>
            [Required]
            public List<string> RequiredCheckList { get; set; } = new List<string>();

            /// <summary>
            /// 入力必須のエラーメッセージ変更チェックボックスの選択結果
            /// </summary>
            [Required(ErrorMessage = "必須変更")]
            public List<string> RequiredErrChangeList { get; set; } = new List<string>();

            /// <summary>
            /// Enumタイプのチェックボックスの選択
            /// </summary>
            public List<SampleEnum> NormalEnumList { get; set; } = new List<SampleEnum>();

            /// <summary>
            /// 入力必須のEnumタイプのチェックボックスの選択
            /// </summary>
            [Required]
            public List<SampleEnum> RequiredEnumList { get; set; } = new List<SampleEnum>();

            /// <summary>
            /// 入力必須のエラーメッセージ変更Enumタイプのチェックボックスの選択
            /// </summary>
            [Required(ErrorMessage = "必須変更")]
            public List<SampleEnum> RequiredErrChangeEnumList { get; set; } = new List<SampleEnum>();
        }

        public CheckButtonModel()
        {
            setCheckList();
        } 

        public void OnGet()
        {
            BindData.NormalCheckList = new List<string>() { "2" };
            BindData.NormalEnumList = new List<SampleEnum>() { SampleEnum.sample3 };
        }

        /// <summary>
        /// Post処理
        /// </summary>
        /// <returns></returns>
        public IActionResult OnPost()
        {
            return RedirectToPage("CheckButtonResult",
                new {
                    normalCheckList = BindData.NormalCheckList,
                    requiredCheckList = BindData.RequiredCheckList,
                    requiredErrChangeList = BindData.RequiredErrChangeList,
                    normalEnumList = BindData.NormalEnumList,
                    requiredEnumList = BindData.RequiredEnumList,
                    requiredErrChangeEnumList = BindData.RequiredErrChangeEnumList
                });
        }

        /// <summary>
        /// 選択用リストの初期化
        /// </summary>
        void setCheckList()
        {
            CheckValues.Add(new KeyValuePair<string, string>("第1項目", "1"));
            CheckValues.Add(new KeyValuePair<string, string>("第2項目", "2"));
            CheckValues.Add(new KeyValuePair<string, string>("第3項目", "3"));
            CheckValues.Add(new KeyValuePair<string, string>("第4項目", "4"));
            CheckValues.Add(new KeyValuePair<string, string>("最終項目", "5"));
        }

    }
}

```

#### VueCheckButton利用サンプルのページを使ったtestcafeのテスト

```js:VueCheckButton.test.ts
import "testcafe";
import {Selector} from "testcafe";
import { ClientFunction } from "testcafe";

fixture("Vue ラジオボタンテスト")
    .page("https://localhost:44357/CheckButton");

//----------------------------------------------------------------------
//  バリデーションを実施しその結果を取得する
//  【変数】
//      id: 対象のinputタグのid属性
//----------------------------------------------------------------------
const getInputValidity = ClientFunction((id) => {
    var element = document.getElementById(id) as HTMLInputElement;
    return element.checkValidity();
});

//----------------------------------------------------------------------
//  バリデーションメッセージを取得する
//  【変数】
//      id: 対象のinputタグのid属性
//----------------------------------------------------------------------
const getInputInvalidMessage = ClientFunction((id) => {
    var element = document.getElementById(id) as HTMLInputElement;
    return element.validationMessage;
});

//----------------------------------------------------------------------
//  バリデーション正常チェック
//  【変数】
//      t:  TestCafeの基本オブジェクト
//      id: 対象のinputタグのid属性
//----------------------------------------------------------------------
async function checkValid(t, id, count, message = '') {
    for (let i = 0; i < count; i++) {
        let itemid = id + '_' + i;
        await t.expect(getInputValidity(itemid)).eql(true, message + "(バリデーション正常)");
        await t.expect(getInputInvalidMessage(itemid)).eql("", message + "(エラーメッセージなし)");
    }
}

//----------------------------------------------------------------------
//  バリデーション異常チェック
//  【変数】
//      t:          TestCafeの基本オブジェクト
//      id:         対象のinputタグのid属性
//      messsage:   メッセージ
//----------------------------------------------------------------------
async function checkInvalidMessageChange(t, id, count, message = '') {
    for (let i = 0; i < count; i++) {
        let itemid = id + '_' + i;
        await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)");
        await t.expect(getInputInvalidMessage(itemid)).contains("変更", message + "（バリデーションメッセージ有）");
    }
}

//----------------------------------------------------------------------
//  バリデーション異常チェック（メッセージが変更されていないことを確認）
//  【変数】
//      t:          TestCafeの基本オブジェクト
//      id:         対象のinputタグのid属性
//      messsage:   メッセージ
//----------------------------------------------------------------------
async function checkInvalidMessageNoChange(t, id, count, message = '') {
    for (let i = 0; i < count; i++) {
        let itemid = id + '_' + i;
        await t.expect(getInputValidity(itemid)).eql(false, message + "(バリデーションエラー)");
        let elementMessage = getInputInvalidMessage(itemid);
        await t.expect(elementMessage).notEql("");
        await t.expect(elementMessage).notEql(null);
        await t.expect(elementMessage).notContains("変更", message + "（バリデーションメッセージ有）");
    }
}

async function setDefatltNoErr(t) {
    await t.click(Selector("#BindData_RequiredCheckList_0"));
    await t.click(Selector("#BindData_RequiredErrChangeList_0"));
    await t.click(Selector("#BindData_RequiredEnumList_0"));
    await t.click(Selector("#BindData_RequiredErrChangeEnumList_0"));
}

test("初期状態および必須チェック", async (t) => {

    await t.click(Selector("#mainsubmit"));

    await checkValid(t, "BindData_NormalCheckList",5);
    await checkInvalidMessageNoChange(t, "BindData_RequiredCheckList", 5);
    await checkInvalidMessageChange(t, "BindData_RequiredErrChangeList", 5);
    await checkValid(t, "BindData_NormalEnumList", 4);
    await checkInvalidMessageNoChange(t, "BindData_RequiredEnumList", 4);
    await checkInvalidMessageChange(t, "BindData_RequiredErrChangeEnumList", 4);

    await setDefatltNoErr(t);

    await t.click(Selector("#mainsubmit"));

    await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目");
    await t.expect(Selector("#RequiredCheckList").textContent).eql("1:第1項目");
    await t.expect(Selector("#RequiredErrChangeList").textContent).eql("1:第1項目");
    await t.expect(Selector("#NormalEnumList").textContent).eql("sample3");
    await t.expect(Selector("#RequiredEnumList").textContent).eql("sample1");
    await t.expect(Selector("#RequiredErrChangeEnumList").textContent).eql("sample1");
});

test("通常ラジオボタンチェック", async (t) => {

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalCheckList_0"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,1:第1項目");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalCheckList_1"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalCheckList").textContent).eql("未選択");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalCheckList_2"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,3:第3項目");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalCheckList_3"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,4:第4項目");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalCheckList_4"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,5:第5項目");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalCheckList_0"));
    await t.click(Selector("#BindData_NormalCheckList_2"));
    await t.click(Selector("#BindData_NormalCheckList_3"));
    await t.click(Selector("#BindData_NormalCheckList_4"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalCheckList").textContent).eql("2:第2項目,1:第1項目,3:第3項目,4:第4項目,5:第5項目");
});


test("Enumラジオボタンチェック", async (t) => {
    await setDefatltNoErr(t);

    await t.click(Selector("#BindData_NormalEnumList_0"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sample1");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalEnumList_1"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sample2");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalEnumList_2"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalEnumList").textContent).eql("未選択");
    await t.click(Selector("#returnLink"));

    await setDefatltNoErr(t);
    await t.click(Selector("#BindData_NormalEnumList_3"));
    await t.click(Selector("#mainsubmit"));
    await t.expect(Selector("#NormalEnumList").textContent).eql("sample3,sampleOther");
});
```
###今後の予定
　実はすでに文字列、整数、実数、時刻あたりの入力は作ってみていますので、整理して出していきたいと思っています。以降の入力については基本的にieやEdgeでもChromeのような入力コントロールにすることと、バリデーションのレベルを統一するようにしています。
　また、htmlと.Netのデータ型のインピーダンスミスマッチを吸収することも目指してます。
　作っていく予定のコントロールは、inputタグのtypeごとに記述すると以下のようになります。

| type | 概要 | サーバーのデータ型 |  備考    |
|:-----------------|:------------------|:------------------:|:------------------|
| text | 文字列 | string | 最小長のバリデーションが実装されていない物があるので対応。 |
| number | 整数 | int系の全て | 整数の対応。ieやEdgeでもChromeのような入力を目指す。 |
| number | 実数 | double系の全て | 実数の対応。ieやEdgeでもChromeのような入力を目指す。 |
| range | - | - | これは基本的に通常のinputで対応する。 |
| time | 時刻 | TimeSpan | 時刻を示すデータ型はそもそも存在しないのでTimeSpanを利用して24時間表記で時分秒まで対応する。<br />ms以下の単位はそもそも整数ででも対応すればよい。 |
| date | 日付 | datetime | 日付はdatetime型の0時0分0秒として扱う。そうすることで上記「time」を単純に加算すれば日時ができるし、入力としてはその方がわかりやすい |
| datetime | 日時| - | 一つの入力として扱うことは疑問。使いにくそうなので作らない。dateとtimeの2つを利用する方が現実的だと思う。 |
| datetime-local | 日時| - | 上に同じ。 |
| month | 月 | datetimeかな | つくらない。必要なら年と月の数値入力を作った方が現実的と思える |
| week  | 週 | - | これは使うパターンが見えないので作らない。 |
| password  | パスワード | string | これは基本的に通常のinputで対応する。 |
| url | URL| - | textのバリデーションとして実装。ただし正規表現を利用した簡易なものにするので、厳密ではない。 |
| email | メールアドレス | - | textのバリデーションとして実装。ただし正規表現を利用した簡易なものにするので、厳密ではない。 |
| color | 色 | stringかなー | これは実際に必要になった状況に応じて作るべきなので対応外 |

###最後に
　今回やたらとソースを張り付けてでかくなってしまいましたが、いずれ全てのソースをgithubにでも公開するつもりです。
　ソースをさらしていますので、おかしなところとかアドバイスがあればお願いします。
　いろいろな意見を聞くことで勉強になりますので、なんか指摘があればコメントしてもらえるとうれしいです。