// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Web.Mvc.Properties; namespace System.Web.Mvc.Html { public static class SelectExtensions { // DropDownList public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name) { return DropDownList(htmlHelper, name, null /* selectList */, null /* optionLabel */, null /* htmlAttributes */); } public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, string optionLabel) { return DropDownList(htmlHelper, name, null /* selectList */, optionLabel, null /* htmlAttributes */); } public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable selectList) { return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, null /* htmlAttributes */); } public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable selectList, object htmlAttributes) { return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable selectList, IDictionary htmlAttributes) { return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, htmlAttributes); } public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable selectList, string optionLabel) { return DropDownList(htmlHelper, name, selectList, optionLabel, null /* htmlAttributes */); } public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable selectList, string optionLabel, object htmlAttributes) { return DropDownList(htmlHelper, name, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable selectList, string optionLabel, IDictionary htmlAttributes) { return DropDownListHelper(htmlHelper, metadata: null, expression: name, selectList: selectList, optionLabel: optionLabel, htmlAttributes: htmlAttributes); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString DropDownListFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList) { return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, null /* htmlAttributes */); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString DropDownListFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList, object htmlAttributes) { return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString DropDownListFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList, IDictionary htmlAttributes) { return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, htmlAttributes); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString DropDownListFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList, string optionLabel) { return DropDownListFor(htmlHelper, expression, selectList, optionLabel, null /* htmlAttributes */); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString DropDownListFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList, string optionLabel, object htmlAttributes) { return DropDownListFor(htmlHelper, expression, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")] [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString DropDownListFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList, string optionLabel, IDictionary htmlAttributes) { if (expression == null) { throw new ArgumentNullException("expression"); } ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); return DropDownListHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString EnumDropDownListFor(this HtmlHelper htmlHelper, Expression> expression) { return EnumDropDownListFor(htmlHelper, expression, optionLabel: null); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString EnumDropDownListFor(this HtmlHelper htmlHelper, Expression> expression, object htmlAttributes) { return EnumDropDownListFor(htmlHelper, expression, optionLabel: null, htmlAttributes: htmlAttributes); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString EnumDropDownListFor(this HtmlHelper htmlHelper, Expression> expression, IDictionary htmlAttributes) { return EnumDropDownListFor(htmlHelper, expression, optionLabel: null, htmlAttributes: htmlAttributes); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString EnumDropDownListFor(this HtmlHelper htmlHelper, Expression> expression, string optionLabel) { return EnumDropDownListFor(htmlHelper, expression, optionLabel, (IDictionary)null); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString EnumDropDownListFor(this HtmlHelper htmlHelper, Expression> expression, string optionLabel, object htmlAttributes) { return EnumDropDownListFor(htmlHelper, expression, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } // Unable to constrain TEnum. Cannot include IComparable, IConvertible, IFormattable because Nullable does // not implement those interfaces (and Int32 does). Enum alone is not compatible with expression restrictions // because that requires a cast from all enum types. And the struct generic constraint disallows passing a // Nullable expression. [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString EnumDropDownListFor(this HtmlHelper htmlHelper, Expression> expression, string optionLabel, IDictionary htmlAttributes) { if (expression == null) { throw Error.ArgumentNull("expression"); } ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); if (metadata == null) { throw Error.Argument("expression", MvcResources.SelectExtensions_InvalidExpressionParameterNoMetadata, expression.ToString()); } if (metadata.ModelType == null) { throw Error.Argument("expression", MvcResources.SelectExtensions_InvalidExpressionParameterNoModelType, expression.ToString()); } if (!EnumHelper.IsValidForEnumHelper(metadata.ModelType)) { string formatString; if (EnumHelper.HasFlags(metadata.ModelType)) { formatString = MvcResources.SelectExtensions_InvalidExpressionParameterTypeHasFlags; } else { formatString = MvcResources.SelectExtensions_InvalidExpressionParameterType; } throw Error.Argument("expression", formatString, metadata.ModelType.FullName, "Flags"); } // Run through same processing as SelectInternal() to determine selected value and ensure it is included // in the select list. string expressionName = ExpressionHelper.GetExpressionText(expression); string expressionFullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionName); Enum currentValue = null; if (!String.IsNullOrEmpty(expressionFullName)) { currentValue = htmlHelper.GetModelStateValue(expressionFullName, metadata.ModelType) as Enum; } if (currentValue == null && !String.IsNullOrEmpty(expressionName)) { // Ignore any select list (enumerable with this name) in the view data currentValue = htmlHelper.ViewData.Eval(expressionName) as Enum; } if (currentValue == null) { currentValue = metadata.Model as Enum; } IList selectList = EnumHelper.GetSelectList(metadata.ModelType, currentValue); if (!String.IsNullOrEmpty(optionLabel) && selectList.Count != 0 && String.IsNullOrEmpty(selectList[0].Text)) { // Were given an optionLabel and the select list has a blank initial slot. Combine. selectList[0].Text = optionLabel; // Use the option label just once; don't pass it down the lower-level helpers. optionLabel = null; } return DropDownListHelper(htmlHelper, metadata, expressionName, selectList, optionLabel, htmlAttributes); } private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string expression, IEnumerable selectList, string optionLabel, IDictionary htmlAttributes) { return SelectInternal(htmlHelper, metadata, optionLabel, expression, selectList, allowMultiple: false, htmlAttributes: htmlAttributes); } // ListBox public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name) { return ListBox(htmlHelper, name, null /* selectList */, null /* htmlAttributes */); } public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable selectList) { return ListBox(htmlHelper, name, selectList, (IDictionary)null); } public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable selectList, object htmlAttributes) { return ListBox(htmlHelper, name, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable selectList, IDictionary htmlAttributes) { return ListBoxHelper(htmlHelper, metadata: null, name: name, selectList: selectList, htmlAttributes: htmlAttributes); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString ListBoxFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList) { return ListBoxFor(htmlHelper, expression, selectList, null /* htmlAttributes */); } [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString ListBoxFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList, object htmlAttributes) { return ListBoxFor(htmlHelper, expression, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")] [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] public static MvcHtmlString ListBoxFor(this HtmlHelper htmlHelper, Expression> expression, IEnumerable selectList, IDictionary htmlAttributes) { if (expression == null) { throw new ArgumentNullException("expression"); } ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); return ListBoxHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), selectList, htmlAttributes); } private static MvcHtmlString ListBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, IEnumerable selectList, IDictionary htmlAttributes) { return SelectInternal(htmlHelper, metadata, optionLabel: null, name: name, selectList: selectList, allowMultiple: true, htmlAttributes: htmlAttributes); } // Helper methods private static IEnumerable GetSelectData(this HtmlHelper htmlHelper, string name) { object o = null; if (htmlHelper.ViewData != null && !String.IsNullOrEmpty(name)) { o = htmlHelper.ViewData.Eval(name); } if (o == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, MvcResources.HtmlHelper_MissingSelectData, name, "IEnumerable")); } IEnumerable selectList = o as IEnumerable; if (selectList == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, MvcResources.HtmlHelper_WrongSelectDataType, name, o.GetType().FullName, "IEnumerable")); } return selectList; } 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"; } if (item.Disabled) { builder.Attributes["disabled"] = "disabled"; } return builder.ToString(TagRenderMode.Normal); } private static IEnumerable GetSelectListWithDefaultValue(IEnumerable selectList, object defaultValue, bool allowMultiple) { IEnumerable defaultValues; if (allowMultiple) { defaultValues = defaultValue as IEnumerable; if (defaultValues == null || defaultValues is string) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, MvcResources.HtmlHelper_SelectExpressionNotEnumerable, "expression")); } } else { defaultValues = new[] { defaultValue }; } IEnumerable values = from object value in defaultValues select Convert.ToString(value, CultureInfo.CurrentCulture); // ToString() by default returns an enum value's name. But selectList may use numeric values. IEnumerable enumValues = from Enum value in defaultValues.OfType() select value.ToString("d"); values = values.Concat(enumValues); HashSet selectedValues = new HashSet(values, StringComparer.OrdinalIgnoreCase); List newSelectList = new List(); foreach (SelectListItem item in selectList) { item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text); newSelectList.Add(item); } return newSelectList; } private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name, IEnumerable selectList, bool allowMultiple, IDictionary htmlAttributes) { string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); if (String.IsNullOrEmpty(fullName)) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "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(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) { if (!usedViewData && !String.IsNullOrEmpty(name)) { defaultValue = htmlHelper.ViewData.Eval(name); } else if (metadata != null) { defaultValue = metadata.Model; } } if (defaultValue != null) { selectList = GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple); } // Convert each ListItem to an if requested. StringBuilder listItemBuilder = BuildItems(optionLabel, selectList); 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. ModelState modelState; if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState)) { if (modelState.Errors.Count > 0) { tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); } } tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata)); return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal); } private static StringBuilder BuildItems(string optionLabel, IEnumerable selectList) { StringBuilder listItemBuilder = new StringBuilder(); // Make optionLabel the first item that gets rendered. if (optionLabel != null) { listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false })); } // Group items in the SelectList if requested. // Treat each item with Group == null as a member of a unique group // so they are added according to the original order. IEnumerable> groupedSelectList = selectList.GroupBy( i => (i.Group == null) ? i.GetHashCode() : i.Group.GetHashCode()); foreach (IGrouping group in groupedSelectList) { SelectListGroup optGroup = group.First().Group; // Wrap if requested. TagBuilder groupBuilder = null; if (optGroup != null) { groupBuilder = new TagBuilder("optgroup"); if (optGroup.Name != null) { groupBuilder.MergeAttribute("label", optGroup.Name); } if (optGroup.Disabled) { groupBuilder.MergeAttribute("disabled", "disabled"); } listItemBuilder.AppendLine(groupBuilder.ToString(TagRenderMode.StartTag)); } foreach (SelectListItem item in group) { listItemBuilder.AppendLine(ListItemToOption(item)); } if (optGroup != null) { listItemBuilder.AppendLine(groupBuilder.ToString(TagRenderMode.EndTag)); } } return listItemBuilder; } } }