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<string, object> 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
This commit is contained in:
dougbu 2014-04-10 18:17:33 -07:00
parent 271c849923
commit 246bb2e3dd
16 changed files with 778 additions and 14 deletions

View File

@ -14,7 +14,7 @@
<div class="col-md-4">
<h2>Back to the main Area.</h2>
<p>Takes you out of the area implicitly.</p>
<p><a class="btn btn-default" href="@Url.Action("Edit", "Home")">Go to Home/Edit</a></p>
<p><a class="btn btn-default" href="@Url.Action("Create", "Home")">Go to Home/Create</a></p>
</div>
<div class="col-md-4">
<h2>Go to another action in the Area.</h2>

View File

@ -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<SelectListItem> _addresses = CreateAddresses();
private static readonly IEnumerable<SelectListItem> _ages = CreateAges();
public IActionResult Index()
{
return View("MyView", User());
@ -22,17 +28,21 @@ namespace MvcSample.Web
/// </summary>
public IActionResult Create()
{
ViewBag.Address = _addresses;
ViewBag.Ages = _ages;
return View();
}
/// <summary>
/// Action that shows metadata when model is non-<c>null</c>.
/// </summary>
/// <returns></returns>
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<SelectListItem> 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<SelectListItem> CreateAges()
{
var ages = Enumerable.Range(27, 47).Select(age => new { Age = age, Display = age.ToString("####"), });
return new SelectList(ages, dataValueField: "Age", dataTextField: "Display");
}
}
}

View File

@ -38,6 +38,46 @@
}
</div>
<div class="row">
<div style="float: left; border: thick solid limegreen; padding-right: 10px">
@using (Html.BeginForm(controllerName: "Home", actionName: "Edit", method: FormMethod.Post))
{
<table>
<tr>
<td>
<label class="control-label col-md-2">Model.Name</label>
</td>
<td>
@Html.TextBox("Name")
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Model.Address</label>
</td>
<td>
@Html.DropDownList("Address", "Select an Address")
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Model.Age</label>
</td>
<td>
@Html.DropDownListFor(model => model.Age, (IEnumerable<SelectListItem>)ViewBag.Ages, htmlAttributes: new { @class = "form-control" })
</td>
</tr>
<tr>
<td>
<input type="submit" value="Save" class="btn btn-default" style="margin-left: 10px" />
</td>
<td></td>
</tr>
</table>
}
</div>
</div>
@helper PropertyListItem(ModelMetadata property)
{
var propertyName = property.PropertyName;

View File

@ -3,4 +3,4 @@
<strong>Hello @Model.Name from Partial</strong>
<a href="@Url.Action("Edit", "Home")">Edit Something!</a>
<a href="@Url.Action("Create", "Home")">Create Something!</a>

View File

@ -141,6 +141,7 @@
<Compile Include="Rendering\HtmlHelperLinkExtensions.cs" />
<Compile Include="Rendering\HtmlHelperNameExtensions.cs" />
<Compile Include="Rendering\HtmlHelperPartialExtensions.cs" />
<Compile Include="Rendering\HtmlHelperSelectExtensions.cs" />
<Compile Include="Rendering\HtmlHelperValidationExtensions.cs" />
<Compile Include="Rendering\HtmlHelperValueExtensions.cs" />
<Compile Include="Rendering\HtmlString.cs" />
@ -156,7 +157,11 @@
<Compile Include="Rendering\IHtmlHelperOfT.cs" />
<Compile Include="Rendering\IView.cs" />
<Compile Include="Rendering\IViewEngine.cs" />
<Compile Include="Rendering\MultiSelectList.cs" />
<Compile Include="Rendering\MvcForm.cs" />
<Compile Include="Rendering\SelectList.cs" />
<Compile Include="Rendering\SelectListGroup.cs" />
<Compile Include="Rendering\SelectListItem.cs" />
<Compile Include="Rendering\ViewEngineResult.cs" />
<Compile Include="RouteConstraintAttribute.cs" />
<Compile Include="RouteDataActionConstraint.cs" />

View File

@ -426,6 +426,54 @@ namespace Microsoft.AspNet.Mvc.Core
return GetString("HtmlHelper_NotContextualized");
}
/// <summary>
/// There is no ViewData item of type '{0}' that has the key '{1}'.
/// </summary>
internal static string HtmlHelper_MissingSelectData
{
get { return GetString("HtmlHelper_MissingSelectData"); }
}
/// <summary>
/// There is no ViewData item of type '{0}' that has the key '{1}'.
/// </summary>
internal static string FormatHtmlHelper_MissingSelectData(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_MissingSelectData"), p0, p1);
}
/// <summary>
/// The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed.
/// </summary>
internal static string HtmlHelper_SelectExpressionNotEnumerable
{
get { return GetString("HtmlHelper_SelectExpressionNotEnumerable"); }
}
/// <summary>
/// The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed.
/// </summary>
internal static string FormatHtmlHelper_SelectExpressionNotEnumerable(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_SelectExpressionNotEnumerable"), p0);
}
/// <summary>
/// The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.
/// </summary>
internal static string HtmlHelper_WrongSelectDataType
{
get { return GetString("HtmlHelper_WrongSelectDataType"); }
}
/// <summary>
/// The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.
/// </summary>
internal static string FormatHtmlHelper_WrongSelectDataType(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_WrongSelectDataType"), p0, p1, p2);
}
/// <summary>
/// Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.
/// </summary>

View File

@ -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))

View File

@ -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<SelectListItem> 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<string, object> 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<SelectListItem> selectList, string optionLabel, object htmlAttributes)
{
return GenerateSelect(metadata, optionLabel, expression, selectList, allowMultiple: false,
htmlAttributes: htmlAttributes);
}
/// <summary>
/// Writes an opening <form> 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<SelectListItem> 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 <option> tag and wrap them with <optgroup> 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<string, object> 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<SelectListItem> 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<SelectListItem, int>(
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<SelectListItem> GetSelectListItems(string name)
{
var value = ViewData.Eval(name);
if (value == null)
{
throw new InvalidOperationException(Resources.FormatHtmlHelper_MissingSelectData(
"IEnumerable<SelectListItem>", name));
}
var selectList = value as IEnumerable<SelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(Resources.FormatHtmlHelper_WrongSelectDataType(
name, value.GetType().FullName, "IEnumerable<SelectListItem>"));
}
return selectList;
}
private IEnumerable<SelectListItem> UpdateSelectListItemsWithDefaultValue(
IEnumerable<SelectListItem> 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<Enum>()
select value.ToString("d");
values = values.Concat(enumValues);
var selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
var newSelectList = new List<SelectListItem>();
foreach (SelectListItem item in selectList)
{
item.Selected = (item.Value != null) ?
selectedValues.Contains(item.Value) :
selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
return newSelectList;
}
}
}

View File

@ -56,6 +56,16 @@ namespace Microsoft.AspNet.Mvc.Rendering
htmlAttributes: htmlAttributes);
}
/// <inheritdoc />
public HtmlString DropDownListFor<TProperty>([NotNull] Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
{
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider);
return GenerateDropDown(metadata, ExpressionHelper.GetExpressionText(expression), selectList,
optionLabel, htmlAttributes);
}
/// <inheritdoc />
public HtmlString DisplayFor<TValue>([NotNull] Expression<Func<TModel, TValue>> expression,
string templateName,

View File

@ -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<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name)
{
return htmlHelper.DropDownList(name, selectList: null, optionLabel: null, htmlAttributes: null);
}
public static HtmlString DropDownList<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
string optionLabel)
{
return htmlHelper.DropDownList(name, selectList: null, optionLabel: optionLabel, htmlAttributes: null);
}
public static HtmlString DropDownList<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
IEnumerable<SelectListItem> selectList)
{
return htmlHelper.DropDownList(name, selectList, optionLabel: null, htmlAttributes: null);
}
public static HtmlString DropDownList<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
IEnumerable<SelectListItem> selectList, object htmlAttributes)
{
return htmlHelper.DropDownList(name, selectList, optionLabel: null, htmlAttributes: htmlAttributes);
}
public static HtmlString DropDownList<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
IEnumerable<SelectListItem> selectList, string optionLabel)
{
return htmlHelper.DropDownList(name, selectList, optionLabel, htmlAttributes: null);
}
public static HtmlString DropDownListFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)
{
return htmlHelper.DropDownListFor(expression, selectList, optionLabel: null, htmlAttributes: null);
}
public static HtmlString DropDownListFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList,
object htmlAttributes)
{
return htmlHelper.DropDownListFor(expression, selectList, optionLabel: null,
htmlAttributes: htmlAttributes);
}
public static HtmlString DropDownListFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList,
string optionLabel)
{
return htmlHelper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes: null);
}
}
}

View File

@ -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.
/// </summary>
/// <typeparam name="TDisplayModel">The type of the model.</typeparam>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="expression">An expression that identifies the object that contains the properties to display.</param>
/// <param name="templateName">The name of the template that is used to render the object.</param>
@ -175,6 +174,43 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <returns>The HTML markup for each property in the model.</returns>
HtmlString DisplayForModel(string templateName, string htmlFieldName, object additionalViewData);
/// <summary>
/// Returns a single-selection HTML {select} element using the specified name of the form field,
/// list items, option label, and HTML attributes.
/// </summary>
/// <param name="name">The name of the form field to return.</param>
/// <param name="selectList">A collection of <see href="SelectListItem"/> objects that are used to populate the
/// drop-down list.</param>
/// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
/// <param name="htmlAttributes">An object that contains the HTML attributes to set for the {select} element.
/// Alternatively, an <see cref="IDictionary{string, object}"/> instance containing the HTML attributes.
/// </param>
/// <returns>An HTML {select} element with an {option} subelement for each item in the list.</returns>
HtmlString DropDownList(
string name,
IEnumerable<SelectListItem> selectList,
string optionLabel,
object htmlAttributes);
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TProperty">The type of the value.</typeparam>
/// <param name="expression">An expression that identifies the value to display.</param>
/// <param name="selectList">A collection of <see href="SelectListItem"/> objects that are used to populate the
/// drop-down list.</param>
/// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
/// <param name="htmlAttributes">An object that contains the HTML attributes to set for the {select} element.
/// Alternatively, an <see cref="IDictionary{string, object}"/> instance containing the HTML attributes.
/// </param>
/// <returns>An HTML {select} element with an {option} subelement for each item in the list.</returns>
HtmlString DropDownListFor<TProperty>(
[NotNull] Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
string optionLabel,
object htmlAttributes);
/// <summary>
/// Converts the value of the specified object to an HTML-encoded string.
/// </summary>

View File

@ -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<SelectListItem>
{
private IList<SelectListGroup> _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)
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="items">The items used to build each <see cref="SelectListItem"/> of the list.</param>
/// <param name="dataValueField">The data value field. Used to match the Value property of the corresponding
/// <see cref="SelectListItem"/>.</param>
/// <param name="dataTextField">The data text field. Used to match the Text property of the corresponding
/// <see cref="SelectListItem"/>.</param>
/// <param name="selectedValues">The selected values field. Used to match the Selected property of the
/// corresponding <see cref="SelectListItem"/>.</param>
/// <param name="dataGroupField">The data group field. Used to match the Group property of the corresponding
/// <see cref="SelectListItem"/>.</param>
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<SelectListGroup>();
}
}
/// <summary>
/// Gets or sets the data group field.
/// </summary>
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<SelectListItem> GetEnumerator()
{
return GetListItems().GetEnumerator();
}
internal IList<SelectListItem> GetListItems()
{
return (!string.IsNullOrEmpty(DataValueField)) ?
GetListItemsWithValueField() :
GetListItemsWithoutValueField();
}
private IList<SelectListItem> GetListItemsWithValueField()
{
var selectedValues = new HashSet<string>(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<SelectListItem> GetListItemsWithoutValueField()
{
var selectedValues = new HashSet<object>();
if (SelectedValues != null)
{
selectedValues.UnionWith(SelectedValues.Cast<object>());
}
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
}
}

View File

@ -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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="items">The items used to build each <see cref="SelectListItem"/> of the list.</param>
/// <param name="dataValueField">The data value field. Used to match the Value property of the corresponding
/// <see cref="SelectListItem"/>.</param>
/// <param name="dataTextField">The data text field. Used to match the Text property of the corresponding
/// <see cref="SelectListItem"/>.</param>
/// <param name="selectedValue">The selected values. Used to match the Selected property of the corresponding
/// <see cref="SelectListItem"/>.</param>
/// <param name="dataGroupField">The data group field. Used to match the Group property of the corresponding
/// <see cref="SelectListItem"/>.</param>
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;
}
}
}

View File

@ -0,0 +1,21 @@

namespace Microsoft.AspNet.Mvc.Rendering
{
/// <summary>
/// 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.
/// </summary>
public class SelectListGroup
{
/// <summary>
/// Gets or sets a value that indicates whether this <see cref="SelectListGroup"/> is disabled.
/// </summary>
public bool Disabled { get; set; }
/// <summary>
/// Represents the value of the optgroup's label.
/// </summary>
public string Name { get; set; }
}
}

View File

@ -0,0 +1,24 @@

namespace Microsoft.AspNet.Mvc.Rendering
{
public class SelectListItem
{
/// <summary>
/// Gets or sets a value that indicates whether this <see cref="SelectListItem"/> is disabled.
/// </summary>
public bool Disabled { get; set; }
/// <summary>
/// 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.
/// </summary>
public SelectListGroup Group { get; set; }
public bool Selected { get; set; }
public string Text { get; set; }
public string Value { get; set; }
}
}

View File

@ -195,6 +195,15 @@
<data name="HtmlHelper_NotContextualized" xml:space="preserve">
<value>Must call 'Contextualize' method before using this HtmlHelper instance.</value>
</data>
<data name="HtmlHelper_MissingSelectData" xml:space="preserve">
<value>There is no ViewData item of type '{0}' that has the key '{1}'.</value>
</data>
<data name="HtmlHelper_SelectExpressionNotEnumerable" xml:space="preserve">
<value>The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed.</value>
</data>
<data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
<value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value>
</data>
<data name="TemplateHelpers_TemplateLimitations" xml:space="preserve">
<value>Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.</value>
</data>