ASP.NET MVC – Extending the DropDownList to show the items grouped by a category


 

Problem

  • You want to Create/Edit an entity that has a foreign key relationship.
  • You want to be able to select the value for the foreign key from a list.
  • You want the items that show in the list to be grouped by a category (with the category un-selectable)

Solution

The first 2 points from the problem statement can be easily accomplished by using a DropDownListFor html helper. For the last thing let’s see how we can solve it in plain html first. There is a html tag called <optgroup> that allows us to group the items:
<select>
    <option value="">[Please select an option]</option>
    <optgroup label="Group 1">
        <option value="1">Option 1</option>
        <option value="2">Option 2</option>
    </optgroup>
    <optgroup label="Group 2">
        <option value="3">Option 3</option>
        <option value="4">Option 4</option>
    </optgroup>
    <optgroup label="Group 3">
        <option value="5">Option 5</option>
        <option value="6">Option 6</option>
    </optgroup>
    <optgroup label="Group 4">
        <option value="7">Option 7</option>
        <option value="8">Option 8</option>
    </optgroup>
</select>

As you can see we can mix the the <option> and <optgroup> tags at the same level but the <optgroup> tag cannot contain another <optgroup> tag inside.

The DropDownList cannot be used to generate the <select> tag as above but we can create our own extension. The DropDownList accepts an IEnumerable<SelectListItem> but we need to pass an IDictionary<string , IEnumerable<SelectListItem>> where the key will represent the category we will use for grouping the items in the DropDownList.

So it is clear that we need to create a html helper but because this helper will be an extension of the actual DropDownList we will create more overloads for the same helper.

Luckily for us the source code for ASP.NET MVC is publicly available and we can just grab de DropDownList implementation and bend it to our will. There are 3 things we need to modify:

  1. Replace the IEnumerable<SelectListItem> with IDictionary<string , IEnumerable<SelectListItem>> in all method signatures
  2. Add another overload for the ListItemToOption method that accepts 2 arguments (see below)
  3. Modify the helper method SelectInternal that does all the magic (see below)

This is only relevant code the full source code can be obtained from the Download tab

ListItemToOption (applies to both MVC2 and MVC3):

internal static string ListItemToOption( SelectListItem item )
{
    TagBuilder builder = new TagBuilder( "option" )
    {
        InnerHtml = HttpUtility.HtmlEncode( item.Text )
    };
    if ( item.Value != null )
    {
        builder.Attributes[ "value" ] = item.Value;
    }
    if ( item.Selected )
    {
        builder.Attributes[ "selected" ] = "selected";
    }
    return builder.ToString( TagRenderMode.Normal );
}

SelectInternal - relevant code only (applies to both MVC2 and MVC3):

Dictionary<string, IEnumerable<SelectListItem>> newSelectListDictionary = 
    new Dictionary<string, IEnumerable<SelectListItem>>( );
foreach ( var category in selectList.Keys )
{
    List<SelectListItem> newSelectList = new List<SelectListItem>( );

    foreach ( SelectListItem item in selectList[ category ] )
    {
        item.Selected = ( item.Value != null ) ? 
            selectedValues.Contains( item.Value ) : 
            selectedValues.Contains( item.Text );
        newSelectList.Add( item );
    }

    newSelectListDictionary.Add( category, newSelectList );
}
selectList = newSelectListDictionary;

How it works

This new extension behaves exactly as a normal DropDownList helper except for the fact that it is accepting an IDictionary<string , IEnumerable<SelectListItem>> instead of an IEnumerable<SelectListItem> and it’s constructing an <optgroup> tag for each key in the dictionary

See it in action

Let’s consider the following model entities:
    public class Continent
    {
        public int ContinentId { get; set; }
        public string Name { get; set; }
    }

    public class Country
    {
        public int CountryId { get; set; }
        public string Name { get; set; }
        public int ContinentId { get; set; }
    }

    public class City
    {
        public int CityId { get; set; }
        public string Name { get; set; }
        public int CountryId { get; set; }
    }
 
In an action method (in CitiesController):
    [HttpGet]
    public ActionResult Create( )
    {
        IDictionary<string , IEnumerable<SelectListItem>> countriesByContinent = new Dictionary<string, IEnumerable<SelectListItem>>( );

        foreach ( var continent in this._continents )
        {
            var countryList = new List<SelectListItem>( );

            foreach ( var country in this._countries.Where( c => c.ContinentId == continent.ContinentId ) )
            {
                countryList.Add( new SelectListItem { Value = country.CountryId.ToString( ), Text = country.Name } );
            }

            countriesByContinent.Add( continent.Name, countryList );
        }

        ViewBag.CountriesList = countriesByContinent;

        return View( );
    }
 
In Create.cshtml:
    @Html.DropDownList( c => c.CountryId , ViewBag.CountriesList as IDictionary<string , IEnumerable<SelectListItem>> , "[Please select a country]" )
In Create.aspx:
    <%: Html.DropDownList( c => c.CountryId , ViewBag.CountriesList as IDictionary<string , IEnumerable<SelectListItem> > , "[Please select a country]" )%>

The result:

 MVC2 | MVC3

References

Download

 

6 Comments

  • Very interesting solution... Thanks!!!

    Although I was not able to find a solution to integrate with what you have in order to bind the fields.
    For example, when I edit a value, by default I have previously selected the country, so then it should be selected on page load.

    Any pointers on this from your solution?

    Once again, thanks!!!

  • @Chris Kettenbach: Sorry for that. I'll move the source code to GitHub soon

  • Cool!! Its very nice solution for "DropDownList to show the items grouped by a category" in MVC3, I have used this one, its working fine. Thanks!!

  • Good done! But does it support data annotation validation? And how can I use this solution in my MVC 4 application? Please, help me and sorry for my bad english.

  • Does it support MVC4?

  • Thanks for this code.
    To have option and optgroup at the same level in one drop down box, the code can be edited as below -

    if (string.IsNullOrWhiteSpace(group.Key))
    {
    foreach (var item in group)
    {
    listItemBuilder.AppendLine(ListItemToOption(item));
    }
    }
    else
    {
    var groupName = groupedSelectListItems.Where(i => i.GroupKey == group.Key).Select(it => it.GroupName).FirstOrDefault();
    listItemBuilder.AppendLine(string.Format("", groupName, group.Key));
    foreach (var item in group)
    {
    listItemBuilder.AppendLine(ListItemToOption(item));
    }
    listItemBuilder.AppendLine("");
    }

Comments have been disabled for this content.