diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs index 4e675e8d81..9d0dab5d6c 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs @@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "DateTime-local", "datetime-local" }, { nameof(DateTimeOffset), "text" }, { "Time", "time" }, + { "Week", "week" }, { "Month", "month" }, { nameof(Byte), "number" }, { nameof(SByte), "number" }, @@ -326,9 +327,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var format = Format; if (string.IsNullOrEmpty(format)) { - format = GetFormat(modelExplorer, inputTypeHint, inputType); + if (!modelExplorer.Metadata.HasNonDefaultEditFormat && + string.Equals("week", inputType, StringComparison.OrdinalIgnoreCase) && + (modelExplorer.Model is DateTime || modelExplorer.Model is DateTimeOffset)) + { + modelExplorer = modelExplorer.GetExplorerForModel(FormatWeekHelper.GetFormattedWeek(modelExplorer)); + } + else + { + format = GetFormat(modelExplorer, inputTypeHint, inputType); + } } - var htmlAttributes = new Dictionary { { "type", inputType } @@ -381,7 +390,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers string format; if (string.Equals("month", inputType, StringComparison.OrdinalIgnoreCase)) { - // A new HTML5 input type that only will be rendered in Rfc3339 mode + // "month" is a new HTML5 input type that only will be rendered in Rfc3339 mode format = "{0:yyyy-MM}"; } else if (string.Equals("decimal", inputTypeHint, StringComparison.OrdinalIgnoreCase) && diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs index 38fe6db05f..4a1d9204bc 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs @@ -372,12 +372,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public static IHtmlContent MonthInputTemplate(IHtmlHelper htmlHelper) { - // A new HTML5 input type that only will be rendered in Rfc3339 mode + // "month" is a new HTML5 input type that only will be rendered in Rfc3339 mode htmlHelper.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339; ApplyRfc3339DateFormattingIfNeeded(htmlHelper, "{0:yyyy-MM}"); return GenerateTextBox(htmlHelper, inputType: "month"); } + public static IHtmlContent WeekInputTemplate(IHtmlHelper htmlHelper) + { + return GenerateTextBox(htmlHelper, inputType: "week"); + } + public static IHtmlContent NumberInputTemplate(IHtmlHelper htmlHelper) { return GenerateTextBox(htmlHelper, inputType: "number"); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/FormatWeekHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/FormatWeekHelper.cs new file mode 100644 index 0000000000..56e8329a69 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/FormatWeekHelper.cs @@ -0,0 +1,56 @@ +// 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; +using System.Globalization; +using System.Threading; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public class FormatWeekHelper + { + public static string GetFormattedWeek(ModelExplorer modelExplorer) + { + var value = modelExplorer.Model; + var metadata = modelExplorer.Metadata; + + if (value is DateTimeOffset dateTimeOffset) + { + value = dateTimeOffset.DateTime; + } + + if (value is DateTime date) + { + var calendar = Thread.CurrentThread.CurrentCulture.Calendar; + var day = calendar.GetDayOfWeek(date); + + // Get the week number consistent with ISO 8601. See blog post: + // https://blogs.msdn.microsoft.com/shawnste/2006/01/24/iso-8601-week-of-year-format-in-microsoft-net/ + if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday) + { + date = date.AddDays(3); + } + + var week = calendar.GetWeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); + var year = calendar.GetYear(date); + var month = calendar.GetMonth(date); + + // Last week (either 52 or 53) includes January dates (1st, 2nd, 3rd) + if (week >= 52 && month == 1) + { + year--; + } + + // First week includes December dates (29th, 30th, 31st) + if (week == 1 && month == 12) + { + year++; + } + + return $"{year:0000}-W{week:00}"; + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs index 1133f84f8c..62d4b3ea86 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs @@ -96,38 +96,43 @@ 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 formatString = _readOnly ? + viewData.ModelMetadata.DisplayFormatString : + viewData.ModelMetadata.EditFormatString; + var formattedModelValue = _model; - if (_model == null && _readOnly) + if (_model == null) { - 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) + if (_readOnly) { - 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; - } - } + formattedModelValue = _metadata.NullDisplayText; } } - - var formatString = _readOnly ? - viewData.ModelMetadata.DisplayFormatString : - viewData.ModelMetadata.EditFormatString; - if (_model != null && !string.IsNullOrEmpty(formatString)) + else if (!string.IsNullOrEmpty(formatString)) { - formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, formattedModelValue); + formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, _model); + } + else if ((string.Equals("week", _templateName, StringComparison.OrdinalIgnoreCase) || + string.Equals("week", viewData.ModelMetadata.DataTypeName, StringComparison.OrdinalIgnoreCase))) + { + // "week" is a new HTML5 input type that only will be rendered in Rfc3339 mode + formattedModelValue = FormatWeekHelper.GetFormattedWeek(_modelExplorer); + } + else if (viewData.ModelMetadata.IsEnum && _model is Enum modelEnum) + { + // Cover the case where the model is an enum and we want the string value of it + 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; + } + } } viewData.TemplateInfo.FormattedModelValue = formattedModelValue; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs index 666129ccf5..f90a3441ed 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs @@ -53,6 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { nameof(DateTimeOffset), DefaultEditorTemplates.DateTimeOffsetTemplate }, { "Time", DefaultEditorTemplates.TimeInputTemplate }, { "Month", DefaultEditorTemplates.MonthInputTemplate }, + { "Week", DefaultEditorTemplates.WeekInputTemplate }, { typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(short).Name, DefaultEditorTemplates.NumberInputTemplate }, diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs index 18cae8aeb7..2d6e5d0faf 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var response = await Client.GetStringAsync("http://localhost/HtmlGeneration_Home/Enum"); // Assert - Assert.Equal($"Vrijdag{Environment.NewLine}Month: January", response, ignoreLineEndingDifferences: true); + Assert.Equal($"Vrijdag{Environment.NewLine}Month: FirstOne", response, ignoreLineEndingDifferences: true); } [Theory] diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs index efa0097ad1..2e91a89a75 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers.Testing; +using Microsoft.AspNetCore.Testing; using Moq; using Xunit; @@ -1053,6 +1054,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "TEXT", null, "text" }, { "time", null, "time" }, { "month", "{0:yyyy-MM}", "month" }, + { "week", null, "week" }, { "UInt16", null, "number" }, { "uint16", null, "number" }, { "UInt32", null, "number" }, @@ -1220,6 +1222,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [InlineData("Time", Html5DateRenderingMode.Rfc3339, "{0:HH:mm:ss.fff}", "time")] [InlineData("Month", Html5DateRenderingMode.CurrentCulture, "{0:yyyy-MM}", "month")] [InlineData("Month", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM}", "month")] + [InlineData("Week", Html5DateRenderingMode.CurrentCulture, null, "week")] + [InlineData("Week", Html5DateRenderingMode.Rfc3339, null, "week")] [InlineData("NullableDate", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")] [InlineData("NullableDateTime", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")] [InlineData("NullableDateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "text")] @@ -1293,6 +1297,210 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers Assert.Equal(expectedTagName, output.TagName); } + // Html5DateRenderingMode.Rfc3339 is enabled by default. + [Theory] + [InlineData("Date", "2000-01-02", "date")] + [InlineData("DateTime", "2000-01-02T03:04:05.060", "datetime-local")] + [InlineData("DateTimeLocal", "2000-01-02T03:04:05.060", "datetime-local")] + [InlineData("DateTimeOffset", "2000-01-02T03:04:05.060Z", "text")] + [InlineData("Time", "03:04:05.060", "time")] + [InlineData("Month", "2000-01", "month")] + [InlineData("Week", "1999-W52", "week")] + public async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_ForDateTime( + string propertyName, + string expectedValue, + string expectedType) + { + // Arrange + var expectedAttributes = new TagHelperAttributeList + { + { "type", expectedType }, + { "id", propertyName }, + { "name", propertyName }, + { "value", expectedValue }, + }; + + var context = new TagHelperContext( + tagName: "input", + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + + var output = new TagHelperOutput( + expectedType, + attributes: new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( + new DefaultTagHelperContent())) + { + TagMode = TagMode.SelfClosing, + }; + + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var model = new DateTime( + year: 2000, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, + millisecond: 60, + kind: DateTimeKind.Utc); + + var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider); + var tagHelper = GetTagHelper( + htmlGenerator, + model: model, + propertyName: propertyName, + metadataProvider: metadataProvider); + tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Equal(expectedType, output.TagName); + } + + // Html5DateRenderingMode.Rfc3339 can be disabled. + [Theory] + [InlineData("Date", null, "02/01/2000", "date")] + [InlineData("Date", "{0:d}", "02/01/2000", "date")] + [InlineData("DateTime", null, "02/01/2000 03:04:05", "datetime-local")] + [InlineData("DateTimeLocal", null, "02/01/2000 03:04:05", "datetime-local")] + [InlineData("DateTimeOffset", null, "02/01/2000 03:04:05", "text")] + [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000Z", "text")] + [InlineData("Time", null, "03:04", "time")] + [InlineData("Time", "{0:t}", "03:04", "time")] + [InlineData("Month", null, "2000-01", "month")] + [InlineData("Month", "{0:yyyy/MM}", "2000/01", "month")] + [InlineData("Week", null, "1999-W52", "week")] + [InlineData("Week", "{0:yyyy/'W1'}", "2000/W1", "week")] + [ReplaceCulture] + public async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_ForDateTimeNotRfc3339( + string propertyName, + string editFormatString, + string expectedValue, + string expectedType) + { + // Arrange + var expectedAttributes = new TagHelperAttributeList + { + { "type", expectedType }, + { "id", propertyName }, + { "name", propertyName }, + { "value", expectedValue }, + }; + + var context = new TagHelperContext( + tagName: "input", + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + + var output = new TagHelperOutput( + expectedType, + attributes: new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( + new DefaultTagHelperContent())) + { + TagMode = TagMode.SelfClosing, + }; + + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + + var model = new DateTime( + year: 2000, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, + millisecond: 60, + kind: DateTimeKind.Utc); + + var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider); + var tagHelper = GetTagHelper( + htmlGenerator, + model: model, + propertyName: propertyName, + metadataProvider: metadataProvider); + tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture; + tagHelper.Format = editFormatString; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Equal(expectedType, output.TagName); + } + + // Html5DateRenderingMode.Rfc3339 can be disabled. + [Theory] + [InlineData("Month", "month")] + [InlineData("Week", "week")] + [ReplaceCulture] + public async Task ProcessAsync_CallsGenerateTextBox_ProducesExpectedValue_OverridesDefaultFormat( + string propertyName, + string expectedType) + { + // Arrange + var expectedAttributes = new TagHelperAttributeList + { + { "type", expectedType }, + { "id", propertyName }, + { "name", propertyName }, + { "value", "non-default format string" }, + }; + + var context = new TagHelperContext( + tagName: "input", + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + + var output = new TagHelperOutput( + expectedType, + attributes: new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( + new DefaultTagHelperContent())) + { + TagMode = TagMode.SelfClosing, + }; + + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + + var model = new DateTime( + year: 2000, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, + millisecond: 60, + kind: DateTimeKind.Utc); + + var htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(metadataProvider); + var tagHelper = GetTagHelper( + htmlGenerator, + model: model, + propertyName: propertyName, + metadataProvider: metadataProvider); + tagHelper.ViewContext.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture; + tagHelper.Format = "non-default format string"; + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Equal(expectedType, output.TagName); + } + private static InputTagHelper GetTagHelper( IHtmlGenerator htmlGenerator, object model, @@ -1383,6 +1591,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [DataType("month")] public DateTimeOffset Month { get; set; } + + [DataType("week")] + public DateTimeOffset Week { get; set; } } private class NestedModel diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/HtmlGeneratorUtilities.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/HtmlGeneratorUtilities.cs new file mode 100644 index 0000000000..a40024c7e4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/HtmlGeneratorUtilities.cs @@ -0,0 +1,49 @@ +// 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 Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.WebEncoders.Testing; +using Moq; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + public static class HtmlGeneratorUtilities + { + public static IHtmlGenerator GetHtmlGenerator(IModelMetadataProvider provider) + { + var options = new MvcViewOptions(); + var urlHelperFactory = new Mock(); + urlHelperFactory + .Setup(f => f.GetUrlHelper(It.IsAny())) + .Returns(Mock.Of()); + + return GetHtmlGenerator(provider, urlHelperFactory.Object, options); + } + + public static IHtmlGenerator GetHtmlGenerator(IModelMetadataProvider provider, IUrlHelperFactory urlHelperFactory, MvcViewOptions options) + { + var optionsAccessor = new Mock>(); + optionsAccessor + .SetupGet(o => o.Value) + .Returns(options); + + var attributeProvider = new DefaultValidationHtmlAttributeProvider( + optionsAccessor.Object, + provider, + new ClientValidatorCache()); + + var htmlGenerator = new DefaultHtmlGenerator( + Mock.Of(), + optionsAccessor.Object, + provider, + urlHelperFactory, + new HtmlTestEncoder(), + attributeProvider); + return htmlGenerator; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs index 1c0a100706..e7e1b568bc 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs @@ -59,6 +59,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { "time", "__TextBox__ class='text-box single-line' type='time'" }, { "Month", "__TextBox__ class='text-box single-line' type='month'" }, { "month", "__TextBox__ class='text-box single-line' type='month'" }, + { "Week", "__TextBox__ class='text-box single-line' type='week'" }, + { "week", "__TextBox__ class='text-box single-line' type='week'" }, { "Byte", "__TextBox__ class='text-box single-line' type='number'" }, { "BYTE", "__TextBox__ class='text-box single-line' type='number'" }, { "SByte", "__TextBox__ class='text-box single-line' type='number'" }, @@ -813,7 +815,7 @@ Environment.NewLine; // Act var result = helper.Editor( string.Empty, - new { htmlAttributes = new { type = "datetime" }}); + new { htmlAttributes = new { type = "datetime" } }); // Assert Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result)); @@ -822,16 +824,23 @@ Environment.NewLine; // Html5DateRenderingMode.Rfc3339 is enabled by default. [Theory] [InlineData(null, null, "2000-01-02T03:04:05.060-05:00", "text")] - [InlineData("date", "{0:d}", "2000-01-02", "date")] + [InlineData("date", null, "2000-01-02", "date")] + [InlineData("date", "{0:d}", "02/01/2000", "date")] [InlineData("datetime", null, "2000-01-02T03:04:05.060", "datetime-local")] [InlineData("datetime-local", null, "2000-01-02T03:04:05.060", "datetime-local")] - [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.060-05:00", "text")] - [InlineData("time", "{0:t}", "03:04:05.060", "time")] + [InlineData("DateTimeOffset", null, "2000-01-02T03:04:05.060-05:00", "text")] + [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000-05:00", "text")] + [InlineData("time", null, "03:04:05.060", "time")] + [InlineData("time", "{0:t}", "03:04", "time")] + [InlineData("month", null, "2000-01", "month")] [InlineData("month", "{0:yyyy-MM}", "2000-01", "month")] - public void Editor_FindsCorrectDateOrTimeTemplate( + [InlineData("week", null, "1999-W52", "week")] + [InlineData("week", "{0:yyyy-'W1'}", "2000-W1", "week")] + [ReplaceCulture] + public void Editor_FindsCorrectDateOrTimeTemplate_WithTimeOffset( string dataTypeName, string editFormatString, - string expectedFormat, + string expectedValue, string expectedType) { // Arrange @@ -840,7 +849,7 @@ Environment.NewLine; $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " + "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" + expectedType + - "]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />"; + "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />"; var offset = TimeSpan.FromHours(-5); var model = new DateTimeOffset( @@ -865,6 +874,7 @@ Environment.NewLine; { dd.DataTypeName = dataTypeName; dd.EditFormatString = editFormatString; // What [DataType] does for given type. + dd.HasNonDefaultEditFormat = true; }); var helper = DefaultTemplatesUtilities.GetHtmlHelper( @@ -884,17 +894,23 @@ Environment.NewLine; // Html5DateRenderingMode.Rfc3339 can be disabled. [Theory] [InlineData(null, null, "02/01/2000 03:04:05 -05:00", "text")] + [InlineData("date", null, "02/01/2000 03:04:05 -05:00", "date")] [InlineData("date", "{0:d}", "02/01/2000", "date")] [InlineData("datetime", null, "02/01/2000 03:04:05 -05:00", "datetime-local")] [InlineData("datetime-local", null, "02/01/2000 03:04:05 -05:00", "datetime-local")] + [InlineData("DateTimeOffset", null, "02/01/2000 03:04:05 -05:00", "text")] [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000-05:00", "text")] + [InlineData("time", null, "02/01/2000 03:04:05 -05:00", "time")] [InlineData("time", "{0:t}", "03:04", "time")] + [InlineData("month", null, "2000-01", "month")] [InlineData("month", "{0:yyyy-MM}", "2000-01", "month")] + [InlineData("week", null, "1999-W52", "week")] + [InlineData("week", "{0:yyyy-'W1'}", "2000-W1", "week")] [ReplaceCulture] - public void Editor_FindsCorrectDateOrTimeTemplate_NotRfc3339( + public void Editor_FindsCorrectDateOrTimeTemplate_WithTimeOffset_NotRfc3339( string dataTypeName, string editFormatString, - string expectedFormat, + string expectedValue, string expectedType) { // Arrange @@ -904,7 +920,7 @@ Environment.NewLine; $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " + "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" + expectedType + - "]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />"; + "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />"; var offset = TimeSpan.FromHours(-5); var model = new DateTimeOffset( @@ -929,6 +945,7 @@ Environment.NewLine; { dd.DataTypeName = dataTypeName; dd.EditFormatString = editFormatString; // What [DataType] does for given type. + dd.HasNonDefaultEditFormat = true; }); var helper = DefaultTemplatesUtilities.GetHtmlHelper( @@ -949,16 +966,23 @@ Environment.NewLine; // Html5DateRenderingMode.Rfc3339 is enabled by default. [Theory] [InlineData(null, null, "2000-01-02T03:04:05.060", "datetime-local")] - [InlineData("date", "{0:d}", "2000-01-02", "date")] + [InlineData("date", null, "2000-01-02", "date")] + [InlineData("date", "{0:d}", "02/01/2000", "date")] [InlineData("datetime", null, "2000-01-02T03:04:05.060", "datetime-local")] [InlineData("datetime-local", null, "2000-01-02T03:04:05.060", "datetime-local")] - [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.060Z", "text")] - [InlineData("time", "{0:t}", "03:04:05.060", "time")] - [InlineData("month", "{0:yyyy-MM}", "2000-01", "month")] + [InlineData("DateTimeOffset", null, "2000-01-02T03:04:05.060Z", "text")] + [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000Z", "text")] + [InlineData("time", null, "03:04:05.060", "time")] + [InlineData("time", "{0:t}", "03:04", "time")] + [InlineData("month", null, "2000-01", "month")] + [InlineData("month", "{0:yyyy/MM}", "2000/01", "month")] + [InlineData("week", null, "1999-W52", "week")] + [InlineData("Week", "{0:yyyy/'W1'}", "2000/W1", "week")] + [ReplaceCulture] public void Editor_FindsCorrectDateOrTimeTemplate_ForDateTime( string dataTypeName, string editFormatString, - string expectedFormat, + string expectedValue, string expectedType) { // Arrange @@ -967,7 +991,7 @@ Environment.NewLine; $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " + "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" + expectedType + - "]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />"; + "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />"; var model = new DateTime( year: 2000, @@ -991,6 +1015,7 @@ Environment.NewLine; { dd.DataTypeName = dataTypeName; dd.EditFormatString = editFormatString; // What [DataType] does for given type. + dd.HasNonDefaultEditFormat = true; }); var helper = DefaultTemplatesUtilities.GetHtmlHelper( @@ -1010,17 +1035,22 @@ Environment.NewLine; // Html5DateRenderingMode.Rfc3339 can be disabled. [Theory] [InlineData(null, null, "02/01/2000 03:04:05", "datetime-local")] + [InlineData("date", null, "02/01/2000 03:04:05", "date")] [InlineData("date", "{0:d}", "02/01/2000", "date")] [InlineData("datetime", null, "02/01/2000 03:04:05", "datetime-local")] [InlineData("datetime-local", null, "02/01/2000 03:04:05", "datetime-local")] + [InlineData("DateTimeOffset", null, "02/01/2000 03:04:05", "text")] [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.0600000Z", "text")] [InlineData("time", "{0:t}", "03:04", "time")] - [InlineData("month", "{0:yyyy-MM}", "2000-01", "month")] + [InlineData("month", null, "2000-01", "month")] + [InlineData("month", "{0:yyyy/MM}", "2000/01", "month")] + [InlineData("week", null, "1999-W52", "week")] + [InlineData("Week", "{0:yyyy/'W1'}", "2000/W1", "week")] [ReplaceCulture] public void Editor_FindsCorrectDateOrTimeTemplate_ForDateTimeNotRfc3339( string dataTypeName, string editFormatString, - string expectedFormat, + string expectedValue, string expectedType) { // Arrange @@ -1030,7 +1060,7 @@ Environment.NewLine; $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " + "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" + expectedType + - "]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />"; + "]]\" value=\"HtmlEncode[[" + expectedValue + "]]\" />"; var model = new DateTime( year: 2000, @@ -1054,6 +1084,7 @@ Environment.NewLine; { dd.DataTypeName = dataTypeName; dd.EditFormatString = editFormatString; // What [DataType] does for given type. + dd.HasNonDefaultEditFormat = true; }); var helper = DefaultTemplatesUtilities.GetHtmlHelper( @@ -1084,6 +1115,8 @@ Environment.NewLine; [InlineData("time", Html5DateRenderingMode.Rfc3339, "time")] [InlineData("month", Html5DateRenderingMode.CurrentCulture, "month")] [InlineData("month", Html5DateRenderingMode.Rfc3339, "month")] + [InlineData("week", Html5DateRenderingMode.CurrentCulture, "week")] + [InlineData("week", Html5DateRenderingMode.Rfc3339, "week")] public void Editor_AppliesNonDefaultEditFormat(string dataTypeName, Html5DateRenderingMode renderingMode, string expectedType) { // Arrange diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/FormatWeekHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/FormatWeekHelperTest.cs new file mode 100644 index 0000000000..9586bf664e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/FormatWeekHelperTest.cs @@ -0,0 +1,43 @@ +// 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; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public class FormatWeekHelperTest + { + // See blog post: https://blogs.msdn.microsoft.com/shawnste/2006/01/24/iso-8601-week-of-year-format-in-microsoft-net/ + [Theory] + [InlineData(2001, 1, 1, "2001-W01")] + [InlineData(2007, 12, 31, "2008-W01")] + [InlineData(2000, 12, 31, "2000-W52")] + [InlineData(2012, 1, 1, "2011-W52")] + [InlineData(2005, 1, 1, "2004-W53")] + [InlineData(2015, 12, 31, "2015-W53")] + public void GetFormattedWeek_ReturnsExpectedFormattedValue(int year, int month, int day, string expectedOutput) + { + // Arrange + var detailsProvider = new DefaultCompositeMetadataDetailsProvider( + Enumerable.Empty()); + var key = ModelMetadataIdentity.ForType(typeof(DateTime)); + var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0], null, null)); + + var provider = new EmptyModelMetadataProvider(); + var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); + var model = new DateTime(year, month, day); + var modelExplorer = new ModelExplorer(provider, metadata, model); + + // Act + var formattedValue = FormatWeekHelper.GetFormattedWeek(modelExplorer); + + // Assert + Assert.Equal(expectedOutput, formattedValue); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs index 7a327b24ea..75649f3223 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs @@ -9,12 +9,10 @@ using System.IO; using System.Linq; using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewEngines; @@ -239,10 +237,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering new ValidationAttributeAdapterProvider(), localizationOptionsAccesor.Object, stringLocalizerFactory: null)); - var optionsAccessor = new Mock>(); - optionsAccessor - .SetupGet(o => o.Value) - .Returns(options); var urlHelperFactory = new Mock(); urlHelperFactory @@ -253,17 +247,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering if (htmlGenerator == null) { - var attributeProvider = new DefaultValidationHtmlAttributeProvider( - optionsAccessor.Object, - provider, - new ClientValidatorCache()); - htmlGenerator = new DefaultHtmlGenerator( - Mock.Of(), - optionsAccessor.Object, - provider, - urlHelperFactory.Object, - new HtmlTestEncoder(), - attributeProvider); + htmlGenerator = HtmlGeneratorUtilities.GetHtmlGenerator(provider, urlHelperFactory.Object, options); } // TemplateRenderer will Contextualize this transient service. diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs index 38de81e40f..c32e06c3bf 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayExtensionsTest.cs @@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { { new FormatModel{ FormatProperty = Status.Created }, - "Value: CreatedKey" + "Value: Created" }, { new FormatModel { FormatProperty = Status.Done },