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 Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
|
@ -117,6 +116,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<IHtmlGenerator, DefaultHtmlGenerator>();
|
||||
services.TryAddSingleton<ExpressionTextCache>();
|
||||
services.TryAddSingleton<IModelExpressionProvider, ModelExpressionProvider>();
|
||||
services.TryAddSingleton<ValidationHtmlAttributeProvider, DefaultValidationHtmlAttributeProvider>();
|
||||
|
||||
//
|
||||
// JSON Helper
|
||||
|
|
@ -132,7 +132,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
// View Components
|
||||
//
|
||||
|
||||
|
||||
// These do caching so they should stay singleton
|
||||
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
|
||||
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
|
||||
|
|
|
|||
|
|
@ -33,30 +33,68 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
new[] { "text", "search", "url", "tel", "email", "password", "number" };
|
||||
|
||||
private readonly IAntiforgery _antiforgery;
|
||||
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;
|
||||
private readonly IModelMetadataProvider _metadataProvider;
|
||||
private readonly IUrlHelperFactory _urlHelperFactory;
|
||||
private readonly HtmlEncoder _htmlEncoder;
|
||||
private readonly ClientValidatorCache _clientValidatorCache;
|
||||
private readonly ValidationHtmlAttributeProvider _validationAttributeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// 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>
|
||||
/// <param name="antiforgery">The <see cref="IAntiforgery"/> instance which is used to generate antiforgery
|
||||
/// 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="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>
|
||||
[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(
|
||||
IAntiforgery antiforgery,
|
||||
IOptions<MvcViewOptions> optionsAccessor,
|
||||
IModelMetadataProvider metadataProvider,
|
||||
IUrlHelperFactory urlHelperFactory,
|
||||
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)
|
||||
{
|
||||
|
|
@ -88,13 +126,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
throw new ArgumentNullException(nameof(clientValidatorCache));
|
||||
}
|
||||
|
||||
if (validationAttributeProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(validationAttributeProvider));
|
||||
}
|
||||
|
||||
_antiforgery = antiforgery;
|
||||
var clientValidatorProviders = optionsAccessor.Value.ClientModelValidatorProviders;
|
||||
_clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders);
|
||||
_metadataProvider = metadataProvider;
|
||||
_urlHelperFactory = urlHelperFactory;
|
||||
_htmlEncoder = htmlEncoder;
|
||||
_clientValidatorCache = clientValidatorCache;
|
||||
_validationAttributeProvider = validationAttributeProvider;
|
||||
|
||||
// Underscores are fine characters in id's.
|
||||
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
|
||||
|
|
@ -1313,7 +1354,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
ModelExplorer modelExplorer,
|
||||
string expression)
|
||||
{
|
||||
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(expression, viewData, _metadataProvider);
|
||||
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(
|
||||
expression,
|
||||
viewData,
|
||||
_metadataProvider);
|
||||
|
||||
var placeholder = modelExplorer.Metadata.Placeholder;
|
||||
if (!string.IsNullOrEmpty(placeholder))
|
||||
{
|
||||
|
|
@ -1335,41 +1380,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
ModelExplorer modelExplorer,
|
||||
string expression)
|
||||
{
|
||||
// Only render attributes if client-side validation is enabled, and then only if we've
|
||||
// never rendered validation for a field with this name in this form.
|
||||
var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null;
|
||||
if (formContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(
|
||||
expression,
|
||||
viewContext.ViewData,
|
||||
_metadataProvider);
|
||||
|
||||
var fullName = GetFullHtmlFieldName(viewContext, expression);
|
||||
if (formContext.RenderedField(fullName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
_validationAttributeProvider.AddAndTrackValidationAttributes(
|
||||
viewContext,
|
||||
modelExplorer,
|
||||
expression,
|
||||
tagBuilder.Attributes);
|
||||
}
|
||||
|
||||
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,
|
||||
CreateUrlHelperFactory(urlHelper),
|
||||
new HtmlTestEncoder(),
|
||||
new ClientValidatorCache())
|
||||
new ClientValidatorCache(),
|
||||
new DefaultValidationHtmlAttributeProvider(options, metadataProvider, new ClientValidatorCache()))
|
||||
{
|
||||
_validationAttributes = validationAttributes;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,13 +253,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
|||
|
||||
if (htmlGenerator == null)
|
||||
{
|
||||
var attributeProvider = new DefaultValidationHtmlAttributeProvider(
|
||||
optionsAccessor.Object,
|
||||
provider,
|
||||
new ClientValidatorCache());
|
||||
htmlGenerator = new DefaultHtmlGenerator(
|
||||
Mock.Of<IAntiforgery>(),
|
||||
optionsAccessor.Object,
|
||||
provider,
|
||||
urlHelperFactory.Object,
|
||||
new HtmlTestEncoder(),
|
||||
new ClientValidatorCache());
|
||||
new ClientValidatorCache(),
|
||||
attributeProvider);
|
||||
}
|
||||
|
||||
// TemplateRenderer will Contextualize this transient service.
|
||||
|
|
|
|||
|
|
@ -681,6 +681,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
{
|
||||
var mvcViewOptionsAccessor = new Mock<IOptions<MvcViewOptions>>();
|
||||
mvcViewOptionsAccessor.SetupGet(accessor => accessor.Value).Returns(new MvcViewOptions());
|
||||
|
||||
var htmlEncoder = Mock.Of<HtmlEncoder>();
|
||||
var antiforgery = new Mock<IAntiforgery>();
|
||||
antiforgery
|
||||
|
|
@ -690,10 +691,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
return new AntiforgeryTokenSet("requestToken", "cookieToken", "formFieldName", "headerName");
|
||||
});
|
||||
|
||||
var optionsAccessor = new Mock<IOptions<MvcOptions>>();
|
||||
optionsAccessor
|
||||
.SetupGet(o => o.Value)
|
||||
.Returns(new MvcOptions());
|
||||
var attributeProvider = new DefaultValidationHtmlAttributeProvider(
|
||||
mvcViewOptionsAccessor.Object,
|
||||
metadataProvider,
|
||||
new ClientValidatorCache());
|
||||
|
||||
return new DefaultHtmlGenerator(
|
||||
antiforgery.Object,
|
||||
|
|
@ -701,7 +702,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
metadataProvider,
|
||||
new UrlHelperFactory(),
|
||||
htmlEncoder,
|
||||
new ClientValidatorCache());
|
||||
new ClientValidatorCache(),
|
||||
attributeProvider);
|
||||
}
|
||||
|
||||
// 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