Custom Validation Logic

The extensibility of the ASP.NET MVC framework means an infinite number of possibilities exist for implementing custom validation logic. However, this section focuses on two core scenarios:

  • Packaging validation logic into a custom data annotation
  • Packaging validation logic into a model object itself

Putting validation logic into a custom data annotation means you can easily reuse the logic across multiple models. Of course, you have to write the code inside the attribute to work with different types of models, but when you do, you can place the new annotation anywhere.

On the other hand, adding validation logic directly to a model object often means the validation logic itself is easier to write (you only need to worry about the logic working with a single type of object). It is, however, more difficult to reuse the logic.

You'll see both approaches in the following sections, starting with writing a custom data annotation.

Custom Annotations

Imagine you want to restrict the last name value of a customer to a limited number of words. For example, you might say that 10 words are too many for a last name. You also might decide that this type of validation (limiting a string to a maximum number of words) is something you can reuse with other models in the Music Store application. If so, the validation logic is a candidate for packaging into a reusable attribute.

All of the validation annotations (like Required and Range) ultimately derive from the ValidationAttribute base class. The base class is abstract and lives in the System.ComponentModel.DataAnnotations namespace. Your validation logic will also live in a class deriving from ValidationAttribute:

using System.ComponentModel.DataAnnotations;

namespace MvcMusicStore.Infrastructure
{
    public class MaxWordsAttribute : ValidationAttribute
    {

    }
}

To implement the validation logic, you need to override one of the IsValid methods provided by the base class. Overriding the IsValid version taking a ValidationContext parameter provides more information to use inside the IsValid method (the ValidationContext parameter will give you access to the model type, model object instance, and friendly display name of the property you are validating, among other pieces of information).

public class MaxWordsAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext)
    {

        return ValidationResult.Success;
    }
}

The first parameter to the IsValid method is the value to validate. If the value is valid you can return a successful validation result, but before you can determine if the value is valid, you'll need to know how many words are too many. You can do this by adding a constructor to the attribute and force the client to pass the maximum number of words as a parameter:

    public class MaxWordsAttribute : ValidationAttribute
    {        
        public MaxWordsAttribute(int maxWords)
        {
            _maxWords = maxWords;
        }

        protected override ValidationResult IsValid(
            object value, ValidationContext validationContext)
        {

            return ValidationResult.Success;
        }

        private readonly int _maxWords;
    }

Now that you've parameterized the maximum word count, you can implement the validation logic to catch an error:

public class MaxWordsAttribute : ValidationAttribute
{
    public MaxWordsAttribute(int maxWords)
    {
        _maxWords = maxWords;
    }

    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext)
    {
        if (value != null)
        {
            var valueAsString = value.ToString();
            if (valueAsString.Split(’ ‘).Length > _maxWords)
            {
                return new ValidationResult("Too many words!");
            }
        }
        return ValidationResult.Success;
    }

    private readonly int _maxWords;
}

You are doing a relatively naïve check for the number of words by splitting the incoming value using the space character and counting the number of strings the Split method generates. If you find too many words, you return a ValidationResult object with a hard-coded error message to indicate a validation error.

The problem with the last block of code is the hard-coded error message. Developers who use the data annotations will expect to have the ability to customize an error message using the ErrorMessage property of ValidationAttribute. To follow the pattern of the other validation attributes, you need to provide a default error message (to be used if the developer doesn't provide a custom error message) and generate the error message using the name of the property you are validating:

public class MaxWordsAttribute : ValidationAttribute
{
    public MaxWordsAttribute(int maxWords)
        :base("{0} has too many words.")
    {
        _maxWords = maxWords;
    }

    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext)
    {
        if (value != null)
        {
            var valueAsString = value.ToString();
            if (valueAsString.Split(’ ‘).Length > _maxWords)
            {
                var errorMessage = FormatErrorMessage(
                        validationContext.DisplayName);
                return new ValidationResult(errorMessage);
            }
        }
        return ValidationResult.Success;
    }

    private readonly int _maxWords;
}

There are two changes in the preceding code:

  • First, you pass along a default error message to the base class constructor. You should pull this default error message from a resource file if you are building an internationalized application.
  • Notice how the default error message includes a parameter placeholder ({0}). The placeholder exists because the second change, the call to the inherited FormatErrorMessage method, will automatically format the string using the display name of the property. FormatErrorMessage ensures we use the correct error message string (even is the string is localized into a resource file). The code needs to pass the value of this name, and the value is available from the DisplayName property of the validationContext parameter. With the validation logic in place, you can apply the attribute to any model property:
[Required]
[StringLength(160)]
[MaxWords(10)]
public string LastName { get; set; }

You could even give the attribute a custom error message:

[Required]
[StringLength(160)]
[MaxWords(10, ErrorMessage="There are too many words in {0}")]
public string LastName { get; set; }

Now if the customer types in too many words, he'll see the message in Figure 6.7 in the view.

note
UnFigure

The MaxWordsAttribute is available as a NuGet package. Search for Wrox.ProMvc3.Validation.MaxWordsAttribute to add the code into your project.

A custom attribute is one approach to providing validation logic for models. As you can see, an attribute is easily reusable across a number of different model classes. In Chapter 8, we'll add client-side validation capabilities for the MaxWordsAttribute.

IValidatableObject

A self-validating model is a model object that knows how to validate itself. A model object can announce this capability by implementing the IValidatableObject interface. As an example, let's implement the check for too many words in the LastName field directly inside the Order model:

public class Order : IValidatableObject 
{
    public IEnumerable<ValidationResult> Validate(
                           ValidationContext validationContext)
    {
        if (LastName != null &&
            LastName.Split(’ ‘).Length > 10)
        {
            yield return new ValidationResult("The last name has too many words!", 
                                             new []{"LastName"});
        }
    }
    
    // rest of Order implementation and properties
    // ...
}

This has a few notable differences from the attribute version.

  • The method the MVC run time calls to perform validation is named Validate instead of IsValid, but more important, the return type and parameters are different.
  • The return type for Validate is an IEnumerable<ValidationResult> instead of a single ValidationResult, because the logic inside is ostensibly validating the entire model and might need to return more than a single validation error.
  • There is no value parameter passed to Validate because you are inside an instance method of the model and can refer to the property values directly.

Notice the code uses the C# yield return syntax to build the enumerable return value, and the code needs to explicitly tell the ValidationResult the name of the field to associate with (in this case LastName, but the last parameter to the ValidationResult constructor will take an array of strings so you can associate the result with multiple properties).

Many validation scenarios are easier to implement using the IValidatableObject approach, particularly scenarios where the code needs to compare multiple properties on the model to make a validation decision.

At this point I've covered everything you need to know about validation annotations, but additional annotations in the MVC framework influence how the run time displays and edits a model. I alluded to these annotations earlier in the chapter when I talked about a “friendly display name,” and now you've finally reached a point where you can dive in.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.28.107