From 8f8bf5af341b5cc07e0611dc9a3a02e471e2bf63 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Tue, 22 Nov 2016 12:16:53 -0800 Subject: [PATCH] Seperate view and model for enum display --- .../ViewFeatures/HtmlHelper.cs | 18 --- .../ViewFeatures/TemplateBuilder.cs | 48 +++++-- .../HtmlGenerationTest.cs | 10 ++ .../HtmlHelperDisplayExtensionsTest.cs | 125 ++++++++++++++++-- .../HtmlHelperEditorExtensionsTest.cs | 68 ++++++++++ .../HtmlGeneration_HomeController.cs | 5 + .../HtmlGenerationWebSite/Models/AClass.cs | 14 ++ .../HtmlGenerationWebSite/Models/DayOfWeek.cs | 16 +++ .../HtmlGenerationWebSite/Models/Month.cs | 14 ++ .../Views/HtmlGeneration_Home/Enum.cshtml | 4 + .../Shared/DisplayTemplates/DayOfWeek.cshtml | 26 ++++ 11 files changed, 303 insertions(+), 45 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperEditorExtensionsTest.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/AClass.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/DayOfWeek.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Models/Month.cs create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Enum.cshtml create mode 100644 test/WebSites/HtmlGenerationWebSite/Views/Shared/DisplayTemplates/DayOfWeek.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs index feaa3b7615..008719ec2f 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -524,23 +523,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures 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/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs index 2040495e9d..1133f84f8c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Globalization; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -81,19 +82,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal _model = null; } - var formattedModelValue = _model; - if (_model == null && _readOnly) - { - formattedModelValue = _metadata.NullDisplayText; - } - - var formatString = _readOnly ? _metadata.DisplayFormatString : _metadata.EditFormatString; - - if (_model != null && !string.IsNullOrEmpty(formatString)) - { - formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, _model); - } - // Normally this shouldn't happen, unless someone writes their own custom Object templates which // don't check to make sure that the object hasn't already been displayed if (_viewData.TemplateInfo.Visited(_modelExplorer)) @@ -108,6 +96,40 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // though _model may have been reset to null. Otherwise we might lose track of the model type /property. viewData.ModelExplorer = _modelExplorer.GetExplorerForModel(_model); + var formattedModelValue = _model; + if (_model == null && _readOnly) + { + formattedModelValue = _metadata.NullDisplayText; + } + else if (viewData.ModelMetadata.IsEnum) + { + // Cover the case where the model is an enum and we want the string value of it + var modelEnum = _model as Enum; + if (modelEnum != null) + { + var value = modelEnum.ToString("d"); + var enumGrouped = viewData.ModelMetadata.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 + formattedModelValue = kvp.Key.Name; + break; + } + } + } + } + + var formatString = _readOnly ? + viewData.ModelMetadata.DisplayFormatString : + viewData.ModelMetadata.EditFormatString; + if (_model != null && !string.IsNullOrEmpty(formatString)) + { + formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, formattedModelValue); + } + viewData.TemplateInfo.FormattedModelValue = formattedModelValue; viewData.TemplateInfo.HtmlFieldPrefix = _viewData.TemplateInfo.GetFullHtmlFieldName(_htmlFieldName); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs index 4c1102fbe3..3773ced1be 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs @@ -71,6 +71,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests } } + [Fact] + public async Task EnumValues_SerializeCorrectly() + { + // Arrange & Act + var response = await Client.GetStringAsync("http://localhost/HtmlGeneration_Home/Enum"); + + // Assert + Assert.Equal($"Vrijdag{Environment.NewLine}Month: January", response, ignoreLineEndingDifferences: true); + } + [Theory] [MemberData(nameof(WebPagesData))] public async Task HtmlGenerationWebSite_GeneratesExpectedResults(string action, string antiforgeryPath) diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs index c5c8c09dc6..38de81e40f 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs @@ -94,6 +94,94 @@ namespace Microsoft.AspNetCore.Mvc.Rendering viewEngine.Verify(); } + public static TheoryData EnumFormatModels + { + get + { + return new TheoryData + { + { + new FormatModel{ FormatProperty = Status.Created }, + "Value: CreatedKey" + }, + { + new FormatModel { FormatProperty = Status.Done }, + "Value: Done" + } + }; + } + } + + public static TheoryData EnumUnformattedModels + { + get + { + return new TheoryData + { + { + new FormatModel {NonFormatProperty = Status.Created }, + "CreatedKey" + }, + { + new FormatModel {NonFormatProperty = Status.Done }, + "Done" + } + }; + } + } + + [Theory] + [MemberData(nameof(EnumUnformattedModels))] + public void Display_UsesTemplateUnFormatted(FormatModel model, string expectedResult) + { + // Arrange + 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("Status", view.Object)) + .Verifiable(); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object); + + // Act + var displayResult = helper.DisplayFor(x => x.NonFormatProperty); + + // Assert + Assert.Equal(expectedResult, HtmlContentUtilities.HtmlContentToString(displayResult)); + } + + [Theory] + [MemberData(nameof(EnumFormatModels))] + public void Display_UsesTemplateFormatted(FormatModel model, string expectedResult) + { + // Arrange + 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("Status", view.Object)) + .Verifiable(); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object); + + // Act + var displayResult = helper.DisplayFor(x => x.FormatProperty); + + // Assert + Assert.Equal(expectedResult, HtmlContentUtilities.HtmlContentToString(displayResult)); + } + [Fact] public void Display_UsesTemplateNameAndAdditionalViewData() { @@ -423,6 +511,29 @@ namespace Microsoft.AspNetCore.Mvc.Rendering Assert.Equal("SomeField", HtmlContentUtilities.HtmlContentToString(displayResult)); } + + public class StatusResource + { + public static string FaultedKey { get { return "Faulted from ResourceType"; } } + } + + public enum Status : byte + { + [Display(Name = "CreatedKey")] + Created, + [Display(Name = "FaultedKey", ResourceType = typeof(StatusResource))] + Faulted, + Done + } + + public class FormatModel + { + [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "Value: {0}")] + public Status FormatProperty { get; set; } + + public Status NonFormatProperty { get; set; } + } + private class SomeModel { public string SomeProperty { get; set; } @@ -432,19 +543,5 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { 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/HtmlHelperEditorExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperEditorExtensionsTest.cs new file mode 100644 index 0000000000..a73fc44a2f --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperEditorExtensionsTest.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.TestCommon; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Rendering +{ + public class HtmlHelperEditorExtensionsTest + { + [Theory] + [MemberData(nameof(HtmlHelperDisplayExtensionsTest.EnumUnformattedModels), + MemberType = typeof(HtmlHelperDisplayExtensionsTest))] + public void Display_UsesTemplateUnFormatted(HtmlHelperDisplayExtensionsTest.FormatModel model, string expectedResult) + { + // Arrange + 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(), "EditorTemplates/Status", /*isMainPage*/ false)) + .Returns(ViewEngineResult.Found("Status", view.Object)) + .Verifiable(); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object); + + // Act + var displayResult = helper.EditorFor(x => x.NonFormatProperty); + + // Assert + Assert.Equal(expectedResult, HtmlContentUtilities.HtmlContentToString(displayResult)); + } + + [Theory] + [MemberData(nameof(HtmlHelperDisplayExtensionsTest.EnumFormatModels), MemberType = typeof(HtmlHelperDisplayExtensionsTest))] + public void Display_UsesTemplateFormatted(HtmlHelperDisplayExtensionsTest.FormatModel model, string expectedResult) + { + // Arrange + 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(), "EditorTemplates/Status", /*isMainPage*/ false)) + .Returns(ViewEngineResult.Found("Status", view.Object)) + .Verifiable(); + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model, viewEngine.Object); + + // Act + var displayResult = helper.EditorFor(x => x.FormatProperty); + + // Assert + Assert.Equal(expectedResult, HtmlContentUtilities.HtmlContentToString(displayResult)); + } + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs b/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs index 98e7e78063..e56daf2ea6 100644 --- a/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs +++ b/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs @@ -54,6 +54,11 @@ namespace HtmlGenerationWebSite.Controllers _productsListWithSelection = new SelectList(_products, "Number", "ProductName", 2); } + public IActionResult Enum() + { + return View(new AClass { DayOfWeek = Models.DayOfWeek.Friday, Month = Month.FirstOne }); + } + public IActionResult Order() { ViewData["Items"] = _productsListWithSelection; diff --git a/test/WebSites/HtmlGenerationWebSite/Models/AClass.cs b/test/WebSites/HtmlGenerationWebSite/Models/AClass.cs new file mode 100644 index 0000000000..23d3035488 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/AClass.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace HtmlGenerationWebSite.Models +{ + public class AClass + { + public DayOfWeek DayOfWeek { get; set; } + [DisplayFormat(DataFormatString = "Month: {0}")] + public Month Month { get; set; } + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Models/DayOfWeek.cs b/test/WebSites/HtmlGenerationWebSite/Models/DayOfWeek.cs new file mode 100644 index 0000000000..059df041de --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/DayOfWeek.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace HtmlGenerationWebSite.Models +{ + public enum DayOfWeek + { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Models/Month.cs b/test/WebSites/HtmlGenerationWebSite/Models/Month.cs new file mode 100644 index 0000000000..23b936a533 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/Month.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace HtmlGenerationWebSite.Models +{ + public enum Month + { + [Display(Name = "January")] + FirstOne, + LastOne + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Enum.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Enum.cshtml new file mode 100644 index 0000000000..8e4ca005d4 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Enum.cshtml @@ -0,0 +1,4 @@ +@using HtmlGenerationWebSite.Models +@model AClass +@Html.DisplayFor(x => x.DayOfWeek) +@Html.DisplayFor(x => x.Month) \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/Shared/DisplayTemplates/DayOfWeek.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/Shared/DisplayTemplates/DayOfWeek.cshtml new file mode 100644 index 0000000000..cfe576c195 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/Shared/DisplayTemplates/DayOfWeek.cshtml @@ -0,0 +1,26 @@ +@model HtmlGenerationWebSite.Models.DayOfWeek + +@switch (Model) +{ + case HtmlGenerationWebSite.Models.DayOfWeek.Monday: + Maandag + break; + case HtmlGenerationWebSite.Models.DayOfWeek.Tuesday: + Dinsdag + break; + case HtmlGenerationWebSite.Models.DayOfWeek.Wednesday: + Woensdag + break; + case HtmlGenerationWebSite.Models.DayOfWeek.Thursday: + Donderdag + break; + case HtmlGenerationWebSite.Models.DayOfWeek.Friday: + Vrijdag + break; + case HtmlGenerationWebSite.Models.DayOfWeek.Saturday: + Zaterdag + break; + case HtmlGenerationWebSite.Models.DayOfWeek.Sunday: + Zondag + break; +} \ No newline at end of file