Suggestions on Validation

Suggestions on Validation

Old forum URL: forums.lhotka.net/forums/t/7329.aspx


rasupit posted on Thursday, July 23, 2009

I would like to suggest a few things to improve the rule validation mechanism.

1. ability to add rule to multiple parameters.  The code sample below (AddBusinessRules) shows that there is opportunity to improve and make the code simpler.
  ex:
  you can define all date properties like the following:
        static var allDateProperties = new[] { baseIncurredStartRuleArg, baseIncurredEndRuleArg, ... };
        static var baseDatePropertieNames = new[] { baseIncurredStartRuleArg.PropertyName, baseIncurredEndRuleArg.PropertyName, ... };

  then add rule
        ValidationRules.AddRule<TimeBasedParameters, IEnumerable<RuleArgs>>(AvailablePeriod, allDateProperties);
        ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, baseDatePropertieNames, false);

2. add option to "by-pass" the validation based on delegate condition.  A lot of time, you would like the validation to be performed on certain condition only. This is currently easy to implement just by embedding the conditional test in the rule handler method.  ex: at the top of your rule handler you can just add code to return true when it pass a condition to "bypass" the validation.
        private static bool AvailablePeriod(TimeBasedParameters target, Csla.Validation.RuleArgs e)
        {
            if ( /*condition to bypass*/ ) return true;
            //validate code goes here
        }
However things start to get hairy when a condition start to become a cross-cutting concern. When you have condition that need to be applied on numbers of validation handlers, and/or condition varies based on certain situation (means complex).  It'll be nice when we can attach this condition while registering the rule if we have the following:

        public void AddRule<T, R>(RuleHandler<T, R> handler, R args, Predicate<T, R> byPassCondition) where R : RuleArgs

notice there is no such Predicate that takes two generic parameter. I just made it up to self explain the intention.

3. ability to postpone the validation checks until the end of property assignment sequence to ensure the validation handler only being execute once .  This will give tremendous performance improvement when you have a lot of AddDependentProperty.  I think we can use the same technique that we use to by pass property check. So similar to ByPassPropertyChecks we can do the following in UI code.
        using ( project.PostponePropertyChecks )
        {
            // update value
        }
        if (project.Isvalid)
            project = project.Save()

So what do you think? any other suggestions?

Thanks,
Ricky Supit.

=======================================
Sample Code
=======================================
protected override void AddBusinessRules()
{
    //
    // Base Incurred
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, baseIncurredStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, baseIncurredEndRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, baseIncurredStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, baseIncurredEndRuleArg);
    ValidationRules.AddDependantProperty(baseIncurredStartRuleArg.PropertyName, baseIncurredEndRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(baseIncurredStartRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(baseIncurredEndRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    //
    // Current Incurred
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, currentIncurredStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, currentIncurredEndRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, currentIncurredStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, currentIncurredEndRuleArg);
    ValidationRules.AddDependantProperty(currentIncurredStartRuleArg.PropertyName, currentIncurredEndRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(currentIncurredStartRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(currentIncurredEndRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    //
    // Base Paid
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, basePaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, basePaidEndRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, basePaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, basePaidEndRuleArg);
    ValidationRules.AddDependantProperty(basePaidStartRuleArg.PropertyName, basePaidEndRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(basePaidStartRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(basePaidEndRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    //
    // Current Paid
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, currentPaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, currentPaidEndRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, currentPaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, currentPaidEndRuleArg);
    ValidationRules.AddDependantProperty(currentPaidStartRuleArg.PropertyName, currentPaidEndRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(currentPaidStartRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(currentPaidEndRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    //
    // Financial Base Paid
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, fnBasePaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, fnBasePaidEndRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, fnBasePaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, fnBasePaidEndRuleArg);
    ValidationRules.AddDependantProperty(fnBasePaidStartRuleArg.PropertyName, fnBasePaidEndRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(fnBasePaidStartRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(fnBasePaidEndRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    //
    // Financial Current Paid
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, fnCurrentPaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(AvailablePeriod, fnCurrentPaidEndRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, fnCurrentPaidStartRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidDateRange, fnCurrentPaidEndRuleArg);
    ValidationRules.AddDependantProperty(fnCurrentPaidStartRuleArg.PropertyName, fnCurrentPaidEndRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(fnCurrentPaidStartRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    ValidationRules.AddDependantProperty(fnCurrentPaidEndRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    //
    // MmosLag
    //
    ValidationRules.AddRule<TimeBasedParameters>(MmosLagIsValid, "MmosLag");
    ValidationRules.AddDependantProperty(MmosLagRuleArg.PropertyName, periodGroupingBasisRuleArg.PropertyName, true);
    //
    // Period Variation
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidPeriodVariation, periodVariationRuleArg);
    ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, baseIncurredStartRuleArg.PropertyName, false);
    ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, baseIncurredEndRuleArg.PropertyName, false);
    ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, basePaidStartRuleArg.PropertyName, false);
    ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, basePaidEndRuleArg.PropertyName, false);
    ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, fnBasePaidStartRuleArg.PropertyName, false);
    ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, fnBasePaidEndRuleArg.PropertyName, false);
    ValidationRules.AddDependantProperty(periodVariationRuleArg.PropertyName, basePeriodTitleRuleArg.PropertyName, false);
    //
    // Period Title
    //
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidPeriodTitle, basePeriodTitleRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidPeriodTitle, currentPeriodTitleRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidPairPeriodTitle, basePeriodTitleRuleArg);
    ValidationRules.AddRule<TimeBasedParameters, Csla.Validation.RuleArgs>(ValidPairPeriodTitle, currentPeriodTitleRuleArg);
    ValidationRules.AddDependantProperty(basePeriodTitleRuleArg.PropertyName, currentPeriodTitleRuleArg.PropertyName, true);
    //
    //
    ValidationRules.AddRule<TimeBasedParameters>(IncurredBasisValidRange, "IncurredBasis");
    ValidationRules.AddRule<TimeBasedParameters>(PaidBasisValidRange, "PaidBasis");
}

RockfordLhotka replied on Thursday, July 23, 2009

On the surface these seem like some good ideas.

I've put an entry in the wish list to ensure this doesn't get lost.

http://www.lhotka.net/cslabugs/edit_bug.aspx?id=447

rasupit replied on Friday, July 24, 2009

Rocky, thanks for considering my suggestions. 

Just to add more analysis, I think suggestion 3 is easier to implement on CSLA 3.6+.  We can still make use of the later implementation of string[] PropertyHasChanged(string propName) where this method returns all property names that their validation rules were called.  So the implementation may look something like the following:

private bool _postponePropertyChecks = false;
private List<string> _postponeProperties = new List<string>();

public PostponePropertyChecksToken BypassPropertyChecks
{
    get { return new PostponePropertyChecksToken(this); }
}
protected override void PropertyHasChanged(string propertyName)
{
    if (_postponePropertyChecks)
        _postponeProperties.Add(propertyName);
    else
        base.PropertyHasChanged(propertyName);
}
public class PostponePropertyChecksToken : IDisposable
{
    private EicoBusinessBase<T> _businessObject;
    private PostponePropertyChecksToken() { }
    internal PostponePropertyChecksToken(EicoBusinessBase<T> businessObject)
    {
        _businessObject = businessObject;
        _businessObject._postponePropertyChecks = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool disposing)
    {
        if (!disposing) return;

        //enumerate all postponed call to property has changed
        _businessObject.PropertyHasChanged(_businessObject._postponeProperties);
        _businessObject._postponeProperties.Clear();
        _businessObject._postponePropertyChecks = false;
        _businessObject = null;
    }
}
protected virtual void PropertyHasChanged(IEnumerable<string> propertyNames)
{
    List<string> completed = new List<string>();
    foreach (string propertyName in propertyNames)
    {
        if (!completed.Contains(propertyName))
        {
            //need to call propertyHasChanged which capable on returning all affected properties
            //which later can be added to completed list
            //string[] propertyNames = _businessObject.PropertyHasChanged(propertyName);
            //completed.AddRange(propertyNames);
        }
    }
}

Copyright (c) Marimer LLC