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:
Doug Bunting 2016-09-02 16:11:59 -07:00
parent 4a5e1f4a72
commit 809d2bf7ec
8 changed files with 567 additions and 51 deletions

View File

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

View File

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

View File

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

View File

@ -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 &lt;form&gt;.
/// </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 &lt;form&gt;.
/// </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);
}
}
}

View File

@ -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;
}

View File

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

View File

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

View File

@ -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; }
}
}
}