スポンサーリンク

ASP.NET Core (C#) タグヘルパーでラジオボタンを作成

スポンサーリンク

ASP.NET Core 5.0ではラジオボタンのタグヘルパーがありません。ラジオボタンを作成するにはHTMLヘルパーを使用する必要があります。今回はEnumの定義を元にラジオボタンを作成するタグヘルパーの作り方です。

環境(バージョン)

.NETのバージョン

% dotnet --version
5.0.101

完成イメージ

作成するタグヘルパーはこんな感じで指定します。

<input radio-enum aps-for="aaaModel.bbb" group-class="ccc" class="ddd" />

生成されるHTMLのイメージはこんな感じです。labelに出力する内容はEnumにSystem.ComponentModel.Description属性が指定されていればその値を使用します。Description属性が指定されていなければEnumの列挙子名より作成します。

<div id="aaaModel_bbb" class="ccc">
    <input type="radio" id="aaaModel_bbb_0 class="ddd" /><label for="aaaModel_bbb_0">XXX</label>
    ・・・
    <input type="radio" id="aaaModel_bbb_9 class="ddd" /><label for="aaaModel_bbb_9">YYY</label>
</div>

ソース

RadioButtionEnumTagHelper.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace SampleApp.Common.Helpers.TagHelpers
{
    // inputタグにradio-enum属性が指定されている場合に今回のタグヘルパーが動作するように設定
    [HtmlTargetElement("input", Attributes = RadioAttributeName)]
    public class RadioButtonEnumTagHelper : TagHelper
    {
        private const string RadioAttributeName = "radio-enum";
        private const string ForAttributeName = "asp-for";
        private const string ClassName = "class";
        private const string GroupClassName = "group-class";
        private const string ReadonlyName = "readonly";
        private const string IdName = "id";
        private const string DisabledName = "disabled";

        [HtmlAttributeNotBound, ViewContext] public ViewContext ViewContext { get; set; }
        [HtmlAttributeName(ForAttributeName)] public ModelExpression For { get; set; }
        [HtmlAttributeName(RadioAttributeName)] public bool RadioMarker { get; set; }
        [HtmlAttributeName(ReadonlyName)] public bool ReadonlyMarker { get; set; }
        [HtmlAttributeName(GroupClassName)] public string GroupClassStyle { get; set; }
        [HtmlAttributeName(ClassName)] public string ClassStyle { get; set; }

        private readonly IHtmlGenerator _generator;

        public RadioButtonEnumTagHelper(IHtmlGenerator generator)
        {
            _generator = generator;
        }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.SuppressOutput();

            // id取得
            var id = output.Attributes.FirstOrDefault(a => a.Name == IdName).Value;
            // radioボタンに引き継がない属性
            string[] ignoreAttributes = { "type", "name", "value", "group-class", "div-class", "id", "readonly" };
            var radioAttributes = output.Attributes.Where(a => !ignoreAttributes.Contains(a.Name)).ToDictionary(a => a.Name, a => a.Value);
            // ID属性を追加(一旦から)
            radioAttributes.Add(IdName, "");
            // readonlyの場合はdisableを追加
            if (ReadonlyMarker) radioAttributes.Add(DisabledName, DisabledName);
            // radioボタンをまとめるdivタグを生成
            if (string.IsNullOrEmpty(GroupClassStyle) == false)
            {
                output.PreElement.AppendHtml($"<div id=\"{id}\" class=\"{ GroupClassStyle}\">");
            }
            else
            {
                output.PreElement.AppendHtml($"<div id=\"{id}\">");
            }

            // Enumの定義毎にradioボタンを作成
            foreach (var enumItem in For.Metadata.EnumNamesAndValues)
            {
                // idは名称+値で作成
                radioAttributes[IdName] = id + "_" + enumItem.Value;
                var labelName = string.Empty;
                if (For.ModelExplorer.ModelType.IsGenericType && For.ModelExplorer.ModelType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    // Null許容型の場合
                    labelName = GetDescription((Enum)System.Enum.Parse(Nullable.GetUnderlyingType(For.ModelExplorer.ModelType), enumItem.Value))
                                ?? ((Enum)System.Enum.Parse(Nullable.GetUnderlyingType(For.ModelExplorer.ModelType), enumItem.Value)).ToString();
                }
                else
                {
                    labelName = GetDescription((Enum)System.Enum.Parse(For.ModelExplorer.ModelType, enumItem.Value))
                                ?? ((Enum)System.Enum.Parse(For.ModelExplorer.ModelType, enumItem.Value)).ToString();
                }

                // 元の属性からinput type="radio"に指定する属性の匿名クラスを動的に作成
                dynamic radioAttributeClass = new ExpandoObject();
                foreach (var item in radioAttributes) { ((IDictionary<string, object>)radioAttributeClass).Add(item.Key, item.Value); }

                /*
                // 作成対象のradioボタンがプロパティの値と同じ場合はdisable属性を削除
                if (enumItem.Key.Equals(For.Model.ToString())) ((IDictionary<string, object>)radioAttributeClass).Remove(DisabledName);
                */

                // ラジオボタンとラベル作成
                var radio = _generator.GenerateRadioButton(ViewContext, For.ModelExplorer, For.Name, enumItem.Key, true, radioAttributeClass);
                var label = _generator.GenerateLabel(ViewContext, For.ModelExplorer, For.Name, labelName, new { @for = radioAttributes[IdName] });

                output.PreElement.AppendHtml(radio);
                output.PreElement.AppendHtml(label);
            }
            if (ReadonlyMarker)
            {
                // readonlyの指定がされている場合はhidden項目を作成
                var hidden = _generator.GenerateHidden(ViewContext, For.ModelExplorer, For.Name, For.Model?.ToString(), false, null);
                output.PreElement.AppendHtml(hidden);
            }
            output.PreElement.AppendHtml("</div>");
        }

        // Enumに設定されているDescription属性を取得
        private string GetDescription(Enum value)
        {
            Type type = value.GetType();
            System.Reflection.FieldInfo fieldInfo = type.GetField(value.ToString());
            if (fieldInfo == null) return null;

            var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
            return attributes.Select(n => n.Description).FirstOrDefault();
        }
    }
}

今回はreadonly属性が指定された場合、各ラジオボタンはdisabledを設定し、hidden項目を追加し、モデルの値をvalueに設定しています。readonlyの場合に、選択値のみ有効にして他の選択値をdisabledにする場合は83〜86行目のコメントアウトを外して95〜100行目をコメントアウトします。

今回作成したタグヘルパーをcshtmlファイルから使用するには_ViewImports.cshtmlに2行追加する必要があります。

_ViewImports.cshtml

@using SampleApp.Common.Helpers.TagHelpers
@addTagHelper *, SampleApp

注意点は@addTagHelperに指定するのは名前空間ではなく、アセンブリ名です。

スポンサーリンク

デモ

Privacy.cshtml.cs

using System;
using System.ComponentModel;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace SampleApp.Pages
{
    public enum SampleEnum
    {
        [Description("第一列挙")] EnumOne,
        [Description("第二列挙")] EnumTwo,
        [Description("第三列挙")] EnumThree,
    };

    public class InputModel{
        public SampleEnum EnumValue1 { get; set; }
        public SampleEnum EnumValue2 { get; set; }
    }

    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
            Input = new InputModel(){
                EnumValue1 = SampleEnum.EnumTwo,
                EnumValue2 = SampleEnum.EnumThree,
                };
        }

        public InputModel Input { get; set; }

        public void OnGet()
        {
        }
    }
}

Privacy.cshtml

@page
@model PrivacyModel
@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
<input radio-enum asp-for="Input.EnumValue1" group-class="form-control"/>
<input radio-enum asp-for="Input.EnumValue2" group-class="form-control" readonly/>

結果はこんな感じです。

コメント

  1. 通りすがり より:

    コレすごい!ありがたく使わせていただきます。

    もし気が向いたら教えてくださると嬉しいです。
    ソース読んでもenumの値に合わせてchecked属性を出し分ける方法が理解できません。
    RadioButtonEnumTagHelperの89行目で実行してる_generator.GenerateRadioButtonの5番目の引数が常にtrueなので全部 checked=”checked” になっちゃうんじゃないかなって思ったけど、使ってみたらちゃんと動いてるし…。

    • marock より:

      _generator.GenerateRadioButtonの5番目の引数(isChecked)は2番目の引数(modelExplorer)がnullでない場合は何を指定しても同じ動作になります。
      すなわち、isCheckedの指定値に関わらず、4番目の引数(value)とmodelExplorer.modelが同じ場合に”checked”が付きます。