Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Runtime customizable model attributes in ASP.NET MVC3

0.00/5 (No votes)
11 Apr 2013 1  
We will be working on adding some validations defined on an XML file.

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 helps you to add these annotations dynamically. In this case, we 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.

<elements>
  <models>
    <model name="HomeViewModel">
      <property name="OrganizationId">
        <validations>
          <validation type="Required" errormessage="Please provide the Internal Organization">
 <!--[More validations ...]-->
        </validation></validations>
      </property>
 <!--[More properties ...]-->
    </model>
  </models>
 <!--[More models ...]-->
</elements>

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 four important elements that we need to identify when this method is executed:

  1. Action that it's being executed.
  2. context.Controller.ControllerContext.RouteData.Values["action"].ToString();
  3. Controller where this Action exists.
  4. context.Controller.ControllerContext.RouteData.Values["controller"].ToString();
  5. Property that is being checked if will be validated or not.
  6. metadata.PropertyName
  7. Model (class) where that property exists.
  8. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here