Fixes #5197 GetEnumSelectList uses IStringLocalizer Fixes #4215 Html.DisplayFor now checks DisplayAttributes on enums
This commit is contained in:
parent
21236cc98e
commit
830983a477
|
|
@ -10,8 +10,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct EnumGroupAndName
|
public struct EnumGroupAndName
|
||||||
{
|
{
|
||||||
|
private Func<string> _name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the EnumGroupAndName structure.
|
/// Initializes a new instance of the <see cref="EnumGroupAndName"/> structure. This constructor should
|
||||||
|
/// not be used in any site where localization is important.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="group">The group name.</param>
|
/// <param name="group">The group name.</param>
|
||||||
/// <param name="name">The name.</param>
|
/// <param name="name">The name.</param>
|
||||||
|
|
@ -28,7 +31,30 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
Group = group;
|
Group = group;
|
||||||
Name = name;
|
_name = () => name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="EnumGroupAndName"/> structure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The group name.</param>
|
||||||
|
/// <param name="name">A <see cref="Func{String}"/> which will return the name.</param>
|
||||||
|
public EnumGroupAndName(
|
||||||
|
string group,
|
||||||
|
Func<string> name)
|
||||||
|
{
|
||||||
|
if (group == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Group = group;
|
||||||
|
_name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -39,6 +65,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name.
|
/// Gets the name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; }
|
public string Name
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _name();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,15 +153,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
// Dictionary does not guarantee order will be preserved.
|
// Dictionary does not guarantee order will be preserved.
|
||||||
var groupedDisplayNamesAndValues = new List<KeyValuePair<EnumGroupAndName, string>>();
|
var groupedDisplayNamesAndValues = new List<KeyValuePair<EnumGroupAndName, string>>();
|
||||||
var namesAndValues = new Dictionary<string, string>();
|
var namesAndValues = new Dictionary<string, string>();
|
||||||
|
var enumLocalizer = _stringLocalizerFactory?.Create(underlyingType);
|
||||||
foreach (var name in Enum.GetNames(underlyingType))
|
foreach (var name in Enum.GetNames(underlyingType))
|
||||||
{
|
{
|
||||||
var field = underlyingType.GetField(name);
|
var field = underlyingType.GetField(name);
|
||||||
var displayName = GetDisplayName(field);
|
|
||||||
var groupName = GetDisplayGroup(field);
|
var groupName = GetDisplayGroup(field);
|
||||||
var value = ((Enum)field.GetValue(obj: null)).ToString("d");
|
var value = ((Enum)field.GetValue(obj: null)).ToString("d");
|
||||||
|
|
||||||
groupedDisplayNamesAndValues.Add(new KeyValuePair<EnumGroupAndName, string>(
|
groupedDisplayNamesAndValues.Add(new KeyValuePair<EnumGroupAndName, string>(
|
||||||
new EnumGroupAndName(groupName, displayName),
|
new EnumGroupAndName(
|
||||||
|
groupName,
|
||||||
|
() => GetDisplayName(field, enumLocalizer)),
|
||||||
value));
|
value));
|
||||||
namesAndValues.Add(name, value);
|
namesAndValues.Add(name, value);
|
||||||
}
|
}
|
||||||
|
|
@ -291,18 +293,19 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return non-empty name specified in a [Display] attribute for a field, if any; field.Name otherwise.
|
private static string GetDisplayName(FieldInfo field, IStringLocalizer stringLocalizer)
|
||||||
private static string GetDisplayName(FieldInfo field)
|
|
||||||
{
|
{
|
||||||
var display = field.GetCustomAttribute<DisplayAttribute>(inherit: false);
|
var display = field.GetCustomAttribute<DisplayAttribute>(inherit: false);
|
||||||
if (display != null)
|
if (display != null)
|
||||||
{
|
{
|
||||||
// Note [Display(Name = "")] is allowed.
|
// Note [Display(Name = "")] is allowed but we will not attempt to localize the empty name.
|
||||||
var name = display.GetName();
|
var name = display.GetName();
|
||||||
if (name != null)
|
if (stringLocalizer != null && !string.IsNullOrEmpty(name) && display.ResourceType == null)
|
||||||
{
|
{
|
||||||
return name;
|
name = stringLocalizer[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return name ?? field.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return field.Name;
|
return field.Name;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -522,6 +523,23 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
string templateName,
|
string templateName,
|
||||||
object additionalViewData)
|
object additionalViewData)
|
||||||
{
|
{
|
||||||
|
var modelEnum = modelExplorer.Model as Enum;
|
||||||
|
if (modelExplorer.Metadata.IsEnum && modelEnum != null)
|
||||||
|
{
|
||||||
|
var value = modelEnum.ToString("d");
|
||||||
|
var enumGrouped = modelExplorer.Metadata.EnumGroupedDisplayNamesAndValues;
|
||||||
|
Debug.Assert(enumGrouped != null);
|
||||||
|
foreach (var kvp in enumGrouped)
|
||||||
|
{
|
||||||
|
if (kvp.Value == value)
|
||||||
|
{
|
||||||
|
// Creates a ModelExplorer with the same Metadata except that the Model is a string instead of an Enum
|
||||||
|
modelExplorer = modelExplorer.GetExplorerForModel(kvp.Key.Name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var templateBuilder = new TemplateBuilder(
|
var templateBuilder = new TemplateBuilder(
|
||||||
_viewEngine,
|
_viewEngine,
|
||||||
_bufferScope,
|
_bufferScope,
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.DotNet.InternalAbstractions;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -353,13 +358,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
|
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
|
||||||
stringLocalizer
|
stringLocalizer
|
||||||
.Setup(s => s["Model_Name"])
|
.Setup(s => s["Model_Name"])
|
||||||
.Returns(new LocalizedString("Model_Name", "name from localizer"));
|
.Returns(() => new LocalizedString("Model_Name", "name from localizer " + CultureInfo.CurrentCulture));
|
||||||
stringLocalizer
|
stringLocalizer
|
||||||
.Setup(s => s["Model_Description"])
|
.Setup(s => s["Model_Description"])
|
||||||
.Returns(new LocalizedString("Model_Description", "description from localizer"));
|
.Returns(() => new LocalizedString("Model_Description", "description from localizer " + CultureInfo.CurrentCulture));
|
||||||
stringLocalizer
|
stringLocalizer
|
||||||
.Setup(s => s["Model_Prompt"])
|
.Setup(s => s["Model_Prompt"])
|
||||||
.Returns(new LocalizedString("Model_Prompt", "prompt from localizer"));
|
.Returns(() => new LocalizedString("Model_Prompt", "prompt from localizer " + CultureInfo.CurrentCulture));
|
||||||
|
|
||||||
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
|
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
|
||||||
stringLocalizerFactory
|
stringLocalizerFactory
|
||||||
|
|
@ -383,9 +388,18 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
provider.CreateDisplayMetadata(context);
|
provider.CreateDisplayMetadata(context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("name from localizer", context.DisplayMetadata.DisplayName());
|
using (new CultureReplacer("en-US", "en-US"))
|
||||||
Assert.Equal("description from localizer", context.DisplayMetadata.Description());
|
{
|
||||||
Assert.Equal("prompt from localizer", context.DisplayMetadata.Placeholder());
|
Assert.Equal("name from localizer en-US", context.DisplayMetadata.DisplayName());
|
||||||
|
Assert.Equal("description from localizer en-US", context.DisplayMetadata.Description());
|
||||||
|
Assert.Equal("prompt from localizer en-US", context.DisplayMetadata.Placeholder());
|
||||||
|
}
|
||||||
|
using (new CultureReplacer("fr-FR", "fr-FR"))
|
||||||
|
{
|
||||||
|
Assert.Equal("name from localizer fr-FR", context.DisplayMetadata.DisplayName());
|
||||||
|
Assert.Equal("description from localizer fr-FR", context.DisplayMetadata.Description());
|
||||||
|
Assert.Equal("prompt from localizer fr-FR", context.DisplayMetadata.Placeholder());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -589,6 +603,48 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
Assert.Equal(expectedDictionary, context.DisplayMetadata.EnumNamesAndValues);
|
Assert.Equal(expectedDictionary, context.DisplayMetadata.EnumNamesAndValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDisplayMetadata_DisplayName_LocalizeWithStringLocalizer()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedKeyValuePairs = new List<KeyValuePair<EnumGroupAndName, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<EnumGroupAndName, string>(new EnumGroupAndName("Zero", string.Empty), "0"),
|
||||||
|
new KeyValuePair<EnumGroupAndName, string>(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayNames.One)), "1"),
|
||||||
|
new KeyValuePair<EnumGroupAndName, string>(new EnumGroupAndName(string.Empty, "dos value"), "2"),
|
||||||
|
new KeyValuePair<EnumGroupAndName, string>(new EnumGroupAndName(string.Empty, "tres value"), "3"),
|
||||||
|
new KeyValuePair<EnumGroupAndName, string>(new EnumGroupAndName(string.Empty, "name from resources"), "-2"),
|
||||||
|
new KeyValuePair<EnumGroupAndName, string>(new EnumGroupAndName("Negatives", "menos uno value"), "-1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var type = typeof(EnumWithDisplayNames);
|
||||||
|
var attributes = new object[0];
|
||||||
|
|
||||||
|
var key = ModelMetadataIdentity.ForType(type);
|
||||||
|
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(attributes));
|
||||||
|
|
||||||
|
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
|
||||||
|
stringLocalizer
|
||||||
|
.Setup(s => s[It.IsAny<string>()])
|
||||||
|
.Returns<string>((index) => new LocalizedString(index, index + " value"));
|
||||||
|
|
||||||
|
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
|
||||||
|
stringLocalizerFactory
|
||||||
|
.Setup(f => f.Create(It.IsAny<Type>()))
|
||||||
|
.Returns(stringLocalizer.Object);
|
||||||
|
|
||||||
|
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.CreateDisplayMetadata(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
expectedKeyValuePairs,
|
||||||
|
context.DisplayMetadata.EnumGroupedDisplayNamesAndValues,
|
||||||
|
KVPEnumGroupAndNameComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
// Type -> expected EnumDisplayNamesAndValues
|
// Type -> expected EnumDisplayNamesAndValues
|
||||||
public static TheoryData<Type, IEnumerable<KeyValuePair<EnumGroupAndName, string>>> EnumDisplayNamesData
|
public static TheoryData<Type, IEnumerable<KeyValuePair<EnumGroupAndName, string>>> EnumDisplayNamesData
|
||||||
{
|
{
|
||||||
|
|
@ -719,12 +775,90 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
provider.CreateDisplayMetadata(context);
|
provider.CreateDisplayMetadata(context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// OrderBy is used because the order of the results may very depending on the platform / client.
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
expectedKeyValuePairs?.OrderBy(item => item.Key.Group, StringComparer.Ordinal)
|
expectedKeyValuePairs,
|
||||||
.ThenBy(item => item.Key.Name, StringComparer.Ordinal),
|
context.DisplayMetadata.EnumGroupedDisplayNamesAndValues,
|
||||||
context.DisplayMetadata.EnumGroupedDisplayNamesAndValues?.OrderBy(item => item.Key.Group, StringComparer.Ordinal)
|
KVPEnumGroupAndNameComparer.Instance);
|
||||||
.ThenBy(item => item.Key.Name, StringComparer.Ordinal));
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithNoIStringLocalizerAndNoResourceType()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var groupTwo = Assert.Single(enumNameAndGroup, e => e.Value.Equals("2", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
using (new CultureReplacer("en-US", "en-US"))
|
||||||
|
{
|
||||||
|
Assert.Equal("Loc_Two_Name", groupTwo.Key.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (new CultureReplacer("fr-FR", "fr-FR"))
|
||||||
|
{
|
||||||
|
Assert.Equal("Loc_Two_Name", groupTwo.Key.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithIStringLocalizerAndNoResourceType()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var groupTwo = Assert.Single(enumNameAndGroup, e => e.Value.Equals("2", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
using (new CultureReplacer("en-US", "en-US"))
|
||||||
|
{
|
||||||
|
Assert.Equal("Loc_Two_Name en-US", groupTwo.Key.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (new CultureReplacer("fr-FR", "fr-FR"))
|
||||||
|
{
|
||||||
|
Assert.Equal("Loc_Two_Name fr-FR", groupTwo.Key.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithNoIStringLocalizerAndResourceType()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var groupThree = Assert.Single(enumNameAndGroup, e => e.Value.Equals("3", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
using (new CultureReplacer("en-US", "en-US"))
|
||||||
|
{
|
||||||
|
Assert.Equal("type three name en-US", groupThree.Key.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (new CultureReplacer("fr-FR", "fr-FR"))
|
||||||
|
{
|
||||||
|
Assert.Equal("type three name fr-FR", groupThree.Key.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithIStringLocalizerAndResourceType()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: true);
|
||||||
|
|
||||||
|
var groupThree = Assert.Single(enumNameAndGroup, e => e.Value.Equals("3", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
using (new CultureReplacer("en-US", "en-US"))
|
||||||
|
{
|
||||||
|
Assert.Equal("type three name en-US", groupThree.Key.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (new CultureReplacer("fr-FR", "fr-FR"))
|
||||||
|
{
|
||||||
|
Assert.Equal("type three name fr-FR", groupThree.Key.Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -848,6 +982,70 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
Assert.Same(attribute, validatorMetadata);
|
Assert.Same(attribute, validatorMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<KeyValuePair<EnumGroupAndName, string>> GetLocalizedEnumGroupedDisplayNamesAndValues(
|
||||||
|
bool useStringLocalizer)
|
||||||
|
{
|
||||||
|
var provider = CreateIStringLocalizerProvider(useStringLocalizer);
|
||||||
|
|
||||||
|
var key = ModelMetadataIdentity.ForType(typeof(EnumWithLocalizedDisplayNames));
|
||||||
|
var attributes = new object[0];
|
||||||
|
|
||||||
|
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(attributes));
|
||||||
|
provider.CreateDisplayMetadata(context);
|
||||||
|
|
||||||
|
return context.DisplayMetadata.EnumGroupedDisplayNamesAndValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataAnnotationsMetadataProvider CreateIStringLocalizerProvider(bool useStringLocalizer)
|
||||||
|
{
|
||||||
|
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
|
||||||
|
stringLocalizer
|
||||||
|
.Setup(loc => loc[It.IsAny<string>()])
|
||||||
|
.Returns<string>((k =>
|
||||||
|
{
|
||||||
|
return new LocalizedString(k, $"{k} {CultureInfo.CurrentCulture}");
|
||||||
|
}));
|
||||||
|
|
||||||
|
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
|
||||||
|
stringLocalizerFactory
|
||||||
|
.Setup(factory => factory.Create(typeof(EnumWithLocalizedDisplayNames)))
|
||||||
|
.Returns(stringLocalizer.Object);
|
||||||
|
|
||||||
|
return new DataAnnotationsMetadataProvider(
|
||||||
|
useStringLocalizer ? stringLocalizerFactory.Object : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KVPEnumGroupAndNameComparer : IEqualityComparer<KeyValuePair<EnumGroupAndName, string>>
|
||||||
|
{
|
||||||
|
public static readonly IEqualityComparer<KeyValuePair<EnumGroupAndName, string>> Instance = new KVPEnumGroupAndNameComparer();
|
||||||
|
|
||||||
|
private KVPEnumGroupAndNameComparer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(KeyValuePair<EnumGroupAndName, string> x, KeyValuePair<EnumGroupAndName, string> y)
|
||||||
|
{
|
||||||
|
using (new CultureReplacer(string.Empty, string.Empty))
|
||||||
|
{
|
||||||
|
return x.Key.Name.Equals(y.Key.Name, StringComparison.Ordinal)
|
||||||
|
&& x.Key.Group.Equals(y.Key.Group, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(KeyValuePair<EnumGroupAndName, string> obj)
|
||||||
|
{
|
||||||
|
using (new CultureReplacer(string.Empty, string.Empty))
|
||||||
|
{
|
||||||
|
var hashcode = HashCodeCombiner.Start();
|
||||||
|
|
||||||
|
hashcode.Add(obj.Key.Name);
|
||||||
|
hashcode.Add(obj.Key.Group);
|
||||||
|
|
||||||
|
return hashcode.CombinedHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class TestValidationAttribute : ValidationAttribute, IClientModelValidator
|
private class TestValidationAttribute : ValidationAttribute, IClientModelValidator
|
||||||
{
|
{
|
||||||
public void AddValidation(ClientModelValidationContext context)
|
public void AddValidation(ClientModelValidationContext context)
|
||||||
|
|
@ -874,6 +1072,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum EnumWithLocalizedDisplayNames
|
||||||
|
{
|
||||||
|
[Display(Name = "Loc_Two_Name")]
|
||||||
|
Two = 2,
|
||||||
|
[Display(Name = nameof(TestResources.Type_Three_Name), ResourceType = typeof(TestResources))]
|
||||||
|
Three = 3
|
||||||
|
}
|
||||||
|
|
||||||
private enum EmptyEnum
|
private enum EmptyEnum
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
// internal properties.
|
// internal properties.
|
||||||
public static class TestResources
|
public static class TestResources
|
||||||
{
|
{
|
||||||
public static string DisplayAttribute_Description { get; } = Resources.DisplayAttribute_Description;
|
public static string Type_Three_Name => "type three name " + CultureInfo.CurrentCulture;
|
||||||
|
|
||||||
public static string DisplayAttribute_Name { get; } = Resources.DisplayAttribute_Name;
|
public static string DisplayAttribute_Description => Resources.DisplayAttribute_Description;
|
||||||
|
|
||||||
public static string DisplayAttribute_Prompt { get; } = Resources.DisplayAttribute_Prompt;
|
public static string DisplayAttribute_Name => Resources.DisplayAttribute_Name;
|
||||||
|
|
||||||
|
public static string DisplayAttribute_Prompt => Resources.DisplayAttribute_Prompt;
|
||||||
|
|
||||||
public static string DisplayAttribute_CultureSensitiveName =>
|
public static string DisplayAttribute_CultureSensitiveName =>
|
||||||
Resources.DisplayAttribute_Name + CultureInfo.CurrentUICulture;
|
Resources.DisplayAttribute_Name + CultureInfo.CurrentUICulture;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
|
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
|
|
@ -14,13 +15,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
internal class TestModelMetadataProvider : DefaultModelMetadataProvider
|
internal class TestModelMetadataProvider : DefaultModelMetadataProvider
|
||||||
{
|
{
|
||||||
// Creates a provider with all the defaults - includes data annotations
|
// Creates a provider with all the defaults - includes data annotations
|
||||||
public static IModelMetadataProvider CreateDefaultProvider()
|
public static IModelMetadataProvider CreateDefaultProvider(IStringLocalizerFactory stringLocalizerFactory = null)
|
||||||
{
|
{
|
||||||
var detailsProviders = new IMetadataDetailsProvider[]
|
var detailsProviders = new IMetadataDetailsProvider[]
|
||||||
{
|
{
|
||||||
new DefaultBindingMetadataProvider(),
|
new DefaultBindingMetadataProvider(),
|
||||||
new DefaultValidationMetadataProvider(),
|
new DefaultValidationMetadataProvider(),
|
||||||
new DataAnnotationsMetadataProvider(stringLocalizerFactory: null),
|
new DataAnnotationsMetadataProvider(stringLocalizerFactory),
|
||||||
new DataMemberRequiredBindingMetadataProvider(),
|
new DataMemberRequiredBindingMetadataProvider(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.WebEncoders.Testing;
|
using Microsoft.Extensions.WebEncoders.Testing;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
@ -161,9 +162,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
|
|
||||||
public static HtmlHelper<TModel> GetHtmlHelper<TModel>(
|
public static HtmlHelper<TModel> GetHtmlHelper<TModel>(
|
||||||
TModel model,
|
TModel model,
|
||||||
ICompositeViewEngine viewEngine)
|
ICompositeViewEngine viewEngine,
|
||||||
|
IStringLocalizerFactory stringLocalizerFactory = null)
|
||||||
{
|
{
|
||||||
return GetHtmlHelper(model, CreateUrlHelper(), viewEngine, TestModelMetadataProvider.CreateDefaultProvider());
|
return GetHtmlHelper(
|
||||||
|
model,
|
||||||
|
CreateUrlHelper(),
|
||||||
|
viewEngine,
|
||||||
|
TestModelMetadataProvider.CreateDefaultProvider(stringLocalizerFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HtmlHelper<TModel> GetHtmlHelper<TModel>(
|
public static HtmlHelper<TModel> GetHtmlHelper<TModel>(
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.TestCommon;
|
using Microsoft.AspNetCore.Mvc.TestCommon;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -17,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
public void DisplayHelpers_FindsModel_WhenViewDataIsNotSet()
|
public void DisplayHelpers_FindsModel_WhenViewDataIsNotSet()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = $"<div class=\"HtmlEncode[[display-label]]\">HtmlEncode[[SomeProperty]]</div>{Environment.NewLine}" +
|
var expected = $"<div class=\"HtmlEncode[[display-label]]\">HtmlEncode[[SomeProperty]]</div>{Environment.NewLine}" +
|
||||||
$"<div class=\"HtmlEncode[[display-field]]\">HtmlEncode[[PropValue]]</div>{Environment.NewLine}";
|
$"<div class=\"HtmlEncode[[display-field]]\">HtmlEncode[[PropValue]]</div>{Environment.NewLine}";
|
||||||
var model = new SomeModel
|
var model = new SomeModel
|
||||||
{
|
{
|
||||||
|
|
@ -223,6 +225,75 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
Assert.Equal("ViewDataValue", HtmlContentUtilities.HtmlContentToString(displayResult));
|
Assert.Equal("ViewDataValue", HtmlContentUtilities.HtmlContentToString(displayResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisplayFor_EnumProperty_IStringLocalizedValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new StatusModel
|
||||||
|
{
|
||||||
|
Status = Status.Created
|
||||||
|
};
|
||||||
|
var view = new Mock<IView>();
|
||||||
|
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
|
||||||
|
.Callback((ViewContext v) => v.Writer.WriteAsync(v.ViewData.TemplateInfo.FormattedModelValue.ToString()))
|
||||||
|
.Returns(Task.FromResult(0));
|
||||||
|
var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
|
||||||
|
viewEngine
|
||||||
|
.Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
|
||||||
|
viewEngine
|
||||||
|
.Setup(v => v.FindView(It.IsAny<ActionContext>(), "DisplayTemplates/Status", /*isMainPage*/ false))
|
||||||
|
.Returns(ViewEngineResult.Found("SomeView", view.Object));
|
||||||
|
|
||||||
|
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
|
||||||
|
stringLocalizer
|
||||||
|
.Setup(s => s["CreatedKey"])
|
||||||
|
.Returns<string>((key) =>
|
||||||
|
{
|
||||||
|
return new LocalizedString(key, "created from IStringLocalizer");
|
||||||
|
});
|
||||||
|
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>();
|
||||||
|
stringLocalizerFactory
|
||||||
|
.Setup(s => s.Create(typeof(Status)))
|
||||||
|
.Returns(stringLocalizer.Object);
|
||||||
|
|
||||||
|
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object, stringLocalizerFactory.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var displayResult = helper.DisplayFor(m => m.Status);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("created from IStringLocalizer", HtmlContentUtilities.HtmlContentToString(displayResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisplayFor_EnumProperty_ResourceTypeLocalizedValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new StatusModel
|
||||||
|
{
|
||||||
|
Status = Status.Faulted
|
||||||
|
};
|
||||||
|
var view = new Mock<IView>();
|
||||||
|
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
|
||||||
|
.Callback((ViewContext v) => v.Writer.WriteAsync(v.ViewData.TemplateInfo.FormattedModelValue.ToString()))
|
||||||
|
.Returns(Task.FromResult(0));
|
||||||
|
var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
|
||||||
|
viewEngine
|
||||||
|
.Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
|
||||||
|
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
|
||||||
|
viewEngine
|
||||||
|
.Setup(v => v.FindView(It.IsAny<ActionContext>(), "DisplayTemplates/Status", /*isMainPage*/ false))
|
||||||
|
.Returns(ViewEngineResult.Found("SomeView", view.Object));
|
||||||
|
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var displayResult = helper.DisplayFor(m => m.Status);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Faulted from ResourceType", HtmlContentUtilities.HtmlContentToString(displayResult));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DisplayFor_UsesTemplateNameAndHtmlFieldName()
|
public void DisplayFor_UsesTemplateNameAndHtmlFieldName()
|
||||||
{
|
{
|
||||||
|
|
@ -356,5 +427,24 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
{
|
{
|
||||||
public string SomeProperty { get; set; }
|
public string SomeProperty { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class StatusModel
|
||||||
|
{
|
||||||
|
public Status Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StatusResource
|
||||||
|
{
|
||||||
|
public static string FaultedKey { get { return "Faulted from ResourceType"; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Status : byte
|
||||||
|
{
|
||||||
|
[Display(Name = "CreatedKey")]
|
||||||
|
Created,
|
||||||
|
[Display(Name = "FaultedKey", ResourceType = typeof(StatusResource))]
|
||||||
|
Faulted,
|
||||||
|
Done
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -1298,6 +1300,32 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
$"The type '{ typeof(StructWithFields).FullName }' is not supported.");
|
$"The type '{ typeof(StructWithFields).FullName }' is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture("en-US", "en-US")]
|
||||||
|
public void GetEnumSelectListTEnum_DisplayAttributeUsesIStringLocalizer()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stringLocalizer = new Mock<IStringLocalizer>();
|
||||||
|
stringLocalizer
|
||||||
|
.Setup(s => s[It.IsAny<string>()])
|
||||||
|
.Returns<string>((s) => { return new LocalizedString(s, s + " " + CultureInfo.CurrentCulture); });
|
||||||
|
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>();
|
||||||
|
stringLocalizerFactory
|
||||||
|
.Setup(s => s.Create(It.IsAny<Type>()))
|
||||||
|
.Returns(stringLocalizer.Object);
|
||||||
|
|
||||||
|
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(stringLocalizerFactory.Object);
|
||||||
|
var metadata = metadataProvider.GetMetadataForType(typeof(EnumWithFields));
|
||||||
|
var htmlHelper = new TestHtmlHelper(metadataProvider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = htmlHelper.GetEnumSelectList<EnumWithDisplayNames>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var zeroSelect = Assert.Single(result, s => s.Value.Equals("0", StringComparison.Ordinal));
|
||||||
|
Assert.Equal("cero en-US", zeroSelect.Text);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetEnumSelectListTEnum_WrapsGetEnumSelectListModelMetadata()
|
public void GetEnumSelectListTEnum_WrapsGetEnumSelectListModelMetadata()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue