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
<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:
-
Replace the IEnumerable<SelectListItem> with IDictionary<string , IEnumerable<SelectListItem>> in all method signatures
-
Add another overload for the ListItemToOption method that accepts 2 arguments (see below)
-
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
See it in action
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; } }
[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( ); }
@Html.DropDownList( c => c.CountryId , ViewBag.CountriesList as IDictionary<string , IEnumerable<SelectListItem>> , "[Please select a country]" )
<%: Html.DropDownList( c => c.CountryId , ViewBag.CountriesList as IDictionary<string , IEnumerable<SelectListItem> > , "[Please select a country]" )%>
The result: