Radio Buttons with Individual Labels for Enum using MVC6 Tag Helpers
Asked Answered
R

2

6

My model contains an enum which I'm trying to bind to a list of radio buttons, so that only one value can be selected when the form is submitted.

public enum Options
{
    [Display(Name="Option A")
    OptionA,
    [Display(Name="Option B")
    OptionB
}

public class MyModel
{
     public Options SelectedOption {get; set;}
     public string TextA{get; set;}
     public string TextB{get; set;}
}

In MVC 5, I would render a radio button input for each enum value, and the user would be able to select just one. So the code in the view for each radio button would probably look like this:

@Html.RadioButtonFor(m => m.SelectedOption, Options.OptionA)

The problem with MVC6 seems to be that the input tag helper does not support an enum property out of the box. So if I try to add <input asp-for="SelectedOption"/>, all I get is a textbox.

So my question is, is there a way to do this using an MVC6 tag helper (out of the box or custom) or is there another way to do this? Maybe something like using the old razor syntax or just adding an input tag and populating its attributes?

Please note that ideally there have to be labels for both radio buttons showing the text specified in the enum's [Display] attributes. Also, only one of the TextA or TextB textboxes can appear at the same time and this should be based on the selected radio button, but this is out of the scope of the question. So here's how the rendered markup should look like (more or less):

<div>
    <div>
        <input id="OptionA" type="radio" name="SelectedOption"/>
        <label for="OptionA">Option A</label>
    </div>
    <div>
        <label for="TextA">Text A</label>
        <input type="text" id="TextA" name="TextA">
    </div>
<div>

<div>
<div>
    <input id="OptionB" type="radio" name="SelectedOption"/>
    <label for="OptionB">Option B</label>
</div>

    <div>
        <label for="TextB">Text A</label>
        <input type="text" id="TextB" name="TextB">
    </div>
<div>
Redletter answered 12/11, 2015 at 20:13 Comment(2)
Absolutely my bad, my mind just kept reading drop down instead of radio buttons! I haven't seen a radio button tag helper in the source code. It would be possible to create your own helper though. It will takes a list of items like the select tag helper, and you could also use Html.GetEnumSelectList<TEnum>() to generate the list of options that would be provided as the asp-items value of your new helper.Parton
No worries, I have subconsiously associated enums with dropdowns too. A custom tag helper sounds like a good idea, but it might not work in my case because of individual labels and the elements present between the radio buttons (each option toggles an extra textbox or two). I'll give it a try on Monday and see if I can make it work. Feel free to post an answer if you feel like it.Redletter
B
1
<enum-radio-button asp-for="ThisEnum"></enum-radio-button>
<enum-radio-button asp-for="ThisEnum" value="ThisEnum.Value001"></enum-radio-button>

ThisEnum.Value001 = Auto Checked radio input..

Asp.Net Core TagHelper for enum radio button list

/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;enum-radio-button&gt; elements with an <c>asp-for</c> attribute, <c>value</c> attribute.
/// </summary>
[HtmlTargetElement("enum-radio-button", Attributes = RadioButtonEnumForAttributeName)]
public class RadioButtonEnumTagHelper : TagHelper
{
    private const string RadioButtonEnumForAttributeName = "asp-for";
    private const string RadioButtonEnumValueAttributeName = "value";

    /// <summary>
    /// Creates a new <see cref="RadioButtonEnumTagHelper"/>.
    /// </summary>
    /// <param name="generator">The <see cref="IHtmlGenerator"/>.</param>
    public RadioButtonEnumTagHelper(IHtmlGenerator generator)
    {
        Generator = generator;
    }

    /// <inheritdoc />
    public override int Order => -1000;

    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    protected IHtmlGenerator Generator { get; }

    /// <summary>
    /// An expression to be evaluated against the current model.
    /// </summary>
    [HtmlAttributeName(RadioButtonEnumForAttributeName)]
    public ModelExpression For { get; set; }

    [HtmlAttributeName(RadioButtonEnumValueAttributeName)]
    public Enum value { get; set; }

    /// <inheritdoc />
    /// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {

        var childContent = await output.GetChildContentAsync();
        string innerContent = childContent.GetContent();
        output.Content.AppendHtml(innerContent);

        output.TagName = "div";
        output.TagMode = TagMode.StartTagAndEndTag;
        output.Attributes.Add("class", "btn-group btn-group-radio");

        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (output == null)
        {
            throw new ArgumentNullException(nameof(output));
        }

        var modelExplorer = For.ModelExplorer;
        var metaData = For.Metadata;

        if (metaData.EnumNamesAndValues != null)
        {
            foreach (var item in metaData.EnumNamesAndValues)
            {
                string enum_id = $"{metaData.ContainerType.Name}_{metaData.PropertyName}_{item.Key}";

                bool enum_ischecked = false;
                if (value != null)
                {
                    if (value != null && item.Key.ToString() == value.ToString())
                    {
                        enum_ischecked = true;
                    }
                }
                else
                {
                    if (For.Model != null && item.Key.ToString() == For.Model.ToString())
                    {
                        enum_ischecked = true;
                    }
                }

                string enum_input_label_name = item.Key;
                var enum_resourced_name = metaData.EnumGroupedDisplayNamesAndValues.Where(x => x.Value == item.Value).FirstOrDefault();
                if (enum_resourced_name.Value != null)
                {
                    enum_input_label_name = enum_resourced_name.Key.Name;
                }

                var enum_radio = Generator.GenerateRadioButton(
                    ViewContext,
                    For.ModelExplorer,
                    metaData.PropertyName,
                    item.Key,
                    false,
                    htmlAttributes: new { @id = enum_id });
                enum_radio.Attributes.Remove("checked");
                if (enum_ischecked)
                {
                    enum_radio.MergeAttribute("checked", "checked");
                }
                output.Content.AppendHtml(enum_radio);

                var enum_label = Generator.GenerateLabel(
                    ViewContext,
                    For.ModelExplorer,
                    For.Name,
                    enum_input_label_name,
                    htmlAttributes: new { @for = enum_id, @Class = "btn btn-default" });
                output.Content.AppendHtml(enum_label);
            }
        }
    }
}

I'm using this style

Html result

Benildas answered 8/7, 2017 at 13:7 Comment(0)
S
0

One approach to implement with static in netcore mvc 6

public static IHtmlContent RadioButtonSwitchEnumFor<TModel, TProperty>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object selectedValue = null, bool? isHorizontal = true, object htmlAttributes = null)
    {
        if (htmlHelper == null) throw new ArgumentNullException(nameof(htmlHelper));
        if (expression == null) throw new ArgumentNullException(nameof(expression));

        var viewData = htmlHelper.ViewData;
        var modelExpressionProvider = (ModelExpressionProvider)htmlHelper.ViewContext.HttpContext.RequestServices.GetService(typeof(IModelExpressionProvider));
        var modelExplorer = modelExpressionProvider.CreateModelExpression(viewData, expression);
        if (modelExplorer == null) throw new InvalidOperationException($"Failed to get model explorer for {modelExplorer}");
        var metaData = modelExplorer.Metadata;

        if (!metaData.ModelType.IsEnum)
        {
            throw new ArgumentException("This helper is intended to be used with enum types");
        }

        // get the field name
        var propertyName = metaData.PropertyName ?? viewData?.ModelMetadata?.Name;
        string ctrlName = propertyName?.Replace(".", "");

        var names = Enum.GetNames(metaData.ModelType);
        var values = Enum.GetValues(metaData.ModelType);

        var sb = new StringBuilder();

        // Create a radio button for each item in the list
        foreach (var name in names)
        {
            string description = string.Empty;
            var propertyValue = names.Where(e => e.StartsWith(name)).Select(e => (int)Enum.Parse(metaData.ModelType, e)).FirstOrDefault();

            var memInfo = metaData.ModelType.GetMember(name);
            var attributes = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
            if (attributes.Length > 0)
            {
                description = ((DisplayAttribute)attributes[0]).ResourceType != null ? ((DisplayAttribute)attributes[0]).GetName() : name;
            }

            // Create tag builder
            var radio = new TagBuilder("input");
            radio.Attributes.Add("id", $"{propertyName}_{name}");
            radio.Attributes.Add("name", ctrlName);
            radio.Attributes.Add("type", "radio");
            radio.Attributes.Add("value", propertyValue.ToString());

            string selectedFlag;
            if (selectedValue != null)
            {
                if (propertyValue == Convert.ToInt32(selectedValue))
                {
                    selectedFlag = "class='btn btn-sm btn-success active'";
                    radio.Attributes.Add("checked", "checked");
                }
                else
                {
                    selectedFlag = "class='btn btn-sm btn-default'";
                }
            }
            else
            {
                var selectedPropertyName = modelExplorer.Model;
                if (selectedPropertyName != null)
                {
                    if (name == selectedPropertyName.ToString())
                    {
                        selectedFlag = "class='btn btn-sm btn-success active'";
                        radio.Attributes.Add("checked", "checked");
                    }
                    else
                    {
                        selectedFlag = "class='btn btn-sm btn-default'";
                    }
                }
                else
                {
                    if (names.First() == name)
                    {
                        selectedFlag = "class='btn btn-sm btn-success active'";
                        radio.Attributes.Add("checked", "checked");
                    }
                    else
                    {
                        selectedFlag = "class='btn btn-sm btn-default'";
                    }
                }
            }

            // Add attributes                    
            radio.MergeAttributes(new RouteValueDictionary(htmlAttributes));

            using (var writer = new StringWriter())
            {
                radio.WriteTo(writer, HtmlEncoder.Default);
                sb.AppendFormat("<label {0}>{1}{2}</label>", selectedFlag, writer.ToString(), HttpUtility.HtmlEncode(description));
            }
        }

        var htmlContent = sb.ToString();

        var wrapperBuilder = new TagBuilder("div");
        wrapperBuilder.MergeAttribute("class", "btn-group btn-toggle radioList");
        wrapperBuilder.MergeAttribute("data-toggle", "buttons");
        wrapperBuilder.InnerHtml.AppendHtml(htmlContent);

        using (var sw = new StringWriter())
        {
            wrapperBuilder.WriteTo(sw, HtmlEncoder.Default);
            return new HtmlString(sw.ToString());
        }
    }
Syriac answered 13/7, 2020 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.