Fixes #5198 Stops caching of Enum display values (#5185)

Fixes #5197 GetEnumSelectList uses IStringLocalizer
Fixes #4215 Html.DisplayFor now checks DisplayAttributes on enums
This commit is contained in:
Ryan Brandenburg 2016-09-07 16:00:17 -07:00 committed by GitHub
parent 21236cc98e
commit 830983a477
9 changed files with 415 additions and 29 deletions

View File

@ -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();
}
}
} }
} }

View File

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

View File

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

View File

@ -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
{ {
} }

View File

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

View File

@ -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(),
}; };

View File

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

View File

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

View File

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