From b9b652084a46e3d313e34a8f993ab3f8e2270637 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Sat, 12 Apr 2014 08:46:02 -0700 Subject: [PATCH] Introducing IClientModelValidator to support client validation * Adding support for validator adapters in DataAnnotationsModelValidatorProvider * Adding Regex and DataType validators --- .../Microsoft.AspNet.Mvc.ModelBinding.kproj | 6 ++ .../Properties/Resources.Designer.cs | 32 ++++++++++ .../Resources.resx | 6 ++ .../ClientModelValidationContext.cs | 14 +++++ .../DataAnnotationsModelValidator.cs | 13 ++++- ...taAnnotationsModelValidatorOfTAttribute.cs | 18 ++++++ .../DataAnnotationsModelValidatorProvider.cs | 58 +++++++++++++------ .../Validation/DataTypeAttributeAdapter.cs | 33 +++++++++++ .../Validation/IClientModelValidator.cs | 9 +++ .../ModelClientValidationRegexRule.cs | 16 +++++ .../Validation/ModelClientValidationRule.cs | 23 +++++--- .../RegularExpressionAttributeAdapter.cs | 20 +++++++ .../Properties/Resources.Designer.cs | 2 +- 13 files changed, 222 insertions(+), 28 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorOfTAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataTypeAttributeAdapter.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IClientModelValidator.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRegexRule.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RegularExpressionAttributeAdapter.cs diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj index cd567a7922..c146034bc9 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj @@ -70,21 +70,27 @@ + + + + + + diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs index 445368e1aa..4e17ae012a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs @@ -42,6 +42,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyNotFound"), p0, p1); } + /// + /// The type '{0}' must have a public constructor which accepts a single parameter of type '{1}'. + /// + internal static string DataAnnotationsModelValidatorProvider_ConstructorRequirements + { + get { return GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements"); } + } + + /// + /// The type '{0}' must have a public constructor which accepts a single parameter of type '{1}'. + /// + internal static string FormatDataAnnotationsModelValidatorProvider_ConstructorRequirements(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements"), p0, p1); + } + /// /// The key is invalid JQuery syntax because it is missing a closing bracket. /// @@ -218,6 +234,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return GetString("ModelBindingContext_ModelMetadataMustBeSet"); } + /// + /// The type '{0}' must derive from '{1}'. + /// + internal static string TypeMustDeriveFromType + { + get { return GetString("TypeMustDeriveFromType"); } + } + + /// + /// The type '{0}' must derive from '{1}'. + /// + internal static string FormatTypeMustDeriveFromType(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TypeMustDeriveFromType"), p0, p1); + } + /// /// The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'. /// diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx index 70fce8fb43..22d1017224 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx @@ -123,6 +123,9 @@ The property {0}.{1} could not be found. + + The type '{0}' must have a public constructor which accepts a single parameter of type '{1}'. + The key is invalid JQuery syntax because it is missing a closing bracket. @@ -156,6 +159,9 @@ The ModelMetadata property must be set before accessing this property. + + The type '{0}' must derive from '{1}'. + The model object inside the metadata claimed to be compatible with '{0}', but was actually '{1}'. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs new file mode 100644 index 0000000000..6b6fe5ef75 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs @@ -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; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs index ec2d9e608d..c5693504d6 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs @@ -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(); } + + public virtual IEnumerable GetClientValidationRules( + [NotNull] ClientModelValidationContext context) + { + return Enumerable.Empty(); + } + + protected virtual string GetErrorMessage([NotNull] ModelMetadata modelMetadata) + { + return Attribute.FormatErrorMessage(modelMetadata.GetDisplayName()); + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorOfTAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorOfTAttribute.cs new file mode 100644 index 0000000000..7fc7fe5527 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorOfTAttribute.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class DataAnnotationsModelValidator : DataAnnotationsModelValidator + where TAttribute : ValidationAttribute + { + public DataAnnotationsModelValidator(TAttribute attribute) + : base(attribute) + { + } + + protected new TAttribute Attribute + { + get { return (TAttribute)base.Attribute; } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs index a41212f1db..b780428e99 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs @@ -18,29 +18,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// 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 _attributeFactories = + BuildAttributeFactoriesDictionary(); // Factories for validation attributes - private static DataAnnotationsModelValidationFactory DefaultAttributeFactory = + private static readonly DataAnnotationsModelValidationFactory _defaultAttributeFactory = (attribute) => new DataAnnotationsModelValidator(attribute); - private static Dictionary AttributeFactories = - new Dictionary(); - // Factories for IValidatableObject models - private static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactory = + private static readonly DataAnnotationsValidatableObjectAdapterFactory _defaultValidatableFactory = () => new ValidatableObjectAdapter(); - private static Dictionary ValidatableFactories = - new Dictionary(); - - 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()) { 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 BuildAttributeFactoriesDictionary() + { + var dict = new Dictionary(); + AddValidationAttributeAdapter(dict, typeof(RegularExpressionAttribute), + (attribute) => new RegularExpressionAttributeAdapter((RegularExpressionAttribute)attribute)); + + AddDataTypeAttributeAdapter(dict, typeof(UrlAttribute), "url"); + + return dict; + } + + private static void AddValidationAttributeAdapter(Dictionary dictionary, + Type validationAttributeType, + DataAnnotationsModelValidationFactory factory) + { + if (validationAttributeType != null) + { + dictionary.Add(validationAttributeType, factory); + } + } + + private static void AddDataTypeAttributeAdapter(Dictionary dictionary, + Type attributeType, + string ruleName) + { + AddValidationAttributeAdapter( + dictionary, + attributeType, + (attribute) => new DataTypeAttributeAdapter((DataTypeAttribute)attribute, ruleName)); + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataTypeAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataTypeAttributeAdapter.cs new file mode 100644 index 0000000000..8ea248d375 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataTypeAttributeAdapter.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// A validation adapter that is used to map 's to a single client side validation + /// rule. + /// + 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 GetClientValidationRules( + [NotNull] ClientModelValidationContext context) + { + var errorMessage = GetErrorMessage(context.ModelMetadata); + return new[] { new ModelClientValidationRule(RuleName, errorMessage) }; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IClientModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IClientModelValidator.cs new file mode 100644 index 0000000000..1d247e1d8d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/IClientModelValidator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public interface IClientModelValidator + { + IEnumerable GetClientValidationRules(ClientModelValidationContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRegexRule.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRegexRule.cs new file mode 100644 index 0000000000..8b94a48eca --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRegexRule.cs @@ -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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRule.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRule.cs index 523964a451..8220736502 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRule.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationRule.cs @@ -7,24 +7,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { private readonly Dictionary _validationParameters = new Dictionary(StringComparer.Ordinal); - private string _validationType = string.Empty; - public string ErrorMessage { get; set; } - - public IDictionary 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; } + /// /// Identifier of the . If client-side unobtrustive validation is /// enabled, use this as part of the generated "data-val" attribute name. Must be /// unique in the set of enabled validation rules. /// - public string ValidationType + public string ValidationType { get; private set; } + + public IDictionary ValidationParameters { - get { return _validationType; } - set { _validationType = value ?? string.Empty; } + get { return _validationParameters; } } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RegularExpressionAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RegularExpressionAttributeAdapter.cs new file mode 100644 index 0000000000..4e1d364f33 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/RegularExpressionAttributeAdapter.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class RegularExpressionAttributeAdapter : DataAnnotationsModelValidator + { + public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute) + : base(attribute) + { + } + + public override IEnumerable GetClientValidationRules( + [NotNull] ClientModelValidationContext context) + { + var errorMessage = GetErrorMessage(context.ModelMetadata); + return new[] { new ModelClientValidationRegexRule(errorMessage, Attribute.Pattern) }; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs index ff03c78311..97d038e5a3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs @@ -75,4 +75,4 @@ namespace Microsoft.AspNet.Mvc.Razor.Host return value; } } -} \ No newline at end of file +}