From 246bb2e3dd39ede7868c14d11b5e078fc5d5f658 Mon Sep 17 00:00:00 2001 From: dougbu Date: Thu, 10 Apr 2014 18:17:33 -0700 Subject: [PATCH] Add `DropDownList()` and `DropDownListFor()` HTML helpers - copy over legacy MVC's `SelectExtensions`, `SelectListItem` and `SelectListGroup` - plus expected `SelectList` and `MultiSelectList` - fixup select HTML helpers to meet WebFx standards and work in new world - usual stuff: `[NotNull]`, `var`, `String` -> `string`, long lines, ... - remove `IDictionary htmlAttributes` overloads - move longest extension method overloads into correct classes / interfaces - add `ViewDataEvaluator.Eval()` overload for an `object` container - rename lower-level helpers to make purposes more obvious - nit: move Raw() methods up from bottom of HtmlHelper.cs - use `DropDownList[For]()` in MVC sample --- .../Areas/Travel/Views/Flight/Fly.cshtml | 2 +- samples/MvcSample.Web/HomeController.cs | 40 ++- .../MvcSample.Web/Views/Home/Create.cshtml | 40 +++ .../Views/Shared/HelloWorldPartial.cshtml | 2 +- .../Microsoft.AspNet.Mvc.Core.kproj | 5 + .../Properties/Resources.Designer.cs | 48 ++++ .../Expressions/ViewDataEvaluator.cs | 6 + .../Rendering/Html/HtmlHelper.cs | 245 +++++++++++++++++- .../Rendering/Html/HtmlHelperOfT.cs | 10 + .../Rendering/HtmlHelperSelectExtensions.cs | 59 +++++ .../Rendering/IHtmlHelperOfT.cs | 38 ++- .../Rendering/MultiSelectList.cs | 180 +++++++++++++ .../Rendering/SelectList.cs | 63 +++++ .../Rendering/SelectListGroup.cs | 21 ++ .../Rendering/SelectListItem.cs | 24 ++ src/Microsoft.AspNet.Mvc.Core/Resources.resx | 9 + 16 files changed, 778 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperSelectExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/MultiSelectList.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/SelectList.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListGroup.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListItem.cs diff --git a/samples/MvcSample.Web/Areas/Travel/Views/Flight/Fly.cshtml b/samples/MvcSample.Web/Areas/Travel/Views/Flight/Fly.cshtml index 14a1ef94ce..7dfa432720 100644 --- a/samples/MvcSample.Web/Areas/Travel/Views/Flight/Fly.cshtml +++ b/samples/MvcSample.Web/Areas/Travel/Views/Flight/Fly.cshtml @@ -14,7 +14,7 @@

Back to the main Area.

Takes you out of the area implicitly.

-

Go to Home/Edit

+

Go to Home/Create

Go to another action in the Area.

diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs index e67de46f67..f7ff8977b1 100644 --- a/samples/MvcSample.Web/HomeController.cs +++ b/samples/MvcSample.Web/HomeController.cs @@ -1,10 +1,16 @@ -using Microsoft.AspNet.Mvc; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Rendering; using MvcSample.Web.Models; namespace MvcSample.Web { public class HomeController : Controller { + private static readonly IEnumerable _addresses = CreateAddresses(); + private static readonly IEnumerable _ages = CreateAges(); + public IActionResult Index() { return View("MyView", User()); @@ -22,17 +28,21 @@ namespace MvcSample.Web /// public IActionResult Create() { + ViewBag.Address = _addresses; + ViewBag.Ages = _ages; + return View(); } /// /// Action that shows metadata when model is non-null. /// - /// - public IActionResult Edit() + public IActionResult Edit(User user) { + ViewBag.Address = _addresses; + ViewBag.Age = _ages; ViewBag.Gift = "the banana"; - ViewData.Model = new User { Name = "Name", Address = "Address in a State", Age = 37, }; + return View("Create"); } @@ -98,5 +108,27 @@ namespace MvcSample.Web { return View(User()); } + + private static IEnumerable CreateAddresses() + { + var addresses = new[] + { + "121 Fake St., Redmond, WA, USA", + "123 Fake St., Redmond, WA, USA", + "125 Fake St., Redmond, WA, USA", + "127 Fake St., Redmond, WA, USA", + "129 Fake St., Redmond, WA, USA", + "131 Fake St., Redmond, WA, USA", + }; + + return new SelectList(addresses); + } + + private static IEnumerable CreateAges() + { + var ages = Enumerable.Range(27, 47).Select(age => new { Age = age, Display = age.ToString("####"), }); + + return new SelectList(ages, dataValueField: "Age", dataTextField: "Display"); + } } } \ No newline at end of file diff --git a/samples/MvcSample.Web/Views/Home/Create.cshtml b/samples/MvcSample.Web/Views/Home/Create.cshtml index 1220da9603..289f4a39e2 100644 --- a/samples/MvcSample.Web/Views/Home/Create.cshtml +++ b/samples/MvcSample.Web/Views/Home/Create.cshtml @@ -38,6 +38,46 @@ }
+
+
+ @using (Html.BeginForm(controllerName: "Home", actionName: "Edit", method: FormMethod.Post)) + { + + + + + + + + + + + + + + + + + +
+ + + @Html.TextBox("Name") +
+ + + @Html.DropDownList("Address", "Select an Address") +
+ + + @Html.DropDownListFor(model => model.Age, (IEnumerable)ViewBag.Ages, htmlAttributes: new { @class = "form-control" }) +
+ +
+ } +
+
+ @helper PropertyListItem(ModelMetadata property) { var propertyName = property.PropertyName; diff --git a/samples/MvcSample.Web/Views/Shared/HelloWorldPartial.cshtml b/samples/MvcSample.Web/Views/Shared/HelloWorldPartial.cshtml index e211035aab..b3caac0643 100644 --- a/samples/MvcSample.Web/Views/Shared/HelloWorldPartial.cshtml +++ b/samples/MvcSample.Web/Views/Shared/HelloWorldPartial.cshtml @@ -3,4 +3,4 @@ Hello @Model.Name from Partial -Edit Something! \ No newline at end of file +Create Something! \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index db0cce19de..8b94db3a49 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -141,6 +141,7 @@ + @@ -156,7 +157,11 @@ + + + + diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 64f9b102d9..f6b97780d3 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -426,6 +426,54 @@ namespace Microsoft.AspNet.Mvc.Core return GetString("HtmlHelper_NotContextualized"); } + /// + /// There is no ViewData item of type '{0}' that has the key '{1}'. + /// + internal static string HtmlHelper_MissingSelectData + { + get { return GetString("HtmlHelper_MissingSelectData"); } + } + + /// + /// There is no ViewData item of type '{0}' that has the key '{1}'. + /// + internal static string FormatHtmlHelper_MissingSelectData(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_MissingSelectData"), p0, p1); + } + + /// + /// The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed. + /// + internal static string HtmlHelper_SelectExpressionNotEnumerable + { + get { return GetString("HtmlHelper_SelectExpressionNotEnumerable"); } + } + + /// + /// The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed. + /// + internal static string FormatHtmlHelper_SelectExpressionNotEnumerable(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_SelectExpressionNotEnumerable"), p0); + } + + /// + /// The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'. + /// + internal static string HtmlHelper_WrongSelectDataType + { + get { return GetString("HtmlHelper_WrongSelectDataType"); } + } + + /// + /// The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'. + /// + internal static string FormatHtmlHelper_WrongSelectDataType(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_WrongSelectDataType"), p0, p1, p2); + } + /// /// Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ViewDataEvaluator.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ViewDataEvaluator.cs index ff772a6335..abc9f250b5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ViewDataEvaluator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Expressions/ViewDataEvaluator.cs @@ -20,6 +20,12 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions return EvalComplexExpression(viewData, expression); } + public static ViewDataInfo Eval(object indexableObject, [NotNull] string expression) + { + // Run through same cases as other Eval() overload but allow a null container. + return (indexableObject == null) ? null : EvalComplexExpression(indexableObject, expression); + } + private static ViewDataInfo EvalComplexExpression(object indexableObject, string expression) { foreach (var expressionPair in GetRightToLeftExpressions(expression)) diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index 6b8781ef33..72f3938a12 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -1,10 +1,11 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; +using System.Linq; using System.Net; -using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; @@ -224,6 +225,17 @@ namespace Microsoft.AspNet.Mvc.Rendering additionalViewData); } + public HtmlString DropDownList(string name, IEnumerable selectList, string optionLabel, + object htmlAttributes) + { + return GenerateDropDown( + metadata: null, + expression: name, + selectList: selectList, + optionLabel: optionLabel, + htmlAttributes: htmlAttributes); + } + public HtmlString Hidden(string name, object value, object htmlAttributes) { return GenerateHidden(metadata: null, name: name, value: value, useViewData: (value == null), @@ -304,6 +316,16 @@ namespace Microsoft.AspNet.Mvc.Rendering htmlAttributes: htmlAttributes); } + public HtmlString Raw(string value) + { + return new HtmlString(value); + } + + public HtmlString Raw(object value) + { + return new HtmlString(value == null ? null : value.ToString()); + } + public virtual HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary htmlAttributes) { var formContext = ViewContext.ClientValidationEnabled ? ViewContext.FormContext : null; @@ -522,6 +544,13 @@ namespace Microsoft.AspNet.Mvc.Rendering htmlAttributes: htmlAttributeDictionary); } + protected HtmlString GenerateDropDown(ModelMetadata metadata, string expression, + IEnumerable selectList, string optionLabel, object htmlAttributes) + { + return GenerateSelect(metadata, optionLabel, expression, selectList, allowMultiple: false, + htmlAttributes: htmlAttributes); + } + /// /// Writes an opening
tag to the response. When the user submits the form, /// the request will be processed by an action method. @@ -713,6 +742,78 @@ namespace Microsoft.AspNet.Mvc.Rendering htmlAttributes: htmlAttributeDictionary); } + protected virtual HtmlString GenerateSelect(ModelMetadata metadata, + string optionLabel, string name, IEnumerable selectList, bool allowMultiple, + object htmlAttributes) + { + var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); + if (string.IsNullOrEmpty(fullName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "name"); + } + + var usedViewData = false; + + // If we got a null selectList, try to use ViewData to get the list of items. + if (selectList == null) + { + selectList = GetSelectListItems(name); + usedViewData = true; + } + + var defaultValue = (allowMultiple) ? + GetModelStateValue(fullName, typeof(string[])) : + 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 && !string.IsNullOrEmpty(name)) + { + if (!usedViewData) + { + defaultValue = ViewData.Eval(name); + } + else if (metadata != null) + { + defaultValue = metadata.Model; + } + } + + if (defaultValue != null) + { + selectList = UpdateSelectListItemsWithDefaultValue(selectList, defaultValue, allowMultiple); + } + + // Convert each ListItem to an if requested. + var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList); + + var tagBuilder = new TagBuilder("select") + { + InnerHtml = listItemBuilder.ToString() + }; + tagBuilder.MergeAttributes(AnonymousObjectToHtmlAttributes(htmlAttributes)); + tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */); + tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); + if (allowMultiple) + { + tagBuilder.MergeAttribute("multiple", "multiple"); + } + + // If there are any errors for a named field, we add the css attribute. + ModelState modelState; + if (ViewData.ModelState.TryGetValue(fullName, out modelState)) + { + if (modelState.Errors.Count > 0) + { + tagBuilder.AddCssClass(ValidationInputCssClassName); + } + } + + tagBuilder.MergeAttributes(GetValidationAttributes(name, metadata)); + + return tagBuilder.ToHtmlString(TagRenderMode.Normal); + } + protected virtual HtmlString GenerateTextBox(ModelMetadata metadata, string name, object value, string format, IDictionary htmlAttributes) { @@ -838,10 +939,9 @@ namespace Microsoft.AspNet.Mvc.Rendering return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } - protected virtual HtmlString GenerateValue(string name, object value, string format, bool useViewData) { - var fullName = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); + var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); var attemptedValue = (string)GetModelStateValue(fullName, typeof(string)); string resolvedValue; @@ -892,14 +992,145 @@ namespace Microsoft.AspNet.Mvc.Rendering } } - public HtmlString Raw(string value) + private StringBuilder GenerateGroupsAndOptions(string optionLabel, IEnumerable selectList) { - return new HtmlString(value); + var listItemBuilder = new StringBuilder(); + + // Make optionLabel the first item that gets rendered. + if (optionLabel != null) + { + listItemBuilder.AppendLine(GenerateOption(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. + var groupedSelectList = selectList.GroupBy( + item => (item.Group == null) ? item.GetHashCode() : item.Group.GetHashCode()); + foreach (var group in groupedSelectList) + { + var 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 (var item in group) + { + listItemBuilder.AppendLine(GenerateOption(item)); + } + + if (optGroup != null) + { + listItemBuilder.AppendLine(groupBuilder.ToString(TagRenderMode.EndTag)); + } + } + + return listItemBuilder; } - public HtmlString Raw(object value) + private string GenerateOption(SelectListItem item) { - return new HtmlString(value == null ? null : value.ToString()); + var builder = new TagBuilder("option") + { + InnerHtml = Encode(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 IEnumerable GetSelectListItems(string name) + { + var value = ViewData.Eval(name); + if (value == null) + { + throw new InvalidOperationException(Resources.FormatHtmlHelper_MissingSelectData( + "IEnumerable", name)); + } + + var selectList = value as IEnumerable; + if (selectList == null) + { + throw new InvalidOperationException(Resources.FormatHtmlHelper_WrongSelectDataType( + name, value.GetType().FullName, "IEnumerable")); + } + + return selectList; + } + + private IEnumerable UpdateSelectListItemsWithDefaultValue( + IEnumerable selectList, + object defaultValue, + bool allowMultiple) + { + IEnumerable defaultValues; + if (allowMultiple) + { + defaultValues = defaultValue as IEnumerable; + if (defaultValues == null || defaultValues is string) + { + throw new InvalidOperationException( + Resources.FormatHtmlHelper_SelectExpressionNotEnumerable("expression")); + } + } + else + { + defaultValues = new[] { defaultValue }; + } + + var 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. + var enumValues = from Enum value in defaultValues.OfType() + select value.ToString("d"); + values = values.Concat(enumValues); + + var selectedValues = new HashSet(values, StringComparer.OrdinalIgnoreCase); + var 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; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs index c354fd1a3f..c2af9e14c2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs @@ -56,6 +56,16 @@ namespace Microsoft.AspNet.Mvc.Rendering htmlAttributes: htmlAttributes); } + /// + public HtmlString DropDownListFor([NotNull] Expression> expression, + IEnumerable selectList, string optionLabel, object htmlAttributes) + { + var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); + + return GenerateDropDown(metadata, ExpressionHelper.GetExpressionText(expression), selectList, + optionLabel, htmlAttributes); + } + /// public HtmlString DisplayFor([NotNull] Expression> expression, string templateName, diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperSelectExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperSelectExtensions.cs new file mode 100644 index 0000000000..02384df28c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperSelectExtensions.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public static class SelectExtensions + { + public static HtmlString DropDownList([NotNull] this IHtmlHelper htmlHelper, string name) + { + return htmlHelper.DropDownList(name, selectList: null, optionLabel: null, htmlAttributes: null); + } + + public static HtmlString DropDownList([NotNull] this IHtmlHelper htmlHelper, string name, + string optionLabel) + { + return htmlHelper.DropDownList(name, selectList: null, optionLabel: optionLabel, htmlAttributes: null); + } + + public static HtmlString DropDownList([NotNull] this IHtmlHelper htmlHelper, string name, + IEnumerable selectList) + { + return htmlHelper.DropDownList(name, selectList, optionLabel: null, htmlAttributes: null); + } + + public static HtmlString DropDownList([NotNull] this IHtmlHelper htmlHelper, string name, + IEnumerable selectList, object htmlAttributes) + { + return htmlHelper.DropDownList(name, selectList, optionLabel: null, htmlAttributes: htmlAttributes); + } + + public static HtmlString DropDownList([NotNull] this IHtmlHelper htmlHelper, string name, + IEnumerable selectList, string optionLabel) + { + return htmlHelper.DropDownList(name, selectList, optionLabel, htmlAttributes: null); + } + + public static HtmlString DropDownListFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, IEnumerable selectList) + { + return htmlHelper.DropDownListFor(expression, selectList, optionLabel: null, htmlAttributes: null); + } + + public static HtmlString DropDownListFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, IEnumerable selectList, + object htmlAttributes) + { + return htmlHelper.DropDownListFor(expression, selectList, optionLabel: null, + htmlAttributes: htmlAttributes); + } + + public static HtmlString DropDownListFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, IEnumerable selectList, + string optionLabel) + { + return htmlHelper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes: null); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs index a758809f80..07d11c07af 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs @@ -141,7 +141,6 @@ namespace Microsoft.AspNet.Mvc.Rendering /// Returns HTML markup for each property in the object that is represented by the specified expression, using the /// template, an HTML field ID, and additional view data. ///
- /// The type of the model. /// The type of the value. /// An expression that identifies the object that contains the properties to display. /// The name of the template that is used to render the object. @@ -175,6 +174,43 @@ namespace Microsoft.AspNet.Mvc.Rendering /// The HTML markup for each property in the model. HtmlString DisplayForModel(string templateName, string htmlFieldName, object additionalViewData); + /// + /// Returns a single-selection HTML {select} element using the specified name of the form field, + /// list items, option label, and HTML attributes. + /// + /// The name of the form field to return. + /// A collection of objects that are used to populate the + /// drop-down list. + /// The text for a default empty item. This parameter can be null. + /// An object that contains the HTML attributes to set for the {select} element. + /// Alternatively, an instance containing the HTML attributes. + /// + /// An HTML {select} element with an {option} subelement for each item in the list. + HtmlString DropDownList( + string name, + IEnumerable selectList, + string optionLabel, + object htmlAttributes); + + /// + /// Returns a single-selection HTML {select} element for the object that is represented + /// by the specified expression using the specified list items, option label, and HTML attributes. + /// + /// The type of the value. + /// An expression that identifies the value to display. + /// A collection of objects that are used to populate the + /// drop-down list. + /// The text for a default empty item. This parameter can be null. + /// An object that contains the HTML attributes to set for the {select} element. + /// Alternatively, an instance containing the HTML attributes. + /// + /// An HTML {select} element with an {option} subelement for each item in the list. + HtmlString DropDownListFor( + [NotNull] Expression> expression, + IEnumerable selectList, + string optionLabel, + object htmlAttributes); + /// /// Converts the value of the specified object to an HTML-encoded string. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/MultiSelectList.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/MultiSelectList.cs new file mode 100644 index 0000000000..ed39386d51 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/MultiSelectList.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Mvc.Rendering.Expressions; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class MultiSelectList : IEnumerable + { + private IList _groups; + + public MultiSelectList([NotNull] IEnumerable items) + : this(items, selectedValues: null) + { + } + + public MultiSelectList([NotNull] IEnumerable items, IEnumerable selectedValues) + : this(items, dataValueField: null, dataTextField: null, selectedValues: selectedValues) + { + } + + public MultiSelectList([NotNull] IEnumerable items, string dataValueField, string dataTextField) + : this(items, dataValueField, dataTextField, selectedValues: null) + { + } + + public MultiSelectList( + [NotNull] IEnumerable items, + string dataValueField, + string dataTextField, + IEnumerable selectedValues) + : this(items, dataValueField, dataTextField, selectedValues, dataGroupField: null) + { + } + + /// + /// Initializes a new instance of the MultiSelectList class by using the items to include in the list, + /// the data value field, the data text field, the selected values, and the data group field. + /// + /// The items used to build each of the list. + /// The data value field. Used to match the Value property of the corresponding + /// . + /// The data text field. Used to match the Text property of the corresponding + /// . + /// The selected values field. Used to match the Selected property of the + /// corresponding . + /// The data group field. Used to match the Group property of the corresponding + /// . + public MultiSelectList( + [NotNull] IEnumerable items, + string dataValueField, + string dataTextField, + IEnumerable selectedValues, + string dataGroupField) + { + Items = items; + DataValueField = dataValueField; + DataTextField = dataTextField; + SelectedValues = selectedValues; + DataGroupField = dataGroupField; + + if (DataGroupField != null) + { + _groups = new List(); + } + } + + /// + /// Gets or sets the data group field. + /// + public string DataGroupField { get; private set; } + + public string DataTextField { get; private set; } + + public string DataValueField { get; private set; } + + public IEnumerable Items { get; private set; } + + public IEnumerable SelectedValues { get; private set; } + + public virtual IEnumerator GetEnumerator() + { + return GetListItems().GetEnumerator(); + } + + internal IList GetListItems() + { + return (!string.IsNullOrEmpty(DataValueField)) ? + GetListItemsWithValueField() : + GetListItemsWithoutValueField(); + } + + private IList GetListItemsWithValueField() + { + var selectedValues = new HashSet(StringComparer.OrdinalIgnoreCase); + if (SelectedValues != null) + { + selectedValues.UnionWith(from object value in SelectedValues + select Convert.ToString(value, CultureInfo.CurrentCulture)); + } + + var listItems = from object item in Items + let value = Eval(item, DataValueField) + select new SelectListItem + { + Group = GetGroup(item), + Value = value, + Text = Eval(item, DataTextField), + Selected = selectedValues.Contains(value) + }; + return listItems.ToList(); + } + + private IList GetListItemsWithoutValueField() + { + var selectedValues = new HashSet(); + if (SelectedValues != null) + { + selectedValues.UnionWith(SelectedValues.Cast()); + } + + var listItems = from object item in Items + select new SelectListItem + { + Group = GetGroup(item), + Text = Eval(item, DataTextField), + Selected = selectedValues.Contains(item) + }; + return listItems.ToList(); + } + + private static string Eval(object container, string expression) + { + var value = container; + if (!string.IsNullOrEmpty(expression)) + { + var viewDataInfo = ViewDataEvaluator.Eval(container, expression); + value = viewDataInfo.Value; + } + + return Convert.ToString(value, CultureInfo.CurrentCulture); + } + + private SelectListGroup GetGroup(object container) + { + if (_groups == null) + { + return null; + } + + var groupName = Eval(container, DataGroupField); + if (string.IsNullOrEmpty(groupName)) + { + return null; + } + + // We use StringComparison.CurrentCulture because the group name is used to display as the value of + // optgroup HTML tag's label attribute. + var group = _groups.FirstOrDefault(g => string.Equals(g.Name, groupName, StringComparison.CurrentCulture)); + if (group == null) + { + group = new SelectListGroup() { Name = groupName }; + _groups.Add(group); + } + + return group; + } + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectList.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectList.cs new file mode 100644 index 0000000000..953b853da5 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectList.cs @@ -0,0 +1,63 @@ +using System.Collections; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class SelectList : MultiSelectList + { + public SelectList([NotNull] IEnumerable items) + : this(items, selectedValue: null) + { + } + + public SelectList([NotNull] IEnumerable items, object selectedValue) + : this(items, dataValueField: null, dataTextField: null, selectedValue: selectedValue) + { + } + + public SelectList([NotNull] IEnumerable items, string dataValueField, string dataTextField) + : this(items, dataValueField, dataTextField, selectedValue: null) + { + } + + public SelectList( + [NotNull] IEnumerable items, + string dataValueField, + string dataTextField, + object selectedValue) + : base(items, dataValueField, dataTextField, ToEnumerable(selectedValue)) + { + SelectedValue = selectedValue; + } + + /// + /// Initializes a new instance of the SelectList class by using the specified items for the list, + /// the data value field, the data text field, a selected value, and the data group field. + /// + /// The items used to build each of the list. + /// The data value field. Used to match the Value property of the corresponding + /// . + /// The data text field. Used to match the Text property of the corresponding + /// . + /// The selected values. Used to match the Selected property of the corresponding + /// . + /// The data group field. Used to match the Group property of the corresponding + /// . + public SelectList( + [NotNull] IEnumerable items, + string dataValueField, + string dataTextField, + object selectedValue, + string dataGroupField) + : base(items, dataValueField, dataTextField, ToEnumerable(selectedValue), dataGroupField) + { + SelectedValue = selectedValue; + } + + public object SelectedValue { get; private set; } + + private static IEnumerable ToEnumerable(object selectedValue) + { + return (selectedValue != null) ? new[] { selectedValue } : null; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListGroup.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListGroup.cs new file mode 100644 index 0000000000..5e8c4d0f12 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListGroup.cs @@ -0,0 +1,21 @@ + +namespace Microsoft.AspNet.Mvc.Rendering +{ + /// + /// Represents the optgroup HTML element and its attributes. + /// In a select list, multiple groups with the same name are supported. + /// They are compared with reference equality. + /// + public class SelectListGroup + { + /// + /// Gets or sets a value that indicates whether this is disabled. + /// + public bool Disabled { get; set; } + + /// + /// Represents the value of the optgroup's label. + /// + public string Name { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListItem.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListItem.cs new file mode 100644 index 0000000000..3a6b15e889 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/SelectListItem.cs @@ -0,0 +1,24 @@ + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class SelectListItem + { + /// + /// Gets or sets a value that indicates whether this is disabled. + /// + public bool Disabled { get; set; } + + /// + /// Represents the optgroup HTML element this item is wrapped into. + /// In a select list, multiple groups with the same name are supported. + /// They are compared with reference equality. + /// + public SelectListGroup Group { get; set; } + + public bool Selected { get; set; } + + public string Text { get; set; } + + public string Value { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 93319628df..2f6151a79f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -195,6 +195,15 @@ Must call 'Contextualize' method before using this HtmlHelper instance. + + There is no ViewData item of type '{0}' that has the key '{1}'. + + + The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed. + + + The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'. + Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.