DropDownListFor for enum with custom values (stored in attributes) doesn't select item - asp.net-mvc

I have enum with assigned string values (in attributes) and would like to have html helper for displaying it. Unfortunately I need to use assigned values as a keys in ddl. Because of that it doesn't select item stored in ViewModel. What to do?
Sample Enum
public enum StatusEnum
{
[Value("AA")]
Active,
[Value("BB")]
Disabled
}
ViewModel
public class UserViewModel
{
public StatusEnum Status { get; set; }
}
View
#Html.DropDownListForEnum(x => x.Status, new {})
DropDownListForEnum helper
public static MvcHtmlString DropDownListForEnum<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
IEnumerable<SelectListItem> items = Enum.GetNames(typeof(TEnum))
.Select(n => (TEnum)Enum.Parse(typeof(TEnum), n))
.Select(e => new SelectListItem() {
Text = e.ToString(),
Value = EnumExtension.GetValue(e).ToString(),
Selected = EnumExtension.GetValue(e) == EnumExtension.GetValue(metadata.Model)
})
.ToList();
return htmlHelper.DropDownListFor(expression, items, htmlAttributes);
}

Instead of passing IEnumerable<SelectListItem> to the dropdown, create and populate SelectList manually and pass that and it will work:
public static MvcHtmlString DropDownListForEnum<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var items = Enum.GetNames(typeof(TEnum))
.Select(n => (TEnum)Enum.Parse(typeof(TEnum), n))
.Select(e => new
{
Id = Convert.ToInt32(e),
Text = e.ToString(),
Value = EnumExtension.GetValue(e).ToString(),
Selected = EnumExtension.GetValue(e) == EnumExtension.GetValue(metadata.Model)
})
.ToList();
var selectList = new SelectList(items, "Text", "Value", selectedValue: items.Find(i => i.Selected).Text);
return htmlHelper.DropDownListFor(expression, selectList, htmlAttributes);
}

Related

Custom DropDownListFor Helper loads an empty value for the selected element

I am using a custom helper, to create a select element that can take HtmlAttribute. I am using the same code as I found here, in #Alexander Puchkov answer (which I'll post for reference).
It's working fine, apart when the DDL helper is loaded with one of the option as the selected item, then the value of the selected item is null/empty. (i.e. on an edit page the DDL loads up with the option that was set on creation, rather than the '-Please select option-'),
The highlighted attribute in the picture shows the problem, the value should not be empty, but should show 'Medium'...
So the text is display correctly but the element has no value. Any ideas on where this issue comes from?
Here is the full code of the helper:
/// <summary>
/// A selectListItem with an extra property to hold HtmlAttributes
/// <para>Used in conjunction with the sdDDL Helpers</para>
/// </summary>
public class SdSelectListItem : SelectListItem
{
public object HtmlAttributes { get; set; }
}
/// <summary>
/// Generate DropDownLists with the possibility of styling the 'option' tags of the generated 'select' tag
/// </summary>
public static class SdDdlHelper
{
public static MvcHtmlString sdDDLFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression, IEnumerable<SdSelectListItem> selectList,
string optionLabel, object htmlAttributes)
{
if (expression == null)
throw new ArgumentNullException("expression");
ModelMetadata metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);
return SelectInternal(htmlHelper, metadata, optionLabel, ExpressionHelper.GetExpressionText(expression), selectList,
false /* allowMultiple */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString sdDDLFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression, IEnumerable<SdSelectListItem> selectList,
object htmlAttributes)
{
if (expression == null)
throw new ArgumentNullException("expression");
ModelMetadata metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);
//-> The below line is my problem (specifically the 'null' param), it set to null if no option label is passed to the method...So if I use this overload, the DDL will load (or re-load) with the default value selected, not the value binded to the property - And if I use the above overload, and set Model.action_priority as the optionLabel, then I get what is shown in the picture...
return SelectInternal(htmlHelper, metadata, null, ExpressionHelper.GetExpressionText(expression), selectList,
false /* allowMultiple */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
#region internal/private methods
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name,
IEnumerable<SdSelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
{
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
if (String.IsNullOrEmpty(fullName))
throw new ArgumentException("No name");
if (selectList == null)
throw new ArgumentException("No selectlist");
object defaultValue = (allowMultiple)
? htmlHelper.GetModelStateValue(fullName, typeof(string[]))
: htmlHelper.GetModelStateValue(fullName, typeof(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 (defaultValue == null)
defaultValue = htmlHelper.ViewData.Eval(fullName);
if (defaultValue != null)
{
IEnumerable defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
IEnumerable<string> values = from object value in defaultValues
select Convert.ToString(value, CultureInfo.CurrentCulture);
HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
List<SdSelectListItem> newSelectList = new List<SdSelectListItem>();
foreach (SdSelectListItem item in selectList)
{
item.Selected = (item.Value != null)
? selectedValues.Contains(item.Value)
: selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
selectList = newSelectList;
}
// Convert each ListItem to an <option> tag
StringBuilder listItemBuilder = new StringBuilder();
// Make optionLabel the first item that gets rendered.
if (optionLabel != null)
listItemBuilder.Append(
ListItemToOption(new SdSelectListItem()
{
Text = optionLabel,
Value = String.Empty,
Selected = false
}));
foreach (SdSelectListItem item in selectList)
{
listItemBuilder.Append(ListItemToOption(item));
}
TagBuilder tagBuilder = new TagBuilder("select")
{
InnerHtml = listItemBuilder.ToString()
};
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
tagBuilder.GenerateId(fullName);
if (allowMultiple)
tagBuilder.MergeAttribute("multiple", "multiple");
// If there are any errors for a named field, we add the css attribute.
System.Web.Mvc.ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState))
{
if (modelState.Errors.Count > 0)
{
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
}
tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(fullName, metadata));
return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.Normal));
}
internal static string ListItemToOption(SdSelectListItem 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";
}
builder.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(item.HtmlAttributes));
return builder.ToString(TagRenderMode.Normal);
}
internal static object GetModelStateValue(this HtmlHelper htmlHelper, string key, Type destinationType)
{
System.Web.Mvc.ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(key, out modelState))
{
if (modelState.Value != null)
{
return modelState.Value.ConvertTo(destinationType, null /* culture */);
}
}
return null;
}
#endregion
}
}
Here is how I call it in the View (using Razor):
#Html.sdDDLFor(x => Model.action_priority, Model.actionPriorityDDL(), Model.action_priority, new
{
#id = "_Action_Priority_DDL",
#class = "form-control"
})
And finally here is the Model.actionPriorityDDL() method:
public List<SdSelectListItem> actionPriorityDDL()
{
action_priority_DDL = new List<SdSelectListItem>();
action_priority_DDL.Add(new SdSelectListItem
{
Value = StringRepository.ActionPriority.high,
Text = StringRepository.ActionPriority.high,
HtmlAttributes = new
{
#class = "lbl-action-priority-high"
}
}
);
action_priority_DDL.Add(new SdSelectListItem
{
Value = StringRepository.ActionPriority.medium,
Text = StringRepository.ActionPriority.medium,
HtmlAttributes = new
{
#class = "lbl-action-priority-medium"
}
}
);
action_priority_DDL.Add(new SdSelectListItem
{
Value = StringRepository.ActionPriority.low,
Text = StringRepository.ActionPriority.low,
HtmlAttributes = new
{
#class = "lbl-action-priority-low"
}
}
);
action_priority_DDL.Add(new SdSelectListItem
{
Value = StringRepository.ActionPriority.psar,
Text = StringRepository.ActionPriority.psar,
HtmlAttributes = new
{
#class = "lbl-action-priority-psar"
}
}
);
return action_priority_DDL;
}
This is caused by a former bug in MVC framework (http://aspnet.codeplex.com/workitem/8311; accessible here: https://web.archive.org/web/20131208041521/http://aspnet.codeplex.com/workitem/8311) that happens when using the DropDownListFor helper within a loop, i.e. the model property has an indexer (like in your example, where the generated select element has an attribute name="actionList[0].action_priority")
I resolved this by copying some code from here https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Html/SelectExtensions.cs
Specifically, in this method
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name,
IEnumerable<ExtendedSelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
replace this
if (selectList == null)
throw new ArgumentException("No selectlist");
with:
bool usedViewData = false;
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
Now, replace this
if (defaultValue == null)
defaultValue = htmlHelper.ViewData.Eval(fullName);
with:
if (defaultValue == null && !String.IsNullOrEmpty(name))
{
if (!usedViewData)
{
defaultValue = htmlHelper.ViewData.Eval(name);
}
else if (metadata != null)
{
defaultValue = metadata.Model;
}
}
Finally, you also need to add this method:
private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
{
object o = null;
if (htmlHelper.ViewData != null)
{
o = htmlHelper.ViewData.Eval(name);
}
if (o == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.HtmlHelper_MissingSelectData,
name,
"IEnumerable<SelectListItem>"));
}
IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.HtmlHelper_WrongSelectDataType,
name,
o.GetType().FullName,
"IEnumerable<SelectListItem>"));
}
return selectList;
}
replacing SelectListItem with your custom class (e.g. SdSelectListItem, or ExtendedSelectListItem , or whatever you named it )

How to add default attribute value of "class" in custom htmlhelper?

I try to create a custom textbox in mvc. I want to add default class value "text-uppercase".I also want add new class values in view.
Myhelper class is;
public static MvcHtmlString Custom_TextBox<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes)
{
ModelMetadata oModelMetadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
if (htmlAttributes == null)
{
htmlAttributes = new Dictionary<string, object>();
}
htmlAttributes.Add("type", "text");
htmlAttributes.Add("name", oModelMetadata.DisplayName);
htmlAttributes.Add("id", oModelMetadata.DisplayName);
htmlAttributes.Add("class", "text-uppercase");
return helper.TextBoxFor(expression, htmlAttributes);
}
And my view code:
#Html.Custom_TextBox(model => model.YazilimAdi,new { #class = "form-control" } )
Actually the error is: An item with the same key has already been added. "class".
How can i add my default class value "text-uppercase"?
if(htmlAttributes.ContainsKey("class"))
{
htmlAttributes["class"] += " text-uppercase";
}
else
{
htmlAttributes.Add("class", "text-uppercase");
}
and put this at the beginning of extension:
if (htmlAttributes == null)
{
htmlAttributes = new Dictionary<string, object>();
}

How to Use check box list in MVC

I need to display check box list with more than one options. User must select minimum one check box, user can select more than one check boxes.
I want to store values of all check boxes (selected) in one field as a string(Data base) with coma separated. This is not mandatory, mandatory is need to store multiple values of each selected check box. Alternate solutions are welcome.
Model
public class Member
{
public string Member_VehicalType { get; set; }
public IList<SelectListItem> Member_VehicalType_List { get; set; }
Controller
[HttpGet]
public ActionResult Create()
{
Member objMemberModel = new Member();
List<SelectListItem> vehical_Types = new List<SelectListItem>();
vehical_Types.Add(new SelectListItem { Text = "Two Wheeler", Value = "1" });
vehical_Types.Add(new SelectListItem { Text = "Four Wheeler", Value = "2" });
objMemberModel.Member_VehicalType_List = vehical_Types;
return View(objMemberModel);
How do I create view with #Html.CheckBoxFor( or #Html.CheckBox(
I recently had to deal with SelectListItem as a checkbox-list. I came up with the following HtmlExtensions, which might be helpful. These extensions provide the same functionality as the #Html.DropDownListFor(model => ...);
Usage: #Html.CheckBoxListFor(model => model.ModelMemberToPutValueIn, Model.Member_VehicalType_List)
public static class HtmlExtensions
{
public static MvcHtmlString CheckBoxListFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, IEnumerable<SelectListItem> items, object htmlAttributes = null)
{
var listName = ExpressionHelper.GetExpressionText(expression);
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
items = GetCheckboxListWithDefaultValues(metaData.Model, items);
return htmlHelper.CheckBoxList(listName, items, htmlAttributes);
}
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string listName, IEnumerable<SelectListItem> items, object htmlAttributes = null)
{
if (items == null) return null;
var container = new TagBuilder("div");
container.AddCssClass("checkbox-list");
container.MergeAttribute("id", HtmlHelper.GenerateIdFromName(listName));
foreach (var item in items)
{
var id = HtmlHelper.GenerateIdFromName(String.Join(".", listName, item.Text));
var div = new TagBuilder("div");
div.AddCssClass("checkbox");
var cb = new TagBuilder("input");
cb.MergeAttribute("type", "checkbox");
cb.MergeAttribute("id", id);
cb.MergeAttribute("name", listName);
cb.MergeAttribute("value", item.Value ?? item.Text);
if (item.Selected) cb.MergeAttribute("checked", "checked");
var label = new TagBuilder("label");
label.MergeAttribute("for", id);
label.MergeAttributes(new RouteValueDictionary(htmlAttributes), true);
label.InnerHtml = item.Text;
div.InnerHtml = cb.ToString(TagRenderMode.SelfClosing) + label;
container.InnerHtml += div;
}
return new MvcHtmlString(container.ToString());
}
private static IEnumerable<SelectListItem> GetCheckboxListWithDefaultValues(object defaultValues, IEnumerable<SelectListItem> selectList)
{
var defaultValuesList = defaultValues as IEnumerable;
if (defaultValuesList == null) return selectList;
var values = from object value in defaultValuesList select Convert.ToString(value, CultureInfo.CurrentCulture);
var selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
var newSelectList = new List<SelectListItem>();
foreach (var item in selectList)
{
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
return newSelectList;
}
}

Radio Button List not being generated from identical code

I am currently pulling my hair out. I have an ASP.NET MVC web site with two forms, both have radio buttons on them. On the first page (that works), the radio list appears just fine. However, on the second page the radio buttons are not even present in the source code. Here is the code chunk from the first page (BestTimeType is an Enum that I made):
//Back End
[DisplayName("Time:")]
public RadioButtonListViewModel<BestTimeType> BestTimeRadioList { get; set; }
public EvalModel()
{
BestTimeRadioList = new RadioButtonListViewModel<BestTimeType>
{
Id = "BestTime",
SelectedValue = BestTimeType.Afternoon,
ListItems = new List<RadioButtonListItem<BestTimeType>>
{
new RadioButtonListItem<BestTimeType>{Text = "Morning", Value = BestTimeType.Morning},
new RadioButtonListItem<BestTimeType>{Text = "Afternoon", Value = BestTimeType.Afternoon},
new RadioButtonListItem<BestTimeType>{Text = "Evening", Value = BestTimeType.Evening}
}
};
}
// Front End
<div class="grid_1 evalLabel reqField" style="padding-top: 5px;">
<%= Html.LabelFor(model => model.BestTimeRadioList)%>
</div>
<div class="grid_4" style="text-align: center; padding: 5px 0px 10px 0px;">
<%= Html.RadioButtonListFor(model => model.BestTimeRadioList) %>
</div>
Here is the code chunk for the second page:
//Back End
[Required(ErrorMessage = "*")]
[DisplayName("HS Diploma:")]
public RadioButtonListViewModel<bool> HsDiplomaRadioList { get; set; }
public EmploymentModel()
{
HsDiplomaRadioList = new RadioButtonListViewModel<bool>
{
Id = "HsDiploma",
SelectedValue = false,
ListItems = new List<RadioButtonListItem<bool>>
{
new RadioButtonListItem<bool> {Text = "Yes", Value = true},
new RadioButtonListItem<bool> {Text = "No", Value = false}
}
};
}
//Front End
<div class="grid_2 employLabel reqField">
<%= Html.LabelFor(model => model.HsDiplomaRadioList) %>
</div>
<div class="grid_3">
<%= Html.RadioButtonListFor(model => model.HsDiplomaRadioList)%>
<%= Html.ValidationMessageFor(model => model.HsDiplomaRadioList)%>
</div>
I also tried making a custom Enum for the HsDiplomaRadioList (Yes/No), but the radio did not show up either.
I must be missing something extremely stupid simple. If any more code is necessary, I will be glad to put them up.
Thanks in advance.
Edit
Here is the code for RadioButtonList:
public static class HtmlHelperExtensions
{
public static string RadioButtonListFor<TModel, TRadioButtonListValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, RadioButtonListViewModel<TRadioButtonListValue>>> expression) where TModel : class
{
return htmlHelper.RadioButtonListFor(expression, null);
}
public static string RadioButtonListFor<TModel, TRadioButtonListValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, RadioButtonListViewModel<TRadioButtonListValue>>> expression, object htmlAttributes) where TModel : class
{
return htmlHelper.RadioButtonListFor(expression, new RouteValueDictionary(htmlAttributes));
}
public static string RadioButtonListFor<TModel, TRadioButtonListValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, RadioButtonListViewModel<TRadioButtonListValue>>> expression, IDictionary<string, object> htmlAttributes) where TModel : class
{
var inputName = GetInputName(expression);
RadioButtonListViewModel<TRadioButtonListValue> radioButtonList = GetValue(htmlHelper, expression);
if (radioButtonList == null)
return String.Empty;
if (radioButtonList.ListItems == null)
return String.Empty;
var divTag = new TagBuilder("div");
divTag.MergeAttribute("id", inputName);
divTag.MergeAttribute("class", "radio");
foreach (var item in radioButtonList.ListItems)
{
var radioButtonTag = RadioButton(htmlHelper, inputName, new SelectListItem { Text = item.Text, Selected = item.Selected, Value = item.Value.ToString() }, htmlAttributes);
divTag.InnerHtml += radioButtonTag;
}
return string.Concat(divTag, htmlHelper.ValidationMessage(inputName, "*"));
}
public static string GetInputName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
if (expression.Body.NodeType == ExpressionType.Call)
{
var methodCallExpression = (MethodCallExpression)expression.Body;
string name = GetInputName(methodCallExpression);
return name.Substring(expression.Parameters[0].Name.Length + 1);
}
return expression.Body.ToString().Substring(expression.Parameters[0].Name.Length + 1);
}
private static string GetInputName(MethodCallExpression expression)
{
// p => p.Foo.Bar().Baz.ToString() => p.Foo OR throw...
var methodCallExpression = expression.Object as MethodCallExpression;
if (methodCallExpression != null)
{
return GetInputName(methodCallExpression);
}
return expression.Object.ToString();
}
public static string RadioButton(this HtmlHelper htmlHelper, string name, SelectListItem listItem,
IDictionary<string, object> htmlAttributes)
{
var inputIdSb = new StringBuilder();
inputIdSb.Append(name)
.Append("_")
.Append(listItem.Value);
var sb = new StringBuilder();
var builder = new TagBuilder("input");
if (listItem.Selected) builder.MergeAttribute("checked", "checked");
builder.MergeAttribute("type", "radio");
builder.MergeAttribute("value", listItem.Value);
builder.MergeAttribute("id", inputIdSb.ToString());
builder.MergeAttribute("name", name + ".SelectedValue");
builder.MergeAttributes(htmlAttributes);
sb.Append(builder.ToString(TagRenderMode.SelfClosing));
sb.Append(RadioButtonLabel(inputIdSb.ToString(), listItem.Text, htmlAttributes));
//sb.Append("<br>");
return sb.ToString();
}
public static string RadioButtonLabel(string inputId, string displayText,
IDictionary<string, object> htmlAttributes)
{
var labelBuilder = new TagBuilder("label");
labelBuilder.MergeAttribute("for", inputId);
labelBuilder.MergeAttributes(htmlAttributes);
labelBuilder.InnerHtml = displayText;
return labelBuilder.ToString(TagRenderMode.Normal);
}
public static TProperty GetValue<TModel, TProperty>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
TModel model = htmlHelper.ViewData.Model;
if (model == null)
{
return default(TProperty);
}
Func<TModel, TProperty> func = expression.Compile();
return func(model);
}
}
Ok finally got around to stack tracing (I should do this first, but I was in a rush last night) and found that for some reason model.HsDiplomaRadio is null. I will need to track down the cause of this.

Support for optgroup in dropdownlist .NET MVC?

Continuing on from this question programmatically creating a drop down list I would like my list to have several optgroup lists too. Is this currently possible?
I know I need to pass a selectList to a dropDownList but do not know how to add text, value, optgroup to the selectList.
I want the end result to produce:
<option value="">Please select</option>
<optgroup label="Option A">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</optgroup>
<optgroup label="Option B">
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</optgroup>
</option>
My extension is a bit more complicated, but it has all overloads as original DropDownList has.
Actually I've created it specially to be able to produce DropDownList with groups and to be transformed in to two joined selects with help of jDoubleSelect
using System;using System.Collections;using System.Collections.Generic;using System.Globalization; using System.Linq;using System.Linq.Expressions;using System.Text; using System.Web;using System.Web.Mvc;using System.Web.Routing;
public class GroupedSelectListItem : SelectListItem
{
public string GroupKey { get; set; }
public string GroupName { get; set; }
}
public static class HtmlHelpers
{
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name)
{
return DropDownListHelper(htmlHelper, name, null, null, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList)
{
return DropDownListHelper(htmlHelper, name, selectList, null, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, string optionLabel)
{
return DropDownListHelper(htmlHelper, name, null, optionLabel, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, IDictionary<string, object> htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, null, htmlAttributes);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, object htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, null, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, string optionLabel)
{
return DropDownListHelper(htmlHelper, name, selectList, optionLabel, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, optionLabel, htmlAttributes);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, object htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, null /* optionLabel */, null /* htmlAttributes */);
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, object htmlAttributes)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, IDictionary<string, object> htmlAttributes)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, null /* optionLabel */, htmlAttributes);
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, null /* htmlAttributes */);
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, object htmlAttributes)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes);
}
private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, string expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
return SelectInternal(htmlHelper, optionLabel, expression, selectList, false /* allowMultiple */, htmlAttributes);
}
// Helper methods
private static IEnumerable<GroupedSelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
{
object o = null;
if (htmlHelper.ViewData != null)
{
o = htmlHelper.ViewData.Eval(name);
}
if (o == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
"Missing Select Data"));
}
var selectList = o as IEnumerable<GroupedSelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
"Wrong Select DataType"));
}
return selectList;
}
internal static string ListItemToOption(GroupedSelectListItem item)
{
var 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);
}
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, string optionLabel, string name, IEnumerable<GroupedSelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)
{
name = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
if (String.IsNullOrEmpty(name))
{
throw new ArgumentException("Null Or Empty", "name");
}
bool usedViewData = false;
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(name, typeof(string[])) : htmlHelper.GetModelStateValue(name, typeof(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 (!usedViewData)
{
if (defaultValue == null)
{
defaultValue = htmlHelper.ViewData.Eval(name);
}
}
if (defaultValue != null)
{
var defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
var values = from object value in defaultValues select Convert.ToString(value, CultureInfo.CurrentCulture);
var selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
var newSelectList = new List<GroupedSelectListItem>();
foreach (var item in selectList)
{
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
selectList = newSelectList;
}
// Convert each ListItem to an <option> tag
var listItemBuilder = new StringBuilder();
// Make optionLabel the first item that gets rendered.
if (optionLabel != null)
{
listItemBuilder.AppendLine(ListItemToOption(new GroupedSelectListItem { Text = optionLabel, Value = String.Empty, Selected = false }));
}
foreach (var group in selectList.GroupBy(i => i.GroupKey))
{
string groupName = selectList.Where(i => i.GroupKey == group.Key).Select(it => it.GroupName).FirstOrDefault();
listItemBuilder.AppendLine(string.Format("<optgroup label=\"{0}\" value=\"{1}\">", groupName, group.Key));
foreach (GroupedSelectListItem item in group)
{
listItemBuilder.AppendLine(ListItemToOption(item));
}
listItemBuilder.AppendLine("</optgroup>");
}
var tagBuilder = new TagBuilder("select")
{
InnerHtml = listItemBuilder.ToString()
};
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("name", name, true /* replaceExisting */);
tagBuilder.GenerateId(name);
if (allowMultiple)
{
tagBuilder.MergeAttribute("multiple", "multiple");
}
// If there are any errors for a named field, we add the css attribute.
ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(name, out modelState))
{
if (modelState.Errors.Count > 0)
{
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
}
return MvcHtmlString.Create(tagBuilder.ToString());
}
internal static object GetModelStateValue(this HtmlHelper helper, string key, Type destinationType)
{
ModelState modelState;
if (helper.ViewData.ModelState.TryGetValue(key, out modelState))
{
if (modelState.Value != null)
{
return modelState.Value.ConvertTo(destinationType, null /* culture */);
}
}
return null;
}
}
This was added to ASP.Net MVC at version 5.2 and is now built-in.
The Group property on SelectListItem allows you to specify a group for each item:
New SelectList constructors also allow you to provide the name of the field that contains the group title on the supplied list of items.
The HtmlHelper DropDownList and DropDownListFor methods now generate optgroup elements based on the groups included on the list of items.
Easy!
I just write a extensions for do this, see it:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Routing;
namespace System.Web.Mvc.Html
{
public static class GroupDropListExtensions
{
public static string GroupDropList(this HtmlHelper helper, string name, IEnumerable<GroupDropListItem> data, string SelectedValue, object htmlAttributes)
{
if (data == null && helper.ViewData != null)
data = helper.ViewData.Eval(name) as IEnumerable<GroupDropListItem>;
if (data == null) return string.Empty;
var select = new TagBuilder("select");
if (htmlAttributes != null)
select.MergeAttributes(new RouteValueDictionary(htmlAttributes));
select.GenerateId(name);
var optgroupHtml = new StringBuilder();
var groups = data.ToList();
foreach (var group in data)
{
var groupTag = new TagBuilder("optgroup");
groupTag.Attributes.Add("label", helper.Encode( group.Name));
var optHtml = new StringBuilder();
foreach (var item in group.Items)
{
var option = new TagBuilder("option");
option.Attributes.Add("value", helper.Encode(item.Value));
if (SelectedValue != null && item.Value == SelectedValue)
option.Attributes.Add("selected", "selected");
option.InnerHtml = helper.Encode(item.Text);
optHtml.AppendLine(option.ToString(TagRenderMode.Normal));
}
groupTag.InnerHtml = optHtml.ToString();
optgroupHtml.AppendLine(groupTag.ToString(TagRenderMode.Normal));
}
select.InnerHtml = optgroupHtml.ToString();
return select.ToString(TagRenderMode.Normal);
}
}
public class GroupDropListItem
{
public string Name { get; set; }
public List<OptionItem> Items { get; set; }
}
public class OptionItem
{
public string Text { get; set; }
public string Value { get; set; }
}
}
Looking through the code on www.codeplex.com/aspnet, it doesn't appear that neither the SelectList nor the DropDownList extension method supports the use of OptGroup in a select. Looks like you'll need to write your own extension method and extend SelectListItem to contain the grouping or generate the select manually in markup.
Data annotations for client validations missing?
In answer to Chrno Love's question above, adding to Serge Zab's solution - Milimetric's fix for MVC3 clientside validation attributes when using a collection of dropdowns in the same view:
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>>
expression,
IEnumerable<GroupedSelectListItem>
selectList, string optionLabel,
IDictionary<string, object> htmlAttributes)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
// fixing clientside validation attributes
// http://stackoverflow.com/questions/4799958/asp-net-mvc-3-unobtrusive-client-validation-does-not-work-with-drop-down-lists/8102022#8102022
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var mergedAttributes =
htmlHelper.GetUnobtrusiveValidationAttributes(ExpressionHelper.GetExpressionText(expression), metadata);
if (htmlAttributes != null)
{
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(htmlAttributes))
{
object value = descriptor.GetValue(htmlAttributes);
mergedAttributes.Add(descriptor.Name, value);
}
}
//return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes);
return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, mergedAttributes);
}
Serge Zab's answer was exactly what I was looking for. Being a die hard VB programmer I ported it to this VB module:
'based on Serge Zab's 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
I have been trying #Serge Zab solution out which works well but was having some trouble with the unobtrusive validation, after some inspection I found the problem.
There seems to be some required attributes missing off the select element if you want the Jquery validation to fire, just after you create your TagBuilder
TagBuilder tagBuilder = new TagBuilder("select");
Add these attributes
tagBuilder.MergeAttribute("data-val", "true",true);
tagBuilder.MergeAttribute("data-val-required", "your validation message", true)
and the unobtrusive validation should fire.
In Serge Zab answer, data attributes, like data_valuename, dont marging in correct form data-valuename.
I replaced code tagBuilder.MergeAttributes(htmlAttributes); in SelectInternal method to this
foreach (var htmlAttribute in htmlAttributes)
{
tagBuilder.MergeAttribute(
htmlAttribute.Key.Replace('_', '-'),
(string)htmlAttribute.Value
);
}
Just a note on using Serge's extension to create more than one select list on the same form. I was having problems getting the second select list to apply the groups, and when I examined the html being generated I realized both had been given the same id. To fix this, go into the SelectInternal function in serge's extension and comment out/delete the following two lines:
tagBuilder.MergeAttribute("name", name, true /* replaceExisting */);
tagBuilder.GenerateId(name);
Alternatively you can just pass unique id's to each (though the DropDownGroupListFor doesn't take the 'string name' parameter so you'll have to add an overload that does)
I needed a solution to handle a multiple selection with optgroup, and I used Serge Zab solution. I've just two comments about it (too long for a comment).
Despite his claims, his solution does not support all existing overloads of DropDownListFor, since it doesn't support MultiSelectList or SelectList, oftenly used in models. But that's not hard to add these.
His solution was not working for me for Multiple selection to initialize selected/not selected items from model : the initial values were not affected.
I've just modified the following method :
private static MvcHtmlString DropDownListHelper(...)
{
return SelectInternal(htmlHelper, optionLabel, expression, selectList, false /* allowMultiple */, htmlAttributes);
}
To this :
private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, string expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
bool allowMultiple = htmlAttributes.ContainsKey("multiple");
return SelectInternal(htmlHelper, optionLabel, expression, selectList, allowMultiple, htmlAttributes);
}
And it worked as expected.
Of course the multiple attribute must be defined :
#Html.DropDownGroupListFor(m => m.Selected, Model.Values, new { multiple = "multiple" })
Thanks to Serge for his answer.

Resources