I have many properties that require 1 or more validation attributes, like the following:
public class TestModel
{
[Some]
[StringLength(6)]
[CustomRequired] // more attributes...
public string Truck { get; set; }
}
Please note all the above annotations work.
I do not want to write that all the time because whenever Some
is applied, all other attributes are also applied to the property. I want to be able to do this:
public class TestModel
{
[Some]
public string Truck { get; set; }
}
Now this is doable by inheriting; therefore, I wrote a custom DataAnnotationsModelMetadataProvider
and overrode the CreateMetadata
. This looks for anything that is decorated with Some
and then adds more attributes to it:
public class TruckNumberMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var attributeList = attributes.ToList();
if (attributeList.OfType<SomeAttribute>().Any())
{
attributeList.Add(new StringLengthAttribute(6));
attributeList.Add(new CustomRequiredAttribute());
}
return base.CreateMetadata(attributeList, containerType, modelAccessor, modelType, propertyName);
}
}
These are the attributes in case it helps:
public class CustomRequiredAttribute : RequiredAttribute
{
public CustomRequiredAttribute()
{
this.ErrorMessage = "Required";
}
}
public class SomeAttribute : RegularExpressionAttribute
{
public SomeAttribute()
: base(@"^[1-9]\d{0,5}$")
{
}
}
Usage
@Html.TextBoxFor(x => x.Truck)
HTML Rendered:
<input name="Truck" id="Truck" type="text" value=""
data-val-required="The Truck field is required."
data-val-regex-pattern="^[1-9]\d{0,5}$"
data-val-regex="The field Truck must match the regular expression '^[1-9]\d{0,5}$'."
data-val="true">
</input>
Problems/Questions
- The
CustomRequired
was applied. But why does it pick up the message from the base class RequiredAttribute
if I am using CustomRequired
. The data-val-required
should just say Required.
- The
StringLenth
of 6 chars is not applied. There is no sign of anything rendered for StringLength
, why?
What your custom DataAnnotationsModelMetadataProvider
is doing is creating/modifying the ModelMetada
associated with your property.
If you inspect the ModelMetadata class you will note that it contains properties such as string DisplayName
and string DisplayFormatString
which are set based on the application of the [Display]
and [DisplayFormat]
attributes. It also contains a bool IsRequired
attribute that determines if the value of a property is required (a bit more on that later).
It does not contain anything relating to a regular expression or the maximum length, or indeed anything related to validation (except the IsRequired
property and the ModelType
which is used to validate that that the value can be converted to the type
).
It is the HtmlHelper
methods that are responsible for generating the html that is passed to the view. To generate the data-val-*
attributes, your TextBoxFor()
method internally calls the GetUnobtrusiveValidationAttributes()
method of the HtmlHelper
class which in turn calls methods in the DataAnnotationsModelValidatorProvider
class which ultimately generate a Dictionary
of the data-val
attribute names and values used to generate the html.
I'll leave it to you to explore the source code if you want more detail (refer links below) but to summarize, it gets a collection of all attributes applied to your Truck
property that inherit from ValidationAttribute
to build the dictionary. In your case the only ValidationAttribute
is [Some]
which derives from RegularExpressionAttribute
so the data-val-regex
and data-val-regex-pattern
attributes are added.
But because you have added your CustomRequiredAttribute
in the TruckNumberMetadataProvider
, the IsRequired
property of the ModelMetadata
has been set to true
. If you insect the GetValidators()
of DataAnnotationsModelValidatorProvider
you will see that a RequiredAttribute
is automatically added to the collection of attributes because you have not applied one to the property. The relevant snippet of code is
if (AddImplicitRequiredAttributeForValueTypes && metadata.IsRequired && !attributes.Any(a => a is RequiredAttribute))
{
attributes = attributes.Concat(new[] { new RequiredAttribute() });
}
which results in the data-val-required
attribute being added to the html (and it uses the default message because it knows nothing about your CustomRequiredAttribute
)
Source code files to get you started if you want to understand the internal workings
- HtmlHelper.cs - refer
GetUnobtrusiveValidationAttributes()
methods at line 413
- ModelValidatorProviders.cs - gets the various ValidatorProviders used for validation
- DataAnnotationsModelValidatorProvider.cs - the ValidatorProvider for
ValidationAttributes
One possible solution if you really want to use just one single ValidationAttribute
would be to have it implement IClientValidatable
and in the GetClientValidationRules()
method, add rules, for example
var rule = new ModelClientValidationRule
{
ValidationType = "required",
ErrorMessage = "Required"
}
which will be read by the ClientDataTypeModelValidatorProvider
(and delete your TruckNumberMetadataProvider
class). However that will create a maintenance nightmare so I recommend you just add the 3 validation attributes to your property