Language preference:

Phase 2 / ValidationAttributes / Enabling the validator with DependencyAttributes

Often there is a relationship between the DataField you are validating and other DataFields. A value of the other DataField determines if this validator is enabled or not. Use the DependencyAttribute to establish that relationship and BLD will know when to enable and disable your validator.

For example, say the Product Entity class should not allow the Discontinued DataField to be changed until the UnitsInStock DataField contains 0. The Discontinued DataField should have a ValidationAttribute that reports an error when its value is True and UnitsInStock is still not zero.

BLD provides DependencyAttributes to enable a validator. The BLD user interface can also use it to disable, hide, or change the appearance of the Field Template hosting the DataField.

In this code, the Product Entity class uses has several DependencyAttributes on the Discontinued property. The DESDA.RequiredAttribute is attached to the one named "ZeroUnitsInStock", making sure it remains disabled until that Dependency condition is met. The DependencyAttribute named "Strict" is not attached to any validator. It has been added for the benefit of the user interface to disable or hide the checkbox is creates for the Discontinued DataField so long as the UnitsInStock data entry field is not 0.

public class ProductMetadata
{
   [DESDA.CompareToValueDependency(Name="ZeroUnitsInStock", DataField="UnitsInStock",
      ValueToCompareAsNative=0, Operator=PeterBlum.DES.ConditionOperator.Equal)]
   [DESDA.RequiredDependency(Name="QtyPerUnitEmpty", 
      DataFields="QuantityPerUnit", NotCondition=true )]
   [DESDA.MultiConditionDependency(Name="Strict", 
      Expression="ZeroUnitsInStock OR QtyPerUnitEmpty")]
   [DESDA.Required(DependencyName="ZeroUnitsInStock")]
   public object Discontinued { get; set; }
} 

Here are the DependencyAttributes that are included with BLD. You can also create your own.

Attribute class Usage
DESDA.RequiredDependencyAttribute The related DataField requires a value or with its NotCondition set to true, it must not have a value.
DESDA.CompareToValueDependencyAttribute The related DataField's value is compared to another value.
DESDA.RangeDependencyAttribute The related DataField's value is compared to a range.
DESDA.CompareTwoDataFieldsDependencyAttribute Two related DataFields are compared to each other.
DESDA.RegexDependencyAttribute The related DataField contains a string that is evaluated against a regular expression.
DESDA.CompareBooleanDependencyAttribute The related DataField is a boolean that is compared to either true or false.
DESDA.ConditionDependencyAttribute Use any of DES's Condition objects to build the logic for this Dependency. Covers cases where no other DependencyAttribute was defined.
DESDA.MultiConditionDependencyAttribute Combines other DependencyAttributes into a boolean expression, where you can employ AND, OR, and NOT operators.

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

In the next topic, you'll learn how to provide textual values associated with the DataField to the user interface.



Open the Source Code Browser (C# only)
Source Code Browser
 
/* ------------------------------------------------
 * Describes the Entity class for: Product
 * Classes:
 *    Product - Entity class. Edit it for validation and to customize metadata at runtime
 *    ProductMetadata - Entity Metadata class. It contains the DataAnnotations.
 *    ProductDAO - 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:19:57 PM
 * ------------------------------------------------*/

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using PeterBlum.DES.DAO.Attributes;
using PeterBlum.DES.DAO.EntityDAO;
using PeterBlum.DES.DAO.EntityFilters;
using PeterBlum.DES.DAO;
// 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(ProductDAO))]
   [MetadataType(typeof(ProductMetadata))]
   public partial class Product : DESDA.ICustomizeDataField
   {
      [DESDA.CurrencyDataType()]
      [DESDA.DisplayName("Value of Stock")]
      [DESDA.ScaffoldColumn(100, true)]
      [DESDA.CalculationDescriptor("if (Not Discontinued) then {UnitPrice * UnitsInStock} else novalue")]
      [DESDA.ColumnRestriction("Customer", DESDA.DenyAccess.All)]
      [DESDA.SortableColumn(SortExpression="UnitPrice * UnitsInStock", Sortable=true)]
      public decimal ValueOfStock
      {
         get 
         { 
            if (!Discontinued)
               if (UnitPrice.HasValue && UnitsInStock.HasValue)
                  return UnitPrice.Value * UnitsInStock.Value;
            return 0.0M;
         }
      }

      [DESDA.CurrencyDataType()]
      [DESDA.CalculationDescriptor("if (!Discontinued) then {UnitPrice * (UnitsInStock + UnitsOnOrder)} else novalue")]
      [DESDA.ScaffoldColumn(101, true)]
      [DESDA.DisplayName("Value of Available Units", ShortDisplayName="Val avail.")]
      [DESDA.ColumnRestriction("Customer", DESDA.DenyAccess.All)]
      public decimal ValueOfAvailableUnits
      {
         get
         {
            if (!Discontinued)
            {
               int vStock = 0;
               if (UnitsInStock.HasValue)
                  vStock = UnitsInStock.Value;
               if (UnitsOnOrder.HasValue)
                  vStock = vStock + UnitsOnOrder.Value;

               if (UnitPrice.HasValue)
                  return UnitPrice.Value * vStock;
            }
            return 0.0M;
         }
      }


/// <summary>
/// Expose a property of the nested Supplier object
/// </summary>
      [AttributeSource(typeof(Supplier), "HomePage")]
      [DESDA.ScaffoldColumn(AfterThisDataField="Supplier")]
      public string SupplierHomePage
      {
         get
         {
            if (Supplier != null)
               return Supplier.HomePage;
            return String.Empty;
         }
      }

      #region ICustomizeDataField Members
      /// <summary>
      /// Modify the business logic to account for runtime situations.
      /// Always consider pModifiableColumn.StaticColumn as a READ ONLY property.
      /// </summary>
      /// <param name="pModifiableColumn"></param>
      public void CustomizeDataField(DESDA.ActiveDataField activeDataField)
      {
         switch (activeDataField.DataField)
         {
            case "Category":
               activeDataField.SelectForeignKeyQueryAttribute("Desc", false);
               break;
         }  // switch

      }

      #endregion
   }  // class Product

   // --- ENTITY METADATA --------------------------------------------------
   /// <summary>
   /// Entity Metadata class.
   /// Companion to the Product 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.TableRestriction("Admin", DESDA.DenyAccess.None)]
   [DESDA.TableRestriction("Customer", DESDA.DenyAccess.Edit | DESDA.DenyAccess.Delete | DESDA.DenyAccess.Insert)]
   public class ProductMetadata
   {
      [DESDA.DisplayName("Product ID", ShortDisplayName="ID")]
      public object ProductID { get; set; }

      [DESDA.CharacterSet(LettersLowercase=true, LettersUppercase=true, Digits=true, 
         Space=true, DiacriticLetters=true, OtherCharacters="'",
         ErrorMessage="Illegal character", SummaryErrorMessage="{LABEL} has an illegal character.")]
      [DESDA.DisplayName("Product Name", ShortDisplayName="Name")]
      [DESDA.Description("The product name.", Prompt="Enter the product name:")]
      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Always)]
      public object ProductName { get; set; }
  
      [DESDA.DisplayName("Quantity Per Unit")]
      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Advanced)]
      [DESDA.ScaffoldColumn("UnitPrice")] // positioned after UnitPrice when scaffolding
      [DESDA.SortableColumn(false)]
      public object QuantityPerUnit { get; set; }

      [DESDA.CurrencyDataType(MaxWholeDigits=4, UseCurrencySymbol=true, AllowNegatives=false)]
      [DESDA.Description("The price of one unit", Prompt="Enter the price for one unit")]
      [DESDA.DisplayName("Unit Price")]
      [DESDA.Filter(DataFormatString="c")]
      [DESDA.RangeEntityFilterPicker(true, 0, 10, 20, 100)]
      [DESDA.CompareBooleanDependency(DataField="Discontinued", ValueToCompare=false)]
      public object UnitPrice { get; set; }

      [DESDA.IntegerDataType(MaxWholeDigits=3, AllowNegatives=false)]
      [DESDA.CompareToValue(500, PeterBlum.DES.ConditionOperator.LessThan)]
      [DefaultValue(10)]
      [DESDA.SortableColumn(false)]
      [DESDA.DisplayName("Units In Stock")]
      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Advanced)]
      [DESDA.RangeEntityFilterPicker(false, 0, 9, 10, 49, 50, 99, 100)]
      [DESDA.ColumnRestriction("Customer", DESDA.DenyAccess.View)]
      public object UnitsInStock { get; set; }

      [DESDA.IntegerDataType(MaxWholeDigits=3, AllowNegatives=false)]
      [DESDA.DisplayName("Units On Order")]
      [DESDA.Range(0, 199)]
      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Advanced)]
      [DESDA.RangeEntityFilterPicker(false, 0, 9, 10, 49, 50, 99, 100)]
      [DESDA.ColumnRestriction("Customer", DESDA.DenyAccess.View)]
      public object UnitsOnOrder { get; set; }

      [DESDA.Enumerated(typeof(ReorderLevelEnum), ValueNameList="10=Medium Low|50=Medium High")]
      [DESDA.DisplayName("Reorder Level")]
      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Advanced)]
      [DESDA.ColumnRestriction("Customer", DESDA.DenyAccess.View)]
      public object ReorderLevel { get; set; }

      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Advanced)]
      [DESDA.ForeignKeyQuery(QueryName="", DisplayDataFields="CompanyName|ContactName", OrderBy="CompanyName")]
      public object Supplier { get; set; }

/// <summary>
/// Each ForeignKeyQuery describes one way to query the Category table
/// that the UI may use. The Product.CustomizeDataField method identifies
/// the default query in ActiveDataField.SelectForeignKeyQueryAttribute.
/// ForeignKey FieldTemplates offer a QueryName property where
/// you can override this default.
/// </summary>
      [DESDA.Filter(AutoGeneratePriority=DESDA.AutoGeneratePriority.Advanced)]
      [DefaultValue(1)]  // = ID of Beverages
      [DESDA.ForeignKeyQuery(QueryName="", Where="", OrderBy="CategoryName")]
      [DESDA.ForeignKeyQuery(QueryName="Desc", Where="", OrderBy="CategoryName desc")]
      [DESDA.ForeignKeyQuery(QueryName="Condiments and Beverages", 
         Where="(CategoryName == \"Condiments\") or (CategoryName == \"Beverages\")", OrderBy="CategoryName")]
      public object Category { get; set; }

/// <summary>
/// The RequiredAttribute is only enabled when the ZeroUnitsInStock dependency
/// determines that the UnitsInStock is 0.
/// The Dependency called "Strict" is available for the UI to enforce
/// stricter rules, where its required if QuantityPerUnit is empty
/// or UnitsInStock are 0. A Field Template must set its DependencyName property
/// to "Strict" to use this dependency.
/// </summary>
      [DESDA.CompareToValueDependency(Name="ZeroUnitsInStock", DataField="UnitsInStock",
         Operator=PeterBlum.DES.ConditionOperator.Equal, ValueToCompareAsNative=0)]
      [DESDA.RequiredDependency(Name="QtyPerUnitEmpty", DataFields="QuantityPerUnit", NotCondition=true )]
      [DESDA.MultiConditionDependency(Name="Strict", 
        Expression="ZeroUnitsInStock OR QtyPerUnitEmpty")]
      [DESDA.Required(DependencyName="ZeroUnitsInStock")]
      [DefaultValue(false)]
      public object Discontinued { get; set; }

      [DESDA.ColumnRestriction("Customer", DESDA.DenyAccess.All)]
      public object Order_Details { get; set; }

   }  // class ProductMetadata

/// <summary>
/// Used by the ReorderLevel property to establish fixed integer values
/// that can be assigned by the user.
/// </summary>
   public enum ReorderLevelEnum
   {
      None = 0,
      Low = 5,
      MediumLow = 10,
      Medium = 30,
      MediumHigh = 50,
      High = 100
   }

   // --- BLD DATAACCESSOBJECT  --------------------------------------------------
   /// <summary>
   /// BLD DataAccessObject class for the Product 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("Products")]
   public class ProductDAO : LINQtoSQLEntityDAO<Product>
   {
      public ProductDAO() : base(typeof(NorthWindDataContext)) { }
      public ProductDAO(object pDataContext) : base(pDataContext) { }

/// <summary>
/// Returns products between a price range. Supports filters
/// and sorting passed in through the SelectArgs.
/// Uses EntityFilters to add a filter to the query. 
/// (There are many ways to adjust the query, such as using LINQ
/// and the PeterBlum.DES.DAO.EntityDAO.WhereClause class which
/// lets you construct a SQL-like statement.)
/// </summary>
/// <param name="startPrice"></param>
/// <param name="endPrice"></param>
/// <param name="selectArgs"></param>
/// <returns></returns>
      [DataObjectMethod(DataObjectMethodType.Select, false)]
      [SelectMethodParameter(ActualName="startPrice", AliasNames="StartPrice")]
      [SelectMethodParameter(ActualName="endPrice", AliasNames="EndPrice")]
      [Description("All Products whose UnitPrice is within these parameters.")]
      [SelectArgs(Filters=true, SortExpression=true, Paging=true)]
      public IEnumerable<Product> SelectPriceRange(
        decimal startPrice, decimal endPrice, SelectArgs selectArgs)
      {
        if (endPrice == 0)
           endPrice = 999999999.99M;

        selectArgs.EntityFilters.Add(
          new RangeEntityFilter("UnitPrice", startPrice, endPrice));
 
        return ExecuteSelectCommand(selectArgs);
      }

/// <summary>
/// Uses a stored procedure called Ten Most Expensive Products that has
/// already been added to the NorthWindDataContext class as a method
/// named Ten_Most_Expensive_Products(). It takes no parameters.
/// </summary>
/// <param name="selectArgs"></param>
/// <returns></returns>
      [DataObjectMethod(DataObjectMethodType.Select, false)]
      [SelectMethodParameter(ActualName="startPrice", AliasNames="StartPrice")]
      [SelectMethodParameter(ActualName="endPrice", AliasNames="EndPrice")]
      [Description("All Products whose UnitPrice is within these parameters.")]
      [SelectArgs(Filters=true, SortExpression=true, Paging=true)]
      public IEnumerable<Product> TopTen(SelectArgs selectArgs)
      {
         NorthWindDataContext vDataContext = new NorthWindDataContext();
         var vProducts = vDataContext.Ten_Most_Expensive_Products();
        return ExecuteSelectCommand(vProducts, selectArgs, "TenMostExpensive");
      }
   }  // class ProductDAO

}