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:
-
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
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