Add attribute to select list option
Asked Answered
F

5

17

I have a list of items in a drop down list within a Razor view. In the database each item has 3 values associated with it - the database id, short name (for display), and long name (for passing to a service). The drop down must show the short name, so I'm populating the drop down with the database id as the value and the short name as the text.

However when a user selects an item I need pass the long name as a query parameter to a search service using jQuery, e.g when Cortina is seleted "Ford Cortina 1979 Blue" needs to be passed to the service. My first thought is store the long name as a data dash attribute but I'm wondering is there a better way. So

  • How do I store all 3 values in the drop down list?
  • If I do use data dash attributes how do I incorporate all the LONG_NAME values into Html.DropDownListFor or somehow add them to the drop down list?

DB:

CARID SHORT_NAME LONG_NAME
1     Viper     Dodge Viper 1982
2     Boxster   Porsche Boxster 2009 Black
3     Cortina   Ford Cortina 1979 Blue

Controller helper to create the drop down:

public static IEnumerable<SelectListItem> GetSelectList(this IEFRepository repository, string typeName)
{
    var vehicle = repository.TypeTypes.FirstOrDefault(t => t.Name.ToUpper() == typeName);
    if (vehicle != null)
    {
        var carList = vehicle.SubTypes.ToList().OrderBy(s => s.Name);
        var selectList = new SelectList(subTypeList, "SubTypeID", "Name");

        return selectList;
    }
}

Here's the code I use to create the drop down:

<div class="editor-field">
    @Html.DropDownListFor(model => model.CarID,
        new SelectList(ViewBag.Cars, "Value", "Text", "1"))
    @Html.ValidationMessageFor(model => model.CarShortName)
</div>

Here's the output:

<select id="CarID" name="CarID" data-val="true" data-val-number="The field CarID must be a number." data-val-required="The CarID field is required.">
    <option value="2">Boxster</option>
    <option value="3">Cortina</option>
    <option selected="selected" value="1">Viper</option>
</select>
Fellows answered 1/7, 2012 at 20:16 Comment(1)
I encountered a similar situation. I had to have a dropdown which would allow the user to choose a record in an object graph. There were 4 related tables each with roughly 5 fields. I ended up having to write a javascript API to deconstruct the html generated by DropDownListFor and then kind of re-invent a dropdown in place of the original one. I asked a couple of questions here but never got any good responses and I feel like DropDownListFor is kind of a forgotten stepchild in the framework.Ramsey
F
1

Just getting back to this now. While @nikeaa's answer is certainly a viable solution I thought it was a bit heavyweight especially using XDocument. As a reminder what I'm dealing with is a TypeType (Cars) and SubType (list of car types - Viper, Granada, Hunter, Zodiac, Wolsley 1660, etc). TypeType could also be Trucks, Bicycles, etc. So here's how I solved it:

I added a JsonResult method on the Controller to return an anonymous object with the 3 properties that I wanted:

public class VehicleController : Controller
{
    // etc.
    public JsonResult GetSubTypesForTypeType(string typeTypeName)
    {
        var cars = pronova2Repository.GetTypeWithSubTypes(typeTypeName);

        return cars == null
        ? Json(new object[0], JsonRequestBehavior.AllowGet)
        : Json(cars.SubTypes.OrderBy(s => s.Name).Select(
            s => new { s.SubTypeID, s.Name, s.Description }).ToArray(),
            JsonRequestBehavior.AllowGet);
    }
    // etc.
}

Then in js:

Populate the drop down:

// populate the cars drop down when the select list is available
if ($('select#SubTypeID').length) {
    var carsSelect = $('select#SubTypeID');
    var carsList = populateCarsList("CARS");
    var carsListHtml = createCarsSelectList(carsList);
    carsSelect.html('');
    carsSelect.append(carsListHtml);

    $('#SubTypeID').change(function (e) {
        clearFormData();
    });
}

Call a function to get the subtypes (cars) via an ajax call:

function populateCarsList(typeTypeName) {
    var carsList;

    $.ajax({
        url: '/Vehicle/GetSubTypesForTypeType',
        data: { typeTypeName: typeTypeName },
        async: false
    }).done(function (data) {
        carsList = data;
    }).error(function (msg, url, line) {
        alert("Error retrieving cars from Vehicle/GetSubTypesForTypeType. Error message: " + line);
    });

    return carsList;
}

Function to create the select list with the added description as a "data-*" attribute:

function createCarsSelectList(selectData) {
    var html = '',
        len = selectData.length,
        selected,
        description;

    for (var i = 0; i < len; i++) {

        // "Viper" should be selected by default
        if (selectData[i].Name.toLocaleUpperCase() === "VIPER") {
            selected = ' selected="selected" ';
        } else {
            selected = '';
        }

        // Add the description (as a "data-" attribute), some descritions are null
        if (selectData[i].Description != null) {
            description = selectData[i].Description;
        } else {
            description = '';
        }

        html += '<option value="' + selectData[i].SubTypeID + '" data-description="' + description + '"' + selected + '>' + selectData[i].Name + '</option>';
    }

    return html;
}
Fellows answered 25/7, 2012 at 13:35 Comment(2)
Parsing XML is significantly "less heavyweight" than a server round-trip just to look up a value. But jQuery is more fun.Bookstack
the difference in these two solutions isn't "weight", but client-side vs server-side.Horsehide
B
29

Everyone forgets the "classic" way to solve these problems: use a foreach loop and actually write the input html. Only downside is you have to add the automatic attribute stuff (like validation, etc), which depending on your purpose may not be a big deal.

Something like:

<select> // add other attributes as expected
@foreach(var type in Model.MyFancyTypes) {
<option value="@type.SubTypeID" data-description="@type.Description" 
    @if(ViewBag.TypeSelected == type.SubTypeID) {
        selected="selected"
    }>@type.Name</option>
}
</select>
Bookstack answered 28/1, 2013 at 19:33 Comment(5)
if checking the selection in a View "smells", you can always build an appropriate ViewModel and corresponding Partial.Bookstack
I tried this, but it asks for a ";" semicolon after the line selected="selected". Intellisense shows its still in c# context and not html context. Please help!Nevertheless
@Suyash maybe try using Razor-escaping <text>? weblogs.asp.net/scottgu/…Bookstack
You can also force a line to Razor instead of C# by adding @: at the frontNucleo
@Nucleo Yup, @: is shorthand for <text> haacked.com/archive/2011/01/06/…Bookstack
U
22

I had a similar situation where I needed to pass a third value to each of the list items to determine the action to take in a jQuery function. Here is my solution (which will allow you to add any number of attributes to each item in the drop down):

First, I created a SelectListItemWithAttributes class as follows:

    public class SelectListItemWithAttributes : SelectListItem {
        public IDictionary<string, string> HtmlAttributes { get; set; }
    }

This allows me to create items for the select list with the extra attributes attached.

Second, I created an HTML helper method called DropDownListWithItemAttributesFor as follows:

public static MvcHtmlString DropDownListWithItemAttributesFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, IEnumerable<SelectListItemWithAttributes> selectList) {
    string name = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression)); 

    var selectDoc = XDocument.Parse(htmlHelper.DropDownList(name, (IEnumerable<SelectListItem>)selectList).ToString());

    var options = from XElement el in selectDoc.Element("select").Descendants()
                          select el;

    for (int i = 0; i < options.Count(); i++){
        var option = options.ElementAt(i);
        var attributes = selectList.ElementAt(i);

        foreach (var attribute in attributes.HtmlAttributes){
                    option.SetAttributeValue(attribute.Key, attribute.Value);
        }
    }

    selectDoc.Root.ReplaceNodes(options.ToArray());
    return MvcHtmlString.Create(selectDoc.ToString());
}

This allows me to create a drop down using the new SelectListWithAttributes class as the attributes. Basically, it creates the HTML for the drop down list, parses it into an XML document, then adds any items in the HtmlAttributes array as additional attributes to each item in the drop down.

Third, in my ViewModel code I have the following:

private List<SelectListItemWithAttributes> pDropDownDatas = null;
public List<SelectListItemWithAttributes> DropDownDatas {
    get {
        var DropDownDataItems = (
            from c in db.GetDropDownDataList(1, 1000)
            where c.AccountTypeID == this.AccountTypeID
            select new SelectListItemWithAttributes() { Text = c.Title, Value = c.ID.ToString(), HtmlAttributes = new Dictionary<string, string> { { "data-callback", c.RequiresCallback.ToString().ToLower() } } } ).ToList()
            ;

        DropDownDataItems.Insert(0, new SelectListItemWithAttributes() { Text = "-- Select --", Value = "", HtmlAttributes = new Dictionary<string, string> { { "data-callback", "false" } } });

        return DropDownDataItems;
    }
}

This builds the list of SelectListItemsWithAttributes that are going to ultimately populate the dropdown. This could be in a controller instead of the viewmodel, I just elected to make it a property of my viewmodel.

Lastly, in the view it would look like this:

@Html.DropDownListWithItemAttributesFor(m => m.DropDownDataID, Model.DropDownDatas)

This will display the drop down on the page using the property from the viewmodel that contains the list of SelectListItemsWithAttributes.

I constructed this solution from various solutions that I found on the internet, so it wasn't all original to me, but I put it together into something that worked for me.

Hope this will help you solve your issue.

Unreality answered 1/7, 2012 at 21:54 Comment(4)
I assume you're parsing the original Helper's output to avoid creating your own TagBuilder and rewriting all the magic that normally occurs?Bookstack
@Bookstack - yes this line does that: var selectDoc = XDocument.Parse(htmlHelper.DropDownList(name, (IEnumerable<SelectListItem>)selectList).ToString());Unreality
This is wonderful. However, using GetFullHtmlFieldName ended up creating the DropDownList with double prefixes, i.e. "ModelName_ModelName_MemberName". I had to change the string name line to just: string name = ExpressionHelper.GetExpressionText(expression);Horsehide
I'd also improve the code by checking to see if attributes.HtmlAttributes is null inside of DropDownListWithItemAttributesFor. Also, did you consider simply using htmlHelper.DropDownListFor instead of htmlHelper.DropDownList to eliminate the need for detecting the name? I also added the ability to pass in an object of htmlAttributes for the select element itself.Beheld
P
8

Inside the controller action that is supposed to receive the form submit you could use the id of the selected value to query your database in order to fetch the long display name and do whatever you was intending to do with it.

The DropDownListFor helper doesn't support adding HTML5 data-* attributes to the options but even if it did they will not be sent as part of a standard form submission. You will have to use javascript to send them to the server using another technique (hidden fields, AJAX, query string parameters, ...).

But if form some reason you need additional attributes on the option tag you could always write a custom helper.

Pituri answered 1/7, 2012 at 20:24 Comment(5)
I won't be submitting the form for the service call I'll be doing it client side with jQuery, so that's why I need to retrieve the long names when populating the drop down initially.Vetiver
What will the jQuery script do with this value?Pituri
Will be passed to a service using jQuery to retrieve further data, something like $.ajax({url: "myservice/?suggestWord=" + carLongName + "&callback=?"Vetiver
Can't you pass the selected id to this service and have the service fetch the corresponding description from the database? But if you really need this long description in the markup you could write a custom html helper that will allow you to do this. The built-in helper doesn't. I have updated my answer to with the custom helper suggestion.Pituri
Thanks for the update. The service I'm using is a remote service and I don't have any control over it, so i have to pass the long name.Vetiver
G
3

@nikeaa Thank you for your code. I found a few issues with it (e.g. when the option list is empty, the select is not rendered correctly; you don't need to replace the options, just modify them, otherwise some attributes of the select are removed) and I added some additional parameters to fully use the power of DropDownListFor. Here is my version of the method:

public static MvcHtmlString DropDownListWithItemAttributesFor<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItemWithAttributes> selectList,
    string optionLabel, IDictionary<string, object> htmlAttributes)
{
    if (selectList == null || !selectList.Any())
        return htmlHelper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes);

    var selectDoc = XDocument.Parse(htmlHelper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes).ToString());

    var options = selectDoc.Element("select").Descendants().ToArray();

    for (int i = 0; i < options.Length; i++)
    {
        var option = options[i];
        var attributes = selectList.ElementAt(i);

        foreach (var attribute in attributes.Attributes)
            option.SetAttributeValue(attribute.Key, attribute.Value);
    }

    return MvcHtmlString.Create(selectDoc.ToString());
}
Guitarfish answered 3/2, 2017 at 14:56 Comment(0)
F
1

Just getting back to this now. While @nikeaa's answer is certainly a viable solution I thought it was a bit heavyweight especially using XDocument. As a reminder what I'm dealing with is a TypeType (Cars) and SubType (list of car types - Viper, Granada, Hunter, Zodiac, Wolsley 1660, etc). TypeType could also be Trucks, Bicycles, etc. So here's how I solved it:

I added a JsonResult method on the Controller to return an anonymous object with the 3 properties that I wanted:

public class VehicleController : Controller
{
    // etc.
    public JsonResult GetSubTypesForTypeType(string typeTypeName)
    {
        var cars = pronova2Repository.GetTypeWithSubTypes(typeTypeName);

        return cars == null
        ? Json(new object[0], JsonRequestBehavior.AllowGet)
        : Json(cars.SubTypes.OrderBy(s => s.Name).Select(
            s => new { s.SubTypeID, s.Name, s.Description }).ToArray(),
            JsonRequestBehavior.AllowGet);
    }
    // etc.
}

Then in js:

Populate the drop down:

// populate the cars drop down when the select list is available
if ($('select#SubTypeID').length) {
    var carsSelect = $('select#SubTypeID');
    var carsList = populateCarsList("CARS");
    var carsListHtml = createCarsSelectList(carsList);
    carsSelect.html('');
    carsSelect.append(carsListHtml);

    $('#SubTypeID').change(function (e) {
        clearFormData();
    });
}

Call a function to get the subtypes (cars) via an ajax call:

function populateCarsList(typeTypeName) {
    var carsList;

    $.ajax({
        url: '/Vehicle/GetSubTypesForTypeType',
        data: { typeTypeName: typeTypeName },
        async: false
    }).done(function (data) {
        carsList = data;
    }).error(function (msg, url, line) {
        alert("Error retrieving cars from Vehicle/GetSubTypesForTypeType. Error message: " + line);
    });

    return carsList;
}

Function to create the select list with the added description as a "data-*" attribute:

function createCarsSelectList(selectData) {
    var html = '',
        len = selectData.length,
        selected,
        description;

    for (var i = 0; i < len; i++) {

        // "Viper" should be selected by default
        if (selectData[i].Name.toLocaleUpperCase() === "VIPER") {
            selected = ' selected="selected" ';
        } else {
            selected = '';
        }

        // Add the description (as a "data-" attribute), some descritions are null
        if (selectData[i].Description != null) {
            description = selectData[i].Description;
        } else {
            description = '';
        }

        html += '<option value="' + selectData[i].SubTypeID + '" data-description="' + description + '"' + selected + '>' + selectData[i].Name + '</option>';
    }

    return html;
}
Fellows answered 25/7, 2012 at 13:35 Comment(2)
Parsing XML is significantly "less heavyweight" than a server round-trip just to look up a value. But jQuery is more fun.Bookstack
the difference in these two solutions isn't "weight", but client-side vs server-side.Horsehide

© 2022 - 2024 — McMap. All rights reserved.