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/>
結果はこんな感じです。
コメント
コレすごい!ありがたく使わせていただきます。
もし気が向いたら教えてくださると嬉しいです。
ソース読んでもenumの値に合わせてchecked属性を出し分ける方法が理解できません。
RadioButtonEnumTagHelperの89行目で実行してる_generator.GenerateRadioButtonの5番目の引数が常にtrueなので全部 checked=”checked” になっちゃうんじゃないかなって思ったけど、使ってみたらちゃんと動いてるし…。
_generator.GenerateRadioButtonの5番目の引数(isChecked)は2番目の引数(modelExplorer)がnullでない場合は何を指定しても同じ動作になります。
すなわち、isCheckedの指定値に関わらず、4番目の引数(value)とmodelExplorer.modelが同じ場合に”checked”が付きます。