Language preference:

Phase 2 / Understanding ValidationAttributes

Validation is the most common business rule on a database. Each DataField needs to have constraints on the data it accepts and validation supplies those constraints. For example, an integer DataField for Age may be constrained to values from 0 to 100.

The class System.ComponentModel.DataAnnotations.ValidationAttribute is the base class for all ValidationAttributes. Its subclasses define properties specific to each validation rule. For example, the RangeAttribute validates a range with properties for Minimum and Maximum. Many DataTypeAttributes are also ValidationAttributes, providing validation for strings that need to be converted to another data type.

BLD supplies many ValidationAttribute classes, including some that implement the same validation rules that Microsoft supplies. BLD's attributes have more features, including better handling of error messages, runtime customization, and a way to enable them based on values of other columns.

Here are some ValidationAttributes associated with the Employee Entity class.

public class EmployeeMetadata
   [DESDA.RegularExpression(@"\S\s+\S", CaseInsensitive=true, 
      ErrorMessage="Enter a building number and street name.")] // requires at least one space between data, so we get a street and building number  
   public object Address { get; set; }

   [DESDA.Range()]   // actual values are assigned in Employee.CustomizeDataField method
   public object BirthDate { get; set; }

   public object Extension { get; set; }

Here are the ValidationAttribute classes available in BLD.

Attribute class Usage
DESDA.CharacterSetAttribute Use on string DataFields that restrict the character set. On Field Templates that use the FilteredTextBox for data entry, this attribute also sets up the character set rules of the FilteredTextBox.
DESDA.CompareToValueAttribute Compare the value of this DataField to a value assigned to the ValueToCompare property.
DESDA.CompareTwoColumnsAttribute Compares the value of this DataField with another DataField.
DESDA.DifferenceAttribute Compare the value of two DataFields to determine how the difference between their values compares to a value in the DifferenceValue property.
DESDA.EmailDomainsValidationAttribute Use with an email address to either require or invalidate specific domains. It compliments the DESDA.EmailAddressDataTypeAttribute.
DESDA.FileExtensionValidationAttribute Use when evaluating a Url that must have a specific file extension or extensions. It compliments the DESDA.UrlDataTypeAttribute.
DESDA.RangeAttribute Establish a range.
DESDA.RegularExpressionAttribute Match a string DataField value to a specific pattern using a regular expression.
DESDA.RequiredAttribute Require a value in the DataField.
DESDA.StringLengthAttribute Impose limits on the number of characters permitted in a string DataField.
DESDA.StringListAttribute Compares a string to a list of strings to see if there is a match.
DESDA.SpecialDatesAttribute Defines a list of dates and associated data. Validates unselectable dates as invalid and provides both selectable and unselectable dates to the user interface layer for presentation.
DESDA.SpecialTimesAttribute Defines a list of times and associated data. Validates unselectable times as invalid and provides both selectable and unselectable times to the user interface layer for presentation.
DESDA.CustomValidationAttribute When no other ValidationAttribute applies.

Remember that many DataTypeAttributes also provide validation. (In fact, System.ComponentModel.DataAnnotations.DataTypeAttribute inherits from System.ComponentModel.ValidationAttribute.) For example, when you add a CreditCardNumberAttribute, you also get validation that ensures the number entered matches the Luhn Algorithm.

The Source Code Browser shows completed DataAnnotations. The ValidationAttributes have been highlighted.

In the next topic, you'll learn how to customize ValidationAttributes at runtime.

Open the Source Code Browser (C# only)
Source Code Browser
/* ------------------------------------------------
 * Describes the Entity class for: Category
 * Classes:
 *    Category - Entity class. Edit it for validation and to customize metadata at runtime
 *    CategoryMetadata - Entity Metadata class. It contains the DataAnnotations.
 *    CategoryDAO - BLD DataAccessObject format version of a Data Access Object.

 * Requires .net 4.0 and these references:
 *    System.ComponentModel.DataAnnotations
 *    PeterBlum.DES
 *    PeterBlum.DES.DataAnnotations
 * Generated: 7/8/2011 4:17:08 PM
 * ------------------------------------------------*/
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using PeterBlum.DES.DAO;
using PeterBlum.DES.DAO.Attributes;
using PeterBlum.DES.DAO.EntityDAO;
using PeterBlum.DES.DataAnnotations;

// Some members of this namespace have identical names to those in System.ComponentModel.DataAnnotations
// making it easy to switch from one to the other by adding the "DESDA." prefix
using DESDA = PeterBlum.DES.DataAnnotations;

namespace PeterBlum.WithDataAnnotations
   // --- ENTITY CLASS --------------------------------------------------
   /// <summary>
   /// Entity class. Edit it for validation and to customize metadata at runtime
   /// </summary>
   public partial class Category
/// <summary>
/// Associated with the CustomValidationAttribute on the Category.CategoryName property,
/// this uses LINQ to SQL to detect an existing Category with the same name
/// and reports it as an error.
/// </summary>
/// <remarks>
/// <para>You probably will create many of these methods. Consider using stored procedures
/// to search the database for duplicates. Write your code below to invoke the stored procedures.</para>
/// </remarks>
/// <param name="pNewName"></param>
/// <param name="pValidationContext"></param>
/// <returns></returns>
      public ValidationResult CheckForDuplicateCategoryNames(
         string pNewName, ValidationContext pValidationContext)
         BaseDAOChangeEntityActionArgs vArgs = (BaseDAOChangeEntityActionArgs)pValidationContext.GetChangeEntityActionArgs();

         ChangeEntityAction vAction = vArgs != null ? vArgs.Action : ChangeEntityAction.Update;
         if ((vAction == ChangeEntityAction.Insert) || (vAction == ChangeEntityAction.Update))
            Category vCategory = (Category)vArgs.Entity;
            int vThisCategoryID = (vAction == ChangeEntityAction.Insert) ? -1 : vCategory.CategoryID;
            NorthWindDataContext vDataContext = new NorthWindDataContext();
            System.Data.Linq.Table<Category> vTable = vDataContext.Categories;
            if (vTable.FirstOrDefault<Category>(category =>
               (String.Compare(category.CategoryName, pNewName, StringComparison.CurrentCultureIgnoreCase) == 0)
               && (category.CategoryID != vThisCategoryID)) != null)
               DESDA.EntityValidationResult vResult = new DESDA.EntityValidationResult(
                  "CheckForDuplicateCategoryNames", // this is the Source parameter. It can be anything. It is used by the BLDPageManager.UpdateErrorMessage event. So make it useful for detecting this particular error
                  "This name already exists. Choose another.",
                  typeof(Category), "CategoryName", pNewName);
               vResult.SummaryErrorMessage = "{LABEL} already exists. Choose another.";
               return vResult;

         return ValidationResult.Success;
   }  // class Category

   // --- ENTITY METADATA --------------------------------------------------
   /// <summary>
   /// Entity Metadata class.
   /// Companion to the Category Entity class that contains the DataAnnotations
   /// on properties with the same names as those in the actual Entity class.
   /// These properties do not require their types to match those in the Entity class.
   /// An Entity Metadata class allows the Entity class to be regenerated without
   /// overwriting DataAnnotations.
   /// </summary>
   [DESDA.InjectionSecurity(DetectScriptInjection=true, DetectSQLInjection=false)]  // impacts CategoryName, but not Description which has its own rules
   [DESDA.TableRestriction("Admin", DESDA.DenyAccess.None)]
   [DESDA.TableRestriction("Customer", DESDA.DenyAccess.Edit | DESDA.DenyAccess.Delete | DESDA.DenyAccess.Insert)]
   public class CategoryMetadata

      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Always, InMultiFieldSearch=true)]
      public object CategoryName { get; set; }
   /// <summary>
   /// In this example, explicitly make this a Multiline text element
   /// so it benefits from the MultilineText_Edit.ascx Field Template.
   /// Like most large textual fields, it should detect illegal hacking case.
   /// When the Peter's Input Security module is setup, the InjectionSecurityAttribute
   /// will block unwanted input.
   /// </summary>
      [DESDA.InjectionSecurity(DetectScriptInjection=true, DetectSQLInjection=true, 
      public object Description { get; set; }

      [DESDA.DbImageDataType(BadFormatErrorMessage="Bad format", ErrorMessage="Must be {EXTENSION}", SupportedFileTypes="jpg")]
      public object Picture { get; set; }

   }  // class CategoryMetadata

   // --- BLD DATAACCESSOBJECT  --------------------------------------------------
   /// <summary>
   /// BLD DataAccessObject class for the Category Entity class.
   /// It provides CRUD actions. The parent class already has default
   /// methods for Update(), Insert(), Delete() and several queries.
   /// </summary>
   /// <remarks>
   /// <para>For documentation, see the BLD DataAccessObject section of the Business Logic User's Guide.</para>
   /// </remarks>
   public class CategoryDAO : LINQtoSQLEntityDAO<Category>
      public CategoryDAO() : base(typeof(NorthWindDataContext)) { }
      public CategoryDAO(object pDataContext) : base(pDataContext) { }

   }  // class CategoryDAO