From e7328ecc530dd12dc82b30c4b47a05eee33014ac Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 9 Aug 2022 16:56:58 -0700 Subject: [PATCH 01/15] Initial prototype --- .../Infrastructure/MvcCoreMvcOptionsSetup.cs | 2 +- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 + .../src/ModelBinding/FormValueProvider.cs | 28 ++++++++++++- .../ModelBinding/FormValueProviderFactory.cs | 26 ++++++++++-- src/Mvc/Mvc.Core/src/MvcOptions.cs | 27 ++++++++++++- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 2 + src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs | 33 ++++++++++++++- .../src/PublicAPI.Unshipped.txt | 1 + .../src/DefaultHtmlGenerator.cs | 40 +++++++++---------- .../src/PublicAPI.Shipped.txt | 1 - .../src/PublicAPI.Unshipped.txt | 2 + .../src/ViewDataDictionary.cs | 20 ++++++++-- .../ModelBinding/FormModelBindingHelper.cs | 18 +++++++++ 13 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 src/Shared/ModelBinding/FormModelBindingHelper.cs diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index 5ae0d2c9105e..5e9793a12663 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -93,7 +93,7 @@ public void Configure(MvcOptions options) options.OutputFormatters.Add(jsonOutputFormatter); // Set up ValueProviders - options.ValueProviderFactories.Add(new FormValueProviderFactory()); + options.ValueProviderFactories.Add(new FormValueProviderFactory(options)); options.ValueProviderFactories.Add(new RouteValueProviderFactory()); options.ValueProviderFactories.Add(new QueryStringValueProviderFactory()); options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory()); diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 304b7408abaf..43320282093d 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -31,6 +31,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs index 1ca941c979fc..4542b418a93a 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding; public class FormValueProvider : BindingSourceValueProvider, IEnumerableValueProvider { private readonly IFormCollection _values; + private readonly HashSet? _invariantValueKeys; private PrefixContainer? _prefixContainer; /// @@ -22,10 +23,12 @@ public class FormValueProvider : BindingSourceValueProvider, IEnumerableValuePro /// The for the data. /// The key value pairs to wrap. /// The culture to return with ValueProviderResult instances. + /// The options. public FormValueProvider( BindingSource bindingSource, IFormCollection values, - CultureInfo? culture) + CultureInfo? culture, + MvcOptions? options) : base(bindingSource) { if (bindingSource == null) @@ -39,9 +42,29 @@ public FormValueProvider( } _values = values; + + if (options?.AllowCultureInvariantFormModelBinding == true && _values.TryGetValue(FormModelBindingHelper.CultureInvariantFieldName, out var invariantKeys)) + { + _invariantValueKeys = new(invariantKeys, StringComparer.OrdinalIgnoreCase); + } + Culture = culture; } + /// + /// Creates a value provider for . + /// + /// The for the data. + /// The key value pairs to wrap. + /// The culture to return with ValueProviderResult instances. + public FormValueProvider( + BindingSource bindingSource, + IFormCollection values, + CultureInfo? culture) + : this(bindingSource, values, culture, options: null) + { + } + /// /// The culture to use. /// @@ -104,7 +127,8 @@ public override ValueProviderResult GetValue(string key) } else { - return new ValueProviderResult(values, Culture); + var culture = _invariantValueKeys?.Contains(key) == true ? CultureInfo.InvariantCulture : Culture; + return new ValueProviderResult(values, culture); } } } diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs index 18bebb69b49c..03c724134751 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs @@ -14,6 +14,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding; /// public class FormValueProviderFactory : IValueProviderFactory { + private readonly MvcOptions? _options; + + /// + /// Creates a new . + /// + /// The options. + public FormValueProviderFactory(MvcOptions? options) + { + _options = options; + } + + /// + /// Creates a new . + /// + public FormValueProviderFactory() + : this(options: null) + { + } + /// public Task CreateValueProviderAsync(ValueProviderFactoryContext context) { @@ -26,13 +45,13 @@ public Task CreateValueProviderAsync(ValueProviderFactoryContext context) if (request.HasFormContentType) { // Allocating a Task only when the body is form data. - return AddValueProviderAsync(context); + return AddValueProviderAsync(context, _options); } return Task.CompletedTask; } - private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) + private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, MvcOptions? options) { var request = context.ActionContext.HttpContext.Request; IFormCollection form; @@ -57,7 +76,8 @@ private static async Task AddValueProviderAsync(ValueProviderFactoryContext cont var valueProvider = new FormValueProvider( BindingSource.Form, form, - CultureInfo.CurrentCulture); + CultureInfo.CurrentCulture, + options); context.ValueProviders.Add(valueProvider); } diff --git a/src/Mvc/Mvc.Core/src/MvcOptions.cs b/src/Mvc/Mvc.Core/src/MvcOptions.cs index 7a59a9834738..53c80bc51617 100644 --- a/src/Mvc/Mvc.Core/src/MvcOptions.cs +++ b/src/Mvc/Mvc.Core/src/MvcOptions.cs @@ -3,6 +3,7 @@ using System.Collections; using System.ComponentModel.DataAnnotations; +using System.Globalization; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; @@ -25,7 +26,8 @@ public class MvcOptions : IEnumerable internal const int DefaultMaxModelBindingCollectionSize = FormReader.DefaultValueCountLimit; internal const int DefaultMaxModelBindingRecursionDepth = 32; - private readonly IReadOnlyList _switches = Array.Empty(); + private readonly CompatibilitySwitch _allowCultureInvariantFormModelBinding; + private readonly IReadOnlyList _switches; private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors; private int _maxModelBindingCollectionSize = DefaultMaxModelBindingCollectionSize; @@ -48,6 +50,13 @@ public MvcOptions() ModelMetadataDetailsProviders = new List(); ModelValidatorProviders = new List(); ValueProviderFactories = new List(); + + _allowCultureInvariantFormModelBinding = new(nameof(AllowCultureInvariantFormModelBinding)); + + _switches = new ICompatibilitySwitch[] + { + _allowCultureInvariantFormModelBinding, + }; } /// @@ -379,6 +388,22 @@ public int MaxModelBindingRecursionDepth /// Defaults to 8192. public int MaxIAsyncEnumerableBufferLimit { get; set; } = 8192; + /// + /// Gets or sets whether form values may be formatted and parsed using + /// when appropriate. + /// + /// + /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are + /// directly entered by the user. However, other inputs (e.g., <input type="number"/>) use culture-invariant + /// formatting both in the HTML source and in the form request. Setting this property to + /// ensures that the correct formatting will be applied for each type of form element. + /// + public bool AllowCultureInvariantFormModelBinding + { + get => _allowCultureInvariantFormModelBinding.Value; + set => _allowCultureInvariantFormModelBinding.Value = value; + } + IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 9742d6caa946..c61be8e753c4 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -7,6 +7,8 @@ Microsoft.AspNetCore.Mvc.ApplicationModels.InferParameterBindingInfoConvention.I *REMOVED*Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, Microsoft.AspNetCore.Mvc.ModelBinding.IValueProvider! valueProvider, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProvider.FormValueProvider(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource! bindingSource, Microsoft.AspNetCore.Http.IFormCollection! values, System.Globalization.CultureInfo? culture, Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void +Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProviderFactory.FormValueProviderFactory(Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void Microsoft.AspNetCore.Mvc.ProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) diff --git a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs index b5d9c21e53b5..2d6474262fa9 100644 --- a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.TagHelpers; @@ -62,15 +63,29 @@ public class InputTagHelper : TagHelper { "time", @"{0:HH\:mm\:ss.fff}" }, }; + private readonly bool _allowCultureInvariantFormModelBinding; + /// /// Creates a new . /// /// The . - public InputTagHelper(IHtmlGenerator generator) + /// The accessor for the . + public InputTagHelper(IHtmlGenerator generator, IOptions optionsAccessor) { + _allowCultureInvariantFormModelBinding = optionsAccessor?.Value.AllowCultureInvariantFormModelBinding ?? false; + Generator = generator; } + /// + /// Creates a new . + /// + /// The . + public InputTagHelper(IHtmlGenerator generator) + : this(generator, optionsAccessor: null) + { + } + /// public override int Order => -1000; @@ -241,6 +256,12 @@ public override void Process(TagHelperContext context, TagHelperOutput output) break; } + if (_allowCultureInvariantFormModelBinding && FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(inputType)) + { + var invariantCultureMetadataTag = GenerateInvariantCultureMetadata(For.Name); + output.PostElement.AppendHtml(invariantCultureMetadataTag); + } + if (tagBuilder != null) { // This TagBuilder contains the one element of interest. @@ -410,6 +431,16 @@ private TagBuilder GenerateTextBox( htmlAttributes); } + private static TagBuilder GenerateInvariantCultureMetadata(string propertyName) + { + var tagBuilder = new TagBuilder("input"); + tagBuilder.Attributes["name"] = FormModelBindingHelper.CultureInvariantFieldName; + tagBuilder.Attributes["type"] = "hidden"; + tagBuilder.Attributes["value"] = propertyName; + + return tagBuilder; + } + // Imitate Generator.GenerateHidden() using Generator.GenerateTextBox(). This adds support for asp-format that // is not available in Generator.GenerateHidden(). private TagBuilder GenerateHidden(ModelExplorer modelExplorer, IDictionary htmlAttributes) diff --git a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..18d800b997b9 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +~Microsoft.AspNetCore.Mvc.TagHelpers.InputTagHelper.InputTagHelper(Microsoft.AspNetCore.Mvc.ViewFeatures.IHtmlGenerator generator, Microsoft.Extensions.Options.IOptions optionsAccessor) -> void diff --git a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs index ce1675bbddb8..40315cf061bf 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs @@ -40,6 +40,7 @@ public class DefaultHtmlGenerator : IHtmlGenerator private readonly IUrlHelperFactory _urlHelperFactory; private readonly HtmlEncoder _htmlEncoder; private readonly ValidationHtmlAttributeProvider _validationAttributeProvider; + private readonly bool _allowCultureInvariantFormModelBinding; /// /// Initializes a new instance of the class. @@ -51,9 +52,11 @@ public class DefaultHtmlGenerator : IHtmlGenerator /// The . /// The . /// The . + /// The accessor for . public DefaultHtmlGenerator( IAntiforgery antiforgery, IOptions optionsAccessor, + IOptions mvcOptionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, @@ -94,6 +97,7 @@ public DefaultHtmlGenerator( _urlHelperFactory = urlHelperFactory; _htmlEncoder = htmlEncoder; _validationAttributeProvider = validationAttributeProvider; + _allowCultureInvariantFormModelBinding = mvcOptionsAccessor?.Value.AllowCultureInvariantFormModelBinding ?? false; // Underscores are fine characters in id's. IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement; @@ -134,7 +138,12 @@ public string Encode(object value) /// public string FormatValue(object value, string format) { - return ViewDataDictionary.FormatValue(value, format); + return ViewDataDictionary.FormatValue(value, format, CultureInfo.CurrentCulture); + } + + private static string FormatValue(object value, string format, IFormatProvider formatProvider) + { + return ViewDataDictionary.FormatValue(value, format, formatProvider); } /// @@ -1282,7 +1291,10 @@ protected virtual TagBuilder GenerateInput( AddMaxLengthAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression); } - var valueParameter = FormatValue(value, format); + var culture = _allowCultureInvariantFormModelBinding && FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(suppliedTypeString) + ? CultureInfo.InvariantCulture + : CultureInfo.CurrentCulture; + var valueParameter = FormatValue(value, format, culture); var usedModelState = false; switch (inputType) { @@ -1329,27 +1341,15 @@ protected virtual TagBuilder GenerateInput( case InputType.Text: default: - var attributeValue = (string)GetModelStateValue(viewContext, fullName, typeof(string)); - if (attributeValue == null) - { - attributeValue = useViewData ? EvalString(viewContext, expression, format) : valueParameter; - } - - var addValue = true; - object typeAttributeValue; - if (htmlAttributes != null && htmlAttributes.TryGetValue("type", out typeAttributeValue)) + if (string.Equals(suppliedTypeString, "file", StringComparison.OrdinalIgnoreCase) || + string.Equals(suppliedTypeString, "image", StringComparison.OrdinalIgnoreCase)) { - var typeAttributeString = typeAttributeValue.ToString(); - if (string.Equals(typeAttributeString, "file", StringComparison.OrdinalIgnoreCase) || - string.Equals(typeAttributeString, "image", StringComparison.OrdinalIgnoreCase)) - { - // 'value' attribute is not needed for 'file' and 'image' input types. - addValue = false; - } + // 'value' attribute is not needed for 'file' and 'image' input types. } - - if (addValue) + else { + var attributeValue = (string)GetModelStateValue(viewContext, fullName, typeof(string)); + attributeValue ??= useViewData ? EvalString(viewContext, expression, format) : valueParameter; tagBuilder.MergeAttribute("value", attributeValue, replaceExisting: isExplicitValue); } diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt index caf420cc56c6..83e8b7fe9b50 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt @@ -191,7 +191,6 @@ Microsoft.AspNetCore.Mvc.ViewEngines.CompositeViewEngine.CompositeViewEngine(Mic ~Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider.CookieTempDataProvider(Microsoft.AspNetCore.DataProtection.IDataProtectionProvider dataProtectionProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions options, Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure.TempDataSerializer tempDataSerializer) -> void ~Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider.LoadTempData(Microsoft.AspNetCore.Http.HttpContext context) -> System.Collections.Generic.IDictionary ~Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider.SaveTempData(Microsoft.AspNetCore.Http.HttpContext context, System.Collections.Generic.IDictionary values) -> void -~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.DefaultHtmlGenerator(Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery, Microsoft.Extensions.Options.IOptions optionsAccessor, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory urlHelperFactory, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, Microsoft.AspNetCore.Mvc.ViewFeatures.ValidationHtmlAttributeProvider validationAttributeProvider) -> void ~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.Encode(object value) -> string ~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.Encode(string value) -> string ~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.FormatValue(object value, string format) -> string diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt index 73d4cbd64fd1..7448f8a126e5 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel? Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel +static Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.FormatValue(object? value, string? format, System.IFormatProvider! formatProvider) -> string? +~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.DefaultHtmlGenerator(Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery, Microsoft.Extensions.Options.IOptions optionsAccessor, Microsoft.Extensions.Options.IOptions mvcOptionsAccessor, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory urlHelperFactory, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, Microsoft.AspNetCore.Mvc.ViewFeatures.ValidationHtmlAttributeProvider validationAttributeProvider) -> void diff --git a/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs b/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs index c9b3357a563f..970811566899 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs @@ -391,11 +391,11 @@ public ICollection Values public string? Eval(string? expression, string? format) { var value = Eval(expression); - return FormatValue(value, format); + return FormatValue(value, format, CultureInfo.CurrentCulture); } /// - /// Formats the given using given . + /// Formats the given using the given . /// /// The value to format. /// @@ -403,6 +403,18 @@ public ICollection Values /// /// The formatted . public static string? FormatValue(object? value, string? format) + => FormatValue(value, format, CultureInfo.CurrentCulture); + + /// + /// Formats the given using the given and . + /// + /// The value to format. + /// + /// The format string (see ). + /// + /// The used to format the value. + /// The formatted . + public static string? FormatValue(object? value, string? format, IFormatProvider formatProvider) { if (value == null) { @@ -411,11 +423,11 @@ public ICollection Values if (string.IsNullOrEmpty(format)) { - return Convert.ToString(value, CultureInfo.CurrentCulture); + return Convert.ToString(value, formatProvider); } else { - return string.Format(CultureInfo.CurrentCulture, format, value); + return string.Format(formatProvider, format, value); } } diff --git a/src/Shared/ModelBinding/FormModelBindingHelper.cs b/src/Shared/ModelBinding/FormModelBindingHelper.cs new file mode 100644 index 000000000000..bc4c8f59dcb5 --- /dev/null +++ b/src/Shared/ModelBinding/FormModelBindingHelper.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Microsoft.AspNetCore.Mvc.ModelBinding; + +internal static class FormModelBindingHelper +{ + public const string CultureInvariantFieldName = "__Invariant"; + + public static bool InputTypeUsesCultureInvariantFormatting(string? inputType) + => inputType switch + { + "number" => true, + _ => false, + }; +} From 36a96335f223ff92f8c7f4ca1a7af3f1d87c2e5e Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 9 Aug 2022 17:25:36 -0700 Subject: [PATCH 02/15] Update PublicAPI.Unshipped.txt --- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index c61be8e753c4..cab3ed56683b 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! mode Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProvider.FormValueProvider(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource! bindingSource, Microsoft.AspNetCore.Http.IFormCollection! values, System.Globalization.CultureInfo? culture, Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProviderFactory.FormValueProviderFactory(Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void +Microsoft.AspNetCore.Mvc.MvcOptions.AllowCultureInvariantFormModelBinding.get -> bool +Microsoft.AspNetCore.Mvc.MvcOptions.AllowCultureInvariantFormModelBinding.set -> void Microsoft.AspNetCore.Mvc.ProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) From 5ed3802c54d84ab2c54127b3b6460ad4ffcea75b Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 10 Aug 2022 12:14:37 -0700 Subject: [PATCH 03/15] Cleanup and improvements --- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 - .../ModelBinding/FormModelBindingHelper.cs | 15 ++++++++---- .../src/ModelBinding/FormValueProvider.cs | 3 ++- src/Mvc/Mvc.Core/src/MvcOptions.cs | 23 +++++-------------- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 4 ++-- src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs | 20 +++------------- .../src/PublicAPI.Unshipped.txt | 1 - .../src/DefaultHtmlGenerator.cs | 8 +++---- .../Mvc.ViewFeatures/src/HtmlHelperOptions.cs | 14 +++++++++++ .../src/PublicAPI.Shipped.txt | 1 + .../src/PublicAPI.Unshipped.txt | 5 +++- .../src/Rendering/ViewContext.cs | 16 +++++++++++++ 12 files changed, 61 insertions(+), 50 deletions(-) rename src/{Shared => Mvc/Mvc.Core/src}/ModelBinding/FormModelBindingHelper.cs (67%) diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 43320282093d..304b7408abaf 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -31,7 +31,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - diff --git a/src/Shared/ModelBinding/FormModelBindingHelper.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs similarity index 67% rename from src/Shared/ModelBinding/FormModelBindingHelper.cs rename to src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs index bc4c8f59dcb5..d6d5d6e39bf6 100644 --- a/src/Shared/ModelBinding/FormModelBindingHelper.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs @@ -10,9 +10,14 @@ internal static class FormModelBindingHelper public const string CultureInvariantFieldName = "__Invariant"; public static bool InputTypeUsesCultureInvariantFormatting(string? inputType) - => inputType switch - { - "number" => true, - _ => false, - }; + => inputType + is "number" + or "color" + or "date" + or "datetime-local" + or "month" + or "range" + or "time" + or "url" + or "week"; } diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs index 4542b418a93a..89e79c4b4cb2 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs @@ -43,7 +43,8 @@ public FormValueProvider( _values = values; - if (options?.AllowCultureInvariantFormModelBinding == true && _values.TryGetValue(FormModelBindingHelper.CultureInvariantFieldName, out var invariantKeys)) + var suppressCultureInvariantFormModelBinding = options?.SuppressCultureInvariantFormModelBinding == true; + if (!suppressCultureInvariantFormModelBinding && _values.TryGetValue(FormModelBindingHelper.CultureInvariantFieldName, out var invariantKeys)) { _invariantValueKeys = new(invariantKeys, StringComparer.OrdinalIgnoreCase); } diff --git a/src/Mvc/Mvc.Core/src/MvcOptions.cs b/src/Mvc/Mvc.Core/src/MvcOptions.cs index 53c80bc51617..4902313c12d2 100644 --- a/src/Mvc/Mvc.Core/src/MvcOptions.cs +++ b/src/Mvc/Mvc.Core/src/MvcOptions.cs @@ -26,8 +26,7 @@ public class MvcOptions : IEnumerable internal const int DefaultMaxModelBindingCollectionSize = FormReader.DefaultValueCountLimit; internal const int DefaultMaxModelBindingRecursionDepth = 32; - private readonly CompatibilitySwitch _allowCultureInvariantFormModelBinding; - private readonly IReadOnlyList _switches; + private readonly IReadOnlyList _switches = Array.Empty(); private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors; private int _maxModelBindingCollectionSize = DefaultMaxModelBindingCollectionSize; @@ -50,13 +49,6 @@ public MvcOptions() ModelMetadataDetailsProviders = new List(); ModelValidatorProviders = new List(); ValueProviderFactories = new List(); - - _allowCultureInvariantFormModelBinding = new(nameof(AllowCultureInvariantFormModelBinding)); - - _switches = new ICompatibilitySwitch[] - { - _allowCultureInvariantFormModelBinding, - }; } /// @@ -389,20 +381,17 @@ public int MaxModelBindingRecursionDepth public int MaxIAsyncEnumerableBufferLimit { get; set; } = 8192; /// - /// Gets or sets whether form values may be formatted and parsed using - /// when appropriate. + /// Gets or sets a value that determines if form values are disallowed to be + /// parsed using . /// /// /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are /// directly entered by the user. However, other inputs (e.g., <input type="number"/>) use culture-invariant /// formatting both in the HTML source and in the form request. Setting this property to - /// ensures that the correct formatting will be applied for each type of form element. + /// will result in always being used to parse form values regardless of + /// their original format. /// - public bool AllowCultureInvariantFormModelBinding - { - get => _allowCultureInvariantFormModelBinding.Value; - set => _allowCultureInvariantFormModelBinding.Value = value; - } + public bool SuppressCultureInvariantFormModelBinding { get; set; } IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index cab3ed56683b..0f024b291afe 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -9,8 +9,8 @@ Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! mode Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProvider.FormValueProvider(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource! bindingSource, Microsoft.AspNetCore.Http.IFormCollection! values, System.Globalization.CultureInfo? culture, Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProviderFactory.FormValueProviderFactory(Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void -Microsoft.AspNetCore.Mvc.MvcOptions.AllowCultureInvariantFormModelBinding.get -> bool -Microsoft.AspNetCore.Mvc.MvcOptions.AllowCultureInvariantFormModelBinding.set -> void +Microsoft.AspNetCore.Mvc.MvcOptions.SuppressCultureInvariantFormModelBinding.get -> bool +Microsoft.AspNetCore.Mvc.MvcOptions.SuppressCultureInvariantFormModelBinding.set -> void Microsoft.AspNetCore.Mvc.ProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) diff --git a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs index 2d6474262fa9..e7016f6a7269 100644 --- a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.TagHelpers; @@ -63,27 +62,13 @@ public class InputTagHelper : TagHelper { "time", @"{0:HH\:mm\:ss.fff}" }, }; - private readonly bool _allowCultureInvariantFormModelBinding; - - /// - /// Creates a new . - /// - /// The . - /// The accessor for the . - public InputTagHelper(IHtmlGenerator generator, IOptions optionsAccessor) - { - _allowCultureInvariantFormModelBinding = optionsAccessor?.Value.AllowCultureInvariantFormModelBinding ?? false; - - Generator = generator; - } - /// /// Creates a new . /// /// The . public InputTagHelper(IHtmlGenerator generator) - : this(generator, optionsAccessor: null) { + Generator = generator; } /// @@ -256,7 +241,8 @@ public override void Process(TagHelperContext context, TagHelperOutput output) break; } - if (_allowCultureInvariantFormModelBinding && FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(inputType)) + if (!ViewContext.SuppressCultureInvariantFormElementValueFormatting && + FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(inputType)) { var invariantCultureMetadataTag = GenerateInvariantCultureMetadata(For.Name); output.PostElement.AppendHtml(invariantCultureMetadataTag); diff --git a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt index 18d800b997b9..7dc5c58110bf 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -~Microsoft.AspNetCore.Mvc.TagHelpers.InputTagHelper.InputTagHelper(Microsoft.AspNetCore.Mvc.ViewFeatures.IHtmlGenerator generator, Microsoft.Extensions.Options.IOptions optionsAccessor) -> void diff --git a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs index 40315cf061bf..8c3e69099f8f 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs @@ -40,7 +40,7 @@ public class DefaultHtmlGenerator : IHtmlGenerator private readonly IUrlHelperFactory _urlHelperFactory; private readonly HtmlEncoder _htmlEncoder; private readonly ValidationHtmlAttributeProvider _validationAttributeProvider; - private readonly bool _allowCultureInvariantFormModelBinding; + private readonly bool _suppressCultureInvariantFormatting; /// /// Initializes a new instance of the class. @@ -52,11 +52,9 @@ public class DefaultHtmlGenerator : IHtmlGenerator /// The . /// The . /// The . - /// The accessor for . public DefaultHtmlGenerator( IAntiforgery antiforgery, IOptions optionsAccessor, - IOptions mvcOptionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, @@ -97,7 +95,7 @@ public DefaultHtmlGenerator( _urlHelperFactory = urlHelperFactory; _htmlEncoder = htmlEncoder; _validationAttributeProvider = validationAttributeProvider; - _allowCultureInvariantFormModelBinding = mvcOptionsAccessor?.Value.AllowCultureInvariantFormModelBinding ?? false; + _suppressCultureInvariantFormatting = optionsAccessor.Value.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting; // Underscores are fine characters in id's. IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement; @@ -1291,7 +1289,7 @@ protected virtual TagBuilder GenerateInput( AddMaxLengthAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression); } - var culture = _allowCultureInvariantFormModelBinding && FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(suppliedTypeString) + var culture = !_suppressCultureInvariantFormatting && FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(suppliedTypeString) ? CultureInfo.InvariantCulture : CultureInfo.CurrentCulture; var valueParameter = FormatValue(value, format, culture); diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs index 5e013aef71c1..df2dfc8ff211 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Microsoft.AspNetCore.Mvc.Rendering; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -60,4 +61,17 @@ public string IdAttributeDotReplacement /// Gets or sets the way hidden inputs are rendered for checkbox tag helpers and html helpers. /// public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } = CheckBoxHiddenInputRenderMode.EndOfForm; + + /// + /// Gets or sets a value that determines if form element values are disallowed to be + /// formatted using . + /// + /// + /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are + /// directly entered by the user. However, other inputs (e.g., <input type="number"/>) use culture-invariant + /// formatting both in the HTML source and in the form request. Setting this property to + /// will result in always being used to format form element values. This may result in + /// invalid HTML being generated. + /// + public bool SuppressCultureInvariantFormValueFormatting { get; set; } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt index 83e8b7fe9b50..caf420cc56c6 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Shipped.txt @@ -191,6 +191,7 @@ Microsoft.AspNetCore.Mvc.ViewEngines.CompositeViewEngine.CompositeViewEngine(Mic ~Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider.CookieTempDataProvider(Microsoft.AspNetCore.DataProtection.IDataProtectionProvider dataProtectionProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions options, Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure.TempDataSerializer tempDataSerializer) -> void ~Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider.LoadTempData(Microsoft.AspNetCore.Http.HttpContext context) -> System.Collections.Generic.IDictionary ~Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider.SaveTempData(Microsoft.AspNetCore.Http.HttpContext context, System.Collections.Generic.IDictionary values) -> void +~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.DefaultHtmlGenerator(Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery, Microsoft.Extensions.Options.IOptions optionsAccessor, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory urlHelperFactory, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, Microsoft.AspNetCore.Mvc.ViewFeatures.ValidationHtmlAttributeProvider validationAttributeProvider) -> void ~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.Encode(object value) -> string ~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.Encode(string value) -> string ~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.FormatValue(object value, string format) -> string diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt index 7448f8a126e5..2b52d650f236 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt @@ -1,5 +1,8 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel? +Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormElementValueFormatting.get -> bool +Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormElementValueFormatting.set -> void +Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.get -> bool +Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.set -> void Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel static Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.FormatValue(object? value, string? format, System.IFormatProvider! formatProvider) -> string? -~Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.DefaultHtmlGenerator(Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery, Microsoft.Extensions.Options.IOptions optionsAccessor, Microsoft.Extensions.Options.IOptions mvcOptionsAccessor, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory urlHelperFactory, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, Microsoft.AspNetCore.Mvc.ViewFeatures.ValidationHtmlAttributeProvider validationAttributeProvider) -> void diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs index 48327508a413..cc1f07aa7387 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -93,6 +94,7 @@ public ViewContext( ValidationSummaryMessageElement = htmlHelperOptions.ValidationSummaryMessageElement; ValidationMessageElement = htmlHelperOptions.ValidationMessageElement; CheckBoxHiddenInputRenderMode = htmlHelperOptions.CheckBoxHiddenInputRenderMode; + SuppressCultureInvariantFormElementValueFormatting = htmlHelperOptions.SuppressCultureInvariantFormValueFormatting; } /// @@ -136,6 +138,7 @@ public ViewContext( ValidationSummaryMessageElement = viewContext.ValidationSummaryMessageElement; ValidationMessageElement = viewContext.ValidationMessageElement; CheckBoxHiddenInputRenderMode = viewContext.CheckBoxHiddenInputRenderMode; + SuppressCultureInvariantFormElementValueFormatting = viewContext.SuppressCultureInvariantFormElementValueFormatting; ExecutingFilePath = viewContext.ExecutingFilePath; View = view; @@ -195,6 +198,19 @@ public virtual FormContext FormContext /// public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } + /// + /// Gets or sets a value that determines if form element values are disallowed to be + /// formatted using . + /// + /// + /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are + /// directly entered by the user. However, other inputs (e.g., <input type="number"/>) use culture-invariant + /// formatting both in the HTML source and in the form request. Setting this property to + /// will result in always being used to format form element values. This may result in + /// invalid HTML being generated. + /// + public bool SuppressCultureInvariantFormElementValueFormatting { get; set; } + /// /// Gets the dynamic view bag. /// From 7514dea120ab0b4e351178c3803d92667fce2f90 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 10 Aug 2022 16:38:39 -0700 Subject: [PATCH 04/15] Update baselines --- .../ModelBinding/FormModelBindingHelper.cs | 2 -- src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs | 19 ++++++++----------- .../Mvc.FunctionalTests/HtmlGenerationTest.cs | 2 +- ...WebSite.HtmlGeneration_Customer.Index.html | 2 +- ...nWebSite.HtmlGeneration_Home.Customer.html | 2 +- ...Site.HtmlGeneration_Home.EmployeeList.html | 6 +++--- ...ite.HtmlGeneration_Home.Order.Encoded.html | 10 +++++----- ...tionWebSite.HtmlGeneration_Home.Order.html | 8 ++++---- ...e.HtmlGeneration_Home.Product.Encoded.html | 4 ++-- ...onWebSite.HtmlGeneration_Home.Product.html | 2 +- ...bSite.HtmlGeneration_Home.ProductList.html | 12 ++++++------ ...ation_Home.ProductListUsingTagHelpers.html | 12 ++++++------ ...oductListUsingTagHelpersWithNullModel.html | 4 ++-- ...WebSite.HtmlGeneration_Home.Warehouse.html | 2 +- ...elpersWebSite.Employee.Create.Invalid.html | 6 +++--- .../TagHelpersWebSite.Employee.Create.html | 6 +++--- 16 files changed, 47 insertions(+), 52 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs index d6d5d6e39bf6..6da90fe5df32 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable enable - namespace Microsoft.AspNetCore.Mvc.ModelBinding; internal static class FormModelBindingHelper diff --git a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs index e7016f6a7269..71bd41d0e58e 100644 --- a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs @@ -244,8 +244,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (!ViewContext.SuppressCultureInvariantFormElementValueFormatting && FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(inputType)) { - var invariantCultureMetadataTag = GenerateInvariantCultureMetadata(For.Name); - output.PostElement.AppendHtml(invariantCultureMetadataTag); + GenerateInvariantCultureMetadata(For.Name, output.PostElement); } if (tagBuilder != null) @@ -417,15 +416,13 @@ private TagBuilder GenerateTextBox( htmlAttributes); } - private static TagBuilder GenerateInvariantCultureMetadata(string propertyName) - { - var tagBuilder = new TagBuilder("input"); - tagBuilder.Attributes["name"] = FormModelBindingHelper.CultureInvariantFieldName; - tagBuilder.Attributes["type"] = "hidden"; - tagBuilder.Attributes["value"] = propertyName; - - return tagBuilder; - } + private static void GenerateInvariantCultureMetadata(string propertyName, TagHelperContent builder) + => builder + .AppendHtml(""); // Imitate Generator.GenerateHidden() using Generator.GenerateTextBox(). This adds support for asp-format that // is not available in Generator.GenerateHidden(). diff --git a/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs b/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs index 426fca79e0ad..56590f1c508d 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs @@ -111,7 +111,7 @@ public async Task HtmlGenerationWebSite_GeneratesExpectedResults(string action, responseContent = responseContent.Trim(); if (antiforgeryPath == null) { - Assert.Equal(expectedContent.Trim(), responseContent, ignoreLineEndingDifferences: true); + ResourceFile.UpdateOrVerify(_resourcesAssembly, outputFile, expectedContent, responseContent); } else { diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Customer.Index.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Customer.Index.html index a61f895061fc..ebb12b5a39c2 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Customer.Index.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Customer.Index.html @@ -3,7 +3,7 @@
- + The value '' is invalid.
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Customer.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Customer.html index ec748e6ea5e6..2afebd632761 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Customer.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Customer.html @@ -3,7 +3,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html index edc86e3450de..446fcb112b05 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html @@ -4,7 +4,7 @@
- +
@@ -35,7 +35,7 @@
- +
@@ -66,7 +66,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html index 4ec3df1116dd..23dd048b6e41 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html @@ -1,4 +1,4 @@ - + @@ -11,7 +11,7 @@
- +
@@ -32,7 +32,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -79,7 +79,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html index 54a6af28f824..b0916d47c052 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html @@ -11,7 +11,7 @@
- +
@@ -32,7 +32,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -79,7 +79,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html index 68230861edd7..3d914599ce9d 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html index a7e6d033146b..da7adffd0125 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html @@ -7,7 +7,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html index 9ad0850e700b..447c8d94b184 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html @@ -5,12 +5,12 @@
- +
- +
@@ -22,12 +22,12 @@
- +
- +
@@ -39,12 +39,12 @@
- +
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html index ae21e4e251ea..dfbdbad03a7b 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html @@ -6,12 +6,12 @@
- +
- +
@@ -24,12 +24,12 @@
- +
- +
@@ -42,12 +42,12 @@
- +
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html index 64eaaeffdae2..d56c1fdaa73d 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html @@ -6,12 +6,12 @@
- +
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html index d86cdb95db99..ca87087f8509 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html @@ -6,7 +6,7 @@

City_1

- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html index 77ad4a00127f..0827ba9e0c2c 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html @@ -23,7 +23,7 @@

Employee

- + The field Age must be between 10 and 100.
@@ -53,14 +53,14 @@

Employee

- + The JoinDate field is required.
- + The value 'z' is not valid for Salary.
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.html index 441241eb8435..b52a0e153904 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.html @@ -21,7 +21,7 @@

Employee

- +
@@ -51,14 +51,14 @@

Employee

- +
- +
From eceea4ce13797b71333352f7072cfc20c8a6896f Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 11 Aug 2022 10:50:48 -0700 Subject: [PATCH 05/15] Only use invariant formatting for numbers --- .../src/ModelBinding/FormModelBindingHelper.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs index 6da90fe5df32..61ab7cf069a5 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs @@ -8,14 +8,5 @@ internal static class FormModelBindingHelper public const string CultureInvariantFieldName = "__Invariant"; public static bool InputTypeUsesCultureInvariantFormatting(string? inputType) - => inputType - is "number" - or "color" - or "date" - or "datetime-local" - or "month" - or "range" - or "time" - or "url" - or "week"; + => inputType is "number" or "range"; } From 85ecf7f86eb6cd77c66fd91c8ed936447c81bf52 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 11 Aug 2022 14:29:40 -0700 Subject: [PATCH 06/15] PR feedback + misc fixes Fixed a couple issues: * Html5DateRenderingMode.CurrentCulture was not forcing CultureInfo.CurrentCulture to be used. * The "value" field in the hidden inputs did not contain the prefix (and thus did not always match the "name" field of the corresponding input). --- .../ModelBinding/FormModelBindingHelper.cs | 12 ----- .../src/ModelBinding/FormValueProvider.cs | 32 +++++++------ .../ModelBinding/FormValueProviderFactory.cs | 10 ++-- src/Mvc/Mvc.Core/src/MvcOptions.cs | 4 +- src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs | 15 +++--- .../src/DefaultHtmlGenerator.cs | 47 +++++++++++++++++-- src/Mvc/Mvc.ViewFeatures/src/FormContext.cs | 31 ++++++++++++ .../Mvc.ViewFeatures/src/HtmlHelperOptions.cs | 4 +- .../src/Rendering/ViewContext.cs | 4 +- ...Site.HtmlGeneration_Home.EmployeeList.html | 6 +-- ...e.HtmlGeneration_Home.Product.Encoded.html | 2 +- ...onWebSite.HtmlGeneration_Home.Product.html | 2 +- ...bSite.HtmlGeneration_Home.ProductList.html | 12 ++--- ...ation_Home.ProductListUsingTagHelpers.html | 12 ++--- ...oductListUsingTagHelpersWithNullModel.html | 4 +- ...WebSite.HtmlGeneration_Home.Warehouse.html | 2 +- 16 files changed, 131 insertions(+), 68 deletions(-) delete mode 100644 src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs deleted file mode 100644 index 61ab7cf069a5..000000000000 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormModelBindingHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Mvc.ModelBinding; - -internal static class FormModelBindingHelper -{ - public const string CultureInvariantFieldName = "__Invariant"; - - public static bool InputTypeUsesCultureInvariantFormatting(string? inputType) - => inputType is "number" or "range"; -} diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs index 89e79c4b4cb2..514c66e87978 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs @@ -13,10 +13,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding; ///
public class FormValueProvider : BindingSourceValueProvider, IEnumerableValueProvider { + internal const string CultureInvariantFieldName = "__Invariant"; + private readonly IFormCollection _values; private readonly HashSet? _invariantValueKeys; private PrefixContainer? _prefixContainer; + /// + /// Creates a value provider for . + /// + /// The for the data. + /// The key value pairs to wrap. + /// The culture to return with ValueProviderResult instances. + public FormValueProvider( + BindingSource bindingSource, + IFormCollection values, + CultureInfo? culture) + : this(bindingSource, values, culture, options: null) + { + } + /// /// Creates a value provider for . /// @@ -44,7 +60,7 @@ public FormValueProvider( _values = values; var suppressCultureInvariantFormModelBinding = options?.SuppressCultureInvariantFormModelBinding == true; - if (!suppressCultureInvariantFormModelBinding && _values.TryGetValue(FormModelBindingHelper.CultureInvariantFieldName, out var invariantKeys)) + if (!suppressCultureInvariantFormModelBinding && _values.TryGetValue(CultureInvariantFieldName, out var invariantKeys)) { _invariantValueKeys = new(invariantKeys, StringComparer.OrdinalIgnoreCase); } @@ -52,20 +68,6 @@ public FormValueProvider( Culture = culture; } - /// - /// Creates a value provider for . - /// - /// The for the data. - /// The key value pairs to wrap. - /// The culture to return with ValueProviderResult instances. - public FormValueProvider( - BindingSource bindingSource, - IFormCollection values, - CultureInfo? culture) - : this(bindingSource, values, culture, options: null) - { - } - /// /// The culture to use. /// diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs index 03c724134751..ea774dfc1f72 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs @@ -19,18 +19,18 @@ public class FormValueProviderFactory : IValueProviderFactory /// /// Creates a new . /// - /// The options. - public FormValueProviderFactory(MvcOptions? options) + public FormValueProviderFactory() + : this(options: null) { - _options = options; } /// /// Creates a new . /// - public FormValueProviderFactory() - : this(options: null) + /// The options. + public FormValueProviderFactory(MvcOptions? options) { + _options = options; } /// diff --git a/src/Mvc/Mvc.Core/src/MvcOptions.cs b/src/Mvc/Mvc.Core/src/MvcOptions.cs index 4902313c12d2..4afafb10a24d 100644 --- a/src/Mvc/Mvc.Core/src/MvcOptions.cs +++ b/src/Mvc/Mvc.Core/src/MvcOptions.cs @@ -381,8 +381,8 @@ public int MaxModelBindingRecursionDepth public int MaxIAsyncEnumerableBufferLimit { get; set; } = 8192; /// - /// Gets or sets a value that determines if form values are disallowed to be - /// parsed using . + /// Gets or sets a value that determines if should always + /// be used to parse form values. /// /// /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are diff --git a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs index 71bd41d0e58e..3e7c2d2d48a5 100644 --- a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs @@ -241,16 +241,17 @@ public override void Process(TagHelperContext context, TagHelperOutput output) break; } - if (!ViewContext.SuppressCultureInvariantFormElementValueFormatting && - FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(inputType)) - { - GenerateInvariantCultureMetadata(For.Name, output.PostElement); - } - if (tagBuilder != null) { // This TagBuilder contains the one element of interest. output.MergeAttributes(tagBuilder); + + if (tagBuilder.Attributes.TryGetValue("name", out var fullName) && + ViewContext.FormContext.InvariantField(fullName)) + { + GenerateInvariantCultureMetadata(fullName, output.PostElement); + } + if (tagBuilder.HasInnerHtml) { // Since this is not the "checkbox" special-case, no guarantee that output is a self-closing @@ -419,7 +420,7 @@ private TagBuilder GenerateTextBox( private static void GenerateInvariantCultureMetadata(string propertyName, TagHelperContent builder) => builder .AppendHtml(""); diff --git a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs index 8c3e69099f8f..e45eeef00db4 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -1289,9 +1290,17 @@ protected virtual TagBuilder GenerateInput( AddMaxLengthAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression); } - var culture = !_suppressCultureInvariantFormatting && FormModelBindingHelper.InputTypeUsesCultureInvariantFormatting(suppliedTypeString) - ? CultureInfo.InvariantCulture - : CultureInfo.CurrentCulture; + CultureInfo culture; + if (ShouldUseInvariantFormattingForInputType(suppliedTypeString, viewContext.Html5DateRenderingMode)) + { + culture = CultureInfo.InvariantCulture; + viewContext.FormContext.InvariantField(fullName, true); + } + else + { + culture = CultureInfo.CurrentCulture; + } + var valueParameter = FormatValue(value, format, culture); var usedModelState = false; switch (inputType) @@ -1554,6 +1563,38 @@ private static string GetInputTypeString(InputType inputType) } } + private bool ShouldUseInvariantFormattingForInputType(string inputType, Html5DateRenderingMode dateRenderingMode) + { + if (!_suppressCultureInvariantFormatting) + { + var isNumberInput = + string.Equals(inputType, "number", StringComparison.OrdinalIgnoreCase) || + string.Equals(inputType, "range", StringComparison.OrdinalIgnoreCase); + + if (isNumberInput) + { + return true; + } + + if (dateRenderingMode != Html5DateRenderingMode.CurrentCulture) + { + var isDateInput = + string.Equals(inputType, "date", StringComparison.OrdinalIgnoreCase) || + string.Equals(inputType, "datetime-local", StringComparison.OrdinalIgnoreCase) || + string.Equals(inputType, "month", StringComparison.OrdinalIgnoreCase) || + string.Equals(inputType, "time", StringComparison.OrdinalIgnoreCase) || + string.Equals(inputType, "week", StringComparison.OrdinalIgnoreCase); + + if (isDateInput) + { + return true; + } + } + } + + return false; + } + private static IEnumerable GetSelectListItems( ViewContext viewContext, string expression) diff --git a/src/Mvc/Mvc.ViewFeatures/src/FormContext.cs b/src/Mvc/Mvc.ViewFeatures/src/FormContext.cs index 31083e19bef3..d6f78f1e278c 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/FormContext.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/FormContext.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Html; +using System.Globalization; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -15,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures; public class FormContext { private Dictionary _renderedFields; + private Dictionary _invariantFields; private Dictionary _formData; private IList _endOfFormContent; @@ -107,6 +109,19 @@ private Dictionary RenderedFields } } + /// + /// Gets a dictionary mapping full HTML field names to indications that corresponding value was formatted + /// using . + /// + private Dictionary InvariantFields + { + get + { + _invariantFields ??= new(StringComparer.Ordinal); + return _invariantFields; + } + } + /// /// Returns an indication based on that the given has /// been rendered in this <form>. @@ -143,4 +158,20 @@ public void RenderedField(string fieldName, bool value) RenderedFields[fieldName] = value; } + + internal bool InvariantField(string fieldName) + { + ArgumentNullException.ThrowIfNull(fieldName); + + InvariantFields.TryGetValue(fieldName, out var result); + + return result; + } + + internal void InvariantField(string fieldName, bool value) + { + ArgumentNullException.ThrowIfNull(fieldName); + + InvariantFields[fieldName] = value; + } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs index df2dfc8ff211..424e55289530 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs @@ -63,8 +63,8 @@ public string IdAttributeDotReplacement public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } = CheckBoxHiddenInputRenderMode.EndOfForm; /// - /// Gets or sets a value that determines if form element values are disallowed to be - /// formatted using . + /// Gets or sets a value that determines if form element values are always + /// formatted using . /// /// /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs index cc1f07aa7387..cf8943fce36b 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs @@ -199,8 +199,8 @@ public virtual FormContext FormContext public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } /// - /// Gets or sets a value that determines if form element values are disallowed to be - /// formatted using . + /// Gets or sets a value that determines if form element values are always + /// formatted using . /// /// /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html index 446fcb112b05..a93298976004 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.EmployeeList.html @@ -4,7 +4,7 @@
- +
@@ -35,7 +35,7 @@
- +
@@ -66,7 +66,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html index 3d914599ce9d..477d075a749d 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.Encoded.html @@ -7,7 +7,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html index da7adffd0125..a7e6d033146b 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Product.html @@ -7,7 +7,7 @@
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html index 447c8d94b184..1434da7d175e 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductList.html @@ -5,12 +5,12 @@
- +
- +
@@ -22,12 +22,12 @@
- +
- +
@@ -39,12 +39,12 @@
- +
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html index dfbdbad03a7b..3a1b89dc4696 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html @@ -6,12 +6,12 @@
- +
- +
@@ -24,12 +24,12 @@
- +
- +
@@ -42,12 +42,12 @@
- +
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html index d56c1fdaa73d..9363027c26bb 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html @@ -6,12 +6,12 @@
- +
- +
diff --git a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html index ca87087f8509..43fb90465dda 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html +++ b/src/Mvc/test/Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html @@ -6,7 +6,7 @@

City_1

- +
From 8d22610668253034e2993196ba7cf8e0ae78a388 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 11 Aug 2022 14:53:06 -0700 Subject: [PATCH 07/15] Update DefaultHtmlGenerator.cs --- src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs index e45eeef00db4..7775ba0974e8 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; From 34ed2e120a0cb798ebbc92941a1fac996e58b982 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 11 Aug 2022 16:51:04 -0700 Subject: [PATCH 08/15] Added InputTagHelper unit test --- src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs | 5 +- .../Mvc.TagHelpers/test/InputTagHelperTest.cs | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs index 3e7c2d2d48a5..3e985f447290 100644 --- a/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs @@ -243,12 +243,15 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (tagBuilder != null) { - // This TagBuilder contains the one element of interest. + // This TagBuilder contains the primary element of interest. output.MergeAttributes(tagBuilder); if (tagBuilder.Attributes.TryGetValue("name", out var fullName) && ViewContext.FormContext.InvariantField(fullName)) { + // If the value attribute used culture-invariant formatting, output a hidden + // element so the form submission includes an entry indicating such. + // This lets the model binding logic decide which CultureInfo to use when parsing form entries. GenerateInvariantCultureMetadata(fullName, output.PostElement); } diff --git a/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs index 38900c3f2c99..3de975a8180f 100644 --- a/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs @@ -833,6 +833,75 @@ public void Process_GeneratesFormattedOutput_ForDateTime(string specifiedType, s Assert.Equal(expectedTagName, output.TagName); } + [Theory] + [InlineData("SomeProperty", "SomeProperty", true)] + [InlineData("SomeProperty", "[0].SomeProperty", true)] + [InlineData("SomeProperty", "[0].SomeProperty", false)] + public void Process_GeneratesInvariantCultureMetadataInput_WhenValueUsesInvariantFormatting(string propertyName, string nameAttributeValue, bool usesInvariantFormatting) + { + // Arrange + var metadataProvider = new EmptyModelMetadataProvider(); + var htmlGenerator = new Mock(MockBehavior.Strict); + var model = false; + var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model); + var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer); + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider); + var tagHelper = new InputTagHelper(htmlGenerator.Object) + { + For = modelExpression, + InputTypeName = "text", + Name = propertyName, + ViewContext = viewContext, + }; + + var tagBuilder = new TagBuilder("input") + { + TagRenderMode = TagRenderMode.SelfClosing, + Attributes = + { + { "name", nameAttributeValue }, + }, + }; + + htmlGenerator + .Setup(mock => mock.GenerateTextBox( + tagHelper.ViewContext, + tagHelper.For.ModelExplorer, + tagHelper.For.Name, + modelExplorer.Model, + null, // format + It.IsAny())) // htmlAttributes + .Returns(tagBuilder) + .Callback(() => viewContext.FormContext.InvariantField(tagBuilder.Attributes["name"], usesInvariantFormatting)) + .Verifiable(); + + var expectedPostElement = usesInvariantFormatting + ? $"" + : string.Empty; + + var attributes = new TagHelperAttributeList + { + { "name", propertyName }, + { "type", "text" }, + }; + var context = new TagHelperContext(attributes, new Dictionary(), "test"); + var output = new TagHelperOutput( + "input", + new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + + // Act + tagHelper.Process(context, output); + + // Assert + htmlGenerator.Verify(); + + Assert.Equal(expectedPostElement, output.PostElement.GetContent()); + } + [Fact] public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeNone() { From 21d0df150cecbd6edef153e90e3552cba6579a62 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Aug 2022 10:05:40 -0700 Subject: [PATCH 09/15] Remove SuppressCultureInvariantFormModelBinding --- .../Infrastructure/MvcCoreMvcOptionsSetup.cs | 2 +- .../src/ModelBinding/FormValueProvider.cs | 19 +------------- .../ModelBinding/FormValueProviderFactory.cs | 26 +++---------------- src/Mvc/Mvc.Core/src/MvcOptions.cs | 14 ---------- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 4 --- 5 files changed, 5 insertions(+), 60 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index 5e9793a12663..5ae0d2c9105e 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -93,7 +93,7 @@ public void Configure(MvcOptions options) options.OutputFormatters.Add(jsonOutputFormatter); // Set up ValueProviders - options.ValueProviderFactories.Add(new FormValueProviderFactory(options)); + options.ValueProviderFactories.Add(new FormValueProviderFactory()); options.ValueProviderFactories.Add(new RouteValueProviderFactory()); options.ValueProviderFactories.Add(new QueryStringValueProviderFactory()); options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory()); diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs index 514c66e87978..1c9c1cfde5d7 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProvider.cs @@ -29,22 +29,6 @@ public FormValueProvider( BindingSource bindingSource, IFormCollection values, CultureInfo? culture) - : this(bindingSource, values, culture, options: null) - { - } - - /// - /// Creates a value provider for . - /// - /// The for the data. - /// The key value pairs to wrap. - /// The culture to return with ValueProviderResult instances. - /// The options. - public FormValueProvider( - BindingSource bindingSource, - IFormCollection values, - CultureInfo? culture, - MvcOptions? options) : base(bindingSource) { if (bindingSource == null) @@ -59,8 +43,7 @@ public FormValueProvider( _values = values; - var suppressCultureInvariantFormModelBinding = options?.SuppressCultureInvariantFormModelBinding == true; - if (!suppressCultureInvariantFormModelBinding && _values.TryGetValue(CultureInvariantFieldName, out var invariantKeys)) + if (_values.TryGetValue(CultureInvariantFieldName, out var invariantKeys) && invariantKeys.Count > 0) { _invariantValueKeys = new(invariantKeys, StringComparer.OrdinalIgnoreCase); } diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs index ea774dfc1f72..18bebb69b49c 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs @@ -14,25 +14,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding; /// public class FormValueProviderFactory : IValueProviderFactory { - private readonly MvcOptions? _options; - - /// - /// Creates a new . - /// - public FormValueProviderFactory() - : this(options: null) - { - } - - /// - /// Creates a new . - /// - /// The options. - public FormValueProviderFactory(MvcOptions? options) - { - _options = options; - } - /// public Task CreateValueProviderAsync(ValueProviderFactoryContext context) { @@ -45,13 +26,13 @@ public Task CreateValueProviderAsync(ValueProviderFactoryContext context) if (request.HasFormContentType) { // Allocating a Task only when the body is form data. - return AddValueProviderAsync(context, _options); + return AddValueProviderAsync(context); } return Task.CompletedTask; } - private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, MvcOptions? options) + private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) { var request = context.ActionContext.HttpContext.Request; IFormCollection form; @@ -76,8 +57,7 @@ private static async Task AddValueProviderAsync(ValueProviderFactoryContext cont var valueProvider = new FormValueProvider( BindingSource.Form, form, - CultureInfo.CurrentCulture, - options); + CultureInfo.CurrentCulture); context.ValueProviders.Add(valueProvider); } diff --git a/src/Mvc/Mvc.Core/src/MvcOptions.cs b/src/Mvc/Mvc.Core/src/MvcOptions.cs index 4afafb10a24d..7a59a9834738 100644 --- a/src/Mvc/Mvc.Core/src/MvcOptions.cs +++ b/src/Mvc/Mvc.Core/src/MvcOptions.cs @@ -3,7 +3,6 @@ using System.Collections; using System.ComponentModel.DataAnnotations; -using System.Globalization; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; @@ -380,19 +379,6 @@ public int MaxModelBindingRecursionDepth /// Defaults to 8192. public int MaxIAsyncEnumerableBufferLimit { get; set; } = 8192; - /// - /// Gets or sets a value that determines if should always - /// be used to parse form values. - /// - /// - /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are - /// directly entered by the user. However, other inputs (e.g., <input type="number"/>) use culture-invariant - /// formatting both in the HTML source and in the form request. Setting this property to - /// will result in always being used to parse form values regardless of - /// their original format. - /// - public bool SuppressCultureInvariantFormModelBinding { get; set; } - IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 0f024b291afe..9742d6caa946 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -7,10 +7,6 @@ Microsoft.AspNetCore.Mvc.ApplicationModels.InferParameterBindingInfoConvention.I *REMOVED*Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, Microsoft.AspNetCore.Mvc.ModelBinding.IValueProvider! valueProvider, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(TModel! model, string! prefix, params System.Linq.Expressions.Expression!>![]! includeExpressions) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProvider.FormValueProvider(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource! bindingSource, Microsoft.AspNetCore.Http.IFormCollection! values, System.Globalization.CultureInfo? culture, Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void -Microsoft.AspNetCore.Mvc.ModelBinding.FormValueProviderFactory.FormValueProviderFactory(Microsoft.AspNetCore.Mvc.MvcOptions? options) -> void -Microsoft.AspNetCore.Mvc.MvcOptions.SuppressCultureInvariantFormModelBinding.get -> bool -Microsoft.AspNetCore.Mvc.MvcOptions.SuppressCultureInvariantFormModelBinding.set -> void Microsoft.AspNetCore.Mvc.ProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) From 60cee51d1ddb05660a69aa4a386f1feddc62f1eb Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Aug 2022 10:15:00 -0700 Subject: [PATCH 10/15] Make new FormatValue overload internal --- src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt | 1 - src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs | 11 +---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt index 2b52d650f236..f9b19362a727 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt @@ -5,4 +5,3 @@ Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormEleme Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.get -> bool Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.set -> void Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel -static Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.FormatValue(object? value, string? format, System.IFormatProvider! formatProvider) -> string? diff --git a/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs b/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs index 970811566899..054575b0f47c 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/ViewDataDictionary.cs @@ -405,16 +405,7 @@ public ICollection Values public static string? FormatValue(object? value, string? format) => FormatValue(value, format, CultureInfo.CurrentCulture); - /// - /// Formats the given using the given and . - /// - /// The value to format. - /// - /// The format string (see ). - /// - /// The used to format the value. - /// The formatted . - public static string? FormatValue(object? value, string? format, IFormatProvider formatProvider) + internal static string? FormatValue(object? value, string? format, IFormatProvider formatProvider) { if (value == null) { From 5f3ba77ebd4df6f5b2e9df46e2fc14d1296b9881 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Aug 2022 10:24:32 -0700 Subject: [PATCH 11/15] Property rename --- src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt | 4 ++-- src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt index f9b19362a727..01966aeffd51 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel? -Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormElementValueFormatting.get -> bool -Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormElementValueFormatting.set -> void +Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormValueFormatting.get -> bool +Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormValueFormatting.set -> void Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.get -> bool Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.set -> void Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs index cf8943fce36b..e8654b6149c9 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs @@ -94,7 +94,7 @@ public ViewContext( ValidationSummaryMessageElement = htmlHelperOptions.ValidationSummaryMessageElement; ValidationMessageElement = htmlHelperOptions.ValidationMessageElement; CheckBoxHiddenInputRenderMode = htmlHelperOptions.CheckBoxHiddenInputRenderMode; - SuppressCultureInvariantFormElementValueFormatting = htmlHelperOptions.SuppressCultureInvariantFormValueFormatting; + SuppressCultureInvariantFormValueFormatting = htmlHelperOptions.SuppressCultureInvariantFormValueFormatting; } /// @@ -138,7 +138,7 @@ public ViewContext( ValidationSummaryMessageElement = viewContext.ValidationSummaryMessageElement; ValidationMessageElement = viewContext.ValidationMessageElement; CheckBoxHiddenInputRenderMode = viewContext.CheckBoxHiddenInputRenderMode; - SuppressCultureInvariantFormElementValueFormatting = viewContext.SuppressCultureInvariantFormElementValueFormatting; + SuppressCultureInvariantFormValueFormatting = viewContext.SuppressCultureInvariantFormValueFormatting; ExecutingFilePath = viewContext.ExecutingFilePath; View = view; @@ -209,7 +209,7 @@ public virtual FormContext FormContext /// will result in always being used to format form element values. This may result in /// invalid HTML being generated. /// - public bool SuppressCultureInvariantFormElementValueFormatting { get; set; } + public bool SuppressCultureInvariantFormValueFormatting { get; set; } /// /// Gets the dynamic view bag. From 1fcd48849feb5d094b8b5b372783a02fe0792cd8 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Aug 2022 13:54:05 -0700 Subject: [PATCH 12/15] Added test for FormValueProvider changes --- .../ModelBinding/FormValueProviderTest.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderTest.cs index 14e87425b0de..abf0619f48cc 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderTest.cs @@ -9,6 +9,31 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding; public class FormValueProviderTest : EnumerableValueProviderTest { + [Fact] + public void GetValue_ReturnsInvariantCulture_IfInvariantEntryExists() + { + // Arrange + var culture = new CultureInfo("fr-FR"); + var invariantCultureKey = "prefix.name"; + var currentCultureKey = "some"; + var values = new Dictionary(BackingStore) + { + { FormValueProvider.CultureInvariantFieldName, new(invariantCultureKey) }, + }; + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, values, culture); + + // Act + var invariantCultureResult = valueProvider.GetValue(invariantCultureKey); + var currentCultureResult = valueProvider.GetValue(currentCultureKey); + + // Assert + Assert.Equal(CultureInfo.InvariantCulture, invariantCultureResult.Culture); + Assert.Equal(BackingStore[invariantCultureKey], invariantCultureResult.Values); + + Assert.Equal(culture, currentCultureResult.Culture); + Assert.Equal(BackingStore[currentCultureKey], currentCultureResult.Values); + } + protected override IEnumerableValueProvider GetEnumerableValueProvider( BindingSource bindingSource, Dictionary values, From 372fc07428be116c4ad2dd4f98ee8f6b764e8b7d Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Aug 2022 15:28:17 -0700 Subject: [PATCH 13/15] Add HTML generation tests --- .../test/DefaultHtmlGeneratorTest.cs | 92 ++++++++++++++++++- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs b/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs index c923a7a0162b..f616df0d4dc9 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Moq; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -327,6 +328,89 @@ public void GenerateTextBox_SearchType_RendersMaxLength(string expression, int e Assert.Equal(expectedValue, int.Parse(attribute.Value, CultureInfo.InvariantCulture)); } + // type, shouldUseInvariantFormatting, dateRenderingMode + public static TheoryData GenerateTextBox_InvariantFormattingData + { + get + { + return new TheoryData + { + {"text", Html5DateRenderingMode.Rfc3339, false }, + {"number", Html5DateRenderingMode.Rfc3339, true }, + {"range", Html5DateRenderingMode.Rfc3339, true }, + {"date", Html5DateRenderingMode.Rfc3339, true }, + {"datetime-local", Html5DateRenderingMode.Rfc3339, true }, + {"month", Html5DateRenderingMode.Rfc3339, true }, + {"time", Html5DateRenderingMode.Rfc3339, true }, + {"week", Html5DateRenderingMode.Rfc3339 , true }, + {"date", Html5DateRenderingMode.CurrentCulture, false }, + {"datetime-local", Html5DateRenderingMode.CurrentCulture, false }, + {"month", Html5DateRenderingMode.CurrentCulture, false }, + {"time", Html5DateRenderingMode.CurrentCulture, false }, + {"week", Html5DateRenderingMode.CurrentCulture, false }, + }; + } + } + + [Theory] + [MemberData(nameof(GenerateTextBox_InvariantFormattingData))] + public void GenerateTextBox_UsesCultureInvariantFormatting_ForAppropriateTypes(string type, Html5DateRenderingMode dateRenderingMode, bool shouldUseInvariantFormatting) + { + // Arrange + var metadataProvider = new TestModelMetadataProvider(); + var htmlHelperOptions = new HtmlHelperOptions() + { + Html5DateRenderingMode = dateRenderingMode, + }; + var htmlGenerator = GetGenerator(metadataProvider, new() { HtmlHelperOptions = htmlHelperOptions }); + var viewContext = GetViewContext(model: null, metadataProvider, htmlHelperOptions); + var expression = nameof(Model.Name); + var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(Model), expression); + var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null); + var htmlAttributes = new Dictionary + { + { "name", "testElement" }, + { "type", type }, + }; + + // Act + _ = htmlGenerator.GenerateTextBox(viewContext, modelExplorer, expression, null, null, htmlAttributes); + + // Assert + var didForceInvariantFormatting = viewContext.FormContext.InvariantField(expression); + Assert.Equal(shouldUseInvariantFormatting, didForceInvariantFormatting); + } + + [Theory] + [MemberData(nameof(GenerateTextBox_InvariantFormattingData))] + public void GenerateTextBox_AlwaysUsesCultureSpecificFormatting_WhenOptionIsSet(string type, Html5DateRenderingMode dateRenderingMode, bool shouldUseInvariantFormatting) + { + // Arrange + var metadataProvider = new TestModelMetadataProvider(); + var htmlHelperOptions = new HtmlHelperOptions() + { + Html5DateRenderingMode = dateRenderingMode, + SuppressCultureInvariantFormValueFormatting = true, + }; + var htmlGenerator = GetGenerator(metadataProvider, new() { HtmlHelperOptions = htmlHelperOptions }); + var viewContext = GetViewContext(model: null, metadataProvider, htmlHelperOptions); + var expression = nameof(Model.Name); + var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(Model), expression); + var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null); + var htmlAttributes = new Dictionary + { + { "name", "testElement" }, + { "type", type }, + }; + + // Act + _ = htmlGenerator.GenerateTextBox(viewContext, modelExplorer, expression, null, null, htmlAttributes); + + // Assert + var didForceInvariantFormatting = viewContext.FormContext.InvariantField(expression); + Assert.False(didForceInvariantFormatting); + } + [Theory] [InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithMaxLength))] [InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithStringLength))] @@ -916,10 +1000,10 @@ public void GenerateAntiforgery_AlwaysGeneratesAntiforgeryToken_IfCannotRenderAt } // GetCurrentValues uses only the IModelMetadataProvider passed to the DefaultHtmlGenerator constructor. - private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvider) + private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvider, MvcViewOptions options = default) { var mvcViewOptionsAccessor = new Mock>(); - mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(new MvcViewOptions()); + mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(options ?? new MvcViewOptions()); var htmlEncoder = Mock.Of(); var antiforgery = new Mock(); @@ -945,7 +1029,7 @@ private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvid } // GetCurrentValues uses only the ModelStateDictionary and ViewDataDictionary from the passed ViewContext. - private static ViewContext GetViewContext(TModel model, IModelMetadataProvider metadataProvider) + private static ViewContext GetViewContext(TModel model, IModelMetadataProvider metadataProvider, HtmlHelperOptions options = default) { var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); var viewData = new ViewDataDictionary(metadataProvider, actionContext.ModelState) @@ -959,7 +1043,7 @@ private static ViewContext GetViewContext(TModel model, IModelMetadataPr viewData, Mock.Of(), TextWriter.Null, - new HtmlHelperOptions()); + options ?? new HtmlHelperOptions()); } public enum RegularEnum From c96114de1bab44ed6c46352706f2e979629d4f18 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Aug 2022 16:10:12 -0700 Subject: [PATCH 14/15] Update JQueryFormValueProvider + tests --- .../ModelBinding/JQueryFormValueProvider.cs | 19 ++++++++++++++ src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 1 + .../JQueryFormValueProviderTest.cs | 25 +++++++++++++++++++ .../test/DefaultHtmlGeneratorTest.cs | 1 - 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProvider.cs index 26bd1f338624..a86584e3f4f4 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProvider.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding; /// public class JQueryFormValueProvider : JQueryValueProvider { + private readonly HashSet? _invariantValueKeys; + /// /// Initializes a new instance of the class. /// @@ -25,5 +27,22 @@ public JQueryFormValueProvider( CultureInfo? culture) : base(bindingSource, values, culture) { + if (values.TryGetValue(FormValueProvider.CultureInvariantFieldName, out var invariantKeys) && invariantKeys.Count > 0) + { + _invariantValueKeys = new(invariantKeys, StringComparer.OrdinalIgnoreCase); + } + } + + /// + public override ValueProviderResult GetValue(string key) + { + var result = base.GetValue(key); + + if (result.Length > 0 && _invariantValueKeys?.Contains(key) == true) + { + return new(result.Values, CultureInfo.InvariantCulture); + } + + return result; } } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 9742d6caa946..b69885465ce6 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -30,6 +30,7 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataP Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.SystemTextJsonValidationMetadataProvider(System.Text.Json.JsonNamingPolicy! namingPolicy) -> void Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.get -> string? Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.set -> void +override Microsoft.AspNetCore.Mvc.ModelBinding.JQueryFormValueProvider.GetValue(string! key) -> Microsoft.AspNetCore.Mvc.ModelBinding.ValueProviderResult virtual Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions.PostConfigure(string? name, TOptions! options) -> void static Microsoft.AspNetCore.Mvc.ControllerBase.Empty.get -> Microsoft.AspNetCore.Mvc.EmptyResult! *REMOVED*virtual Microsoft.AspNetCore.Mvc.ModelBinding.DefaultPropertyFilterProvider.PropertyIncludeExpressions.get -> System.Collections.Generic.IEnumerable!>!>? diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderTest.cs index 42bdafa0aba3..29d285500695 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderTest.cs @@ -46,4 +46,29 @@ public override void GetValue_EmptyKey() // Assert Assert.Equal("some-value", (string)result); } + + [Fact] + public void GetValue_ReturnsInvariantCulture_IfInvariantEntryExists() + { + // Arrange + var culture = new CultureInfo("fr-FR"); + var invariantCultureKey = "prefix.name"; + var currentCultureKey = "some"; + var values = new Dictionary(BackingStore) + { + { FormValueProvider.CultureInvariantFieldName, new(invariantCultureKey) }, + }; + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, values, culture); + + // Act + var invariantCultureResult = valueProvider.GetValue(invariantCultureKey); + var currentCultureResult = valueProvider.GetValue(currentCultureKey); + + // Assert + Assert.Equal(CultureInfo.InvariantCulture, invariantCultureResult.Culture); + Assert.Equal(BackingStore[invariantCultureKey], invariantCultureResult.Values); + + Assert.Equal(culture, currentCultureResult.Culture); + Assert.Equal(BackingStore[currentCultureKey], currentCultureResult.Values); + } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs b/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs index f616df0d4dc9..9eaaf2e1dfbc 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs @@ -15,7 +15,6 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Moq; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; From f8e99950c6e85ebf2bb66c7f169b51a1238e01a6 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Aug 2022 17:02:22 -0700 Subject: [PATCH 15/15] API review feedback --- .../src/DefaultHtmlGenerator.cs | 6 ++--- .../Mvc.ViewFeatures/src/HtmlHelperOptions.cs | 10 +++----- .../src/PublicAPI.Unshipped.txt | 9 ++++--- .../src/Rendering/FormInputRenderMode.cs | 25 +++++++++++++++++++ .../src/Rendering/ViewContext.cs | 16 ------------ .../test/DefaultHtmlGeneratorTest.cs | 2 +- 6 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 src/Mvc/Mvc.ViewFeatures/src/Rendering/FormInputRenderMode.cs diff --git a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs index 7775ba0974e8..caabe80c567e 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs @@ -40,7 +40,7 @@ public class DefaultHtmlGenerator : IHtmlGenerator private readonly IUrlHelperFactory _urlHelperFactory; private readonly HtmlEncoder _htmlEncoder; private readonly ValidationHtmlAttributeProvider _validationAttributeProvider; - private readonly bool _suppressCultureInvariantFormatting; + private readonly FormInputRenderMode _formInputRenderMode; /// /// Initializes a new instance of the class. @@ -95,7 +95,7 @@ public DefaultHtmlGenerator( _urlHelperFactory = urlHelperFactory; _htmlEncoder = htmlEncoder; _validationAttributeProvider = validationAttributeProvider; - _suppressCultureInvariantFormatting = optionsAccessor.Value.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting; + _formInputRenderMode = optionsAccessor.Value.HtmlHelperOptions.FormInputRenderMode; // Underscores are fine characters in id's. IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement; @@ -1564,7 +1564,7 @@ private static string GetInputTypeString(InputType inputType) private bool ShouldUseInvariantFormattingForInputType(string inputType, Html5DateRenderingMode dateRenderingMode) { - if (!_suppressCultureInvariantFormatting) + if (_formInputRenderMode == FormInputRenderMode.DetectCultureFromInputType) { var isNumberInput = string.Equals(inputType, "number", StringComparison.OrdinalIgnoreCase) || diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs index 424e55289530..d4ce05f2811f 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using Microsoft.AspNetCore.Mvc.Rendering; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -63,15 +62,12 @@ public string IdAttributeDotReplacement public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } = CheckBoxHiddenInputRenderMode.EndOfForm; /// - /// Gets or sets a value that determines if form element values are always - /// formatted using . + /// Gets or sets a value that determines how form <input/> elements are rendered. /// /// /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are /// directly entered by the user. However, other inputs (e.g., <input type="number"/>) use culture-invariant - /// formatting both in the HTML source and in the form request. Setting this property to - /// will result in always being used to format form element values. This may result in - /// invalid HTML being generated. + /// formatting both in the HTML source and in the form request. /// - public bool SuppressCultureInvariantFormValueFormatting { get; set; } + public FormInputRenderMode FormInputRenderMode { get; set; } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt index 01966aeffd51..adb19c0812e2 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt @@ -1,7 +1,8 @@ #nullable enable *REMOVED*Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel? -Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormValueFormatting.get -> bool -Microsoft.AspNetCore.Mvc.Rendering.ViewContext.SuppressCultureInvariantFormValueFormatting.set -> void -Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.get -> bool -Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.SuppressCultureInvariantFormValueFormatting.set -> void +Microsoft.AspNetCore.Mvc.Rendering.FormInputRenderMode +Microsoft.AspNetCore.Mvc.Rendering.FormInputRenderMode.AlwaysUseCurrentCulture = 1 -> Microsoft.AspNetCore.Mvc.Rendering.FormInputRenderMode +Microsoft.AspNetCore.Mvc.Rendering.FormInputRenderMode.DetectCultureFromInputType = 0 -> Microsoft.AspNetCore.Mvc.Rendering.FormInputRenderMode +Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.FormInputRenderMode.get -> Microsoft.AspNetCore.Mvc.Rendering.FormInputRenderMode +Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions.FormInputRenderMode.set -> void Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.Model.get -> TModel diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/FormInputRenderMode.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/FormInputRenderMode.cs new file mode 100644 index 000000000000..6d48f96c1c03 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/FormInputRenderMode.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace Microsoft.AspNetCore.Mvc.Rendering; + +/// +/// Used for configuring how form inputs should be rendered with respect to +/// the current locale. +/// +public enum FormInputRenderMode +{ + /// + /// When appropriate, use to format HTML input element values. + /// Generate a hidden HTML form input for each value that uses culture-invariant formatting + /// so model binding logic can parse with the correct culture. + /// + DetectCultureFromInputType = 0, + + /// + /// Always use to format input element values. + /// + AlwaysUseCurrentCulture = 1, +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs index e8654b6149c9..48327508a413 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/ViewContext.cs @@ -3,7 +3,6 @@ #nullable enable -using System.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -94,7 +93,6 @@ public ViewContext( ValidationSummaryMessageElement = htmlHelperOptions.ValidationSummaryMessageElement; ValidationMessageElement = htmlHelperOptions.ValidationMessageElement; CheckBoxHiddenInputRenderMode = htmlHelperOptions.CheckBoxHiddenInputRenderMode; - SuppressCultureInvariantFormValueFormatting = htmlHelperOptions.SuppressCultureInvariantFormValueFormatting; } /// @@ -138,7 +136,6 @@ public ViewContext( ValidationSummaryMessageElement = viewContext.ValidationSummaryMessageElement; ValidationMessageElement = viewContext.ValidationMessageElement; CheckBoxHiddenInputRenderMode = viewContext.CheckBoxHiddenInputRenderMode; - SuppressCultureInvariantFormValueFormatting = viewContext.SuppressCultureInvariantFormValueFormatting; ExecutingFilePath = viewContext.ExecutingFilePath; View = view; @@ -198,19 +195,6 @@ public virtual FormContext FormContext /// public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } - /// - /// Gets or sets a value that determines if form element values are always - /// formatted using . - /// - /// - /// Some form elements (e.g., <input type="text"/>) require culture-specific formatting and parsing because their values are - /// directly entered by the user. However, other inputs (e.g., <input type="number"/>) use culture-invariant - /// formatting both in the HTML source and in the form request. Setting this property to - /// will result in always being used to format form element values. This may result in - /// invalid HTML being generated. - /// - public bool SuppressCultureInvariantFormValueFormatting { get; set; } - /// /// Gets the dynamic view bag. /// diff --git a/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs b/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs index 9eaaf2e1dfbc..02bc8e5a0250 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/DefaultHtmlGeneratorTest.cs @@ -389,7 +389,7 @@ public void GenerateTextBox_AlwaysUsesCultureSpecificFormatting_WhenOptionIsSet( var htmlHelperOptions = new HtmlHelperOptions() { Html5DateRenderingMode = dateRenderingMode, - SuppressCultureInvariantFormValueFormatting = true, + FormInputRenderMode = FormInputRenderMode.AlwaysUseCurrentCulture, }; var htmlGenerator = GetGenerator(metadataProvider, new() { HtmlHelperOptions = htmlHelperOptions }); var viewContext = GetViewContext(model: null, metadataProvider, htmlHelperOptions);