Refactor `IHtmlGenerator.GenerateSelect()` and address #2240
- add `IHtmlGenerator.GetCurrentValues()` method - bring together bits of `GenerateSelect()` and `UpdateSelectListItemsWithDefaultValue()` - get rid of ugly `out` parameter - also allows `<option/>` tag helpers to run before `<select/>` helper generation - match `null` values and `SelectListItem`s with empty values - match `enum` names correctly - add doc comments for `IHtmlGenerator.GenerateSelect()` methods
This commit is contained in:
parent
0e783ace58
commit
9ac6ebd2b2
|
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNet.Mvc.Core;
|
using Microsoft.AspNet.Mvc.Core;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
|
@ -20,6 +21,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
public class DefaultHtmlGenerator : IHtmlGenerator
|
public class DefaultHtmlGenerator : IHtmlGenerator
|
||||||
{
|
{
|
||||||
private const string HiddenListItem = @"<li style=""display:none""></li>";
|
private const string HiddenListItem = @"<li style=""display:none""></li>";
|
||||||
|
private static readonly MethodInfo ConvertEnumFromStringMethod =
|
||||||
|
typeof(DefaultHtmlGenerator).GetTypeInfo().GetDeclaredMethod(nameof(ConvertEnumFromString));
|
||||||
|
|
||||||
private readonly AntiForgery _antiForgery;
|
private readonly AntiForgery _antiForgery;
|
||||||
private readonly IScopedInstance<ActionBindingContext> _bindingContextAccessor;
|
private readonly IScopedInstance<ActionBindingContext> _bindingContextAccessor;
|
||||||
|
|
@ -377,16 +380,16 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
bool allowMultiple,
|
bool allowMultiple,
|
||||||
object htmlAttributes)
|
object htmlAttributes)
|
||||||
{
|
{
|
||||||
ICollection<string> ignored;
|
var currentValues = GetCurrentValues(viewContext, modelExplorer, expression, allowMultiple);
|
||||||
return GenerateSelect(
|
return GenerateSelect(
|
||||||
viewContext,
|
viewContext,
|
||||||
modelExplorer,
|
modelExplorer,
|
||||||
optionLabel,
|
optionLabel,
|
||||||
expression,
|
expression,
|
||||||
selectList,
|
selectList,
|
||||||
|
currentValues,
|
||||||
allowMultiple,
|
allowMultiple,
|
||||||
htmlAttributes,
|
htmlAttributes);
|
||||||
selectedValues: out ignored);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -396,9 +399,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
string optionLabel,
|
string optionLabel,
|
||||||
string expression,
|
string expression,
|
||||||
IEnumerable<SelectListItem> selectList,
|
IEnumerable<SelectListItem> selectList,
|
||||||
|
IReadOnlyCollection<string> currentValues,
|
||||||
bool allowMultiple,
|
bool allowMultiple,
|
||||||
object htmlAttributes,
|
object htmlAttributes)
|
||||||
out ICollection<string> selectedValues)
|
|
||||||
{
|
{
|
||||||
var fullName = GetFullHtmlFieldName(viewContext, expression);
|
var fullName = GetFullHtmlFieldName(viewContext, expression);
|
||||||
if (string.IsNullOrEmpty(fullName))
|
if (string.IsNullOrEmpty(fullName))
|
||||||
|
|
@ -407,7 +410,6 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got a null selectList, try to use ViewData to get the list of items.
|
// If we got a null selectList, try to use ViewData to get the list of items.
|
||||||
var usedViewData = false;
|
|
||||||
if (selectList == null)
|
if (selectList == null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(expression))
|
if (string.IsNullOrEmpty(expression))
|
||||||
|
|
@ -419,39 +421,13 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
selectList = GetSelectListItems(viewContext, expression);
|
selectList = GetSelectListItems(viewContext, expression);
|
||||||
usedViewData = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var type = allowMultiple ? typeof(string[]) : typeof(string);
|
modelExplorer = modelExplorer ??
|
||||||
var defaultValue = GetModelStateValue(viewContext, fullName, type);
|
ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
|
||||||
|
if (currentValues != null)
|
||||||
// If ModelState did not contain a current value, fall back to ViewData- or ModelExplorer-supplied value.
|
|
||||||
if (defaultValue == null)
|
|
||||||
{
|
{
|
||||||
if (modelExplorer == null)
|
selectList = UpdateSelectListItemsWithDefaultValue(modelExplorer, selectList, currentValues);
|
||||||
{
|
|
||||||
// Html.DropDownList() and Html.ListBox() helper case.
|
|
||||||
// Cannot use ViewData if it contains the select list.
|
|
||||||
if (!usedViewData)
|
|
||||||
{
|
|
||||||
defaultValue = viewContext.ViewData.Eval(expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// <select/>, Html.DropDownListFor() and Html.ListBoxFor() helper case. Do not use ViewData.
|
|
||||||
defaultValue = modelExplorer.Model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultValue != null)
|
|
||||||
{
|
|
||||||
selectList =
|
|
||||||
UpdateSelectListItemsWithDefaultValue(selectList, defaultValue, allowMultiple, out selectedValues);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
selectedValues = new string[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert each ListItem to an <option> tag and wrap them with <optgroup> if requested.
|
// Convert each ListItem to an <option> tag and wrap them with <optgroup> if requested.
|
||||||
|
|
@ -758,6 +734,125 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
.SelectMany(v => v.GetClientValidationRules(validationContext));
|
.SelectMany(v => v.GetClientValidationRules(validationContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual IReadOnlyCollection<string> GetCurrentValues(
|
||||||
|
[NotNull] ViewContext viewContext,
|
||||||
|
ModelExplorer modelExplorer,
|
||||||
|
string expression,
|
||||||
|
bool allowMultiple)
|
||||||
|
{
|
||||||
|
var fullName = GetFullHtmlFieldName(viewContext, expression);
|
||||||
|
if (string.IsNullOrEmpty(fullName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(expression));
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = allowMultiple ? typeof(string[]) : typeof(string);
|
||||||
|
var rawValue = GetModelStateValue(viewContext, fullName, type);
|
||||||
|
|
||||||
|
// If ModelState did not contain a current value, fall back to ViewData- or ModelExplorer-supplied value.
|
||||||
|
if (rawValue == null)
|
||||||
|
{
|
||||||
|
if (modelExplorer == null)
|
||||||
|
{
|
||||||
|
// Html.DropDownList() and Html.ListBox() helper case.
|
||||||
|
rawValue = viewContext.ViewData.Eval(expression);
|
||||||
|
if (rawValue is IEnumerable<SelectListItem>)
|
||||||
|
{
|
||||||
|
// This ViewData item contains the fallback selectList collection for GenerateSelect().
|
||||||
|
// Do not try to use this collection.
|
||||||
|
rawValue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// <select/>, Html.DropDownListFor() and Html.ListBoxFor() helper case. Do not use ViewData.
|
||||||
|
rawValue = modelExplorer.Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawValue == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert raw value to a collection.
|
||||||
|
IEnumerable rawValues;
|
||||||
|
if (allowMultiple)
|
||||||
|
{
|
||||||
|
rawValues = rawValue as IEnumerable;
|
||||||
|
if (rawValues == null || rawValues is string)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatHtmlHelper_SelectExpressionNotEnumerable(nameof(expression)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rawValues = new[] { rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
modelExplorer = modelExplorer ??
|
||||||
|
ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
|
||||||
|
|
||||||
|
var enumNames = modelExplorer.Metadata.EnumNamesAndValues;
|
||||||
|
var isTargetEnum = modelExplorer.Metadata.IsEnum;
|
||||||
|
var innerType =
|
||||||
|
Nullable.GetUnderlyingType(modelExplorer.Metadata.ModelType) ?? modelExplorer.Metadata.ModelType;
|
||||||
|
|
||||||
|
// Convert raw value collection to strings.
|
||||||
|
var currentValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var value in rawValues)
|
||||||
|
{
|
||||||
|
// Add original or converted string.
|
||||||
|
var stringValue = (value as string) ?? Convert.ToString(value, CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
// Do not add simple names of enum properties here because whitespace isn't relevant for their binding.
|
||||||
|
// Will add matching names just below.
|
||||||
|
if (enumNames == null || !enumNames.ContainsKey(stringValue.Trim()))
|
||||||
|
{
|
||||||
|
currentValues.Add(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remainder handles isEnum cases. Convert.ToString() returns field names for enum values but select
|
||||||
|
// list may (well, should) contain integer values.
|
||||||
|
var enumValue = value as Enum;
|
||||||
|
if (isTargetEnum && enumValue == null && value != null)
|
||||||
|
{
|
||||||
|
var valueType = value.GetType();
|
||||||
|
if (typeof(long).IsAssignableFrom(valueType) || typeof(ulong).IsAssignableFrom(valueType))
|
||||||
|
{
|
||||||
|
// E.g. user added an int to a ViewData entry and called a string-based HTML helper.
|
||||||
|
enumValue = ConvertEnumFromInteger(value, innerType);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(stringValue))
|
||||||
|
{
|
||||||
|
// E.g. got a string from ModelState.
|
||||||
|
var methodInfo = ConvertEnumFromStringMethod.MakeGenericMethod(innerType);
|
||||||
|
enumValue = (Enum)methodInfo.Invoke(obj: null, parameters: new[] { stringValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enumValue != null)
|
||||||
|
{
|
||||||
|
// Add integer value.
|
||||||
|
var integerString = enumValue.ToString("d");
|
||||||
|
currentValues.Add(integerString);
|
||||||
|
|
||||||
|
// Add all simple names for this value.
|
||||||
|
var matchingNames = enumNames
|
||||||
|
.Where(kvp => string.Equals(integerString, kvp.Value, StringComparison.Ordinal))
|
||||||
|
.Select(kvp => kvp.Key);
|
||||||
|
foreach (var name in matchingNames)
|
||||||
|
{
|
||||||
|
currentValues.Add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (IReadOnlyCollection<string>)currentValues;
|
||||||
|
}
|
||||||
|
|
||||||
internal static string EvalString(ViewContext viewContext, string key, string format)
|
internal static string EvalString(ViewContext viewContext, string key, string format)
|
||||||
{
|
{
|
||||||
return Convert.ToString(viewContext.ViewData.Eval(key, format), CultureInfo.CurrentCulture);
|
return Convert.ToString(viewContext.ViewData.Eval(key, format), CultureInfo.CurrentCulture);
|
||||||
|
|
@ -990,6 +1085,32 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
return UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules);
|
return UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Enum ConvertEnumFromInteger(object value, Type targetType)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (Enum)Enum.ToObject(targetType, value);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
when (exception is FormatException || exception.InnerException is FormatException)
|
||||||
|
{
|
||||||
|
// The integer was too large for this enum type.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ConvertEnumFromString<TEnum>(string value) where TEnum : struct
|
||||||
|
{
|
||||||
|
TEnum enumValue;
|
||||||
|
if (Enum.TryParse(value, out enumValue))
|
||||||
|
{
|
||||||
|
return enumValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not return default(TEnum) when parse was unsuccessful.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool EvalBoolean(ViewContext viewContext, string key)
|
private static bool EvalBoolean(ViewContext viewContext, string key)
|
||||||
{
|
{
|
||||||
return Convert.ToBoolean(viewContext.ViewData.Eval(key), CultureInfo.InvariantCulture);
|
return Convert.ToBoolean(viewContext.ViewData.Eval(key), CultureInfo.InvariantCulture);
|
||||||
|
|
@ -1060,48 +1181,26 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<SelectListItem> UpdateSelectListItemsWithDefaultValue(
|
private static IEnumerable<SelectListItem> UpdateSelectListItemsWithDefaultValue(
|
||||||
|
ModelExplorer modelExplorer,
|
||||||
IEnumerable<SelectListItem> selectList,
|
IEnumerable<SelectListItem> selectList,
|
||||||
object defaultValue,
|
IReadOnlyCollection<string> currentValues)
|
||||||
bool allowMultiple,
|
|
||||||
out ICollection<string> selectedValues)
|
|
||||||
{
|
{
|
||||||
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 =
|
|
||||||
defaultValues.OfType<object>().Select(value => Convert.ToString(value, CultureInfo.CurrentCulture));
|
|
||||||
|
|
||||||
// ToString() by default returns an enum value's name. But selectList may use numeric values.
|
|
||||||
var enumValues = defaultValues.OfType<Enum>().Select(value => value.ToString());
|
|
||||||
values = values.Concat(enumValues);
|
|
||||||
|
|
||||||
selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// Perform deep copy of selectList to avoid changing user's Selected property values.
|
// Perform deep copy of selectList to avoid changing user's Selected property values.
|
||||||
var newSelectList = new List<SelectListItem>();
|
var newSelectList = new List<SelectListItem>();
|
||||||
foreach (SelectListItem item in selectList)
|
foreach (SelectListItem item in selectList)
|
||||||
{
|
{
|
||||||
var newItem = new SelectListItem
|
var value = item.Value ?? item.Text;
|
||||||
|
var selected = currentValues.Contains(value);
|
||||||
|
var copy = new SelectListItem
|
||||||
{
|
{
|
||||||
Disabled = item.Disabled,
|
Disabled = item.Disabled,
|
||||||
Group = item.Group,
|
Group = item.Group,
|
||||||
Selected = selectedValues.Contains(item.Value ?? item.Text),
|
Selected = selected,
|
||||||
Text = item.Text,
|
Text = item.Text,
|
||||||
Value = item.Value,
|
Value = item.Value,
|
||||||
};
|
};
|
||||||
newSelectList.Add(newItem);
|
|
||||||
|
newSelectList.Add(copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSelectList;
|
return newSelectList;
|
||||||
|
|
|
||||||
|
|
@ -691,8 +691,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
ViewContext,
|
ViewContext,
|
||||||
modelExplorer,
|
modelExplorer,
|
||||||
optionLabel,
|
optionLabel,
|
||||||
expression: expression,
|
expression,
|
||||||
selectList: selectList,
|
selectList,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
htmlAttributes: htmlAttributes);
|
htmlAttributes: htmlAttributes);
|
||||||
if (tagBuilder == null)
|
if (tagBuilder == null)
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,40 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
object routeValues,
|
object routeValues,
|
||||||
object htmlAttributes);
|
object htmlAttributes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a <select> element for the <paramref name="expression"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
|
||||||
|
/// <param name="modelExplorer">
|
||||||
|
/// <see cref="ModelExplorer"/> for the <paramref name="expression"/>. If <c>null</c>, determines validation
|
||||||
|
/// attributes using <paramref name="viewContext"/> and the <paramref name="expression"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="optionLabel">Optional text for a default empty <option> element.</param>
|
||||||
|
/// <param name="expression">Expression name, relative to the current model.</param>
|
||||||
|
/// <param name="selectList">
|
||||||
|
/// A collection of <see cref="SelectListItem"/> objects used to populate the <select> element with
|
||||||
|
/// <optgroup> and <option> elements. If <c>null</c>, finds this collection at
|
||||||
|
/// <c>ViewContext.ViewData[expression]</c>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="allowMultiple">
|
||||||
|
/// If <c>true</c>, includes a <c>multiple</c> attribute in the generated HTML. Otherwise generates a
|
||||||
|
/// single-selection <select> element.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="htmlAttributes">
|
||||||
|
/// An <see cref="object"/> that contains the HTML attributes for the <select> element. Alternatively, an
|
||||||
|
/// <see cref="IDictionary{string, object}"/> instance containing the HTML attributes.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A new <see cref="TagBuilder"/> describing the <select> element.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Combines <see cref="TemplateInfo.HtmlFieldPrefix"/> and <paramref name="expression"/> to set
|
||||||
|
/// <select> element's "name" attribute. Sanitizes <paramref name="expression"/> to set element's "id"
|
||||||
|
/// attribute.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// See <see cref="GetCurrentValues"/> for information about how current values are determined.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
TagBuilder GenerateSelect(
|
TagBuilder GenerateSelect(
|
||||||
[NotNull] ViewContext viewContext,
|
[NotNull] ViewContext viewContext,
|
||||||
ModelExplorer modelExplorer,
|
ModelExplorer modelExplorer,
|
||||||
|
|
@ -168,15 +202,55 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
bool allowMultiple,
|
bool allowMultiple,
|
||||||
object htmlAttributes);
|
object htmlAttributes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a <select> element for the <paramref name="expression"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
|
||||||
|
/// <param name="modelExplorer">
|
||||||
|
/// <see cref="ModelExplorer"/> for the <paramref name="expression"/>. If <c>null</c>, determines validation
|
||||||
|
/// attributes using <paramref name="viewContext"/> and the <paramref name="expression"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="optionLabel">Optional text for a default empty <option> element.</param>
|
||||||
|
/// <param name="expression">Expression name, relative to the current model.</param>
|
||||||
|
/// <param name="selectList">
|
||||||
|
/// A collection of <see cref="SelectListItem"/> objects used to populate the <select> element with
|
||||||
|
/// <optgroup> and <option> elements. If <c>null</c>, finds this collection at
|
||||||
|
/// <c>ViewContext.ViewData[expression]</c>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="currentValues">
|
||||||
|
/// An <see cref="IReadOnlyCollection{string}"/> containing values for <option> elements to select. If
|
||||||
|
/// <c>null</c>, selects <option> elements based on <see cref="SelectListItem.Selected"/> values in
|
||||||
|
/// <paramref name="selectList"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="allowMultiple">
|
||||||
|
/// If <c>true</c>, includes a <c>multiple</c> attribute in the generated HTML. Otherwise generates a
|
||||||
|
/// single-selection <select> element.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="htmlAttributes">
|
||||||
|
/// An <see cref="object"/> that contains the HTML attributes for the <select> element. Alternatively, an
|
||||||
|
/// <see cref="IDictionary{string, object}"/> instance containing the HTML attributes.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A new <see cref="TagBuilder"/> describing the <select> element.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Combines <see cref="TemplateInfo.HtmlFieldPrefix"/> and <paramref name="expression"/> to set
|
||||||
|
/// <select> element's "name" attribute. Sanitizes <paramref name="expression"/> to set element's "id"
|
||||||
|
/// attribute.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// See <see cref="GetCurrentValues"/> for information about how the <paramref name="currentValues"/>
|
||||||
|
/// collection may be created.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
TagBuilder GenerateSelect(
|
TagBuilder GenerateSelect(
|
||||||
[NotNull] ViewContext viewContext,
|
[NotNull] ViewContext viewContext,
|
||||||
ModelExplorer modelExplorer,
|
ModelExplorer modelExplorer,
|
||||||
string optionLabel,
|
string optionLabel,
|
||||||
string expression,
|
string expression,
|
||||||
IEnumerable<SelectListItem> selectList,
|
IEnumerable<SelectListItem> selectList,
|
||||||
|
IReadOnlyCollection<string> currentValues,
|
||||||
bool allowMultiple,
|
bool allowMultiple,
|
||||||
object htmlAttributes,
|
object htmlAttributes);
|
||||||
out ICollection<string> selectedValues);
|
|
||||||
|
|
||||||
TagBuilder GenerateTextArea(
|
TagBuilder GenerateTextArea(
|
||||||
[NotNull] ViewContext viewContext,
|
[NotNull] ViewContext viewContext,
|
||||||
|
|
@ -216,5 +290,45 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
[NotNull] ViewContext viewContext,
|
[NotNull] ViewContext viewContext,
|
||||||
ModelExplorer modelExplorer,
|
ModelExplorer modelExplorer,
|
||||||
string expression);
|
string expression);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of current values for the given <paramref name="expression"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
|
||||||
|
/// <param name="modelExplorer">
|
||||||
|
/// <see cref="ModelExplorer"/> for the <paramref name="expression"/>. If <c>null</c>, calculates the
|
||||||
|
/// <paramref name="expression"/> result using <see cref="ViewDataDictionary.Eval(string)"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="expression">Expression name, relative to the current model.</param>
|
||||||
|
/// <param name="allowMultiple">
|
||||||
|
/// If <c>true</c>, require a collection <paramref name="expression"/> result. Otherwise, treat result as a
|
||||||
|
/// single value.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// <para>
|
||||||
|
/// <c>null</c> if no <paramref name="expression"/> result is found. Otherwise an
|
||||||
|
/// <see cref="IReadOnlyCollection{string}"/> containing current values for the given
|
||||||
|
/// <paramref name="expression"/>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Converts the <paramref name="expression"/> result to a <see cref="string"/>. If that result is an
|
||||||
|
/// <see cref="System.Collections.IEnumerable"/> type, instead converts each item in the collection and returns
|
||||||
|
/// them separately.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// If the <paramref name="expression"/> result or the element type is an <see cref="System.Enum"/>, returns a
|
||||||
|
/// <see cref="string"/> containing the integer representation of the <see cref="System.Enum"/> value as well
|
||||||
|
/// as all <see cref="System.Enum"/> names for that value. Otherwise returns the default <see cref="string"/>
|
||||||
|
/// conversion of the value.
|
||||||
|
/// </para>
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// See <see cref="GenerateSelect"/> for information about how the return value may be used.
|
||||||
|
/// </remarks>
|
||||||
|
IReadOnlyCollection<string> GetCurrentValues(
|
||||||
|
[NotNull] ViewContext viewContext,
|
||||||
|
ModelExplorer modelExplorer,
|
||||||
|
string expression,
|
||||||
|
bool allowMultiple);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,16 +79,20 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
// Ensure GenerateSelect() _never_ looks anything up in ViewData.
|
// Ensure GenerateSelect() _never_ looks anything up in ViewData.
|
||||||
var items = Items ?? Enumerable.Empty<SelectListItem>();
|
var items = Items ?? Enumerable.Empty<SelectListItem>();
|
||||||
|
|
||||||
ICollection<string> selectedValues;
|
var currentValues = Generator.GetCurrentValues(
|
||||||
|
ViewContext,
|
||||||
|
For.ModelExplorer,
|
||||||
|
expression: For.Name,
|
||||||
|
allowMultiple: allowMultiple);
|
||||||
var tagBuilder = Generator.GenerateSelect(
|
var tagBuilder = Generator.GenerateSelect(
|
||||||
ViewContext,
|
ViewContext,
|
||||||
For.ModelExplorer,
|
For.ModelExplorer,
|
||||||
optionLabel: null,
|
optionLabel: null,
|
||||||
expression: For.Name,
|
expression: For.Name,
|
||||||
selectList: items,
|
selectList: items,
|
||||||
|
currentValues: currentValues,
|
||||||
allowMultiple: allowMultiple,
|
allowMultiple: allowMultiple,
|
||||||
htmlAttributes: null,
|
htmlAttributes: null);
|
||||||
selectedValues: out selectedValues);
|
|
||||||
|
|
||||||
if (tagBuilder != null)
|
if (tagBuilder != null)
|
||||||
{
|
{
|
||||||
|
|
@ -98,7 +102,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
|
|
||||||
// Whether or not (not being highly unlikely) we generate anything, could update contained <option/>
|
// Whether or not (not being highly unlikely) we generate anything, could update contained <option/>
|
||||||
// elements. Provide selected values for <option/> tag helpers. They'll run next.
|
// elements. Provide selected values for <option/> tag helpers. They'll run next.
|
||||||
ViewContext.FormContext.FormData[SelectedValuesFormDataKey] = selectedValues;
|
ViewContext.FormContext.FormData[SelectedValuesFormDataKey] = currentValues;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,646 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.DataProtection;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
using Microsoft.Framework.WebEncoders;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
|
{
|
||||||
|
public class DefaultHtmlGeneratorTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void GetCurrentValues_WithEmptyViewData_ReturnsNull(bool allowMultiple)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: allowMultiple);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void GetCurrentValues_WithNullExpressionResult_ReturnsNull(bool allowMultiple)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: allowMultiple);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void GetCurrentValues_WithSelectListInViewData_ReturnsNull(bool allowMultiple)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Name)] = Enumerable.Empty<SelectListItem>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: allowMultiple);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("some string")] // treated as if it were not IEnumerable
|
||||||
|
[InlineData(23)]
|
||||||
|
[InlineData(RegularEnum.Three)]
|
||||||
|
public void GetCurrentValues_AllowMultipleWithNonEnumerableInViewData_Throws(object value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Name)] = value;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: true));
|
||||||
|
Assert.Equal(
|
||||||
|
"The parameter 'expression' must evaluate to an IEnumerable when multiple selection is allowed.",
|
||||||
|
exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawValue, allowMultiple -> expected current values
|
||||||
|
public static TheoryData<object, bool, IReadOnlyCollection<string>> GetCurrentValues_StringAndCollectionData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<object, bool, IReadOnlyCollection<string>>
|
||||||
|
{
|
||||||
|
// ModelStateDictionary converts single values to arrays and visa-versa.
|
||||||
|
{ string.Empty, false, new [] { string.Empty } },
|
||||||
|
{ string.Empty, true, new [] { string.Empty } },
|
||||||
|
{ "some string", false, new [] { "some string" } },
|
||||||
|
{ "some string", true, new [] { "some string" } },
|
||||||
|
{ new [] { "some string" }, false, new [] { "some string" } },
|
||||||
|
{ new [] { "some string" }, true, new [] { "some string" } },
|
||||||
|
{ new [] { "some string", "some other string" }, false, new [] { "some string" } },
|
||||||
|
{
|
||||||
|
new [] { "some string", "some other string" },
|
||||||
|
true,
|
||||||
|
new [] { "some string", "some other string" }
|
||||||
|
},
|
||||||
|
// { new string[] { null }, false, null } would fall back to other sources.
|
||||||
|
{ new string[] { null }, true, new [] { string.Empty } },
|
||||||
|
{ new [] { string.Empty }, false, new [] { string.Empty } },
|
||||||
|
{ new [] { string.Empty }, true, new [] { string.Empty } },
|
||||||
|
{
|
||||||
|
new [] { null, "some string", "some other string" },
|
||||||
|
true,
|
||||||
|
new [] { string.Empty, "some string", "some other string" }
|
||||||
|
},
|
||||||
|
// ignores duplicates
|
||||||
|
{
|
||||||
|
new [] { null, "some string", null, "some other string", null, "some string", null },
|
||||||
|
true,
|
||||||
|
new [] { string.Empty, "some string", "some other string" }
|
||||||
|
},
|
||||||
|
// ignores case of duplicates
|
||||||
|
{
|
||||||
|
new [] { "some string", "SoMe StriNg", "Some String", "soME STRing", "SOME STRING" },
|
||||||
|
true,
|
||||||
|
new [] { "some string" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringAndCollectionData))]
|
||||||
|
public void GetCurrentValues_WithModelStateEntryAndViewData_ReturnsModelStateEntry(
|
||||||
|
object rawValue,
|
||||||
|
bool allowMultiple,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model { Name = "ignored property value" };
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Name)] = "ignored ViewData value";
|
||||||
|
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: allowMultiple);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringAndCollectionData))]
|
||||||
|
public void GetCurrentValues_WithModelStateEntryModelExplorerAndViewData_ReturnsModelStateEntry(
|
||||||
|
object rawValue,
|
||||||
|
bool allowMultiple,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model { Name = "ignored property value" };
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Name)] = "ignored ViewData value";
|
||||||
|
|
||||||
|
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), "ignored model value");
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: allowMultiple);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawValue -> expected current values
|
||||||
|
public static TheoryData<string, string[]> GetCurrentValues_StringData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string, string[]>
|
||||||
|
{
|
||||||
|
// 1. If given a ModelExplorer, GetCurrentValues does not use ViewData even if expression result is
|
||||||
|
// null.
|
||||||
|
// 2. Otherwise if ViewData entry exists, GetCurrentValue does not fall back to ViewData.Model even
|
||||||
|
// if entry is null.
|
||||||
|
// 3. Otherwise, GetCurrentValue does not fall back anywhere else even if ViewData.Model is null.
|
||||||
|
{ null, null },
|
||||||
|
{ string.Empty, new [] { string.Empty } },
|
||||||
|
{ "some string", new [] { "some string" } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringData))]
|
||||||
|
public void GetCurrentValues_WithModelExplorerAndViewData_ReturnsExpressionResult(
|
||||||
|
string rawValue,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model { Name = "ignored property value" };
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Name)] = "ignored ViewData value";
|
||||||
|
|
||||||
|
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), rawValue);
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue: null,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringData))]
|
||||||
|
public void GetCurrentValues_WithViewData_ReturnsViewDataEntry(
|
||||||
|
object rawValue,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model { Name = "ignored property value" };
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Name)] = rawValue;
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue: null,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringData))]
|
||||||
|
public void GetCurrentValues_WithModel_ReturnsModel(string rawValue, IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model { Name = rawValue };
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue: null,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Name),
|
||||||
|
allowMultiple: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawValue -> expected current values
|
||||||
|
public static TheoryData<string[], string[]> GetCurrentValues_StringCollectionData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string[], string[]>
|
||||||
|
{
|
||||||
|
{ new string[] { null }, new [] { string.Empty } },
|
||||||
|
{ new [] { string.Empty }, new [] { string.Empty } },
|
||||||
|
{ new [] { "some string" }, new [] { "some string" } },
|
||||||
|
{ new [] { "some string", "some other string" }, new [] { "some string", "some other string" } },
|
||||||
|
{
|
||||||
|
new [] { null, "some string", "some other string" },
|
||||||
|
new [] { string.Empty, "some string", "some other string" }
|
||||||
|
},
|
||||||
|
// ignores duplicates
|
||||||
|
{
|
||||||
|
new [] { null, "some string", null, "some other string", null, "some string", null },
|
||||||
|
new [] { string.Empty, "some string", "some other string" }
|
||||||
|
},
|
||||||
|
// ignores case of duplicates
|
||||||
|
{
|
||||||
|
new [] { "some string", "SoMe StriNg", "Some String", "soME STRing", "SOME STRING" },
|
||||||
|
new [] { "some string" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringCollectionData))]
|
||||||
|
public void GetCurrentValues_CollectionWithModelExplorerAndViewData_ReturnsExpressionResult(
|
||||||
|
string[] rawValue,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model { Collection = { "ignored property value" } };
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Collection)] = new[] { "ignored ViewData value" };
|
||||||
|
|
||||||
|
var modelExplorer =
|
||||||
|
metadataProvider.GetModelExplorerForType(typeof(List<string>), new List<string>(rawValue));
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue: null,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Collection), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
expression: nameof(Model.Collection),
|
||||||
|
allowMultiple: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringCollectionData))]
|
||||||
|
public void GetCurrentValues_CollectionWithViewData_ReturnsViewDataEntry(
|
||||||
|
object[] rawValue,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model { Collection = { "ignored property value" } };
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
viewContext.ViewData[nameof(Model.Collection)] = rawValue;
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue: null,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Collection), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Collection),
|
||||||
|
allowMultiple: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_StringCollectionData))]
|
||||||
|
public void GetCurrentValues_CollectionWithModel_ReturnsModel(
|
||||||
|
string[] rawValue,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var model = new Model();
|
||||||
|
model.Collection.AddRange(rawValue);
|
||||||
|
|
||||||
|
var viewContext = GetViewContext<Model>(model, metadataProvider);
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue: null,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(nameof(Model.Collection), valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: nameof(Model.Collection),
|
||||||
|
allowMultiple: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// property name, rawValue -> expected current values
|
||||||
|
public static TheoryData<string, object, string[]> GetCurrentValues_ValueToConvertData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string, object, string[]>
|
||||||
|
{
|
||||||
|
{ nameof(Model.FlagsEnum), FlagsEnum.All, new [] { "-1", "All" } },
|
||||||
|
{ nameof(Model.FlagsEnum), FlagsEnum.FortyTwo, new [] { "42", "FortyTwo" } },
|
||||||
|
{ nameof(Model.FlagsEnum), FlagsEnum.None, new [] { "0", "None" } },
|
||||||
|
{ nameof(Model.FlagsEnum), FlagsEnum.Two, new [] { "2", "Two" } },
|
||||||
|
{ nameof(Model.FlagsEnum), string.Empty, new [] { string.Empty } },
|
||||||
|
{ nameof(Model.FlagsEnum), "All", new [] { "-1", "All" } },
|
||||||
|
{ nameof(Model.FlagsEnum), "FortyTwo", new [] { "42", "FortyTwo" } },
|
||||||
|
{ nameof(Model.FlagsEnum), "None", new [] { "0", "None" } },
|
||||||
|
{ nameof(Model.FlagsEnum), "Two", new [] { "2", "Two" } },
|
||||||
|
{ nameof(Model.FlagsEnum), "Two, Four", new [] { "Two, Four", "6" } },
|
||||||
|
{ nameof(Model.FlagsEnum), "garbage", new [] { "garbage" } },
|
||||||
|
{ nameof(Model.FlagsEnum), "0", new [] { "0", "None" } },
|
||||||
|
{ nameof(Model.FlagsEnum), " 43", new [] { " 43", "43" } },
|
||||||
|
{ nameof(Model.FlagsEnum), "-5 ", new [] { "-5 ", "-5" } },
|
||||||
|
{ nameof(Model.FlagsEnum), 0, new [] { "0", "None" } },
|
||||||
|
{ nameof(Model.FlagsEnum), 1, new [] { "1", "One" } },
|
||||||
|
{ nameof(Model.FlagsEnum), 43, new [] { "43" } },
|
||||||
|
{ nameof(Model.FlagsEnum), -5, new [] { "-5" } },
|
||||||
|
{ nameof(Model.FlagsEnum), int.MaxValue, new [] { "2147483647" } },
|
||||||
|
{ nameof(Model.FlagsEnum), (uint)int.MaxValue + 1, new [] { "2147483648" } },
|
||||||
|
{ nameof(Model.FlagsEnum), uint.MaxValue, new [] { "4294967295" } }, // converted to string & used
|
||||||
|
|
||||||
|
{ nameof(Model.Id), string.Empty, new [] { string.Empty } },
|
||||||
|
{ nameof(Model.Id), "garbage", new [] { "garbage" } }, // no compatibility checks
|
||||||
|
{ nameof(Model.Id), "0", new [] { "0" } },
|
||||||
|
{ nameof(Model.Id), " 43", new [] { " 43" } },
|
||||||
|
{ nameof(Model.Id), "-5 ", new [] { "-5 " } },
|
||||||
|
{ nameof(Model.Id), 0, new [] { "0" } },
|
||||||
|
{ nameof(Model.Id), 1, new [] { "1" } },
|
||||||
|
{ nameof(Model.Id), 43, new [] { "43" } },
|
||||||
|
{ nameof(Model.Id), -5, new [] { "-5" } },
|
||||||
|
{ nameof(Model.Id), int.MaxValue, new [] { "2147483647" } },
|
||||||
|
{ nameof(Model.Id), (uint)int.MaxValue + 1, new [] { "2147483648" } }, // no limit checks
|
||||||
|
{ nameof(Model.Id), uint.MaxValue, new [] { "4294967295" } }, // no limit checks
|
||||||
|
|
||||||
|
{ nameof(Model.NullableEnum), RegularEnum.Zero, new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.NullableEnum), RegularEnum.One, new [] { "1", "One" } },
|
||||||
|
{ nameof(Model.NullableEnum), RegularEnum.Two, new [] { "2", "Two" } },
|
||||||
|
{ nameof(Model.NullableEnum), RegularEnum.Three, new [] { "3", "Three" } },
|
||||||
|
{ nameof(Model.NullableEnum), string.Empty, new [] { string.Empty } },
|
||||||
|
{ nameof(Model.NullableEnum), "Zero", new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.NullableEnum), "Two", new [] { "2", "Two" } },
|
||||||
|
{ nameof(Model.NullableEnum), "One, Two", new [] { "One, Two", "3", "Three" } },
|
||||||
|
{ nameof(Model.NullableEnum), "garbage", new [] { "garbage" } },
|
||||||
|
{ nameof(Model.NullableEnum), "0", new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.NullableEnum), " 43", new [] { " 43", "43" } },
|
||||||
|
{ nameof(Model.NullableEnum), "-5 ", new [] { "-5 ", "-5" } },
|
||||||
|
{ nameof(Model.NullableEnum), 0, new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.NullableEnum), 1, new [] { "1", "One" } },
|
||||||
|
{ nameof(Model.NullableEnum), 43, new [] { "43" } },
|
||||||
|
{ nameof(Model.NullableEnum), -5, new [] { "-5" } },
|
||||||
|
{ nameof(Model.NullableEnum), int.MaxValue, new [] { "2147483647" } },
|
||||||
|
{ nameof(Model.NullableEnum), (uint)int.MaxValue + 1, new [] { "2147483648" } },
|
||||||
|
{ nameof(Model.NullableEnum), uint.MaxValue, new [] { "4294967295" } },
|
||||||
|
|
||||||
|
{ nameof(Model.RegularEnum), RegularEnum.Zero, new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.RegularEnum), RegularEnum.One, new [] { "1", "One" } },
|
||||||
|
{ nameof(Model.RegularEnum), RegularEnum.Two, new [] { "2", "Two" } },
|
||||||
|
{ nameof(Model.RegularEnum), RegularEnum.Three, new [] { "3", "Three" } },
|
||||||
|
{ nameof(Model.RegularEnum), string.Empty, new [] { string.Empty } },
|
||||||
|
{ nameof(Model.RegularEnum), "Zero", new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.RegularEnum), "Two", new [] { "2", "Two" } },
|
||||||
|
{ nameof(Model.RegularEnum), "One, Two", new [] { "One, Two", "3", "Three" } },
|
||||||
|
{ nameof(Model.RegularEnum), "garbage", new [] { "garbage" } },
|
||||||
|
{ nameof(Model.RegularEnum), "0", new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.RegularEnum), " 43", new [] { " 43", "43" } },
|
||||||
|
{ nameof(Model.RegularEnum), "-5 ", new [] { "-5 ", "-5" } },
|
||||||
|
{ nameof(Model.RegularEnum), 0, new [] { "0", "Zero" } },
|
||||||
|
{ nameof(Model.RegularEnum), 1, new [] { "1", "One" } },
|
||||||
|
{ nameof(Model.RegularEnum), 43, new [] { "43" } },
|
||||||
|
{ nameof(Model.RegularEnum), -5, new [] { "-5" } },
|
||||||
|
{ nameof(Model.RegularEnum), int.MaxValue, new [] { "2147483647" } },
|
||||||
|
{ nameof(Model.RegularEnum), (uint)int.MaxValue + 1, new [] { "2147483648" } },
|
||||||
|
{ nameof(Model.RegularEnum), uint.MaxValue, new [] { "4294967295" } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetCurrentValues_ValueToConvertData))]
|
||||||
|
public void GetCurrentValues_ValueConvertedAsExpected(
|
||||||
|
string propertyName,
|
||||||
|
object rawValue,
|
||||||
|
IReadOnlyCollection<string> expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||||
|
var htmlGenerator = GetGenerator(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
|
||||||
|
var valueProviderResult = new ValueProviderResult(
|
||||||
|
rawValue,
|
||||||
|
attemptedValue: null,
|
||||||
|
culture: CultureInfo.InvariantCulture);
|
||||||
|
viewContext.ModelState.SetModelValue(propertyName, valueProviderResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlGenerator.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer: null,
|
||||||
|
expression: propertyName,
|
||||||
|
allowMultiple: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<string>(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentValues uses only the IModelMetadataProvider passed to the DefaultHtmlGenerator constructor.
|
||||||
|
private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvider)
|
||||||
|
{
|
||||||
|
var mvcOptionsAccessor = new Mock<IOptions<MvcOptions>>();
|
||||||
|
mvcOptionsAccessor.SetupGet(accessor => accessor.Options).Returns(new MvcOptions());
|
||||||
|
var htmlEncoder = Mock.Of<IHtmlEncoder>();
|
||||||
|
var dataOptionsAccessor = new Mock<IOptions<DataProtectionOptions>>();
|
||||||
|
dataOptionsAccessor.SetupGet(accessor => accessor.Options).Returns(new DataProtectionOptions());
|
||||||
|
var antiForgery = new AntiForgery(
|
||||||
|
Mock.Of<IClaimUidExtractor>(),
|
||||||
|
Mock.Of<IDataProtectionProvider>(),
|
||||||
|
Mock.Of<IAntiForgeryAdditionalDataProvider>(),
|
||||||
|
mvcOptionsAccessor.Object,
|
||||||
|
htmlEncoder,
|
||||||
|
dataOptionsAccessor.Object);
|
||||||
|
|
||||||
|
return new DefaultHtmlGenerator(
|
||||||
|
antiForgery,
|
||||||
|
Mock.Of<IScopedInstance<ActionBindingContext>>(),
|
||||||
|
metadataProvider,
|
||||||
|
Mock.Of<IUrlHelper>(),
|
||||||
|
htmlEncoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentValues uses only the ModelStateDictionary and ViewDataDictionary from the passed ViewContext.
|
||||||
|
private static ViewContext GetViewContext<TModel>(TModel model, IModelMetadataProvider metadataProvider)
|
||||||
|
{
|
||||||
|
var actionContext = new ActionContext();
|
||||||
|
var viewData = new ViewDataDictionary<TModel>(metadataProvider, actionContext.ModelState)
|
||||||
|
{
|
||||||
|
Model = model,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ViewContext(
|
||||||
|
actionContext,
|
||||||
|
Mock.Of<IView>(),
|
||||||
|
viewData,
|
||||||
|
Mock.Of<ITempDataDictionary>(),
|
||||||
|
TextWriter.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RegularEnum
|
||||||
|
{
|
||||||
|
Zero,
|
||||||
|
One,
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FlagsEnum
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
One = 1,
|
||||||
|
Two = 2,
|
||||||
|
Four = 4,
|
||||||
|
FortyTwo = 42,
|
||||||
|
All = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Model
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public RegularEnum RegularEnum { get; set; }
|
||||||
|
|
||||||
|
public FlagsEnum FlagsEnum { get; set; }
|
||||||
|
|
||||||
|
public RegularEnum? NullableEnum { get; set; }
|
||||||
|
|
||||||
|
public List<string> Collection { get; } = new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -243,12 +243,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
Assert.Equal(expectedTagName, output.TagName);
|
Assert.Equal(expectedTagName, output.TagName);
|
||||||
|
|
||||||
Assert.NotNull(viewContext.FormContext?.FormData);
|
Assert.NotNull(viewContext.FormContext?.FormData);
|
||||||
var keyValuePair = Assert.Single(
|
Assert.Single(
|
||||||
viewContext.FormContext.FormData,
|
viewContext.FormContext.FormData,
|
||||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||||
Assert.NotNull(keyValuePair.Value);
|
|
||||||
var selectedValues = Assert.IsAssignableFrom<ICollection<string>>(keyValuePair.Value);
|
|
||||||
Assert.InRange(selectedValues.Count, 0, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -341,12 +338,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
Assert.Equal(expectedTagName, output.TagName);
|
Assert.Equal(expectedTagName, output.TagName);
|
||||||
|
|
||||||
Assert.NotNull(viewContext.FormContext?.FormData);
|
Assert.NotNull(viewContext.FormContext?.FormData);
|
||||||
var keyValuePair = Assert.Single(
|
Assert.Single(
|
||||||
viewContext.FormContext.FormData,
|
viewContext.FormContext.FormData,
|
||||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||||
Assert.NotNull(keyValuePair.Value);
|
|
||||||
var selectedValues = Assert.IsAssignableFrom<ICollection<string>>(keyValuePair.Value);
|
|
||||||
Assert.InRange(selectedValues.Count, 0, 1);
|
|
||||||
|
|
||||||
Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
|
Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
|
||||||
Assert.Equal(savedGroup, items.Select(item => item.Group));
|
Assert.Equal(savedGroup, items.Select(item => item.Group));
|
||||||
|
|
@ -446,12 +440,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
Assert.Equal(expectedTagName, output.TagName);
|
Assert.Equal(expectedTagName, output.TagName);
|
||||||
|
|
||||||
Assert.NotNull(viewContext.FormContext?.FormData);
|
Assert.NotNull(viewContext.FormContext?.FormData);
|
||||||
var keyValuePair = Assert.Single(
|
Assert.Single(
|
||||||
viewContext.FormContext.FormData,
|
viewContext.FormContext.FormData,
|
||||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||||
Assert.NotNull(keyValuePair.Value);
|
|
||||||
var selectedValues = Assert.IsAssignableFrom<ICollection<string>>(keyValuePair.Value);
|
|
||||||
Assert.InRange(selectedValues.Count, 0, 1);
|
|
||||||
|
|
||||||
Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
|
Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
|
||||||
Assert.Equal(savedGroup, items.Select(item => item.Group));
|
Assert.Equal(savedGroup, items.Select(item => item.Group));
|
||||||
|
|
@ -504,17 +495,25 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
var modelExpression = new ModelExpression(string.Empty, modelExplorer);
|
var modelExpression = new ModelExpression(string.Empty, modelExplorer);
|
||||||
viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = propertyName;
|
viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = propertyName;
|
||||||
|
|
||||||
ICollection<string> selectedValues = new string[0];
|
var currentValues = new string[0];
|
||||||
|
htmlGenerator
|
||||||
|
.Setup(real => real.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
string.Empty, // expression
|
||||||
|
false)) // allowMultiple
|
||||||
|
.Returns(currentValues)
|
||||||
|
.Verifiable();
|
||||||
htmlGenerator
|
htmlGenerator
|
||||||
.Setup(real => real.GenerateSelect(
|
.Setup(real => real.GenerateSelect(
|
||||||
viewContext,
|
viewContext,
|
||||||
modelExplorer,
|
modelExplorer,
|
||||||
null, // optionLabel
|
null, // optionLabel
|
||||||
string.Empty, // name
|
string.Empty, // expression
|
||||||
expectedItems,
|
expectedItems,
|
||||||
false, // allowMultiple
|
currentValues,
|
||||||
null, // htmlAttributes
|
false, // allowMultiple
|
||||||
out selectedValues))
|
null)) // htmlAttributes
|
||||||
.Returns((TagBuilder)null)
|
.Returns((TagBuilder)null)
|
||||||
.Verifiable();
|
.Verifiable();
|
||||||
|
|
||||||
|
|
@ -536,7 +535,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
var keyValuePair = Assert.Single(
|
var keyValuePair = Assert.Single(
|
||||||
viewContext.FormContext.FormData,
|
viewContext.FormContext.FormData,
|
||||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||||
Assert.Same(selectedValues, keyValuePair.Value);
|
Assert.Same(currentValues, keyValuePair.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -569,17 +568,25 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
|
|
||||||
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||||
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
|
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
|
||||||
ICollection<string> selectedValues = new string[0];
|
var currentValues = new string[0];
|
||||||
|
htmlGenerator
|
||||||
|
.Setup(real => real.GetCurrentValues(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
propertyName, // expression
|
||||||
|
allowMultiple))
|
||||||
|
.Returns(currentValues)
|
||||||
|
.Verifiable();
|
||||||
htmlGenerator
|
htmlGenerator
|
||||||
.Setup(real => real.GenerateSelect(
|
.Setup(real => real.GenerateSelect(
|
||||||
viewContext,
|
viewContext,
|
||||||
modelExplorer,
|
modelExplorer,
|
||||||
null, // optionLabel
|
null, // optionLabel
|
||||||
propertyName, // name
|
propertyName, // expression
|
||||||
It.IsAny<IEnumerable<SelectListItem>>(),
|
It.IsAny<IEnumerable<SelectListItem>>(),
|
||||||
|
currentValues,
|
||||||
allowMultiple,
|
allowMultiple,
|
||||||
null, // htmlAttributes
|
null)) // htmlAttributes
|
||||||
out selectedValues))
|
|
||||||
.Returns((TagBuilder)null)
|
.Returns((TagBuilder)null)
|
||||||
.Verifiable();
|
.Verifiable();
|
||||||
|
|
||||||
|
|
@ -600,7 +607,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||||
var keyValuePair = Assert.Single(
|
var keyValuePair = Assert.Single(
|
||||||
viewContext.FormContext.FormData,
|
viewContext.FormContext.FormData,
|
||||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||||
Assert.Same(selectedValues, keyValuePair.Value);
|
Assert.Same(currentValues, keyValuePair.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NameAndId
|
public class NameAndId
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue