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.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
|
|
@ -20,6 +21,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
public class DefaultHtmlGenerator : IHtmlGenerator
|
||||
{
|
||||
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 IScopedInstance<ActionBindingContext> _bindingContextAccessor;
|
||||
|
|
@ -377,16 +380,16 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
bool allowMultiple,
|
||||
object htmlAttributes)
|
||||
{
|
||||
ICollection<string> ignored;
|
||||
var currentValues = GetCurrentValues(viewContext, modelExplorer, expression, allowMultiple);
|
||||
return GenerateSelect(
|
||||
viewContext,
|
||||
modelExplorer,
|
||||
optionLabel,
|
||||
expression,
|
||||
selectList,
|
||||
currentValues,
|
||||
allowMultiple,
|
||||
htmlAttributes,
|
||||
selectedValues: out ignored);
|
||||
htmlAttributes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -396,9 +399,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
string optionLabel,
|
||||
string expression,
|
||||
IEnumerable<SelectListItem> selectList,
|
||||
IReadOnlyCollection<string> currentValues,
|
||||
bool allowMultiple,
|
||||
object htmlAttributes,
|
||||
out ICollection<string> selectedValues)
|
||||
object htmlAttributes)
|
||||
{
|
||||
var fullName = GetFullHtmlFieldName(viewContext, expression);
|
||||
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.
|
||||
var usedViewData = false;
|
||||
if (selectList == null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(expression))
|
||||
|
|
@ -419,39 +421,13 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
}
|
||||
|
||||
selectList = GetSelectListItems(viewContext, expression);
|
||||
usedViewData = true;
|
||||
}
|
||||
|
||||
var type = allowMultiple ? typeof(string[]) : typeof(string);
|
||||
var defaultValue = GetModelStateValue(viewContext, fullName, type);
|
||||
|
||||
// If ModelState did not contain a current value, fall back to ViewData- or ModelExplorer-supplied value.
|
||||
if (defaultValue == null)
|
||||
modelExplorer = modelExplorer ??
|
||||
ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
|
||||
if (currentValues != null)
|
||||
{
|
||||
if (modelExplorer == null)
|
||||
{
|
||||
// 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];
|
||||
selectList = UpdateSelectListItemsWithDefaultValue(modelExplorer, selectList, currentValues);
|
||||
}
|
||||
|
||||
// Convert each ListItem to an <option> tag and wrap them with <optgroup> if requested.
|
||||
|
|
@ -746,18 +722,137 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
modelExplorer.Metadata,
|
||||
_metadataProvider,
|
||||
viewContext.HttpContext.RequestServices);
|
||||
|
||||
|
||||
var validatorProviderContext = new ModelValidatorProviderContext(modelExplorer.Metadata);
|
||||
validatorProvider.GetValidators(validatorProviderContext);
|
||||
|
||||
var validators = validatorProviderContext.Validators;
|
||||
|
||||
return
|
||||
return
|
||||
validators
|
||||
.OfType<IClientModelValidator>()
|
||||
.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)
|
||||
{
|
||||
return Convert.ToString(viewContext.ViewData.Eval(key, format), CultureInfo.CurrentCulture);
|
||||
|
|
@ -990,6 +1085,32 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
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)
|
||||
{
|
||||
return Convert.ToBoolean(viewContext.ViewData.Eval(key), CultureInfo.InvariantCulture);
|
||||
|
|
@ -1060,48 +1181,26 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
}
|
||||
|
||||
private static IEnumerable<SelectListItem> UpdateSelectListItemsWithDefaultValue(
|
||||
ModelExplorer modelExplorer,
|
||||
IEnumerable<SelectListItem> selectList,
|
||||
object defaultValue,
|
||||
bool allowMultiple,
|
||||
out ICollection<string> selectedValues)
|
||||
IReadOnlyCollection<string> currentValues)
|
||||
{
|
||||
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.
|
||||
var newSelectList = new List<SelectListItem>();
|
||||
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,
|
||||
Group = item.Group,
|
||||
Selected = selectedValues.Contains(item.Value ?? item.Text),
|
||||
Selected = selected,
|
||||
Text = item.Text,
|
||||
Value = item.Value,
|
||||
};
|
||||
newSelectList.Add(newItem);
|
||||
|
||||
newSelectList.Add(copy);
|
||||
}
|
||||
|
||||
return newSelectList;
|
||||
|
|
|
|||
|
|
@ -691,8 +691,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
ViewContext,
|
||||
modelExplorer,
|
||||
optionLabel,
|
||||
expression: expression,
|
||||
selectList: selectList,
|
||||
expression,
|
||||
selectList,
|
||||
allowMultiple: false,
|
||||
htmlAttributes: htmlAttributes);
|
||||
if (tagBuilder == null)
|
||||
|
|
|
|||
|
|
@ -159,6 +159,40 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
object routeValues,
|
||||
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(
|
||||
[NotNull] ViewContext viewContext,
|
||||
ModelExplorer modelExplorer,
|
||||
|
|
@ -168,15 +202,55 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
bool allowMultiple,
|
||||
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(
|
||||
[NotNull] ViewContext viewContext,
|
||||
ModelExplorer modelExplorer,
|
||||
string optionLabel,
|
||||
string expression,
|
||||
IEnumerable<SelectListItem> selectList,
|
||||
IReadOnlyCollection<string> currentValues,
|
||||
bool allowMultiple,
|
||||
object htmlAttributes,
|
||||
out ICollection<string> selectedValues);
|
||||
object htmlAttributes);
|
||||
|
||||
TagBuilder GenerateTextArea(
|
||||
[NotNull] ViewContext viewContext,
|
||||
|
|
@ -216,5 +290,45 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
[NotNull] ViewContext viewContext,
|
||||
ModelExplorer modelExplorer,
|
||||
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.
|
||||
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(
|
||||
ViewContext,
|
||||
For.ModelExplorer,
|
||||
optionLabel: null,
|
||||
expression: For.Name,
|
||||
selectList: items,
|
||||
currentValues: currentValues,
|
||||
allowMultiple: allowMultiple,
|
||||
htmlAttributes: null,
|
||||
selectedValues: out selectedValues);
|
||||
htmlAttributes: 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/>
|
||||
// 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.NotNull(viewContext.FormContext?.FormData);
|
||||
var keyValuePair = Assert.Single(
|
||||
Assert.Single(
|
||||
viewContext.FormContext.FormData,
|
||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||
Assert.NotNull(keyValuePair.Value);
|
||||
var selectedValues = Assert.IsAssignableFrom<ICollection<string>>(keyValuePair.Value);
|
||||
Assert.InRange(selectedValues.Count, 0, 1);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -341,12 +338,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Assert.Equal(expectedTagName, output.TagName);
|
||||
|
||||
Assert.NotNull(viewContext.FormContext?.FormData);
|
||||
var keyValuePair = Assert.Single(
|
||||
Assert.Single(
|
||||
viewContext.FormContext.FormData,
|
||||
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(savedGroup, items.Select(item => item.Group));
|
||||
|
|
@ -446,12 +440,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Assert.Equal(expectedTagName, output.TagName);
|
||||
|
||||
Assert.NotNull(viewContext.FormContext?.FormData);
|
||||
var keyValuePair = Assert.Single(
|
||||
Assert.Single(
|
||||
viewContext.FormContext.FormData,
|
||||
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(savedGroup, items.Select(item => item.Group));
|
||||
|
|
@ -504,17 +495,25 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var modelExpression = new ModelExpression(string.Empty, modelExplorer);
|
||||
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
|
||||
.Setup(real => real.GenerateSelect(
|
||||
viewContext,
|
||||
modelExplorer,
|
||||
null, // optionLabel
|
||||
string.Empty, // name
|
||||
null, // optionLabel
|
||||
string.Empty, // expression
|
||||
expectedItems,
|
||||
false, // allowMultiple
|
||||
null, // htmlAttributes
|
||||
out selectedValues))
|
||||
currentValues,
|
||||
false, // allowMultiple
|
||||
null)) // htmlAttributes
|
||||
.Returns((TagBuilder)null)
|
||||
.Verifiable();
|
||||
|
||||
|
|
@ -536,7 +535,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var keyValuePair = Assert.Single(
|
||||
viewContext.FormContext.FormData,
|
||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||
Assert.Same(selectedValues, keyValuePair.Value);
|
||||
Assert.Same(currentValues, keyValuePair.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -569,17 +568,25 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
|
||||
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||
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
|
||||
.Setup(real => real.GenerateSelect(
|
||||
viewContext,
|
||||
modelExplorer,
|
||||
null, // optionLabel
|
||||
propertyName, // name
|
||||
null, // optionLabel
|
||||
propertyName, // expression
|
||||
It.IsAny<IEnumerable<SelectListItem>>(),
|
||||
currentValues,
|
||||
allowMultiple,
|
||||
null, // htmlAttributes
|
||||
out selectedValues))
|
||||
null)) // htmlAttributes
|
||||
.Returns((TagBuilder)null)
|
||||
.Verifiable();
|
||||
|
||||
|
|
@ -600,7 +607,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var keyValuePair = Assert.Single(
|
||||
viewContext.FormContext.FormData,
|
||||
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
|
||||
Assert.Same(selectedValues, keyValuePair.Value);
|
||||
Assert.Same(currentValues, keyValuePair.Value);
|
||||
}
|
||||
|
||||
public class NameAndId
|
||||
|
|
|
|||
Loading…
Reference in New Issue