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
This commit is contained in:
Doug Bunting 2017-09-02 16:57:28 -07:00
parent 1d1a5203db
commit 7e4a8fe479
6 changed files with 320 additions and 96 deletions

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "Date", "date" }, { "Date", "date" },
{ "DateTime", "datetime-local" }, { "DateTime", "datetime-local" },
{ "DateTime-local", "datetime-local" }, { "DateTime-local", "datetime-local" },
{ nameof(DateTimeOffset), "text" },
{ "Time", "time" }, { "Time", "time" },
{ nameof(Byte), "number" }, { nameof(Byte), "number" },
{ nameof(SByte), "number" }, { nameof(SByte), "number" },
@ -234,8 +235,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ {
foreach (var hint in GetInputTypeHints(modelExplorer)) foreach (var hint in GetInputTypeHints(modelExplorer))
{ {
string inputType; if (_defaultInputTypes.TryGetValue(hint, out var inputType))
if (_defaultInputTypes.TryGetValue(hint, out inputType))
{ {
inputTypeHint = hint; inputTypeHint = hint;
return inputType; return inputType;
@ -252,8 +252,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ {
if (modelExplorer.Model != null) if (modelExplorer.Model != null)
{ {
bool potentialBool; if (!bool.TryParse(modelExplorer.Model.ToString(), out var potentialBool))
if (!bool.TryParse(modelExplorer.Model.ToString(), out potentialBool))
{ {
throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidStringResult( throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidStringResult(
ForAttributeName, ForAttributeName,
@ -353,8 +352,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
private TagBuilder GenerateHidden(ModelExplorer modelExplorer) private TagBuilder GenerateHidden(ModelExplorer modelExplorer)
{ {
var value = For.Model; var value = For.Model;
var byteArrayValue = value as byte[]; if (value is byte[] byteArrayValue)
if (byteArrayValue != null)
{ {
value = Convert.ToBase64String(byteArrayValue); value = Convert.ToBase64String(byteArrayValue);
} }
@ -380,7 +378,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
private string GetFormat(ModelExplorer modelExplorer, string inputTypeHint, string inputType) private string GetFormat(ModelExplorer modelExplorer, string inputTypeHint, string inputType)
{ {
string format; string format;
string rfc3339Format;
if (string.Equals("decimal", inputTypeHint, StringComparison.OrdinalIgnoreCase) && if (string.Equals("decimal", inputTypeHint, StringComparison.OrdinalIgnoreCase) &&
string.Equals("text", inputType, StringComparison.Ordinal) && string.Equals("text", inputType, StringComparison.Ordinal) &&
string.IsNullOrEmpty(modelExplorer.Metadata.EditFormatString)) string.IsNullOrEmpty(modelExplorer.Metadata.EditFormatString))
@ -389,14 +386,30 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// EditFormatString has precedence over this fall-back format. // EditFormatString has precedence over this fall-back format.
format = "{0:0.00}"; format = "{0:0.00}";
} }
else if (_rfc3339Formats.TryGetValue(inputType, out rfc3339Format) && else if (ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 &&
ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 &&
!modelExplorer.Metadata.HasNonDefaultEditFormat && !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 // Rfc3339 mode _may_ override EditFormatString in a limited number of cases. Happens only when
// must be a default format (i.e. came from a built-in [DataType] attribute). // EditFormatString has a default format i.e. came from a [DataType] attribute.
format = rfc3339Format; 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 else
{ {
@ -428,7 +441,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
fieldType = modelExplorer.Metadata.UnderlyingOrModelType; 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; yield return typeName;
} }

View File

@ -197,8 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesObject); var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesObject);
object htmlClassObject; if (htmlAttributes.TryGetValue("class", out var htmlClassObject))
if (htmlAttributes.TryGetValue("class", out htmlClassObject))
{ {
var htmlClassName = htmlClassObject + " " + className; var htmlClassName = htmlClassObject + " " + className;
htmlAttributes["class"] = htmlClassName; htmlAttributes["class"] = htmlClassName;
@ -347,10 +346,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return GenerateTextBox(htmlHelper, inputType: "email"); 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}"); 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) public static IHtmlContent DateTimeLocalInputTemplate(IHtmlHelper htmlHelper)

View File

@ -50,6 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ "Date", DefaultEditorTemplates.DateInputTemplate }, { "Date", DefaultEditorTemplates.DateInputTemplate },
{ "DateTime", DefaultEditorTemplates.DateTimeLocalInputTemplate }, { "DateTime", DefaultEditorTemplates.DateTimeLocalInputTemplate },
{ "DateTime-local", DefaultEditorTemplates.DateTimeLocalInputTemplate }, { "DateTime-local", DefaultEditorTemplates.DateTimeLocalInputTemplate },
{ nameof(DateTimeOffset), DefaultEditorTemplates.DateTimeOffsetTemplate },
{ "Time", DefaultEditorTemplates.TimeInputTemplate }, { "Time", DefaultEditorTemplates.TimeInputTemplate },
{ typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate },
{ typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate },
@ -115,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var defaultActions = GetDefaultActions(); var defaultActions = GetDefaultActions();
var modeViewPath = _readOnly ? DisplayTemplateViewPath : EditorTemplateViewPath; var modeViewPath = _readOnly ? DisplayTemplateViewPath : EditorTemplateViewPath;
foreach (string viewName in GetViewNames()) foreach (var viewName in GetViewNames())
{ {
var viewEngineResult = _viewEngine.GetView(_viewContext.ExecutingFilePath, viewName, isMainPage: false); var viewEngineResult = _viewEngine.GetView(_viewContext.ExecutingFilePath, viewName, isMainPage: false);
if (!viewEngineResult.Success) if (!viewEngineResult.Success)
@ -141,8 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
} }
Func<IHtmlHelper, IHtmlContent> defaultAction; if (defaultActions.TryGetValue(viewName, out var defaultAction))
if (defaultActions.TryGetValue(viewName, out defaultAction))
{ {
return defaultAction(MakeHtmlHelper(_viewContext, _viewData)); return defaultAction(MakeHtmlHelper(_viewContext, _viewData));
} }
@ -255,8 +255,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
var newHelper = viewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>(); var newHelper = viewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>();
var contextable = newHelper as IViewContextAware; if (newHelper is IViewContextAware contextable)
if (contextable != null)
{ {
var newViewContext = new ViewContext(viewContext, viewContext.View, viewData, viewContext.Writer); var newViewContext = new ViewContext(viewContext, viewContext.View, viewData, viewContext.Writer);
contextable.Contextualize(newViewContext); contextable.Contextualize(newViewContext);

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
<validationMessageElement class=""field-validation-error"">An error occurred.</validationMessageElement> <validationMessageElement class=""field-validation-error"">An error occurred.</validationMessageElement>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" /> <input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div> <div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""2000-01-02T03:04:05.060"" /> </div> <div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""text"" value=""2000-01-02T03:04:05.060&#x2B;00:00"" /> </div>
<div class=""validation-summary-errors""><validationSummaryElement>MySummary</validationSummaryElement> <div class=""validation-summary-errors""><validationSummaryElement>MySummary</validationSummaryElement>
<ul><li>A model error occurred.</li> <ul><li>A model error occurred.</li>
@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
<validationMessageElement class=""field-validation-error"">An error occurred.</validationMessageElement> <validationMessageElement class=""field-validation-error"">An error occurred.</validationMessageElement>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" /> <input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div> <div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""2000-01-02T03:04:05.060"" /> </div> <div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""text"" value=""2000-01-02T03:04:05.060&#x2B;00:00"" /> </div>
False"; False";
@ -59,7 +59,7 @@ False";
<ValidationInView class=""field-validation-error"" data-valmsg-for=""Error"" data-valmsg-replace=""true"">An error occurred.</ValidationInView> <ValidationInView class=""field-validation-error"" data-valmsg-for=""Error"" data-valmsg-replace=""true"">An error occurred.</ValidationInView>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" /> <input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div> <div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" data-val=""true"" data-val-required=""The MyDate field is required."" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInView></div> <div class=""editor-field""><input class=""text-box single-line"" data-val=""true"" data-val-required=""The MyDate field is required."" id=""MyDate"" name=""MyDate"" type=""text"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInView></div>
True True
<div class=""validation-summary-errors""><ValidationSummaryInPartialView>MySummary</ValidationSummaryInPartialView> <div class=""validation-summary-errors""><ValidationSummaryInPartialView>MySummary</ValidationSummaryInPartialView>
@ -68,7 +68,7 @@ True
<ValidationInPartialView class=""field-validation-error"" data-valmsg-for=""Error"" data-valmsg-replace=""true"">An error occurred.</ValidationInPartialView> <ValidationInPartialView class=""field-validation-error"" data-valmsg-for=""Error"" data-valmsg-replace=""true"">An error occurred.</ValidationInPartialView>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" /> <input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<div class=""editor-label""><label for=""MyDate"">MyDate</label></div> <div class=""editor-label""><label for=""MyDate"">MyDate</label></div>
<div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""datetime-local"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInPartialView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInPartialView></div> <div class=""editor-field""><input class=""text-box single-line"" id=""MyDate"" name=""MyDate"" type=""text"" value=""02/01/2000 03:04:05 &#x2B;00:00"" /> <ValidationInPartialView class=""field-validation-valid"" data-valmsg-for=""MyDate"" data-valmsg-replace=""true""></ValidationInPartialView></div>
True"; True";

View File

@ -388,7 +388,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[Theory] [Theory]
[InlineData("datetime", "datetime")] [InlineData("datetime", "datetime")]
[InlineData(null, "datetime-local")] [InlineData(null, "text")]
[InlineData("hidden", "hidden")] [InlineData("hidden", "hidden")]
public void Process_GeneratesFormattedOutput(string specifiedType, string expectedType) public void Process_GeneratesFormattedOutput(string specifiedType, string expectedType)
{ {
@ -457,6 +457,77 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName); 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<object, object>(),
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] [Fact]
public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters() public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters()
{ {
@ -966,6 +1037,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "datetime", null, "datetime-local" }, { "datetime", null, "datetime-local" },
{ "datetime-local", null, "datetime-local" }, { "datetime-local", null, "datetime-local" },
{ "DATETIME-local", null, "datetime-local" }, { "DATETIME-local", null, "datetime-local" },
{ "datetimeOffset", null, "text" },
{ "Decimal", "{0:0.00}", "text" }, { "Decimal", "{0:0.00}", "text" },
{ "Double", null, "text" }, { "Double", null, "text" },
{ "Int16", null, "number" }, { "Int16", null, "number" },
@ -1139,15 +1211,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[InlineData("Date", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")] [InlineData("Date", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
[InlineData("DateTime", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")] [InlineData("DateTime", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")]
[InlineData("DateTime", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "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.CurrentCulture, null, "text")]
[InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")] [InlineData("DateTimeOffset", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fffK}", "text")]
[InlineData("DateTimeLocal", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")] [InlineData("DateTimeLocal", Html5DateRenderingMode.CurrentCulture, null, "datetime-local")]
[InlineData("DateTimeLocal", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "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.CurrentCulture, "{0:t}", "time")] // Format from [DataType].
[InlineData("Time", Html5DateRenderingMode.Rfc3339, "{0:HH:mm:ss.fff}", "time")] [InlineData("Time", Html5DateRenderingMode.Rfc3339, "{0:HH:mm:ss.fff}", "time")]
[InlineData("NullableDate", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")] [InlineData("NullableDate", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-dd}", "date")]
[InlineData("NullableDateTime", Html5DateRenderingMode.Rfc3339, "{0:yyyy-MM-ddTHH:mm:ss.fff}", "datetime-local")] [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( public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributesForRfc3339(
string propertyName, string propertyName,
Html5DateRenderingMode dateRenderingMode, Html5DateRenderingMode dateRenderingMode,

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -52,6 +53,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ "datetime", "__TextBox__ class='text-box single-line' type='datetime-local'" }, { "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'" },
{ "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'" },
{ "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'" }, { "Byte", "__TextBox__ class='text-box single-line' type='number'" },
@ -776,7 +779,7 @@ Environment.NewLine;
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset"); var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset");
var expectedInput = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " + var expectedInput = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
$"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " + $"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
"name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[datetime]]\" value=\"HtmlEncode[[2000-01-02T03:04:05.006]]\" />"; "name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[datetime]]\" value=\"HtmlEncode[[2000-01-02T03:04:05.060+00:00]]\" />";
var offset = TimeSpan.FromHours(0); var offset = TimeSpan.FromHours(0);
var model = new DateTimeOffset( var model = new DateTimeOffset(
@ -786,7 +789,7 @@ Environment.NewLine;
hour: 3, hour: 3,
minute: 4, minute: 4,
second: 5, second: 5,
millisecond: 6, millisecond: 60,
offset: offset); offset: offset);
var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict); var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
viewEngine viewEngine
@ -814,13 +817,19 @@ Environment.NewLine;
Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result)); Assert.Equal(expectedInput, HtmlContentUtilities.HtmlContentToString(result));
} }
// DateTime-local is not special-cased unless using Html5DateRenderingMode.Rfc3339. // Html5DateRenderingMode.Rfc3339 is enabled by default.
[Theory] [Theory]
[InlineData(null, null, "2000-01-02T03:04:05.060-05:00", "text")]
[InlineData("date", "{0:d}", "2000-01-02", "date")] [InlineData("date", "{0:d}", "2000-01-02", "date")]
[InlineData("datetime", null, "2000-01-02T03:04:05.006", "datetime-local")] [InlineData("datetime", null, "2000-01-02T03:04:05.060", "datetime-local")]
[InlineData("datetime-local", null, "2000-01-02T03:04:05.006", "datetime-local")] [InlineData("datetime-local", null, "2000-01-02T03:04:05.060", "datetime-local")]
[InlineData("time", "{0:t}", "03:04:05.006", "time")] [InlineData("DateTimeOffset", "{0:o}", "2000-01-02T03:04:05.060-05:00", "text")]
public void Editor_FindsCorrectDateOrTimeTemplate(string dataTypeName, string editFormatString, string expectedFormat, string expectedType) [InlineData("time", "{0:t}", "03:04:05.060", "time")]
public void Editor_FindsCorrectDateOrTimeTemplate(
string dataTypeName,
string editFormatString,
string expectedFormat,
string expectedType)
{ {
// Arrange // Arrange
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset"); var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("DateTimeOffset");
@ -830,63 +839,7 @@ Environment.NewLine;
expectedType + expectedType +
"]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />"; "]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />";
var offset = TimeSpan.FromHours(0); var offset = TimeSpan.FromHours(-5);
var model = new DateTimeOffset(
year: 2000,
month: 1,
day: 2,
hour: 3,
minute: 4,
second: 5,
millisecond: 6,
offset: offset);
var viewEngine = new Mock<ICompositeViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
viewEngine
.Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
var provider = new TestModelMetadataProvider();
provider.ForType<DateTimeOffset>().DisplayDetails(dd =>
{
dd.DataTypeName = dataTypeName;
dd.EditFormatString = editFormatString; // What [DataType] does for given type.
});
var helper = DefaultTemplatesUtilities.GetHtmlHelper(
model,
Mock.Of<IUrlHelper>(),
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 =
"<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
$"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
"name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
expectedType +
"]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />";
// Place DateTime-local value in current timezone.
var offset = string.Equals(string.Empty, dataTypeName) ? DateTimeOffset.Now.Offset : TimeSpan.FromHours(0);
var model = new DateTimeOffset( var model = new DateTimeOffset(
year: 2000, year: 2000,
month: 1, month: 1,
@ -916,7 +869,193 @@ Environment.NewLine;
Mock.Of<IUrlHelper>(), Mock.Of<IUrlHelper>(),
viewEngine.Object, viewEngine.Object,
provider); 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 =
"<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
$"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
"name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
expectedType +
"]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />";
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<ICompositeViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
viewEngine
.Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
var provider = new TestModelMetadataProvider();
provider.ForType<DateTimeOffset>().DisplayDetails(dd =>
{
dd.DataTypeName = dataTypeName;
dd.EditFormatString = editFormatString; // What [DataType] does for given type.
});
var helper = DefaultTemplatesUtilities.GetHtmlHelper(
model,
Mock.Of<IUrlHelper>(),
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 = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
$"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
"name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
expectedType +
"]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />";
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<ICompositeViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
viewEngine
.Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
var provider = new TestModelMetadataProvider();
provider.ForType<DateTime>().DisplayDetails(dd =>
{
dd.DataTypeName = dataTypeName;
dd.EditFormatString = editFormatString; // What [DataType] does for given type.
});
var helper = DefaultTemplatesUtilities.GetHtmlHelper(
model,
Mock.Of<IUrlHelper>(),
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 =
"<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
$"data-val-required=\"HtmlEncode[[{requiredMessage}]]\" id=\"HtmlEncode[[FieldPrefix]]\" " +
"name=\"HtmlEncode[[FieldPrefix]]\" type=\"HtmlEncode[[" +
expectedType +
"]]\" value=\"HtmlEncode[[" + expectedFormat + "]]\" />";
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<ICompositeViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
viewEngine
.Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>(), /*isMainPage*/ false))
.Returns(ViewEngineResult.NotFound(string.Empty, Enumerable.Empty<string>()));
var provider = new TestModelMetadataProvider();
provider.ForType<DateTime>().DisplayDetails(dd =>
{
dd.DataTypeName = dataTypeName;
dd.EditFormatString = editFormatString; // What [DataType] does for given type.
});
var helper = DefaultTemplatesUtilities.GetHtmlHelper(
model,
Mock.Of<IUrlHelper>(),
viewEngine.Object,
provider);
helper.Html5DateRenderingMode = Html5DateRenderingMode.CurrentCulture;
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; helper.ViewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
// Act // Act
@ -927,6 +1066,8 @@ Environment.NewLine;
} }
[Theory] [Theory]
[InlineData(null, Html5DateRenderingMode.CurrentCulture, "text")]
[InlineData(null, Html5DateRenderingMode.Rfc3339, "text")]
[InlineData("date", Html5DateRenderingMode.CurrentCulture, "date")] [InlineData("date", Html5DateRenderingMode.CurrentCulture, "date")]
[InlineData("date", Html5DateRenderingMode.Rfc3339, "date")] [InlineData("date", Html5DateRenderingMode.Rfc3339, "date")]
[InlineData("datetime", Html5DateRenderingMode.CurrentCulture, "datetime-local")] [InlineData("datetime", Html5DateRenderingMode.CurrentCulture, "datetime-local")]