SelectListItem with data-attributes
Asked Answered
K

6

85

Is there anyway to have a SelectList prepopulated on ViewModel with data-attributes ?

I want to do

@Html.DropdownListFor(m=> m.CityId, Model.Cities);

so it generates code like :

<select id="City" class="location_city_input" name="City">
    <option data-geo-lat="-32.522779" data-geo-lng="-55.765835" data-geo-zoom="6" />
    <option data-geo-lat="-34.883611" data-geo-lng="-56.181944" data-geo-zoom="13" data-geo-name="Montevideo" data-child=".state1" value="1">Montevideo</option>               
    <option data-geo-lat="-34.816667" data-geo-lng="-55.95" data-geo-zoom="13" data-geo-name="Canelones, Ciudad de la Costa" data-child=".state41" value="41">Ciudad de la Costa</option>
</select>
Kimura answered 15/7, 2013 at 23:9 Comment(0)
Y
119

Here's the simple solution.

Not everything has to be written with extension method in .NET code. One of the great things about MVC is it gives you easy access to construct your own HTML.

With MVC4 you can get the id and name of the element on the expression tree with the helpers HTML.NameFor and HTML.IdFor

<select name="@Html.NameFor(Function(model) model.CityId)"
        id="@Html.IdFor(Function(model) model.CityId)"
        class="location_city_input">
    @For Each city In Model.Cities
        @<option value="@city.Value"
                 @(If(city.Value = Model.CityId, "selected", ""))
                 data-geo-lat="@city.Lat"
                 data-geo-lng="@city.Lng"
                 data-geo-zoom="@city.Zoom">
            @city.Text
        </option>
    Next
</select>

Assuming Model.Cities is a collection of items that expose each of those properties. Then you should be all set.

If you want reusability, consider making it an editor template for anything that is an Enumerable of Cities

Yanina answered 31/12, 2014 at 20:0 Comment(6)
Since razor2 you can simple do selected="@city.Value == Model.CityId" and it will generate the right markup (either selected="selected" or nothing)Wallop
How do you make this still work with validation? My select does not correctly highlight in red anymore when there is a validation error.Prehensible
The code posted by @Wallop did not work until I added parentheses: selected="@(city.Value == Model.CityId)"Remington
@TawabWakil, that sounds about right. I didn't run the code, I simply commented by knowing the theory.Wallop
@Prehensible that is why "Not everything has to be written with extension method in .NET code" is not a simple solution. Luckily in MVC 4 and above, they've provided a way to get the unobtrusive validation attributes (Html.GetUnobtrusiveValidationAttributes), but it's not pretty. Look at this answer for an in-liner, or you could write an HTML helper for it to make it easier to read: https://mcmap.net/q/246540/-how-to-use-html-getunobtrusivevalidationattributesCostanzo
@skeletank: to make this work with jquery validation, you need to add data-val="true" and data-val-required="@Html.DisplayNameFor(...) field is required." on the <select />.Puffy
S
14

You'll have to extend SelectListItem, and then extend DropDownListFor to use the extended SelectListItem.

Have a look at this solution:

Adding html class tag under <option> in Html.DropDownList

Sherleysherline answered 16/7, 2013 at 1:41 Comment(5)
this looks like the correct answer but seems kinda ugly. I rather write the select myself than extending SelectListItem and DropDownListFor I think. I'm not sure.Kimura
I don't know why you think it's ugly, but it seems more logical to me. Each SelectListItem represents an option tag in the final html, and what you need to do is to add custom html attributes to the option tag (SelectListItem), so it only makes sense to extend the SelectListItem.Sherleysherline
i think it's ugly from the mvc framework perspective. but the solution is exactly how to deal with.Kimura
Agreed, quite nasty just to add some simple attributes :(Barnette
Really ugly solution. Has it been covered in the latest (5.2) version of MVC or you still heave to write custom code for that? ThanksSeemaseeming
C
7

Here's how I ended up doing it without an extension but still enabling unobtrusive validation to continue to work & be bound to a ViewModel property.

Created an Html Helper to get the validation attributes as a string:

    public static IHtmlString GetUnobtrusiveValidationAttributesFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> propertySelector)
    {
        string propertyName = html.NameFor(propertySelector).ToString();
        ModelMetadata metaData = ModelMetadata.FromLambdaExpression(propertySelector, html.ViewData);
        IDictionary<string, object> attributeCollection = html.GetUnobtrusiveValidationAttributes(propertyName, metaData);

        return html.Raw(String.Join(" ", attributeCollection.Select(kvp => kvp.Key + "=\"" + kvp.Value.ToString() + "\"")));
    }

Use this helper in a select list in the view:

<select name="@Html.NameFor(m => m.CityId)" id="@Html.IdFor(m => m.CityId)"
    @Html.GetUnobtrusiveValidationAttributesFor(m => m.CityId)
    class="location_city_input">
    @foreach(var city in Model.Cities)
    {
        <option value="@city.Id.ToString()" @(city.Id == Model.CityId ? "selected" : "") 
            data-geo-lat="@city.Lat" data-geo-lng="@city.Lng" data-geo-zoom="@city.Zoom">
            @city.Name
        </option>
    }
</select>

This would output something like this:

<select id="CityId" name="CityId" 
    data-val-required="The SelectedTaxRateID field is required." data-val="true" 
    class="location_city_input">
    <option value="1" selected data-geo-lat="-34.883611" data-geo-lng="-56.181944" data-geo-zoom="13">Montevideo</option>               
    <option value="41" data-geo-lat="-34.816667" data-geo-lng="-55.95" data-geo-zoom="13">Ciudad de la Costa</option>
</select>

I will leave the conditional data- attributes up to you since those are just a matter of forming the appropriate Razor expressions.

Costanzo answered 10/10, 2018 at 2:6 Comment(0)
L
6

MVC when it converts object names to attribute names, it relaces "_" with "-", so its:

@Html.DropDownList(a=>a.websiteid, Model.GetItems, new{ data_rel="selected" })

NOT MY ANSWER, ANSWER CREDIT GOES TO About bruce (sqlwork.com) from the ASP>NET Forums.

How can I add data-rel="selected" attribute into dropdownlistfor htmlAttributes?

JUST WANTED TO HELP OUT AS THIS SAVED ME FROM CODING A HACK! ENJOY.

Liana answered 30/5, 2018 at 22:42 Comment(2)
This only added the data-rel to the select not to each optionPlain
Great to know, although it doesn't add the attributes as wanted (like ParoX mentioned).Ambroid
V
2

I had a similar requirement, I created a extension. Hope it helps for the ones who wants to create an extension.

/*cs file*/
/*This contains your information with List<vmListItem>*/
public class vmListItem
{
   public int Id { get; set; }
   public string Name { get; set; }
   public string Tag { get; set; }
}

/*This contains the attributes in select, using List<vmAttribute>. Check cshtml */
public class vmAttribute
{
   public string Key { get; set; }
   public string Value { get; set; }
}

    /// <summary>
    /// Creates a dropdownlist using a list with data attributes included
    /// </summary>
    /// <param name="helper"></param>
    /// <param name="id">id and name of the select</param>
    /// <param name="attributes">list of attrs for select</param>
    /// <param name="items"><list of options/param>
    /// <param name="idSelected">id selected in option</param>
    /// <param name="tagName">data-tagName you can choose the name of your tag</param>
    /// <param name="textHeader">first option in select</param>
    /// <returns></returns>
    public static MvcHtmlString DropDownListForWithTag(this HtmlHelper helper, string id, List<vmAttribute> attributes, List<vmListItem> items, int idSelected, string tagName = "tag", string textHeader= "")
    {
        var select = new TagBuilder("select");
        select.GenerateId(id);
        select.MergeAttribute("name", id);
        foreach (vmAttribute att in atributos) select.MergeAttribute(att.Key, att.Value);

        TagBuilder headerOption = new TagBuilder("option");
        headerOption .MergeAttribute("value", null);
        headerOption .InnerHtml = textHeader;
        select.InnerHtml += headerOption ;

        foreach(var item in items)
        {                
            TagBuilder option = new TagBuilder("option");
            option.MergeAttribute("value", item.Id.ToString());
            option.MergeAttribute("data-" + tagName, item.Tag);
            if (idSelected == item.Id) option.MergeAttribute("selected", "selected");
            option.InnerHtml = item.Name;

            select.InnerHtml += option.ToString();
        }

        return new MvcHtmlString(select.ToString());
    }

/*cshtml file*/
@Html.DropDownListForWithTag("MovimientoBienMotivoId", new List<vmAttribute> {
                        new vmAttribute("class", "form-control"),
                        new vmAttribute("data-val", "true"),
                        new vmAttribute("data-val-required", "El campo Motivo es obligatorio"),
                        new vmAttribute("onchange", "movValidarCambioMotivo()"),
                    }, (List<vmListItem>)ViewBag.MovimientoBienMotivoId, Model.MovimientoBienMotivoId, "codigo", "Seleccione")
                    @Html.ValidationMessageFor(model => model.ColumnId, "", new { @class = "text-danger" })


/*html results*/

enter image description here

Virgina answered 23/4, 2018 at 17:59 Comment(1)
Thank you best solution of all. I customized this extension to suit my needs.Dervish
V
0

Here is another solution for adding multiple attributes to select option elements. Hope this helps

/// <summary>
/// itemAttributes can use to add custom attributes for each option items 
/// </summary>
public class SelectListItemWithAttributes
{
    public object itemAttributes { get; set; }

    public SelectListItem SelectListItem { get; set; }
}

/// <summary>
/// This can generate a dropdown select list with multiple custom attributes for both select tag and It's option tags
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <typeparam name="TProperty"></typeparam>
/// <param name="htmlHelper"></param>
/// <param name="expression"></param>
/// <param name="list">List of SelectListItemWithAttributes items</param>
/// <param name="optionLabel">This text is show when nothing is selected (like placeholder)</param>
/// <param name="htmlAttributes">select tag element attributes</param>
/// <returns>
/// Success : html string of dropdown select list
/// expression not provides : ArgumentNullException
/// </returns>
public static MvcHtmlString DropDownListWithAttributesFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItemWithAttributes> list, string optionLabel, IDictionary<string, object> htmlAttributes = null)
{
    if (expression == null)
    {
        throw new ArgumentNullException("expression");
    }

    // getting model property for binding
    ModelMetadata metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);
    string name = ExpressionHelper.GetExpressionText((LambdaExpression)expression);
    return DropDownListWithAttributes(htmlHelper, metadata, name, list, optionLabel, htmlAttributes);
}

/// <summary>
/// generating option items with mandatory attributes and user custom attributes, then appending to select tag
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="metadata"></param>
/// <param name="name">model property name for data binding</param>
/// <param name="list">list of SelectListItemWithAttributes</param>
/// <param name="optionLabel">This text is show when nothing is selected (like placeholder)</param>
/// <param name="htmlAttributes">select tag element attributes</param>
/// <returns>
/// Success : html string of dropdown select list
/// </returns>
private static MvcHtmlString DropDownListWithAttributes(this HtmlHelper htmlHelper, ModelMetadata metadata, string name, IEnumerable<SelectListItemWithAttributes> list, string optionLabel , IDictionary<string, object> htmlAttributes = null)
{
    string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
    if (String.IsNullOrEmpty(fullName))
    {
        throw new ArgumentException("name");
    }

    // main select tag
    TagBuilder dropdown = new TagBuilder("select");
    dropdown.Attributes.Add("name", fullName);

    // select element must have an accessible name refer : https://dequeuniversity.com/rules/axe/4.4/select-name?application=axeAPI
    dropdown.Attributes.Add("aria-label",fullName);

    if (htmlAttributes != null)
    {
        dropdown.MergeAttributes(htmlAttributes); 
    }
    dropdown.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));

    //initial option string with first element
    var options = ("<option value='" + String.Empty + "'>" + (optionLabel == null ? "Select" : optionLabel) + "</option>");


    TagBuilder option;
    //Adding other option elements with all attributes
    foreach (var item in list)
    {
        option = new TagBuilder("option");
        option.MergeAttribute("value", item.SelectListItem.Value);

        if (item.itemAttributes != null)
        {
            option.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(item.itemAttributes));
    }

    // including slect and disabled if provided
    if (item.SelectListItem.Selected)
    {
        option.MergeAttribute("selected", string.Empty);
    }

    if (item.SelectListItem.Disabled)
    {
        option.MergeAttribute("disabled", string.Empty);
    }

    option.SetInnerText(item.SelectListItem.Text);
    options += option.ToString(TagRenderMode.Normal) + "\n";
    }

    dropdown.InnerHtml = options;         
    return MvcHtmlString.Create(dropdown.ToString(TagRenderMode.Normal));
}
}
Virginavirginal answered 5/10, 2022 at 8:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.