From 7e4a8fe479e3573173443a28dca089e2148ddb6d Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Sat, 2 Sep 2017 16:57:28 -0700 Subject: [PATCH] Auto-select `type="text"` for `DateTimeOffset` values - #6648 - a different take on #4871 - `DateTime` can also round-trip `DateTimeKind.UTC` with `[DataType("datetimeoffset")]` or `[UIHint("datetimeoffset")]` - since they're now handled differently by default, add more `DateTime` tests - expand tests involving `Html5DateRenderingMode.CurrentCulture` nits: make VS-suggested changes to files updated in this PR --- .../InputTagHelper.cs | 41 ++- .../Internal/DefaultEditorTemplates.cs | 7 +- .../ViewFeatures/TemplateRenderer.cs | 9 +- .../HtmlHelperOptionsTest.cs | 8 +- .../InputTagHelperTest.cs | 80 +++++- .../Internal/DefaultEditorTemplatesTest.cs | 271 +++++++++++++----- 6 files changed, 320 insertions(+), 96 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs index 0b618900a8..18407ace88 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs @@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "Date", "date" }, { "DateTime", "datetime-local" }, { "DateTime-local", "datetime-local" }, + { nameof(DateTimeOffset), "text" }, { "Time", "time" }, { nameof(Byte), "number" }, { nameof(SByte), "number" }, @@ -234,8 +235,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { foreach (var hint in GetInputTypeHints(modelExplorer)) { - string inputType; - if (_defaultInputTypes.TryGetValue(hint, out inputType)) + if (_defaultInputTypes.TryGetValue(hint, out var inputType)) { inputTypeHint = hint; return inputType; @@ -252,8 +252,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { if (modelExplorer.Model != null) { - bool potentialBool; - if (!bool.TryParse(modelExplorer.Model.ToString(), out potentialBool)) + if (!bool.TryParse(modelExplorer.Model.ToString(), out var potentialBool)) { throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidStringResult( ForAttributeName, @@ -353,8 +352,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private TagBuilder GenerateHidden(ModelExplorer modelExplorer) { var value = For.Model; - var byteArrayValue = value as byte[]; - if (byteArrayValue != null) + if (value is byte[] byteArrayValue) { value = Convert.ToBase64String(byteArrayValue); } @@ -380,7 +378,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private string GetFormat(ModelExplorer modelExplorer, string inputTypeHint, string inputType) { string format; - string rfc3339Format; if (string.Equals("decimal", inputTypeHint, StringComparison.OrdinalIgnoreCase) && string.Equals("text", inputType, StringComparison.Ordinal) && string.IsNullOrEmpty(modelExplorer.Metadata.EditFormatString)) @@ -389,14 +386,30 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers // EditFormatString has precedence over this fall-back format. format = "{0:0.00}"; } - else if (_rfc3339Formats.TryGetValue(inputType, out rfc3339Format) && - ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 && + else if (ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 && !modelExplorer.Metadata.HasNonDefaultEditFormat && - (typeof(DateTime) == modelExplorer.Metadata.UnderlyingOrModelType || typeof(DateTimeOffset) == modelExplorer.Metadata.UnderlyingOrModelType)) + (typeof(DateTime) == modelExplorer.Metadata.UnderlyingOrModelType || + typeof(DateTimeOffset) == modelExplorer.Metadata.UnderlyingOrModelType)) { - // Rfc3339 mode _may_ override EditFormatString in a limited number of cases e.g. EditFormatString - // must be a default format (i.e. came from a built-in [DataType] attribute). - format = rfc3339Format; + // Rfc3339 mode _may_ override EditFormatString in a limited number of cases. Happens only when + // EditFormatString has a default format i.e. came from a [DataType] attribute. + if (string.Equals("text", inputType) && + string.Equals(nameof(DateTimeOffset), inputTypeHint, StringComparison.OrdinalIgnoreCase)) + { + // Auto-select a format that round-trips Offset and sub-Second values in a DateTimeOffset. Not + // done if user chose the "text" type in .cshtml file or with data annotations i.e. when + // inputTypeHint==null or "text". + format = _rfc3339Formats["datetime"]; + } + else if (_rfc3339Formats.TryGetValue(inputType, out var rfc3339Format)) + { + format = rfc3339Format; + } + else + { + // Otherwise use default EditFormatString. + format = modelExplorer.Metadata.EditFormatString; + } } else { @@ -428,7 +441,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers fieldType = modelExplorer.Metadata.UnderlyingOrModelType; } - foreach (string typeName in TemplateRenderer.GetTypeNames(modelExplorer.Metadata, fieldType)) + foreach (var typeName in TemplateRenderer.GetTypeNames(modelExplorer.Metadata, fieldType)) { yield return typeName; } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs index 4ef3de57a3..4db4c74011 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs @@ -197,8 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesObject); - object htmlClassObject; - if (htmlAttributes.TryGetValue("class", out htmlClassObject)) + if (htmlAttributes.TryGetValue("class", out var htmlClassObject)) { var htmlClassName = htmlClassObject + " " + className; htmlAttributes["class"] = htmlClassName; @@ -347,10 +346,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal return GenerateTextBox(htmlHelper, inputType: "email"); } - public static IHtmlContent DateTimeInputTemplate(IHtmlHelper htmlHelper) + public static IHtmlContent DateTimeOffsetTemplate(IHtmlHelper htmlHelper) { ApplyRfc3339DateFormattingIfNeeded(htmlHelper, "{0:yyyy-MM-ddTHH:mm:ss.fffK}"); - return GenerateTextBox(htmlHelper, inputType: "datetime"); + return GenerateTextBox(htmlHelper, inputType: "text"); } public static IHtmlContent DateTimeLocalInputTemplate(IHtmlHelper htmlHelper) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs index 0652fdd7c1..9ccc1cd361 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateRenderer.cs @@ -50,6 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { "Date", DefaultEditorTemplates.DateInputTemplate }, { "DateTime", DefaultEditorTemplates.DateTimeLocalInputTemplate }, { "DateTime-local", DefaultEditorTemplates.DateTimeLocalInputTemplate }, + { nameof(DateTimeOffset), DefaultEditorTemplates.DateTimeOffsetTemplate }, { "Time", DefaultEditorTemplates.TimeInputTemplate }, { typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate }, @@ -115,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal var defaultActions = GetDefaultActions(); var modeViewPath = _readOnly ? DisplayTemplateViewPath : EditorTemplateViewPath; - foreach (string viewName in GetViewNames()) + foreach (var viewName in GetViewNames()) { var viewEngineResult = _viewEngine.GetView(_viewContext.ExecutingFilePath, viewName, isMainPage: false); if (!viewEngineResult.Success) @@ -141,8 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } } - Func defaultAction; - if (defaultActions.TryGetValue(viewName, out defaultAction)) + if (defaultActions.TryGetValue(viewName, out var defaultAction)) { return defaultAction(MakeHtmlHelper(_viewContext, _viewData)); } @@ -255,8 +255,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { var newHelper = viewContext.HttpContext.RequestServices.GetRequiredService(); - var contextable = newHelper as IViewContextAware; - if (contextable != null) + if (newHelper is IViewContextAware contextable) { var newViewContext = new ViewContext(viewContext, viewContext.View, viewData, viewContext.Writer); contextable.Contextualize(newViewContext); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs index 0f8cc97ea1..76faef248d 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests An error occurred.
-
+
MySummary
  • A model error occurred.
  • @@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests An error occurred.
    -
    +
    False"; @@ -59,7 +59,7 @@ False"; An error occurred.
    -
    +
    True
    MySummary @@ -68,7 +68,7 @@ True An error occurred.
    -
    +
    True"; diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs index e9799a4cbb..987623ac57 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs @@ -388,7 +388,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [Theory] [InlineData("datetime", "datetime")] - [InlineData(null, "datetime-local")] + [InlineData(null, "text")] [InlineData("hidden", "hidden")] public void Process_GeneratesFormattedOutput(string specifiedType, string expectedType) { @@ -457,6 +457,77 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers Assert.Equal(expectedTagName, output.TagName); } + [Theory] + [InlineData("datetime", "datetime")] + [InlineData(null, "datetime-local")] + [InlineData("hidden", "hidden")] + public void Process_GeneratesFormattedOutput_ForDateTime(string specifiedType, string expectedType) + { + // Arrange + var expectedAttributes = new TagHelperAttributeList + { + { "type", expectedType }, + { "id", nameof(Model.DateTime) }, + { "name", nameof(Model.DateTime) }, + { "valid", "from validation attributes" }, + { "value", "datetime: 2011-08-31T05:30:45.0000000Z" }, + }; + var expectedTagName = "not-input"; + var container = new Model + { + DateTime = new DateTime(2011, 8, 31, hour: 5, minute: 30, second: 45, kind: DateTimeKind.Utc), + }; + + var allAttributes = new TagHelperAttributeList + { + { "type", specifiedType }, + }; + var context = new TagHelperContext( + tagName: "input", + allAttributes: allAttributes, + items: new Dictionary(), + uniqueId: "test"); + var output = new TagHelperOutput( + expectedTagName, + new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => + { + throw new Exception("getChildContentAsync should not be called."); + }) + { + TagMode = TagMode.StartTagOnly, + }; + + var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider()) + { + ValidationAttributes = + { + { "valid", "from validation attributes" }, + } + }; + + var tagHelper = GetTagHelper( + htmlGenerator, + container, + typeof(Model), + model: container.DateTime, + propertyName: nameof(Model.DateTime), + expressionName: nameof(Model.DateTime)); + tagHelper.Format = "datetime: {0:o}"; + tagHelper.InputTypeName = specifiedType; + + // Act + tagHelper.Process(context, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Empty(output.PreContent.GetContent()); + Assert.Empty(output.Content.GetContent()); + Assert.Empty(output.PostContent.GetContent()); + Assert.Equal(TagMode.StartTagOnly, output.TagMode); + Assert.Equal(expectedTagName, output.TagName); + } + [Fact] public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters() { @@ -966,6 +1037,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "datetime", null, "datetime-local" }, { "datetime-local", null, "datetime-local" }, { "DATETIME-local", null, "datetime-local" }, + { "datetimeOffset", null, "text" }, { "Decimal", "{0:0.00}", "text" }, { "Double", null, "text" }, { "Int16", null, "number" }, @@ -1139,15 +1211,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [InlineData("Date", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")] [InlineData("DateTime", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")] [InlineData("DateTime", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")] - [InlineData("DateTimeOffset", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")] - [InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")] + [InlineData("DateTimeOffset", Html5DateRenderingMode.CurrentCulture, null, "text")] + [InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "text")] [InlineData("DateTimeLocal", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")] [InlineData("DateTimeLocal", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")] [InlineData("Time", Html5DateRenderingMode.CurrentCulture, "{0:t}", "time")] // Format from [DataType]. [InlineData("Time", Html5DateRenderingMode.Rfc3339, "{0:HH:mm:ss.fff}", "time")] [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.fff}", "datetime-local")] + [InlineData("NullableDateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "text")] public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributesForRfc3339( string propertyName, Html5DateRenderingMode dateRenderingMode, diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs index f587cf7806..035f12de3e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Testing; using Moq; using Xunit; @@ -52,6 +53,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { "datetime", "__TextBox__ class='text-box single-line' type='datetime-local'" }, { "DateTime-local", "__TextBox__ class='text-box single-line' type='datetime-local'" }, { "DATETIME-LOCAL", "__TextBox__ class='text-box single-line' type='datetime-local'" }, + { "datetimeoffset", "__TextBox__ class='text-box single-line' type='text'" }, + { "DateTimeOffset", "__TextBox__ class='text-box single-line' type='text'" }, { "Time", "__TextBox__ class='text-box single-line' type='time'" }, { "time", "__TextBox__ class='text-box single-line' type='time'" }, { "Byte", "__TextBox__ class='text-box single-line' type='number'" }, @@ -776,7 +779,7 @@ Environment.NewLine; var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset"); var expectedInput = ""; + "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[datetime]]\" value=\"HtmlEncode[[2000-01-02T03:04:05.060+00:00]]\" />"; var offset = TimeSpan.FromHours(0); var model = new DateTimeOffset( @@ -786,7 +789,7 @@ Environment.NewLine; hour: 3, minute: 4, second: 5, - millisecond: 6, + millisecond: 60, offset: offset); var viewEngine = new Mock(MockBehavior.Strict); viewEngine @@ -814,13 +817,19 @@ Environment.NewLine; Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result)); } - // DateTime-local is not special-cased unless using Html5DateRenderingMode.Rfc3339. + // 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("datetime", null, "2000-01-02T03:04:05.006", "datetime-local")] - [InlineData("datetime-local", null, "2000-01-02T03:04:05.006", "datetime-local")] - [InlineData("time", "{0:t}", "03:04:05.006", "time")] - public void Editor_FindsCorrectDateOrTimeTemplate(string dataTypeName, string editFormatString, string expectedFormat, string expectedType) + [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")] + public void Editor_FindsCorrectDateOrTimeTemplate( + string dataTypeName, + string editFormatString, + string expectedFormat, + string expectedType) { // Arrange var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset"); @@ -830,63 +839,7 @@ Environment.NewLine; expectedType + "]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />"; - var offset = TimeSpan.FromHours(0); - var model = new DateTimeOffset( - year: 2000, - month: 1, - day: 2, - hour: 3, - minute: 4, - second: 5, - millisecond: 6, - offset: offset); - 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(), It.IsAny(), /*isMainPage*/ false)) - .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty())); - - var provider = new TestModelMetadataProvider(); - provider.ForType().DisplayDetails(dd => - { - dd.DataTypeName = dataTypeName; - dd.EditFormatString = editFormatString; // What [DataType] does for given type. - }); - - var helper = DefaultTemplatesUtilities.GetHtmlHelper( - model, - Mock.Of(), - viewEngine.Object, - provider); - helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; - - // Act - var result = helper.Editor(string.Empty); - - // Assert - Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result)); - } - - [Theory] - [InlineData("date", "{0:d}", "2000-01-02", "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("time", "{0:t}", "03:04:05.060", "time")] - public void Editor_AppliesRfc3339(string dataTypeName, string editFormatString, string expectedFormat, string expectedType) - { - // Arrange - var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset"); - var expectedInput = - ""; - - // Place DateTime-local value in current timezone. - var offset = string.Equals(string.Empty, dataTypeName) ? DateTimeOffset.Now.Offset : TimeSpan.FromHours(0); + var offset = TimeSpan.FromHours(-5); var model = new DateTimeOffset( year: 2000, month: 1, @@ -916,7 +869,193 @@ Environment.NewLine; Mock.Of(), viewEngine.Object, provider); - helper.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339; + helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; + + // Act + var result = helper.Editor(string.Empty); + + // Assert + Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result)); + } + + // Html5DateRenderingMode.Rfc3339 can be disabled. + [Theory] + [InlineData(null, null, "02/01/2000 03:04:05 -05:00", "text")] + [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", "{0:o}", "2000-01-02T03:04:05.0600000-05:00", "text")] + [InlineData("time", "{0:t}", "03:04", "time")] + [ReplaceCulture] + public void Editor_FindsCorrectDateOrTimeTemplate_NotRfc3339( + string dataTypeName, + string editFormatString, + string expectedFormat, + string expectedType) + { + // Arrange + var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset"); + var expectedInput = + ""; + + var offset = TimeSpan.FromHours(-5); + var model = new DateTimeOffset( + year: 2000, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, + millisecond: 60, + offset: offset); + 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(), It.IsAny(), /*isMainPage*/ false)) + .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty())); + + var provider = new TestModelMetadataProvider(); + provider.ForType().DisplayDetails(dd => + { + dd.DataTypeName = dataTypeName; + dd.EditFormatString = editFormatString; // What [DataType] does for given type. + }); + + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + Mock.Of(), + viewEngine.Object, + provider); + helper.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture; + helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; + + // Act + var result = helper.Editor(string.Empty); + + // Assert + Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result)); + } + + // 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("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")] + public void Editor_FindsCorrectDateOrTimeTemplate_ForDateTime( + string dataTypeName, + string editFormatString, + string expectedFormat, + string expectedType) + { + // Arrange + var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTime"); + var expectedInput = ""; + + var model = new DateTime( + year: 2000, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, + millisecond: 60, + kind: DateTimeKind.Utc); + 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(), It.IsAny(), /*isMainPage*/ false)) + .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty())); + + var provider = new TestModelMetadataProvider(); + provider.ForType().DisplayDetails(dd => + { + dd.DataTypeName = dataTypeName; + dd.EditFormatString = editFormatString; // What [DataType] does for given type. + }); + + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + Mock.Of(), + viewEngine.Object, + provider); + helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; + + // Act + var result = helper.Editor(string.Empty); + + // Assert + Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result)); + } + + // Html5DateRenderingMode.Rfc3339 can be disabled. + [Theory] + [InlineData(null, null, "02/01/2000 03:04:05", "datetime-local")] + [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", "{0:o}", "2000-01-02T03:04:05.0600000Z", "text")] + [InlineData("time", "{0:t}", "03:04", "time")] + [ReplaceCulture] + public void Editor_FindsCorrectDateOrTimeTemplate_ForDateTimeNotRfc3339( + string dataTypeName, + string editFormatString, + string expectedFormat, + string expectedType) + { + // Arrange + var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTime"); + var expectedInput = + ""; + + var model = new DateTime( + year: 2000, + month: 1, + day: 2, + hour: 3, + minute: 4, + second: 5, + millisecond: 60, + kind: DateTimeKind.Utc); + 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(), It.IsAny(), /*isMainPage*/ false)) + .Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty())); + + var provider = new TestModelMetadataProvider(); + provider.ForType().DisplayDetails(dd => + { + dd.DataTypeName = dataTypeName; + dd.EditFormatString = editFormatString; // What [DataType] does for given type. + }); + + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + Mock.Of(), + viewEngine.Object, + provider); + helper.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture; helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; // Act @@ -927,6 +1066,8 @@ Environment.NewLine; } [Theory] + [InlineData(null, Html5DateRenderingMode.CurrentCulture, "text")] + [InlineData(null, Html5DateRenderingMode.Rfc3339, "text")] [InlineData("date", Html5DateRenderingMode.CurrentCulture, "date")] [InlineData("date", Html5DateRenderingMode.Rfc3339, "date")] [InlineData("datetime", Html5DateRenderingMode.CurrentCulture, "datetime-local")]