Language preference:

Phase 2 / ValidationAttributes / Using the CustomValidationAttribute

Some types of validation are too specialized to be handled by an attribute. For example, a login table should check the database for duplicate user names. For these, add the DESDA.CustomValidationAttribute to invoke a method added to your Entity class.

The PeterBlum.DES.DataAnnotations.CustomValidationAttribute is used for any server-side only validation that you could not implement through any other ValidationAttribute. This attribute supports validation on individual properties and on the overall Entity class object (checking several properties together).

NoteSystem.ComponentModel.DataAnnotations.CustomValidationAttribute is also available and works similarly, but does not offer many improvements made by BLD to ValidationAttributes. If you have used it, you can quickly convert to BLD’s version by changing the declaration to “DESDA.CustomValidationAttribute”.

The validator’s logic is placed in a method in your Entity class (or another class of your choice). The user interface layer doesn’t normally invoke it as there are no “validator controls” associated with it. The BLD DataAccessObjects know to invoke it and all other ValidationAttributes prior to saving.

In this example, the Category table does not allow duplicate CategoryName values. The CategoryName property uses the DESDA.CustomValidationAttribute to invoke the CheckForDuplicateCategoryNames() method on the Category class.

[EntityDAOType(typeof(CategoryDAO))]
[MetadataType(typeof(CategoryMetadata))]
public partial class Category
{
   public ValidationResult CheckForDuplicateCategoryNames(
      string newName, ValidationContext validationContext)
   {
   // must declare using PeterBlum.DES.DataAnnotations to get the GetChangeEntityActionArgs method
      BaseDAOChangeEntityActionArgs vArgs = (BaseDAOChangeEntityActionArgs)validationContext.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 =>
            (category.CategoryName.Equal(newName, StringComparison.CurrentCultureIgnoreCase))
            && (category.CategoryID != vThisCategoryID)) != null)
         {
            DESDA.EntityValidationResult vResult = new DESDA.EntityValidationResult(
               "This name already exists. Choose another.",
               typeof(Category), "CategoryName", newName);
            vResult.SummaryErrorMessage = "{LABEL} already exists. Choose another.";
            return vResult;
         }
      }
      return ValidationResult.Success;
   }
}

public class CategoryMetadata
{
   [DESDA.CustomValidation(MethodName="CheckForDuplicateCategoryNames")]
   public object CategoryName { get; set; }
}

The error itself is communicated by the DESDA.EntityValidationResult object. This subclass of System.ComponentModel.DataAnnotations.ValidationResult tracks more data that is useful to the caller, such as two versions of the error message and the DataField which should reflect the error. BLD's user interface will show this duplicate name error message next to the Category Name textbox, just like the error messages from other ValidatorAttributes. If you used ValidationResult instead, the error message could only appear in the ValidationSummary control.

The Source Code Browser shows completed DataAnnotations. The DESDA.CustomValidationAttribute has been highlighted.

In the next topic, you'll learn about enabling validators based on values of other columns.



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>
   [EntityDAOType(typeof(CategoryDAO))]
   [MetadataType(typeof(CategoryMetadata))]
   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.Required()]
      [DESDA.CustomValidation(MethodName="CheckForDuplicateCategoryNames")]
      [DESDA.DisplayName("Name")]
      [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.DataType(DataType.MultilineText)]
      [DESDA.Filter(InMultiFieldSearch=true)]
      [DESDA.InjectionSecurity(DetectScriptInjection=true, DetectSQLInjection=true, 
         SQLDetectionLevel=PeterBlum.DES.Web.SQLDetectionLevel.MediumLow, 
         HTMLTagMode=PeterBlum.DES.Web.HTMLTagMode.IllegalExceptTags,
         HTMLTags="br|img|span|div|a")]
      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>
   [TableName("Categories")]
   public class CategoryDAO : LINQtoSQLEntityDAO<Category>
   {
      public CategoryDAO() : base(typeof(NorthWindDataContext)) { }
      public CategoryDAO(object pDataContext) : base(pDataContext) { }

   }  // class CategoryDAO
}