Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Add IValidationAttributeProvider to make `DefaultHtmlGenerator.AddV…
Browse files Browse the repository at this point in the history
…alidationAttributes()` available

- #5028
- helpers similar to our HTML or tag helpers can use the new singleton to examine validation attributes
 - in the most common case, helpers add validation attributes to a `TagBuilder` but that is not required
- separating the `ValidationAttributesProvider` from `DefaultHtmlGenerator` avoids creating two instances of that singleton
 - would be even uglier to require callers to cast an `IHtmlGenerator` to `IValidationAttributeProvider`
  • Loading branch information
dougbu committed Sep 2, 2016
1 parent fae0e9a commit 7eb39c5
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
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;
Expand Down Expand Up @@ -117,6 +116,7 @@ internal static void AddViewServices(IServiceCollection services)
services.TryAddSingleton<IHtmlGenerator, DefaultHtmlGenerator>();
services.TryAddSingleton<ExpressionTextCache>();
services.TryAddSingleton<IModelExpressionProvider, ModelExpressionProvider>();
services.TryAddSingleton<IValidationAttributeProvider, ValidationAttributeProvider>();

//
// JSON Helper
Expand All @@ -132,7 +132,7 @@ internal static void AddViewServices(IServiceCollection services)
//
// View Components
//

// These do caching so they should stay singleton
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,17 @@ public class DefaultHtmlGenerator : IHtmlGenerator
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 IValidationAttributeProvider _validationAttributeProvider;

/// <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="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>
Expand All @@ -56,7 +55,57 @@ public DefaultHtmlGenerator(
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache)
ClientValidatorCache clientValidatorCache) : this(
antiforgery,
optionsAccessor,
metadataProvider,
urlHelperFactory,
htmlEncoder,
clientValidatorCache,
() => new ValidationAttributeProvider(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="IValidationAttributeProvider"/>.</param>
public DefaultHtmlGenerator(
IAntiforgery antiforgery,
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache,
IValidationAttributeProvider validationAttributeProvider) : this(
antiforgery,
optionsAccessor,
metadataProvider,
urlHelperFactory,
htmlEncoder,
clientValidatorCache,
() => validationAttributeProvider)
{
}

// All parameter names must match the public constructor just above.
// Note validationAttributeProvider is not evaluated until after all arguments have been null-checked.
private DefaultHtmlGenerator(
IAntiforgery antiforgery,
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache,
Func<IValidationAttributeProvider> validationAttributeProvider)
{
if (antiforgery == null)
{
Expand Down Expand Up @@ -88,13 +137,16 @@ public DefaultHtmlGenerator(
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;
Expand Down Expand Up @@ -1324,41 +1376,11 @@ protected virtual void AddValidationAttributes(
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;
}

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.AddValidationAttributes(
viewContext,
modelExplorer,
expression,
tagBuilder.Attributes);
}

private static Enum ConvertEnumFromInteger(object value, Type targetType)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
/// <summary>
/// Contract for a service providing validation attributes for expressions.
/// </summary>
public interface IValidationAttributeProvider
{
/// <summary>
/// Adds validation 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 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>
/// Adds nothing to <paramref name="attributes"/> if client-side validation is disabled or if attributes have
/// already been generated for the <paramref name="expression"/> in the current &lt;form&gt;.
/// </remarks>
void AddValidationAttributes(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
IDictionary<string, string> attributes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
/// <summary>
/// Default implementation of <see cref="IValidationAttributeProvider"/>.
/// </summary>
public class ValidationAttributeProvider : IValidationAttributeProvider
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly ClientValidatorCache _clientValidatorCache;
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;

/// <summary>
/// Initializes a new <see cref="ValidationAttributeProvider"/> 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 ValidationAttributeProvider(
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 virtual void AddValidationAttributes(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
IDictionary<string, string> attributes)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}

if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}

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

var fullName = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(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,
attributes);

for (var i = 0; i < validators.Count; i++)
{
var validator = validators[i];
validator.AddValidation(validationContext);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvid
{
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
Expand All @@ -690,11 +691,6 @@ private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvid
return new AntiforgeryTokenSet("requestToken", "cookieToken", "formFieldName", "headerName");
});

var optionsAccessor = new Mock<IOptions<MvcOptions>>();
optionsAccessor
.SetupGet(o => o.Value)
.Returns(new MvcOptions());

return new DefaultHtmlGenerator(
antiforgery.Object,
mvcViewOptionsAccessor.Object,
Expand Down
Loading

0 comments on commit 7eb39c5

Please sign in to comment.