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>
rsenna
  • 11,775
  • 1
  • 54
  • 60
Bart Calixto
  • 19,210
  • 11
  • 78
  • 114

6 Answers6

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

KyleMit
  • 30,350
  • 66
  • 462
  • 664
  • 3
    Interesting... I didn't know about html.namefor etc. I'll give it a try – Bart Calixto Dec 31 '14 at 23:53
  • 1
    Great advice for adding real easy flexibility to your html. – ChandlerPelhams Mar 11 '15 at 16:21
  • 7
    Since razor2 you can simple do `selected="@city.Value == Model.CityId"` and it will generate the right markup (either `selected="selected"` or nothing) – Diego Sep 11 '15 at 14:37
  • 4
    How do you make this still work with validation? My select does not correctly highlight in red anymore when there is a validation error. – skeletank Sep 22 '16 at 12:44
  • 4
    The code posted by @Diego did not work until I added parentheses: `selected="@(city.Value == Model.CityId)"` – Tawab Wakil Jan 11 '17 at 21:45
  • 1
    @TawabWakil, that sounds about right. I didn't run the code, I simply commented by knowing the theory. – Diego Jan 12 '17 at 12:39
  • @skeletank 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://stackoverflow.com/a/37748259/263832 – xr280xr Oct 10 '18 at 01:30
  • @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 . – David Liang Mar 12 '20 at 21:17
13

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

Community
  • 1
  • 1
ataravati
  • 8,891
  • 9
  • 57
  • 89
  • 2
    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. – Bart Calixto Jul 16 '13 at 15:30
  • 4
    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. – ataravati Jul 16 '13 at 15:34
  • 1
    i think it's ugly from the mvc framework perspective. but the solution is exactly how to deal with. – Bart Calixto Jul 16 '13 at 16:12
  • 1
    Agreed, quite nasty just to add some simple attributes :( – drogon Sep 17 '13 at 18:25
  • 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? Thanks – Aleksander Bethke Mar 07 '16 at 14:28
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.

xr280xr
  • 12,621
  • 7
  • 81
  • 125
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.

Takumi
  • 85
  • 1
  • 5
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

amc software
  • 747
  • 1
  • 7
  • 18
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));
}
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459