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”が付きます。