Create the CustomValidationProvider class
public class CustomValidationProvider{ private readonly string _validationsFile = ""; public ConventionModelValidatorProvider(string validationsFile) { _validationsFile = validationsFile; } }
When creating the instance of ConventionValidatorProvider, it needs to receive the name of the xml that will be used to load the validations. This instance is created in Global.asax file
ModelValidatorProviders.Providers.Clear(); ModelValidatorProviders.Providers.Add( new CustomValidationProvider(ConfigurationManager.AppSettings.Get("validationsFile")) );
Validations XML
As we said earlier, we will define the validations for our models in an xml file. This XML file has the following structure
In this structure we define the models and properties that will be validated. For each defined property one or more validations need to exist; these are the types that we will be using for this example
<validation type="Required" errorMessage="The product description is required" />
The field will be requierd and if not provided, the value on errorMessage attribute will be displayed
<validation type="StringLengthAttribute" min="5" max="10" errorMessage = "5-10 characters" />
The field will be treated as string and its length value needs to be greater or equal than value specified on min attribute and lower or equal to value specified in max atribute, otherwise the value on errorMessage attribute will be displayed
<validation type="RangeAttribute" min="5" max="10" errorMessage = "specify something between 5 and 10" />
The field will be treated as numeric and its value needs to be greater or equal than value specified on min attribute and lower or equal to value specified in max atribute, otherwise the value on errorMessage attribute will be displayed
<validation type="RegularExpressionAttribute" errorMessage = "Code should start with 0x and be followed by only digits or A-F letters"> <regex> <![CDATA[ [0][x][0-9a-fA-F]+ ]]> </regex> </validation>
The field will be tested against the specified regular expression; if it does not comply with the regex, the value on errorMessage attribute will be displayed
Adding the validations to the page
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
1. Action that it’s being executed
context.Controller.ControllerContext.RouteData.Values["action"].ToString();
2. Controller where this Action exists
context.Controller.ControllerContext.RouteData.Values["controller"].ToString();
3. Property that is being checked if will be validated or not
metadata.PropertyName
4. Model (class) where that property exists
metadata.ContainerType.Name
Once we know this elements, the rest is only read the XML file to determine if the the type and quantity of validations that the current property needs. In order to keep this post as clean as possible, I’ll ommit the code that is used to read the XML file. If you need information on how to do that, you can check this article or have a look on LinqToXML.
We need to filter the XML file that we previously defined to get the validations for the property of the model that is being checked. If at least on validation exists, we start looping on the list and we create the validations using a sort of simple factory. Each validation is created with the counterpart class defined on System.ComponentModel.DataAnnotations Namespace. In this example we’re only defining a few validation types. You can define more if needed.
switch (validationType) { case "Required": attr = new RequiredAttribute(); break; case "StringLengthAttribute": var attribute1 = validation.Attribute("max"); var xAttribute2 = validation.Attribute("max"); if (xAttribute2 != null) { int max = int.Parse(attribute1 != null && String.IsNullOrEmpty(attribute1.Value) ? "0" : xAttribute2.Value); var attribute2 = validation.Attribute("min"); var xAttribute3 = validation.Attribute("min"); if (xAttribute3 != null) { int min = int.Parse(attribute2 != null && String.IsNullOrEmpty(attribute2.Value) ? "0" : xAttribute3.Value); attr = new StringLengthAttribute(max); ((StringLengthAttribute)attr).MinimumLength = min; } } break; case "RegularExpressionAttribute": var regex = validation.Descendants().Single(a => a.Name == "regex").Value.Trim(); attr = new RegularExpressionAttribute(regex); break; case "RangeAttribute": var attribute3 = validation.Attribute("max"); var xAttribute4 = validation.Attribute("max"); if (xAttribute4 != null) { double rangeMax = double.Parse(attribute3 != null && String.IsNullOrEmpty(attribute3.Value) ? "0" : xAttribute4.Value); var attribute2 = validation.Attribute("min"); var xAttribute3 = validation.Attribute("min"); if (xAttribute3 != null) { double rangeMin = double.Parse(attribute2 != null && String.IsNullOrEmpty(attribute2.Value) ? "0" : xAttribute3.Value); attr = new RangeAttribute(rangeMin, rangeMax); } } break; case "DataTypeAttribute": attr = new DataTypeAttribute(String.IsNullOrEmpty(attribute.Value) ? "" : attribute.Value); break; }
The attribute instance that is created corresponds to the validation that will be performed on screen for that property. Each validation has its own instance, and we store each instance that we create in a list of List<Attribute>
type. Once we’re done with the XML reading, the only thing we need to do is to call the base class to do the rest of the work and return the result.
return base.GetValidators(metadata, context, newAttributes);
Wrapping up
As we mentioned at the beginning of the post, using this approach you will gain a lot of flexibility on how you’re adding your model validations; also, you have a reusable way to add them to your model classes no matter where they are located.