Render `maxlength` attribute for an input tag, when MaxLength or StringLength validation attributes are applied to the model class.
This commit is contained in:
parent
b7335ac768
commit
6d9aa281c5
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
|
|
@ -17,15 +18,18 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public class MvcViewOptions : IEnumerable<ICompatibilitySwitch>
|
||||
{
|
||||
private readonly CompatibilitySwitch<bool> _suppressTempDataAttributePrefix;
|
||||
private readonly CompatibilitySwitch<bool> _allowRenderingMaxLengthAttribute;
|
||||
private readonly ICompatibilitySwitch[] _switches;
|
||||
private HtmlHelperOptions _htmlHelperOptions = new HtmlHelperOptions();
|
||||
|
||||
public MvcViewOptions()
|
||||
{
|
||||
_suppressTempDataAttributePrefix = new CompatibilitySwitch<bool>(nameof(SuppressTempDataAttributePrefix));
|
||||
_allowRenderingMaxLengthAttribute = new CompatibilitySwitch<bool>(nameof(AllowRenderingMaxLengthAttribute));
|
||||
_switches = new[]
|
||||
{
|
||||
_suppressTempDataAttributePrefix,
|
||||
_allowRenderingMaxLengthAttribute
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +91,18 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
set => _suppressTempDataAttributePrefix.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether the maxlength attribute should be rendered for compatible HTML elements,
|
||||
/// when they're bound to models marked with either
|
||||
/// <see cref="StringLengthAttribute"/> or <see cref="MaxLengthAttribute"/> attributes.
|
||||
/// </summary>
|
||||
/// <remarks>If both attributes are specified, the one with the smaller value will be used for the rendered `maxlength` attribute.</remarks>
|
||||
public bool AllowRenderingMaxLengthAttribute
|
||||
{
|
||||
get => _allowRenderingMaxLengthAttribute.Value;
|
||||
set => _allowRenderingMaxLengthAttribute.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list <see cref="IViewEngine"/>s used by this application.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
|
@ -30,6 +31,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
private static readonly string[] _placeholderInputTypes =
|
||||
new[] { "text", "search", "url", "tel", "email", "password", "number" };
|
||||
|
||||
// See: (http://www.w3.org/TR/html5/sec-forms.html#apply)
|
||||
private static readonly string[] _maxLengthInputTypes =
|
||||
new[] { "text", "search", "url", "tel", "email", "password" };
|
||||
|
||||
private readonly IAntiforgery _antiforgery;
|
||||
private readonly IModelMetadataProvider _metadataProvider;
|
||||
private readonly IUrlHelperFactory _urlHelperFactory;
|
||||
|
|
@ -92,8 +97,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
|
||||
// Underscores are fine characters in id's.
|
||||
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
|
||||
|
||||
AllowRenderingMaxLengthAttribute = optionsAccessor.Value.AllowRenderingMaxLengthAttribute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether the maxlength attribute should be rendered for compatible HTML input elements,
|
||||
/// when they're bound to models marked with either
|
||||
/// <see cref="StringLengthAttribute"/> or <see cref="MaxLengthAttribute"/> attributes.
|
||||
/// </summary>
|
||||
/// <remarks>If both attributes are specified, the one with the smaller value will be used for the rendered `maxlength` attribute.</remarks>
|
||||
protected bool AllowRenderingMaxLengthAttribute { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string IdAttributeDotReplacement { get; }
|
||||
|
||||
|
|
@ -724,6 +739,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
}
|
||||
|
||||
AddPlaceholderAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression);
|
||||
if (AllowRenderingMaxLengthAttribute)
|
||||
{
|
||||
AddMaxLengthAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression);
|
||||
}
|
||||
|
||||
AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression);
|
||||
|
||||
// If there are any errors for a named field, we add this CSS attribute.
|
||||
|
|
@ -1239,6 +1259,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
AddPlaceholderAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression);
|
||||
}
|
||||
|
||||
if (AllowRenderingMaxLengthAttribute && _maxLengthInputTypes.Contains(suppliedTypeString))
|
||||
{
|
||||
AddMaxLengthAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression);
|
||||
}
|
||||
|
||||
var valueParameter = FormatValue(value, format);
|
||||
var usedModelState = false;
|
||||
switch (inputType)
|
||||
|
|
@ -1373,6 +1398,43 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a maxlength attribute to the <paramref name="tagBuilder" />.
|
||||
/// </summary>
|
||||
/// <param name="viewData">A <see cref="ViewDataDictionary"/> instance for the current scope.</param>
|
||||
/// <param name="tagBuilder">A <see cref="TagBuilder"/> instance.</param>
|
||||
/// <param name="modelExplorer">The <see cref="ModelExplorer"/> for the <paramref name="expression"/>.</param>
|
||||
/// <param name="expression">Expression name, relative to the current model.</param>
|
||||
protected virtual void AddMaxLengthAttribute(
|
||||
ViewDataDictionary viewData,
|
||||
TagBuilder tagBuilder,
|
||||
ModelExplorer modelExplorer,
|
||||
string expression)
|
||||
{
|
||||
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(
|
||||
expression,
|
||||
viewData,
|
||||
_metadataProvider);
|
||||
|
||||
int? maxLengthValue = null;
|
||||
foreach (var attribute in modelExplorer.Metadata.ValidatorMetadata)
|
||||
{
|
||||
if (attribute is MaxLengthAttribute maxLengthAttribute && (!maxLengthValue.HasValue || maxLengthValue.Value > maxLengthAttribute.Length))
|
||||
{
|
||||
maxLengthValue = maxLengthAttribute.Length;
|
||||
}
|
||||
else if (attribute is StringLengthAttribute stringLengthAttribute && (!maxLengthValue.HasValue || maxLengthValue.Value > stringLengthAttribute.MaximumLength))
|
||||
{
|
||||
maxLengthValue = stringLengthAttribute.MaximumLength;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxLengthValue.HasValue)
|
||||
{
|
||||
tagBuilder.MergeAttribute("maxlength", maxLengthValue.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds validation attributes to the <paramref name="tagBuilder" /> if client validation
|
||||
/// is enabled.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
values[nameof(MvcViewOptions.SuppressTempDataAttributePrefix)] = true;
|
||||
}
|
||||
|
||||
if (Version >= CompatibilityVersion.Version_2_2)
|
||||
{
|
||||
values[nameof(MvcViewOptions.AllowRenderingMaxLengthAttribute)] = true;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
|
|
@ -190,6 +191,169 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
Assert.Equal(expected, attribute.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithMaxLength), ModelWithMaxLengthMetadata.MaxLengthAttributeValue)]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithStringLength), ModelWithMaxLengthMetadata.StringLengthAttributeValue)]
|
||||
public void GenerateTextArea_RendersMaxLength(string expression, int expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
var htmlGenerator = GetGenerator(metadataProvider);
|
||||
var viewContext = GetViewContext<ModelWithMaxLengthMetadata>(model: null, metadataProvider: metadataProvider);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(ModelWithMaxLengthMetadata), expression);
|
||||
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null);
|
||||
var htmlAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", "testElement" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var tagBuilder = htmlGenerator.GenerateTextArea(viewContext, modelExplorer, expression, rows: 1, columns: 1, htmlAttributes);
|
||||
|
||||
// Assert
|
||||
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "maxlength");
|
||||
Assert.Equal(expectedValue, Int32.Parse(attribute.Value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithMaxLength), ModelWithMaxLengthMetadata.MaxLengthAttributeValue)]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithStringLength), ModelWithMaxLengthMetadata.StringLengthAttributeValue)]
|
||||
public void GeneratePassword_RendersMaxLength(string expression, int expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
var htmlGenerator = GetGenerator(metadataProvider);
|
||||
var viewContext = GetViewContext<ModelWithMaxLengthMetadata>(model: null, metadataProvider: metadataProvider);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(ModelWithMaxLengthMetadata), expression);
|
||||
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null);
|
||||
var htmlAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", "testElement" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var tagBuilder = htmlGenerator.GeneratePassword(viewContext, modelExplorer, expression, null, htmlAttributes);
|
||||
|
||||
// Assert
|
||||
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "maxlength");
|
||||
Assert.Equal(expectedValue, Int32.Parse(attribute.Value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithMaxLength), ModelWithMaxLengthMetadata.MaxLengthAttributeValue)]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithStringLength), ModelWithMaxLengthMetadata.StringLengthAttributeValue)]
|
||||
public void GenerateTextBox_RendersMaxLength(string expression, int expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
var htmlGenerator = GetGenerator(metadataProvider);
|
||||
var viewContext = GetViewContext<ModelWithMaxLengthMetadata>(model: null, metadataProvider: metadataProvider);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(ModelWithMaxLengthMetadata), expression);
|
||||
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null);
|
||||
var htmlAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", "testElement" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var tagBuilder = htmlGenerator.GenerateTextBox(viewContext, modelExplorer, expression, null, null, htmlAttributes);
|
||||
|
||||
// Assert
|
||||
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "maxlength");
|
||||
Assert.Equal(expectedValue, Int32.Parse(attribute.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateTextBox_RendersMaxLength_WithMinimumValueFromBothAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
var htmlGenerator = GetGenerator(metadataProvider);
|
||||
var viewContext = GetViewContext<ModelWithMaxLengthMetadata>(model: null, metadataProvider: metadataProvider);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(ModelWithMaxLengthMetadata), nameof(ModelWithMaxLengthMetadata.FieldWithBothAttributes));
|
||||
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null);
|
||||
var htmlAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", "testElement" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var tagBuilder = htmlGenerator.GenerateTextBox(viewContext, modelExplorer, nameof(ModelWithMaxLengthMetadata.FieldWithBothAttributes), null, null, htmlAttributes);
|
||||
|
||||
// Assert
|
||||
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "maxlength");
|
||||
Assert.Equal(Math.Min(ModelWithMaxLengthMetadata.MaxLengthAttributeValue, ModelWithMaxLengthMetadata.StringLengthAttributeValue), Int32.Parse(attribute.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateTextBox_DoesNotRenderMaxLength_WhenNoAttributesPresent()
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
var htmlGenerator = GetGenerator(metadataProvider);
|
||||
var viewContext = GetViewContext<ModelWithMaxLengthMetadata>(model: null, metadataProvider: metadataProvider);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(ModelWithMaxLengthMetadata), nameof(ModelWithMaxLengthMetadata.FieldWithoutAttributes));
|
||||
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null);
|
||||
var htmlAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", "testElement" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var tagBuilder = htmlGenerator.GenerateTextBox(viewContext, modelExplorer, nameof(ModelWithMaxLengthMetadata.FieldWithoutAttributes), null, null, htmlAttributes);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(tagBuilder.Attributes, a => a.Key == "maxlength");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithMaxLength), ModelWithMaxLengthMetadata.MaxLengthAttributeValue)]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithStringLength), ModelWithMaxLengthMetadata.StringLengthAttributeValue)]
|
||||
public void GenerateTextBox_SearchType_RendersMaxLength(string expression, int expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
var htmlGenerator = GetGenerator(metadataProvider);
|
||||
var viewContext = GetViewContext<ModelWithMaxLengthMetadata>(model: null, metadataProvider: metadataProvider);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(ModelWithMaxLengthMetadata), expression);
|
||||
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null);
|
||||
var htmlAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", "testElement" },
|
||||
{ "type", "search"}
|
||||
};
|
||||
|
||||
// Act
|
||||
var tagBuilder = htmlGenerator.GenerateTextBox(viewContext, modelExplorer, expression, null, null, htmlAttributes);
|
||||
|
||||
// Assert
|
||||
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "maxlength");
|
||||
Assert.Equal(expectedValue, Int32.Parse(attribute.Value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithMaxLength))]
|
||||
[InlineData(nameof(ModelWithMaxLengthMetadata.FieldWithStringLength))]
|
||||
public void GenerateHidden_DoesNotRenderMaxLength(string expression)
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
var htmlGenerator = GetGenerator(metadataProvider);
|
||||
var viewContext = GetViewContext<ModelWithMaxLengthMetadata>(model: null, metadataProvider: metadataProvider);
|
||||
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(ModelWithMaxLengthMetadata), expression);
|
||||
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, null);
|
||||
var htmlAttributes = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", "testElement" },
|
||||
};
|
||||
|
||||
// Act
|
||||
var tagBuilder = htmlGenerator.GenerateHidden(viewContext, modelExplorer, expression, null, false, htmlAttributes);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(tagBuilder.Attributes, a => a.Key == "maxlength");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateValidationMessage_WithNullExpression_Throws()
|
||||
{
|
||||
|
|
@ -759,7 +923,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvider)
|
||||
{
|
||||
var mvcViewOptionsAccessor = new Mock<IOptions<MvcViewOptions>>();
|
||||
mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(new MvcViewOptions());
|
||||
mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(new MvcViewOptions() { AllowRenderingMaxLengthAttribute = true });
|
||||
|
||||
var htmlEncoder = Mock.Of<HtmlEncoder>();
|
||||
var antiforgery = new Mock<IAntiforgery>();
|
||||
|
|
@ -820,6 +984,24 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
All = -1,
|
||||
}
|
||||
|
||||
private class ModelWithMaxLengthMetadata
|
||||
{
|
||||
internal const int MaxLengthAttributeValue = 77;
|
||||
internal const int StringLengthAttributeValue = 7;
|
||||
|
||||
[MaxLength(MaxLengthAttributeValue)]
|
||||
public string FieldWithMaxLength { get; set; }
|
||||
|
||||
[StringLength(StringLengthAttributeValue)]
|
||||
public string FieldWithStringLength { get; set; }
|
||||
|
||||
public string FieldWithoutAttributes { get; set; }
|
||||
|
||||
[MaxLength(MaxLengthAttributeValue)]
|
||||
[StringLength(StringLengthAttributeValue)]
|
||||
public string FieldWithBothAttributes { get; set; }
|
||||
}
|
||||
|
||||
private class Model
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue