Saturday, January 24, 2009

Custom Validation Step 1: The ValidationErrorClass

 

Why write my own validation?

I confess I hate the Asp.Net validation controls. There are 3 main reasons for my dislike:

  1. I don’t like having my validation, which is essentially business logic, spread through my markup. In my opinion, validation belongs in business objects.
  2. I want all of my validation in one place, not spread across multiple procedures or markup tags.
  3. The damn things are always firing when I don’t want them to.

Now I know you can come up with solutions for these problems, but why am I struggling trying to fit ASP.Net’s validation model to my architecture. I’m a Domain Driven Design guy. With a little effort I can implement a validation model that integrates my UI validation cleanly with my domain (business object) validation, is very simple to use, and does a better job of giving me the UI features that I’m looking for. I want to emphasize “simple”. This is an approach that can be understood and implemented by any developer.

 

Design Overview

Before getting into the code, this is where we’re going.  First I want to create a class that will represent a validation error.  The ValidationError class will contain members for the error message, the field that failed validation, and any other information that I need to have about an error.  Next, the BAL implementation.  All of my BAL entities will store a generic list of ValidationError objects.  This List<ValidationError> will represent any validation errors for my entitites, and if I ever want to check if an entity is valid, I just need to check if the ValidationErrors list is empty.  Now it’s time for the special sauce, the UI implements the same mechanism for tracking  errors on a page.  Every page has a PageErrors member that is a List<ValidationErrors> that represents all errors on the page.  These could be errors from one or more entities, or page level errors that are specific to the UI.  The point is that since I use List<ValidationError> in both my entities and in my UI pages, validation becomes a simple matter of just running the appropriate Validate() methods and then appending any resulting ValidationErrors to the PageErrors list.

­­­­­­­­

Create the ValidationError Class­ and a BALBase to Use It­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­

I start with the business layer. I want to create a ValidationError class that represents a single error message, and I want to create a BALBase class as sho­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­wn below.

The BALBase serves as a base class for all of my business objects (domain entities) and it defines the interface that ensures that my business objects do two important things. First, they all have a ValidationErrors member which is implemented as a List<ValidationError>. Second, they all must implement the Validate() method, which is responsible for running validation and populating the ValidationErrors.

ValidationError is our container for all the data we need regarding a single error message. It contains the ErrorMessage text itself, the FieldName that failed validation, a UIFieldName (the label text used by the UI), and a SortOrder field that can be used later to assign a sort order to our error messages. If you look at the code included below, you’ll notice that ErrorMessage has some extra logic that looks for a pseudo tag in the error message text and replaces it with either the UIFieldName or the FieldName. This gives us the flexibility to let the UI map text that it uses to field names that our model uses (the label text is almost never the same). We’ll go over how that’s done in a later post when I get to implementing page validation on an ASP.Net page.

Some heavily commented code for my implementation of both classes is included below. The comments give more detail about each piece of this design. Next time I’ll look at how to use this plumbing to implement validation in one of my business objects.

public abstract class BALBase

{

 

     //

     // ValidationErrors

     //

     private List<ValidationError> _validationErrors;

     public List<ValidationError> ValidationErrors

     {

         get

         {

             if (_validationErrors == null)

               { _validationErrors = new List<ValidationError>(); }

             return _validationErrors;

         }

         set { _validationErrors = value; }

     }

 

 

     //

     // Validate

     // This method should be contained in the validation

     // of each concrete business object class.  The validation

     // region should contain contain all of the validation

     // functions for the class and an implementation of

     // Validate that calls all of them. Each validation

     // function is responsible for adding it's own ValidationError

     // to the ValidationErrors list if the method fails.

     //

     public abstract List<ValidationError> Validate();

}



// ValidationError

// This is just a container for a single validation error. 

// FieldName should be set to the name of the BAL Object

// property that failed validation.  The ErrorMessage should

// contain message that will be sent back to the application.

// Note that ErrorMessage should not include the field name.

// Instead it should contain <FieldName> tags, which can then

// be replaced with the field name by the application.  This

// addresses the issue of UI's that use a different name for

// the property.

public class ValidationError : IComparable

{

    #region "Properties"

        // FieldName

        // FieldName is required.  It tells us which entity field

        // value triggered the validation error.

        public string FieldName { get; set; }

 

        // UIFieldName

        // The UI label will often be different than the field name

        // on the business object.  This field allows the UI to set

        // a name that is consistent with the UI label.  If set, this

        // is the name that will be used in ErrorMessage.     

        public string UIFieldName { get; set; }

 

        // ErrorMessage

        // Contains the <FieldName> marker instead of field names. 

        // This allows us to replace with a UI-friendly name, the

        // UIFieldName.  If UIFieldName isn't set use FieldName.

        private string _errorMessage;

        public string ErrorMessage

        {

            get

            {

                if (UIFieldName == CommonBase.String_NullValue)

                { return _errorMessage.Replace("<FieldName>", FieldName); }

                else

                { return _errorMessage.Replace("<FieldName>", UIFieldName); }

            }

            set

            {

                _errorMessage = value;

            }

        }

 

        // SortOrder

        public int SortOrder { get; set; }

    #endregion

 

    //

    // Constructor

    //

    public ValidationError(string fieldName, string errorMessage)

    {

        FieldName = fieldName;

        ErrorMessage = errorMessage;

        UIFieldName = CommonBase.String_NullValue;

        SortOrder = int.MaxValue;

    }

 

    //

    // CompareTo

    // Implementation of IComparable.

    //

    int IComparable.CompareTo(object obj)

    {

        ValidationError error2 = (ValidationError)obj;

        return FieldName.CompareTo(error2.FieldName);

    }

}

 

No comments:

Post a Comment