Introducing IClientModelValidator to support client validation

* Adding support for validator adapters in DataAnnotationsModelValidatorProvider
* Adding Regex and DataType validators
This commit is contained in:
Pranav K 2014-04-12 08:46:02 -07:00
parent f34ed467e4
commit b9b652084a
13 changed files with 222 additions and 28 deletions

View File

@ -70,21 +70,27 @@
<Compile Include="Properties\Resources.Designer.cs" />
<Compile Include="RequestContext.cs" />
<Compile Include="Validation\AssociatedValidatorProvider.cs" />
<Compile Include="Validation\ClientModelValidationContext.cs" />
<Compile Include="Validation\CompositeModelValidator.cs" />
<Compile Include="Validation\DataAnnotationsModelValidator.cs" />
<Compile Include="Validation\DataAnnotationsModelValidatorOfTAttribute.cs" />
<Compile Include="Validation\DataAnnotationsModelValidatorProvider.cs" />
<Compile Include="Validation\DataMemberModelValidatorProvider.cs" />
<Compile Include="Validation\DataTypeAttributeAdapter.cs" />
<Compile Include="Validation\ErrorModelValidator.cs" />
<Compile Include="Validation\FieldValidationMetadata.cs" />
<Compile Include="Validation\IClientModelValidator.cs" />
<Compile Include="Validation\IModelValidator.cs" />
<Compile Include="Validation\IModelValidatorProvider.cs" />
<Compile Include="Validation\InvalidModelValidatorProvider.cs" />
<Compile Include="Validation\ModelClientValidationRegexRule.cs" />
<Compile Include="Validation\ModelClientValidationRule.cs" />
<Compile Include="Validation\ModelValidatedEventArgs.cs" />
<Compile Include="Validation\ModelValidatingEventArgs.cs" />
<Compile Include="Validation\ModelValidationContext.cs" />
<Compile Include="Validation\ModelValidationNode.cs" />
<Compile Include="Validation\ModelValidationResult.cs" />
<Compile Include="Validation\RegularExpressionAttributeAdapter.cs" />
<Compile Include="Validation\RequiredMemberModelValidator.cs" />
<Compile Include="Validation\ValidatableObjectAdapter.cs" />
<Compile Include="ValueProviders\CompositeValueProvider.cs" />

View File

@ -42,6 +42,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyNotFound"), p0, p1);
}
/// <summary>
/// The type '{0}' must have a public constructor which accepts a single parameter of type '{1}'.
/// </summary>
internal static string DataAnnotationsModelValidatorProvider_ConstructorRequirements
{
get { return GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements"); }
}
/// <summary>
/// The type '{0}' must have a public constructor which accepts a single parameter of type '{1}'.
/// </summary>
internal static string FormatDataAnnotationsModelValidatorProvider_ConstructorRequirements(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements"), p0, p1);
}
/// <summary>
/// The key is invalid JQuery syntax because it is missing a closing bracket.
/// </summary>
@ -218,6 +234,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("ModelBindingContext_ModelMetadataMustBeSet");
}
/// <summary>
/// The type '{0}' must derive from '{1}'.
/// </summary>
internal static string TypeMustDeriveFromType
{
get { return GetString("TypeMustDeriveFromType"); }
}
/// <summary>
/// The type '{0}' must derive from '{1}'.
/// </summary>
internal static string FormatTypeMustDeriveFromType(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TypeMustDeriveFromType"), p0, p1);
}
/// <summary>
/// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.
/// </summary>

View File

@ -123,6 +123,9 @@
<data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value>
</data>
<data name="DataAnnotationsModelValidatorProvider_ConstructorRequirements" xml:space="preserve">
<value>The type '{0}' must have a public constructor which accepts a single parameter of type '{1}'.</value>
</data>
<data name="JQuerySyntaxMissingClosingBracket" xml:space="preserve">
<value>The key is invalid JQuery syntax because it is missing a closing bracket.</value>
</data>
@ -156,6 +159,9 @@
<data name="ModelBindingContext_ModelMetadataMustBeSet" xml:space="preserve">
<value>The ModelMetadata property must be set before accessing this property.</value>
</data>
<data name="TypeMustDeriveFromType" xml:space="preserve">
<value>The type '{0}' must derive from '{1}'.</value>
</data>
<data name="ValidatableObjectAdapter_IncompatibleType" xml:space="preserve">
<value>The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'.</value>
</data>

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ClientModelValidationContext
{
public ClientModelValidationContext([NotNull] ModelMetadata metadata)
{
ModelMetadata = metadata;
}
public ModelMetadata ModelMetadata { get; private set; }
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class DataAnnotationsModelValidator : IModelValidator
public class DataAnnotationsModelValidator : IModelValidator, IClientModelValidator
{
public DataAnnotationsModelValidator([NotNull] ValidationAttribute attribute)
{
@ -53,5 +53,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return Enumerable.Empty<ModelValidationResult>();
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(
[NotNull] ClientModelValidationContext context)
{
return Enumerable.Empty<ModelClientValidationRule>();
}
protected virtual string GetErrorMessage([NotNull] ModelMetadata modelMetadata)
{
return Attribute.FormatErrorMessage(modelMetadata.GetDisplayName());
}
}
}

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class DataAnnotationsModelValidator<TAttribute> : DataAnnotationsModelValidator
where TAttribute : ValidationAttribute
{
public DataAnnotationsModelValidator(TAttribute attribute)
: base(attribute)
{
}
protected new TAttribute Attribute
{
get { return (TAttribute)base.Attribute; }
}
}
}

View File

@ -18,29 +18,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public class DataAnnotationsModelValidatorProvider : AssociatedValidatorProvider
{
// A factory for validators based on ValidationAttribute
// A factory for validators based on ValidationAttribute.
private delegate IModelValidator DataAnnotationsModelValidationFactory(ValidationAttribute attribute);
// A factory for validators based on IValidatableObject
private delegate IModelValidator DataAnnotationsValidatableObjectAdapterFactory();
private static bool _addImplicitRequiredAttributeForValueTypes = true;
private readonly Dictionary<Type, DataAnnotationsModelValidationFactory> _attributeFactories =
BuildAttributeFactoriesDictionary();
// Factories for validation attributes
private static DataAnnotationsModelValidationFactory DefaultAttributeFactory =
private static readonly DataAnnotationsModelValidationFactory _defaultAttributeFactory =
(attribute) => new DataAnnotationsModelValidator(attribute);
private static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories =
new Dictionary<Type, DataAnnotationsModelValidationFactory>();
// Factories for IValidatableObject models
private static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactory =
private static readonly DataAnnotationsValidatableObjectAdapterFactory _defaultValidatableFactory =
() => new ValidatableObjectAdapter();
private static Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory> ValidatableFactories =
new Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory>();
public static bool AddImplicitRequiredAttributeForValueTypes
private static bool AddImplicitRequiredAttributeForValueTypes
{
get { return _addImplicitRequiredAttributeForValueTypes; }
set { _addImplicitRequiredAttributeForValueTypes = value; }
@ -54,9 +50,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
foreach (var attribute in attributes.OfType<ValidationAttribute>())
{
DataAnnotationsModelValidationFactory factory;
if (!AttributeFactories.TryGetValue(attribute.GetType(), out factory))
if (!_attributeFactories.TryGetValue(attribute.GetType(), out factory))
{
factory = DefaultAttributeFactory;
factory = _defaultAttributeFactory;
}
results.Add(factory(attribute));
}
@ -64,15 +60,41 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Produce a validator if the type supports IValidatableObject
if (typeof(IValidatableObject).IsAssignableFrom(metadata.ModelType))
{
DataAnnotationsValidatableObjectAdapterFactory factory;
if (!ValidatableFactories.TryGetValue(metadata.ModelType, out factory))
{
factory = DefaultValidatableFactory;
}
results.Add(factory());
results.Add(_defaultValidatableFactory());
}
return results;
}
private static Dictionary<Type, DataAnnotationsModelValidationFactory> BuildAttributeFactoriesDictionary()
{
var dict = new Dictionary<Type, DataAnnotationsModelValidationFactory>();
AddValidationAttributeAdapter(dict, typeof(RegularExpressionAttribute),
(attribute) => new RegularExpressionAttributeAdapter((RegularExpressionAttribute)attribute));
AddDataTypeAttributeAdapter(dict, typeof(UrlAttribute), "url");
return dict;
}
private static void AddValidationAttributeAdapter(Dictionary<Type, DataAnnotationsModelValidationFactory> dictionary,
Type validationAttributeType,
DataAnnotationsModelValidationFactory factory)
{
if (validationAttributeType != null)
{
dictionary.Add(validationAttributeType, factory);
}
}
private static void AddDataTypeAttributeAdapter(Dictionary<Type, DataAnnotationsModelValidationFactory> dictionary,
Type attributeType,
string ruleName)
{
AddValidationAttributeAdapter(
dictionary,
attributeType,
(attribute) => new DataTypeAttributeAdapter((DataTypeAttribute)attribute, ruleName));
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A validation adapter that is used to map <see cref="DataTypeAttribute"/>'s to a single client side validation
/// rule.
/// </summary>
public class DataTypeAttributeAdapter : DataAnnotationsModelValidator
{
public DataTypeAttributeAdapter(DataTypeAttribute attribute,
[NotNull] string ruleName)
: base(attribute)
{
if (string.IsNullOrEmpty(ruleName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "ruleName");
}
RuleName = ruleName;
}
public string RuleName { get; private set; }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(
[NotNull] ClientModelValidationContext context)
{
var errorMessage = GetErrorMessage(context.ModelMetadata);
return new[] { new ModelClientValidationRule(RuleName, errorMessage) };
}
}
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IClientModelValidator
{
IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context);
}
}

View File

@ -0,0 +1,16 @@
using System.Runtime.CompilerServices;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ModelClientValidationRegexRule : ModelClientValidationRule
{
private const string ValidationType = "regex";
private const string ValidationRuleName = "pattern";
public ModelClientValidationRegexRule(string errorMessage, string pattern)
: base(ValidationType, errorMessage)
{
ValidationParameters.Add(ValidationRuleName, pattern);
}
}
}

View File

@ -7,24 +7,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
private readonly Dictionary<string, object> _validationParameters =
new Dictionary<string, object>(StringComparer.Ordinal);
private string _validationType = string.Empty;
public string ErrorMessage { get; set; }
public IDictionary<string, object> ValidationParameters
public ModelClientValidationRule([NotNull] string errorMessage)
: this(validationType: string.Empty, errorMessage: errorMessage)
{
get { return _validationParameters; }
}
public ModelClientValidationRule([NotNull] string validationType,
[NotNull] string errorMessage)
{
ValidationType = validationType;
ErrorMessage = errorMessage;
}
public string ErrorMessage { get; private set; }
/// <summary>
/// Identifier of the <see cref="ModelClientValidationRule"/>. If client-side unobtrustive validation is
/// enabled, use this <see langref="string"/> as part of the generated "data-val" attribute name. Must be
/// unique in the set of enabled validation rules.
/// </summary>
public string ValidationType
public string ValidationType { get; private set; }
public IDictionary<string, object> ValidationParameters
{
get { return _validationType; }
set { _validationType = value ?? string.Empty; }
get { return _validationParameters; }
}
}
}

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class RegularExpressionAttributeAdapter : DataAnnotationsModelValidator<RegularExpressionAttribute>
{
public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute)
: base(attribute)
{
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(
[NotNull] ClientModelValidationContext context)
{
var errorMessage = GetErrorMessage(context.ModelMetadata);
return new[] { new ModelClientValidationRegexRule(errorMessage, Attribute.Pattern) };
}
}
}

View File

@ -75,4 +75,4 @@ namespace Microsoft.AspNet.Mvc.Razor.Host
return value;
}
}
}
}