Add `ValidationHtmlAttributeProvider` to make `AddValidationAttributes()` available
- #5028 - helpers similar to our HTML or tag helpers can use the new singleton to examine or add validation attributes - in the most common case, helpers add validation attributes to a `TagBuilder` - separate `DefaultValidationHtmlAttributeProvider` from `DefaultHtmlGenerator` - avoids creating two instances of the `DefaultHtmlGenerator` singleton - would be even uglier to require callers to cast an `IHtmlGenerator` to `ValidationHtmlAttributeProvider` - `[Obsolete]` old `DefaultHtmlGenerator` constructor
This commit is contained in:
parent
4a5e1f4a72
commit
809d2bf7ec
|
|
@ -6,7 +6,6 @@ using System.Buffers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
@ -117,6 +116,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<IHtmlGenerator, DefaultHtmlGenerator>();
|
services.TryAddSingleton<IHtmlGenerator, DefaultHtmlGenerator>();
|
||||||
services.TryAddSingleton<ExpressionTextCache>();
|
services.TryAddSingleton<ExpressionTextCache>();
|
||||||
services.TryAddSingleton<IModelExpressionProvider, ModelExpressionProvider>();
|
services.TryAddSingleton<IModelExpressionProvider, ModelExpressionProvider>();
|
||||||
|
services.TryAddSingleton<ValidationHtmlAttributeProvider, DefaultValidationHtmlAttributeProvider>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// JSON Helper
|
// JSON Helper
|
||||||
|
|
@ -132,7 +132,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
//
|
//
|
||||||
// View Components
|
// View Components
|
||||||
//
|
//
|
||||||
|
|
||||||
// These do caching so they should stay singleton
|
// These do caching so they should stay singleton
|
||||||
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
|
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
|
||||||
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
|
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
|
||||||
|
|
|
||||||
|
|
@ -33,30 +33,68 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
new[] { "text", "search", "url", "tel", "email", "password", "number" };
|
new[] { "text", "search", "url", "tel", "email", "password", "number" };
|
||||||
|
|
||||||
private readonly IAntiforgery _antiforgery;
|
private readonly IAntiforgery _antiforgery;
|
||||||
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;
|
|
||||||
private readonly IModelMetadataProvider _metadataProvider;
|
private readonly IModelMetadataProvider _metadataProvider;
|
||||||
private readonly IUrlHelperFactory _urlHelperFactory;
|
private readonly IUrlHelperFactory _urlHelperFactory;
|
||||||
private readonly HtmlEncoder _htmlEncoder;
|
private readonly HtmlEncoder _htmlEncoder;
|
||||||
private readonly ClientValidatorCache _clientValidatorCache;
|
private readonly ValidationHtmlAttributeProvider _validationAttributeProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
/// Initializes a new instance of the <see cref="DefaultHtmlGenerator"/> class.
|
/// Initializes a new instance of the <see cref="DefaultHtmlGenerator"/> class.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This constructor is obsolete and will be removed in a future version. The recommended alternative is to
|
||||||
|
/// use <see cref="DefaultHtmlGenerator(IAntiforgery, IOptions{MvcViewOptions}, IModelMetadataProvider,
|
||||||
|
/// IUrlHelperFactory, HtmlEncoder, ClientValidatorCache, ValidationHtmlAttributeProvider)"/>.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="antiforgery">The <see cref="IAntiforgery"/> instance which is used to generate antiforgery
|
/// <param name="antiforgery">The <see cref="IAntiforgery"/> instance which is used to generate antiforgery
|
||||||
/// tokens.</param>
|
/// tokens.</param>
|
||||||
/// <param name="optionsAccessor">The accessor for <see cref="MvcOptions"/>.</param>
|
/// <param name="optionsAccessor">The accessor for <see cref="MvcViewOptions"/>.</param>
|
||||||
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
||||||
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
|
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
|
||||||
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
|
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
|
||||||
/// <param name="clientValidatorCache">The <see cref="ClientValidatorCache"/> that provides
|
/// <param name="clientValidatorCache">The <see cref="ClientValidatorCache"/> that provides
|
||||||
/// a list of <see cref="IClientModelValidator"/>s.</param>
|
/// a list of <see cref="IClientModelValidator"/>s.</param>
|
||||||
|
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended " +
|
||||||
|
"alternative is to use the other public constructor.")]
|
||||||
public DefaultHtmlGenerator(
|
public DefaultHtmlGenerator(
|
||||||
IAntiforgery antiforgery,
|
IAntiforgery antiforgery,
|
||||||
IOptions<MvcViewOptions> optionsAccessor,
|
IOptions<MvcViewOptions> optionsAccessor,
|
||||||
IModelMetadataProvider metadataProvider,
|
IModelMetadataProvider metadataProvider,
|
||||||
IUrlHelperFactory urlHelperFactory,
|
IUrlHelperFactory urlHelperFactory,
|
||||||
HtmlEncoder htmlEncoder,
|
HtmlEncoder htmlEncoder,
|
||||||
ClientValidatorCache clientValidatorCache)
|
ClientValidatorCache clientValidatorCache) : this(
|
||||||
|
antiforgery,
|
||||||
|
optionsAccessor,
|
||||||
|
metadataProvider,
|
||||||
|
urlHelperFactory,
|
||||||
|
htmlEncoder,
|
||||||
|
clientValidatorCache,
|
||||||
|
new DefaultValidationHtmlAttributeProvider(optionsAccessor, metadataProvider, clientValidatorCache))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DefaultHtmlGenerator"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="antiforgery">The <see cref="IAntiforgery"/> instance which is used to generate antiforgery
|
||||||
|
/// tokens.</param>
|
||||||
|
/// <param name="optionsAccessor">The accessor for <see cref="MvcViewOptions"/>.</param>
|
||||||
|
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
||||||
|
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
|
||||||
|
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
|
||||||
|
/// <param name="clientValidatorCache">The <see cref="ClientValidatorCache"/> that provides
|
||||||
|
/// a list of <see cref="IClientModelValidator"/>s.</param>
|
||||||
|
/// <param name="validationAttributeProvider">The <see cref="ValidationHtmlAttributeProvider"/>.</param>
|
||||||
|
public DefaultHtmlGenerator(
|
||||||
|
IAntiforgery antiforgery,
|
||||||
|
IOptions<MvcViewOptions> optionsAccessor,
|
||||||
|
IModelMetadataProvider metadataProvider,
|
||||||
|
IUrlHelperFactory urlHelperFactory,
|
||||||
|
HtmlEncoder htmlEncoder,
|
||||||
|
ClientValidatorCache clientValidatorCache,
|
||||||
|
ValidationHtmlAttributeProvider validationAttributeProvider)
|
||||||
{
|
{
|
||||||
if (antiforgery == null)
|
if (antiforgery == null)
|
||||||
{
|
{
|
||||||
|
|
@ -88,13 +126,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
throw new ArgumentNullException(nameof(clientValidatorCache));
|
throw new ArgumentNullException(nameof(clientValidatorCache));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (validationAttributeProvider == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(validationAttributeProvider));
|
||||||
|
}
|
||||||
|
|
||||||
_antiforgery = antiforgery;
|
_antiforgery = antiforgery;
|
||||||
var clientValidatorProviders = optionsAccessor.Value.ClientModelValidatorProviders;
|
|
||||||
_clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders);
|
|
||||||
_metadataProvider = metadataProvider;
|
_metadataProvider = metadataProvider;
|
||||||
_urlHelperFactory = urlHelperFactory;
|
_urlHelperFactory = urlHelperFactory;
|
||||||
_htmlEncoder = htmlEncoder;
|
_htmlEncoder = htmlEncoder;
|
||||||
_clientValidatorCache = clientValidatorCache;
|
_validationAttributeProvider = validationAttributeProvider;
|
||||||
|
|
||||||
// Underscores are fine characters in id's.
|
// Underscores are fine characters in id's.
|
||||||
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
|
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
|
||||||
|
|
@ -1313,7 +1354,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
ModelExplorer modelExplorer,
|
ModelExplorer modelExplorer,
|
||||||
string expression)
|
string expression)
|
||||||
{
|
{
|
||||||
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(expression, viewData, _metadataProvider);
|
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(
|
||||||
|
expression,
|
||||||
|
viewData,
|
||||||
|
_metadataProvider);
|
||||||
|
|
||||||
var placeholder = modelExplorer.Metadata.Placeholder;
|
var placeholder = modelExplorer.Metadata.Placeholder;
|
||||||
if (!string.IsNullOrEmpty(placeholder))
|
if (!string.IsNullOrEmpty(placeholder))
|
||||||
{
|
{
|
||||||
|
|
@ -1335,41 +1380,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
ModelExplorer modelExplorer,
|
ModelExplorer modelExplorer,
|
||||||
string expression)
|
string expression)
|
||||||
{
|
{
|
||||||
// Only render attributes if client-side validation is enabled, and then only if we've
|
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(
|
||||||
// never rendered validation for a field with this name in this form.
|
expression,
|
||||||
var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null;
|
viewContext.ViewData,
|
||||||
if (formContext == null)
|
_metadataProvider);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fullName = GetFullHtmlFieldName(viewContext, expression);
|
_validationAttributeProvider.AddAndTrackValidationAttributes(
|
||||||
if (formContext.RenderedField(fullName))
|
viewContext,
|
||||||
{
|
modelExplorer,
|
||||||
return;
|
expression,
|
||||||
}
|
tagBuilder.Attributes);
|
||||||
|
|
||||||
formContext.RenderedField(fullName, true);
|
|
||||||
|
|
||||||
modelExplorer = modelExplorer ??
|
|
||||||
ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
|
|
||||||
|
|
||||||
|
|
||||||
var validators = _clientValidatorCache.GetValidators(modelExplorer.Metadata, _clientModelValidatorProvider);
|
|
||||||
if (validators.Count > 0)
|
|
||||||
{
|
|
||||||
var validationContext = new ClientModelValidationContext(
|
|
||||||
viewContext,
|
|
||||||
modelExplorer.Metadata,
|
|
||||||
_metadataProvider,
|
|
||||||
tagBuilder.Attributes);
|
|
||||||
|
|
||||||
for (var i = 0; i < validators.Count; i++)
|
|
||||||
{
|
|
||||||
var validator = validators[i];
|
|
||||||
validator.AddValidation(validationContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Enum ConvertEnumFromInteger(object value, Type targetType)
|
private static Enum ConvertEnumFromInteger(object value, Type targetType)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="ValidationHtmlAttributeProvider"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultValidationHtmlAttributeProvider : ValidationHtmlAttributeProvider
|
||||||
|
{
|
||||||
|
private readonly IModelMetadataProvider _metadataProvider;
|
||||||
|
private readonly ClientValidatorCache _clientValidatorCache;
|
||||||
|
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="DefaultValidationHtmlAttributeProvider"/> instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="optionsAccessor">The accessor for <see cref="MvcViewOptions"/>.</param>
|
||||||
|
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
||||||
|
/// <param name="clientValidatorCache">The <see cref="ClientValidatorCache"/> that provides
|
||||||
|
/// a list of <see cref="IClientModelValidator"/>s.</param>
|
||||||
|
public DefaultValidationHtmlAttributeProvider(
|
||||||
|
IOptions<MvcViewOptions> optionsAccessor,
|
||||||
|
IModelMetadataProvider metadataProvider,
|
||||||
|
ClientValidatorCache clientValidatorCache)
|
||||||
|
{
|
||||||
|
if (optionsAccessor == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(optionsAccessor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadataProvider == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(metadataProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientValidatorCache == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(clientValidatorCache));
|
||||||
|
}
|
||||||
|
|
||||||
|
_clientValidatorCache = clientValidatorCache;
|
||||||
|
_metadataProvider = metadataProvider;
|
||||||
|
|
||||||
|
var clientValidatorProviders = optionsAccessor.Value.ClientModelValidatorProviders;
|
||||||
|
_clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void AddValidationAttributes(
|
||||||
|
ViewContext viewContext,
|
||||||
|
ModelExplorer modelExplorer,
|
||||||
|
IDictionary<string, string> attributes)
|
||||||
|
{
|
||||||
|
if (viewContext == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(viewContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelExplorer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(modelExplorer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null;
|
||||||
|
if (formContext == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validators = _clientValidatorCache.GetValidators(
|
||||||
|
modelExplorer.Metadata,
|
||||||
|
_clientModelValidatorProvider);
|
||||||
|
if (validators.Count > 0)
|
||||||
|
{
|
||||||
|
var validationContext = new ClientModelValidationContext(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer.Metadata,
|
||||||
|
_metadataProvider,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
for (var i = 0; i < validators.Count; i++)
|
||||||
|
{
|
||||||
|
var validator = validators[i];
|
||||||
|
validator.AddValidation(validationContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contract for a service providing validation attributes for expressions.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ValidationHtmlAttributeProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds validation-related HTML attributes to the <paramref name="attributes" /> if client validation is
|
||||||
|
/// enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
|
||||||
|
/// <param name="modelExplorer">The <see cref="ModelExplorer"/> for an expression.</param>
|
||||||
|
/// <param name="attributes">
|
||||||
|
/// The <see cref="Dictionary{TKey, TValue}"/> to receive the validation attributes. Maps the validation
|
||||||
|
/// attribute names to their <see cref="string"/> values. Values must be HTML encoded before they are written
|
||||||
|
/// to an HTML document or response.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Adds nothing to <paramref name="attributes"/> if client-side validation is disabled.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract void AddValidationAttributes(
|
||||||
|
ViewContext viewContext,
|
||||||
|
ModelExplorer modelExplorer,
|
||||||
|
IDictionary<string, string> attributes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds validation-related HTML attributes to the <paramref name="attributes" /> if client validation is
|
||||||
|
/// enabled and validation attributes have not yet been added for this <paramref name="expression"/> in the
|
||||||
|
/// current <form>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</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>
|
||||||
|
/// <param name="attributes">
|
||||||
|
/// The <see cref="Dictionary{TKey, TValue}"/> to receive the validation attributes. Maps the validation
|
||||||
|
/// attribute names to their <see cref="string"/> values. Values must be HTML encoded before they are written
|
||||||
|
/// to an HTML document or response.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Tracks the <paramref name="expression"/> in the current <see cref="FormContext"/> to avoid generating
|
||||||
|
/// duplicate validation attributes. That is, validation attributes are added only if no previous call has
|
||||||
|
/// added them for a field with this name in the <form>.
|
||||||
|
/// </remarks>
|
||||||
|
public virtual void AddAndTrackValidationAttributes(
|
||||||
|
ViewContext viewContext,
|
||||||
|
ModelExplorer modelExplorer,
|
||||||
|
string expression,
|
||||||
|
IDictionary<string, string> attributes)
|
||||||
|
{
|
||||||
|
if (viewContext == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(viewContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelExplorer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(modelExplorer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't track fields when client-side validation is disabled.
|
||||||
|
var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null;
|
||||||
|
if (formContext == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fullName = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression);
|
||||||
|
if (formContext.RenderedField(fullName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formContext.RenderedField(fullName, true);
|
||||||
|
|
||||||
|
AddValidationAttributes(viewContext, modelExplorer, attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,7 +50,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
metadataProvider,
|
metadataProvider,
|
||||||
CreateUrlHelperFactory(urlHelper),
|
CreateUrlHelperFactory(urlHelper),
|
||||||
new HtmlTestEncoder(),
|
new HtmlTestEncoder(),
|
||||||
new ClientValidatorCache())
|
new ClientValidatorCache(),
|
||||||
|
new DefaultValidationHtmlAttributeProvider(options, metadataProvider, new ClientValidatorCache()))
|
||||||
{
|
{
|
||||||
_validationAttributes = validationAttributes;
|
_validationAttributes = validationAttributes;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -253,13 +253,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
|
|
||||||
if (htmlGenerator == null)
|
if (htmlGenerator == null)
|
||||||
{
|
{
|
||||||
|
var attributeProvider = new DefaultValidationHtmlAttributeProvider(
|
||||||
|
optionsAccessor.Object,
|
||||||
|
provider,
|
||||||
|
new ClientValidatorCache());
|
||||||
htmlGenerator = new DefaultHtmlGenerator(
|
htmlGenerator = new DefaultHtmlGenerator(
|
||||||
Mock.Of<IAntiforgery>(),
|
Mock.Of<IAntiforgery>(),
|
||||||
optionsAccessor.Object,
|
optionsAccessor.Object,
|
||||||
provider,
|
provider,
|
||||||
urlHelperFactory.Object,
|
urlHelperFactory.Object,
|
||||||
new HtmlTestEncoder(),
|
new HtmlTestEncoder(),
|
||||||
new ClientValidatorCache());
|
new ClientValidatorCache(),
|
||||||
|
attributeProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateRenderer will Contextualize this transient service.
|
// TemplateRenderer will Contextualize this transient service.
|
||||||
|
|
|
||||||
|
|
@ -681,6 +681,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
{
|
{
|
||||||
var mvcViewOptionsAccessor = new Mock<IOptions<MvcViewOptions>>();
|
var mvcViewOptionsAccessor = new Mock<IOptions<MvcViewOptions>>();
|
||||||
mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(new MvcViewOptions());
|
mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(new MvcViewOptions());
|
||||||
|
|
||||||
var htmlEncoder = Mock.Of<HtmlEncoder>();
|
var htmlEncoder = Mock.Of<HtmlEncoder>();
|
||||||
var antiforgery = new Mock<IAntiforgery>();
|
var antiforgery = new Mock<IAntiforgery>();
|
||||||
antiforgery
|
antiforgery
|
||||||
|
|
@ -690,10 +691,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
return new AntiforgeryTokenSet("requestToken", "cookieToken", "formFieldName", "headerName");
|
return new AntiforgeryTokenSet("requestToken", "cookieToken", "formFieldName", "headerName");
|
||||||
});
|
});
|
||||||
|
|
||||||
var optionsAccessor = new Mock<IOptions<MvcOptions>>();
|
var attributeProvider = new DefaultValidationHtmlAttributeProvider(
|
||||||
optionsAccessor
|
mvcViewOptionsAccessor.Object,
|
||||||
.SetupGet(o => o.Value)
|
metadataProvider,
|
||||||
.Returns(new MvcOptions());
|
new ClientValidatorCache());
|
||||||
|
|
||||||
return new DefaultHtmlGenerator(
|
return new DefaultHtmlGenerator(
|
||||||
antiforgery.Object,
|
antiforgery.Object,
|
||||||
|
|
@ -701,7 +702,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
metadataProvider,
|
metadataProvider,
|
||||||
new UrlHelperFactory(),
|
new UrlHelperFactory(),
|
||||||
htmlEncoder,
|
htmlEncoder,
|
||||||
new ClientValidatorCache());
|
new ClientValidatorCache(),
|
||||||
|
attributeProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentValues uses only the ModelStateDictionary and ViewDataDictionary from the passed ViewContext.
|
// GetCurrentValues uses only the ModelStateDictionary and ViewDataDictionary from the passed ViewContext.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
|
{
|
||||||
|
public class DefaultValidationHtmlAttributeProviderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture]
|
||||||
|
public void AddValidationAttributes_AddsAttributes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = $"The field {nameof(Model.HasValidatorsProperty)} must be a number.";
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var attributeProvider = GetAttributeProvider(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var modelExplorer = metadataProvider
|
||||||
|
.GetModelExplorerForType(typeof(Model), model: null)
|
||||||
|
.GetExplorerForProperty(nameof(Model.HasValidatorsProperty));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
attributeProvider.AddValidationAttributes(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
attributes,
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("data-val", kvp.Key);
|
||||||
|
Assert.Equal("true", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("data-val-number", kvp.Key);
|
||||||
|
Assert.Equal(expectedMessage, kvp.Value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture]
|
||||||
|
public void AddAndTrackValidationAttributes_AddsAttributes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = $"The field {nameof(Model.HasValidatorsProperty)} must be a number.";
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var attributeProvider = GetAttributeProvider(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var modelExplorer = metadataProvider
|
||||||
|
.GetModelExplorerForType(typeof(Model), model: null)
|
||||||
|
.GetExplorerForProperty(nameof(Model.HasValidatorsProperty));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
attributeProvider.AddAndTrackValidationAttributes(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
nameof(Model.HasValidatorsProperty),
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
attributes,
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("data-val", kvp.Key);
|
||||||
|
Assert.Equal("true", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("data-val-number", kvp.Key);
|
||||||
|
Assert.Equal(expectedMessage, kvp.Value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddValidationAttributes_AddsNothing_IfClientSideValidationDisabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var attributeProvider = GetAttributeProvider(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
viewContext.ClientValidationEnabled = false;
|
||||||
|
|
||||||
|
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var modelExplorer = metadataProvider
|
||||||
|
.GetModelExplorerForType(typeof(Model), model: null)
|
||||||
|
.GetExplorerForProperty(nameof(Model.HasValidatorsProperty));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
attributeProvider.AddValidationAttributes(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddAndTrackValidationAttributes_DoesNotCallAddMethod_IfClientSideValidationDisabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
viewContext.ClientValidationEnabled = false;
|
||||||
|
|
||||||
|
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var modelExplorer = metadataProvider
|
||||||
|
.GetModelExplorerForType(typeof(Model), model: null)
|
||||||
|
.GetExplorerForProperty(nameof(Model.HasValidatorsProperty));
|
||||||
|
|
||||||
|
var attributeProviderMock = new Mock<ValidationHtmlAttributeProvider>() { CallBase = true };
|
||||||
|
attributeProviderMock
|
||||||
|
.Setup(p => p.AddValidationAttributes(
|
||||||
|
It.IsAny<ViewContext>(),
|
||||||
|
It.IsAny<ModelExplorer>(),
|
||||||
|
It.IsAny<IDictionary<string, string>>()))
|
||||||
|
.Verifiable();
|
||||||
|
var attributeProvider = attributeProviderMock.Object;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
attributeProvider.AddAndTrackValidationAttributes(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
nameof(Model.HasValidatorsProperty),
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(attributes);
|
||||||
|
attributeProviderMock.Verify(
|
||||||
|
p => p.AddValidationAttributes(
|
||||||
|
It.IsAny<ViewContext>(),
|
||||||
|
It.IsAny<ModelExplorer>(),
|
||||||
|
It.IsAny<IDictionary<string, string>>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddValidationAttributes_AddsAttributes_EvenIfPropertyAlreadyRendered()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = $"The field {nameof(Model.HasValidatorsProperty)} must be a number.";
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var attributeProvider = GetAttributeProvider(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
viewContext.FormContext.RenderedField(nameof(Model.HasValidatorsProperty), value: true);
|
||||||
|
|
||||||
|
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var modelExplorer = metadataProvider
|
||||||
|
.GetModelExplorerForType(typeof(Model), model: null)
|
||||||
|
.GetExplorerForProperty(nameof(Model.HasValidatorsProperty));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
attributeProvider.AddValidationAttributes(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
attributes,
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("data-val", kvp.Key);
|
||||||
|
Assert.Equal("true", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("data-val-number", kvp.Key);
|
||||||
|
Assert.Equal(expectedMessage, kvp.Value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddAndTrackValidationAttributes_DoesNotCallAddMethod_IfPropertyAlreadyRendered()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
viewContext.FormContext.RenderedField(nameof(Model.HasValidatorsProperty), value: true);
|
||||||
|
|
||||||
|
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var modelExplorer = metadataProvider
|
||||||
|
.GetModelExplorerForType(typeof(Model), model: null)
|
||||||
|
.GetExplorerForProperty(nameof(Model.HasValidatorsProperty));
|
||||||
|
|
||||||
|
var attributeProviderMock = new Mock<ValidationHtmlAttributeProvider>() { CallBase = true };
|
||||||
|
attributeProviderMock
|
||||||
|
.Setup(p => p.AddValidationAttributes(
|
||||||
|
It.IsAny<ViewContext>(),
|
||||||
|
It.IsAny<ModelExplorer>(),
|
||||||
|
It.IsAny<IDictionary<string, string>>()))
|
||||||
|
.Verifiable();
|
||||||
|
var attributeProvider = attributeProviderMock.Object;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
attributeProvider.AddAndTrackValidationAttributes(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
nameof(Model.HasValidatorsProperty),
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(attributes);
|
||||||
|
attributeProviderMock.Verify(
|
||||||
|
p => p.AddValidationAttributes(
|
||||||
|
It.IsAny<ViewContext>(),
|
||||||
|
It.IsAny<ModelExplorer>(),
|
||||||
|
It.IsAny<IDictionary<string, string>>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddValidationAttributes_AddsNothing_IfPropertyHasNoValidators()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var attributeProvider = GetAttributeProvider(metadataProvider);
|
||||||
|
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
|
||||||
|
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var modelExplorer = metadataProvider
|
||||||
|
.GetModelExplorerForType(typeof(Model), model: null)
|
||||||
|
.GetExplorerForProperty(nameof(Model.Property));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
attributeProvider.AddValidationAttributes(
|
||||||
|
viewContext,
|
||||||
|
modelExplorer,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewContext GetViewContext<TModel>(TModel model, IModelMetadataProvider metadataProvider)
|
||||||
|
{
|
||||||
|
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||||
|
var viewData = new ViewDataDictionary<TModel>(metadataProvider, actionContext.ModelState)
|
||||||
|
{
|
||||||
|
Model = model,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ViewContext(
|
||||||
|
actionContext,
|
||||||
|
Mock.Of<IView>(),
|
||||||
|
viewData,
|
||||||
|
Mock.Of<ITempDataDictionary>(),
|
||||||
|
TextWriter.Null,
|
||||||
|
new HtmlHelperOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationHtmlAttributeProvider GetAttributeProvider(IModelMetadataProvider metadataProvider)
|
||||||
|
{
|
||||||
|
// Add validation properties for float, double and decimal properties. Ignore everything else.
|
||||||
|
var mvcViewOptions = new MvcViewOptions();
|
||||||
|
mvcViewOptions.ClientModelValidatorProviders.Add(new NumericClientModelValidatorProvider());
|
||||||
|
|
||||||
|
var mvcViewOptionsAccessor = new Mock<IOptions<MvcViewOptions>>();
|
||||||
|
mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(mvcViewOptions);
|
||||||
|
|
||||||
|
return new DefaultValidationHtmlAttributeProvider(
|
||||||
|
mvcViewOptionsAccessor.Object,
|
||||||
|
metadataProvider,
|
||||||
|
new ClientValidatorCache());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Model
|
||||||
|
{
|
||||||
|
public double HasValidatorsProperty { get; set; }
|
||||||
|
|
||||||
|
public string Property { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue