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 classRequiredAttribute
if I am usingCustomRequired
. Thedata-val-required
should just say Required. - The
StringLenth
of 6 chars is not applied. There is no sign of anything rendered forStringLength
, why?
What your custom
DataAnnotationsModelMetadataProvider
is doing is creating/modifying theModelMetada
associated with your property.If you inspect the ModelMetadata class you will note that it contains properties such as
string DisplayName
andstring DisplayFormatString
which are set based on the application of the[Display]
and[DisplayFormat]
attributes. It also contains abool 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 theModelType
which is used to validate that that the value can be converted to thetype
).It is the
HtmlHelper
methods that are responsible for generating the html that is passed to the view. To generate thedata-val-*
attributes, yourTextBoxFor()
method internally calls theGetUnobtrusiveValidationAttributes()
method of theHtmlHelper
class which in turn calls methods in theDataAnnotationsModelValidatorProvider
class which ultimately generate aDictionary
of thedata-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 fromValidationAttribute
to build the dictionary. In your case the onlyValidationAttribute
is[Some]
which derives fromRegularExpressionAttribute
so thedata-val-regex
anddata-val-regex-pattern
attributes are added.But because you have added your
CustomRequiredAttribute
in theTruckNumberMetadataProvider
, theIsRequired
property of theModelMetadata
has been set totrue
. If you insect theGetValidators()
ofDataAnnotationsModelValidatorProvider
you will see that aRequiredAttribute
is automatically added to the collection of attributes because you have not applied one to the property. The relevant snippet of code iswhich results in the
data-val-required
attribute being added to the html (and it uses the default message because it knows nothing about yourCustomRequiredAttribute
)Source code files to get you started if you want to understand the internal workings
GetUnobtrusiveValidationAttributes()
methods at line 413ValidationAttributes
One possible solution if you really want to use just one single
ValidationAttribute
would be to have it implementIClientValidatable
and in theGetClientValidationRules()
method, add rules, for examplewhich will be read by the
ClientDataTypeModelValidatorProvider
(and delete yourTruckNumberMetadataProvider
class). However that will create a maintenance nightmare so I recommend you just add the 3 validation attributes to your property