Tuesday, March 5, 2013

Runtime customizable model attributes in ASP.net MVC3

It can be because you want to implement I18N without the use of resources, or your model classes are outside of your MVC project, or any other reason you have that make you want not being tied to the inline (compile time) annotations on your model.

Whatever is the case, you can solve this by creating a custom class that help you to add these annotations dinamically. In this case, will be working on adding some validations defined on an XML file.

Create the CustomValidationProvider class

This CustomValidationProvider class, that will inherit from DataAnnotationsModelValidatorProvider, and it will help you adding all the validations that you need in the view accordingly.

 
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"))
);
It is important to notice that first we clear all the ModelValidatorProviders, in order to remove the one that comes with ASP.NET MVC by default.

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

Every time a view is loaded, the GetValidators method from the ModelValidator class is called for each propety that we have in the form. By this, all the needed validations are added to the list that will be used by MVC to determine which fields have a specific constraint that needs to be satisfied before saving. 

That said, we need then to create our own implementation of the GetValidators method by overriding whatever the base class have right now:

protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
        

Before we go further, there are 4 important elements that we need to identify when this method is executed:

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.

Jorge Gaona

Software developer with main experience in Microsoft Technologies but who also enjoys working with other languages. More about me

0 comentarios:

Post a Comment

 

Copyright @ 2013 A learning journey.