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" },
{ "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;
}

View File

@ -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)

View File

@ -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<IHtmlHelper, IHtmlContent> 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<IHtmlHelper>();
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);

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
<validationMessageElement class=""field-validation-error"">An error occurred.</validationMessageElement>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<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>
<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>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<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";
@ -59,7 +59,7 @@ False";
<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="""" />
<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
<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>
<input id=""Prefix!Property1"" name=""Prefix.Property1"" type=""text"" value="""" />
<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";

View File

@ -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<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]
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,

View File

@ -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 = "<input class=\"HtmlEncode[[text-box single-line]]\" data-val=\"HtmlEncode[[true]]\" " +
$"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 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<ICompositeViewEngine>(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<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 offset = TimeSpan.FromHours(-5);
var model = new DateTimeOffset(
year: 2000,
month: 1,
@ -916,7 +869,193 @@ Environment.NewLine;
Mock.Of<IUrlHelper>(),
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 =
"<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";
// 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")]