From 68bc79e3fab302c678a57625b9b1334ba6070421 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Mon, 22 Aug 2016 16:34:55 -0700 Subject: [PATCH] Fixes #5198 Stops caching of Enum display values Fixes #5197 GetEnumSelectList uses IStringLocalizer Fixes #4215 Html.DisplayFor now checks DisplayAttributes on enums --- .../ModelBinding/EnumGroupAndName.cs | 38 ++- .../DataAnnotationsMetadataProvider.cs | 17 +- .../ViewFeatures/HtmlHelper.cs | 18 ++ .../DataAnnotationsMetadataProviderTest.cs | 228 +++++++++++++++++- .../Internal/TestResources.cs | 8 +- .../TestModelMetadataProvider.cs | 5 +- .../Rendering/DefaultTemplatesUtilities.cs | 10 +- .../HtmlHelperDisplayExtensionsTest.cs | 92 ++++++- .../Rendering/HtmlHelperSelectTest.cs | 28 +++ 9 files changed, 415 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/EnumGroupAndName.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/EnumGroupAndName.cs index 42f0843d76..fd44fc3c11 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/EnumGroupAndName.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/EnumGroupAndName.cs @@ -10,8 +10,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// public struct EnumGroupAndName { + private Func _name; + /// - /// Initializes a new instance of the EnumGroupAndName structure. + /// Initializes a new instance of the structure. This constructor should + /// not be used in any site where localization is important. /// /// The group name. /// The name. @@ -28,7 +31,30 @@ public EnumGroupAndName(string group, string name) } Group = group; - Name = name; + _name = () => name; + } + + /// + /// Initializes a new instance of the structure. + /// + /// The group name. + /// A which will return the name. + public EnumGroupAndName( + string group, + Func name) + { + if (group == null) + { + throw new ArgumentNullException(nameof(group)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Group = group; + _name = name; } /// @@ -39,6 +65,12 @@ public EnumGroupAndName(string group, string name) /// /// Gets the name. /// - public string Name { get; } + public string Name + { + get + { + return _name(); + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs index 2cbb4f2ac4..f8b91947a8 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs @@ -153,15 +153,17 @@ public void CreateDisplayMetadata(DisplayMetadataProviderContext context) // Dictionary does not guarantee order will be preserved. var groupedDisplayNamesAndValues = new List>(); var namesAndValues = new Dictionary(); + var enumLocalizer = _stringLocalizerFactory?.Create(underlyingType); foreach (var name in Enum.GetNames(underlyingType)) { var field = underlyingType.GetField(name); - var displayName = GetDisplayName(field); var groupName = GetDisplayGroup(field); var value = ((Enum)field.GetValue(obj: null)).ToString("d"); groupedDisplayNamesAndValues.Add(new KeyValuePair( - new EnumGroupAndName(groupName, displayName), + new EnumGroupAndName( + groupName, + () => GetDisplayName(field, enumLocalizer)), value)); namesAndValues.Add(name, value); } @@ -291,18 +293,19 @@ public void CreateValidationMetadata(ValidationMetadataProviderContext context) } } - // Return non-empty name specified in a [Display] attribute for a field, if any; field.Name otherwise. - private static string GetDisplayName(FieldInfo field) + private static string GetDisplayName(FieldInfo field, IStringLocalizer stringLocalizer) { var display = field.GetCustomAttribute(inherit: false); 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(); - if (name != null) + if (stringLocalizer != null && !string.IsNullOrEmpty(name) && display.ResourceType == null) { - return name; + name = stringLocalizer[name]; } + + return name ?? field.Name; } return field.Name; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs index a82fa8b9cb..85405bbc2b 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -522,6 +523,23 @@ protected virtual IHtmlContent GenerateDisplay( string templateName, 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( _viewEngine, _bufferScope, diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs index 380ee418b4..f53fa7d348 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs @@ -2,12 +2,17 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Testing; +using Microsoft.DotNet.InternalAbstractions; using Microsoft.Extensions.Localization; using Moq; using Xunit; @@ -353,13 +358,13 @@ public void CreateDisplayMetadata_DisplayAttribute_LocalizeProperties() var stringLocalizer = new Mock(MockBehavior.Strict); stringLocalizer .Setup(s => s["Model_Name"]) - .Returns(new LocalizedString("Model_Name", "name from localizer")); + .Returns(() => new LocalizedString("Model_Name", "name from localizer " + CultureInfo.CurrentCulture)); stringLocalizer .Setup(s => s["Model_Description"]) - .Returns(new LocalizedString("Model_Description", "description from localizer")); + .Returns(() => new LocalizedString("Model_Description", "description from localizer " + CultureInfo.CurrentCulture)); stringLocalizer .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(MockBehavior.Strict); stringLocalizerFactory @@ -383,9 +388,18 @@ public void CreateDisplayMetadata_DisplayAttribute_LocalizeProperties() provider.CreateDisplayMetadata(context); // Assert - Assert.Equal("name from localizer", context.DisplayMetadata.DisplayName()); - Assert.Equal("description from localizer", context.DisplayMetadata.Description()); - Assert.Equal("prompt from localizer", context.DisplayMetadata.Placeholder()); + using (new CultureReplacer("en-US", "en-US")) + { + 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] @@ -589,6 +603,48 @@ public void CreateDisplayMetadata_EnumNamesAndValues_ReflectsModelType( Assert.Equal(expectedDictionary, context.DisplayMetadata.EnumNamesAndValues); } + [Fact] + public void CreateDisplayMetadata_DisplayName_LocalizeWithStringLocalizer() + { + // Arrange + var expectedKeyValuePairs = new List> + { + new KeyValuePair(new EnumGroupAndName("Zero", string.Empty), "0"), + new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayNames.One)), "1"), + new KeyValuePair(new EnumGroupAndName(string.Empty, "dos value"), "2"), + new KeyValuePair(new EnumGroupAndName(string.Empty, "tres value"), "3"), + new KeyValuePair(new EnumGroupAndName(string.Empty, "name from resources"), "-2"), + new KeyValuePair(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(MockBehavior.Strict); + stringLocalizer + .Setup(s => s[It.IsAny()]) + .Returns((index) => new LocalizedString(index, index + " value")); + + var stringLocalizerFactory = new Mock(MockBehavior.Strict); + stringLocalizerFactory + .Setup(f => f.Create(It.IsAny())) + .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 public static TheoryData>> EnumDisplayNamesData { @@ -719,12 +775,90 @@ public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_ReflectsModel provider.CreateDisplayMetadata(context); // Assert - // OrderBy is used because the order of the results may very depending on the platform / client. Assert.Equal( - expectedKeyValuePairs?.OrderBy(item => item.Key.Group, StringComparer.Ordinal) - .ThenBy(item => item.Key.Name, StringComparer.Ordinal), - context.DisplayMetadata.EnumGroupedDisplayNamesAndValues?.OrderBy(item => item.Key.Group, StringComparer.Ordinal) - .ThenBy(item => item.Key.Name, StringComparer.Ordinal)); + expectedKeyValuePairs, + context.DisplayMetadata.EnumGroupedDisplayNamesAndValues, + KVPEnumGroupAndNameComparer.Instance); + } + + [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] @@ -848,6 +982,70 @@ public void CreateValidationDetails_ValidatableObject_AlreadyInContext_Ignores() Assert.Same(attribute, validatorMetadata); } + private IEnumerable> 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(MockBehavior.Strict); + stringLocalizer + .Setup(loc => loc[It.IsAny()]) + .Returns((k => + { + return new LocalizedString(k, $"{k} {CultureInfo.CurrentCulture}"); + })); + + var stringLocalizerFactory = new Mock(MockBehavior.Strict); + stringLocalizerFactory + .Setup(factory => factory.Create(typeof(EnumWithLocalizedDisplayNames))) + .Returns(stringLocalizer.Object); + + return new DataAnnotationsMetadataProvider( + useStringLocalizer ? stringLocalizerFactory.Object : null); + } + + private class KVPEnumGroupAndNameComparer : IEqualityComparer> + { + public static readonly IEqualityComparer> Instance = new KVPEnumGroupAndNameComparer(); + + private KVPEnumGroupAndNameComparer() + { + } + + public bool Equals(KeyValuePair x, KeyValuePair 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 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 { public void AddValidation(ClientModelValidationContext context) @@ -874,6 +1072,14 @@ private class ClassWithProperties 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 { } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/TestResources.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/TestResources.cs index 9a6dcc3f85..4d02719fa8 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/TestResources.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/TestResources.cs @@ -10,11 +10,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding // internal properties. 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 => Resources.DisplayAttribute_Name + CultureInfo.CurrentUICulture; diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs index 53bbb14d54..4b9a418f64 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.Extensions.Localization; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding @@ -14,13 +15,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding internal class TestModelMetadataProvider : DefaultModelMetadataProvider { // 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[] { new DefaultBindingMetadataProvider(), new DefaultValidationMetadataProvider(), - new DataAnnotationsMetadataProvider(stringLocalizerFactory: null), + new DataAnnotationsMetadataProvider(stringLocalizerFactory), new DataMemberRequiredBindingMetadataProvider(), }; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs index a2cc705b71..d61113ebd7 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs @@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Microsoft.Extensions.WebEncoders.Testing; using Moq; @@ -161,9 +162,14 @@ public static HtmlHelper GetHtmlHelper(TModel model, IModelMetad public static HtmlHelper GetHtmlHelper( 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 GetHtmlHelper( diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs index d1c294c1e8..c5c8c09dc6 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.Extensions.Localization; using Moq; using Xunit; @@ -17,7 +19,7 @@ public class HtmlHelperDisplayExtensionsTest public void DisplayHelpers_FindsModel_WhenViewDataIsNotSet() { // Arrange - var expected = $"
HtmlEncode[[SomeProperty]]
{Environment.NewLine}" + + var expected = $"
HtmlEncode[[SomeProperty]]
{Environment.NewLine}" + $"
HtmlEncode[[PropValue]]
{Environment.NewLine}"; var model = new SomeModel { @@ -223,6 +225,75 @@ public void DisplayFor_UsesTemplateNameAndAdditionalViewData() Assert.Equal("ViewDataValue", HtmlContentUtilities.HtmlContentToString(displayResult)); } + [Fact] + public void DisplayFor_EnumProperty_IStringLocalizedValue() + { + // Arrange + var model = new StatusModel + { + Status = Status.Created + }; + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => v.Writer.WriteAsync(v.ViewData.TemplateInfo.FormattedModelValue.ToString())) + .Returns(Task.FromResult(0)); + var viewEngine = new Mock(MockBehavior.Strict); + viewEngine + .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny(), /*isMainPage*/ false)) + .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty())); + viewEngine + .Setup(v => v.FindView(It.IsAny(), "DisplayTemplates/Status", /*isMainPage*/ false)) + .Returns(ViewEngineResult.Found("SomeView", view.Object)); + + var stringLocalizer = new Mock(MockBehavior.Strict); + stringLocalizer + .Setup(s => s["CreatedKey"]) + .Returns((key) => + { + return new LocalizedString(key, "created from IStringLocalizer"); + }); + var stringLocalizerFactory = new Mock(); + 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(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => v.Writer.WriteAsync(v.ViewData.TemplateInfo.FormattedModelValue.ToString())) + .Returns(Task.FromResult(0)); + var viewEngine = new Mock(MockBehavior.Strict); + viewEngine + .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny(), /*isMainPage*/ false)) + .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty())); + viewEngine + .Setup(v => v.FindView(It.IsAny(), "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] public void DisplayFor_UsesTemplateNameAndHtmlFieldName() { @@ -356,5 +427,24 @@ private class SomeModel { 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 + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs index 969cb70c38..73c7692cda 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Localization; using Moq; using Xunit; @@ -1298,6 +1300,32 @@ public void GetEnumSelectListTEnum_ThrowsWithNonEnum() $"The type '{ typeof(StructWithFields).FullName }' is not supported."); } + [Fact] + [ReplaceCulture("en-US", "en-US")] + public void GetEnumSelectListTEnum_DisplayAttributeUsesIStringLocalizer() + { + // Arrange + var stringLocalizer = new Mock(); + stringLocalizer + .Setup(s => s[It.IsAny()]) + .Returns((s) => { return new LocalizedString(s, s + " " + CultureInfo.CurrentCulture); }); + var stringLocalizerFactory = new Mock(); + stringLocalizerFactory + .Setup(s => s.Create(It.IsAny())) + .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(); + + // Assert + var zeroSelect = Assert.Single(result, s => s.Value.Equals("0", StringComparison.Ordinal)); + Assert.Equal("cero en-US", zeroSelect.Text); + } + [Fact] public void GetEnumSelectListTEnum_WrapsGetEnumSelectListModelMetadata() {