Serge Zab's answer was exactly what I was looking for. As a tough VB programmer, I put it in this VB module:
'based on Serge Zab answer on http://stackoverflow.com/questions/607188/support-for-optgroup-in-dropdownlist-net-mvc Imports System.Collections Imports System.Collections.Generic Imports System.Globalization Imports System.Linq Imports System.Linq.Expressions Imports System.Text Imports System.Web Imports System.Web.Mvc Imports System.Web.Routing Public Class GroupedSelectListItem Inherits SelectListItem Public Property GroupKey() As String Get Return m_GroupKey End Get Set(value As String) m_GroupKey = Value End Set End Property Private m_GroupKey As String Public Property GroupName() As String Get Return m_GroupName End Get Set(value As String) m_GroupName = Value End Set End Property Private m_GroupName As String End Class Public Module HtmlHelpers <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, Nothing, Nothing, Nothing) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem)) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, selectList, Nothing, Nothing) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, optionLabel As String) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, Nothing, optionLabel, Nothing) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, selectList, Nothing, htmlAttributes) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As Object) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, selectList, Nothing, New RouteValueDictionary(htmlAttributes)) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, selectList, optionLabel, Nothing) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, selectList, optionLabel, htmlAttributes) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As Object) As MvcHtmlString Return DropDownListHelper(htmlHelper, name, selectList, optionLabel, New RouteValueDictionary(htmlAttributes)) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem)) As MvcHtmlString ' optionLabel ' htmlAttributes Return DropDownGroupListFor(htmlHelper, expression, selectList, Nothing, Nothing) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As Object) As MvcHtmlString ' optionLabel Return DropDownGroupListFor(htmlHelper, expression, selectList, Nothing, New RouteValueDictionary(htmlAttributes)) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString ' optionLabel Return DropDownGroupListFor(htmlHelper, expression, selectList, Nothing, htmlAttributes) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String) As MvcHtmlString ' htmlAttributes Return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, Nothing) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As Object) As MvcHtmlString Return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, New RouteValueDictionary(htmlAttributes)) End Function <System.Runtime.CompilerServices.Extension> _ Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString If expression Is Nothing Then Throw New ArgumentNullException("expression") End If Return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes) End Function Private Function DropDownListHelper(htmlHelper As HtmlHelper, expression As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString ' allowMultiple Return SelectInternal(htmlHelper, optionLabel, expression, selectList, False, htmlAttributes) End Function ' Helper methods <System.Runtime.CompilerServices.Extension> _ Private Function GetSelectData(htmlHelper As HtmlHelper, name As String) As IEnumerable(Of GroupedSelectListItem) Dim o As Object = Nothing If htmlHelper.ViewData IsNot Nothing Then o = htmlHelper.ViewData.Eval(name) End If If o Is Nothing Then Throw New InvalidOperationException([String].Format(CultureInfo.CurrentCulture, "Missing Select Data", name, "IEnumerable<GroupedSelectListItem>")) End If Dim selectList As IEnumerable(Of GroupedSelectListItem) = TryCast(o, IEnumerable(Of GroupedSelectListItem)) If selectList Is Nothing Then Throw New InvalidOperationException([String].Format(CultureInfo.CurrentCulture, "Wrong Select DataType", name, o.[GetType]().FullName, "IEnumerable<GroupedSelectListItem>")) End If Return selectList End Function Friend Function ListItemToOption(item As GroupedSelectListItem) As String Dim builder As New TagBuilder("option") With { _ .InnerHtml = HttpUtility.HtmlEncode(item.Text) _ } If item.Value IsNot Nothing Then builder.Attributes("value") = item.Value End If If item.Selected Then builder.Attributes("selected") = "selected" End If Return builder.ToString(TagRenderMode.Normal) End Function <System.Runtime.CompilerServices.Extension> _ Private Function SelectInternal(htmlHelper__1 As HtmlHelper, optionLabel As String, name As String, selectList As IEnumerable(Of GroupedSelectListItem), allowMultiple As Boolean, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString name = htmlHelper__1.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name) If [String].IsNullOrEmpty(name) Then Throw New ArgumentException("Null Or Empty", "name") End If Dim usedViewData As Boolean = False ' If we got a null selectList, try to use ViewData to get the list of items. If selectList Is Nothing Then selectList = htmlHelper__1.GetSelectData(name) usedViewData = True End If Dim defaultValue As Object = If((allowMultiple), htmlHelper__1.GetModelStateValue(name, GetType(String())), htmlHelper__1.GetModelStateValue(name, GetType(String))) ' If we haven't already used ViewData to get the entire list of items then we need to ' use the ViewData-supplied value before using the parameter-supplied value. If Not usedViewData Then If defaultValue Is Nothing Then defaultValue = htmlHelper__1.ViewData.Eval(name) End If End If If defaultValue IsNot Nothing Then Dim defaultValues As IEnumerable = If((allowMultiple), TryCast(defaultValue, IEnumerable), New String() {defaultValue}) Dim values As IEnumerable(Of String) = From value In defaultValues Select (Convert.ToString(value, CultureInfo.CurrentCulture)) Dim selectedValues As New HashSet(Of String)(values, StringComparer.OrdinalIgnoreCase) Dim newSelectList As New List(Of GroupedSelectListItem)() For Each item As GroupedSelectListItem In selectList item.Selected = If((item.Value IsNot Nothing), selectedValues.Contains(item.Value), selectedValues.Contains(item.Text)) newSelectList.Add(item) Next selectList = newSelectList End If ' Convert each ListItem to an <option> tag Dim listItemBuilder As New StringBuilder() ' Make optionLabel the first item that gets rendered. If optionLabel IsNot Nothing Then listItemBuilder.AppendLine(ListItemToOption(New GroupedSelectListItem() With { _ .Text = optionLabel, _ .Value = [String].Empty, _ .Selected = False _ })) End If For Each group As Object In selectList.GroupBy(Function(i) i.GroupKey) Dim groupName As String = selectList.Where(Function(i) i.GroupKey = group.Key).[Select](Function(it) it.GroupName).FirstOrDefault() listItemBuilder.AppendLine(String.Format("<optgroup label=""{0}"" value=""{1}"">", groupName, group.Key)) For Each item As GroupedSelectListItem In group listItemBuilder.AppendLine(ListItemToOption(item)) Next listItemBuilder.AppendLine("</optgroup>") Next Dim tagBuilder As New TagBuilder("select") With { _ .InnerHtml = listItemBuilder.ToString() _ } TagBuilder.MergeAttributes(htmlAttributes) ' replaceExisting TagBuilder.MergeAttribute("name", name, True) TagBuilder.GenerateId(name) If allowMultiple Then TagBuilder.MergeAttribute("multiple", "multiple") End If ' If there are any errors for a named field, we add the css attribute. Dim modelState As ModelState = Nothing If htmlHelper__1.ViewData.ModelState.TryGetValue(name, modelState) Then If modelState.Errors.Count > 0 Then TagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName) End If End If Return MvcHtmlString.Create(TagBuilder.ToString()) End Function <System.Runtime.CompilerServices.Extension> _ Friend Function GetModelStateValue(helper As HtmlHelper, key As String, destinationType As Type) As Object Dim modelState As ModelState = Nothing If helper.ViewData.ModelState.TryGetValue(key, modelState) Then If modelState.Value IsNot Nothing Then ' culture Return modelState.Value.ConvertTo(destinationType, Nothing) End If End If Return Nothing End Function End Module