23

I have two date fields: StartDate and EndDate. StartDate must be earlier than EndDate.

If the user changes the StartDate to something greater than the EndDate, a red border appears around that DatePicker, and vise versa. If the user changes the 2nd box so that the date range is now correct, the 1st box still has the Validation Error.

How can I validate both date fields when either one of them changes?

enter image description here

I'm using IDataErrorInfo

public string GetValidationError(string propertyName)
{
    switch (propertyName)
    {
        case "StartDate":
            if (StartDate > EndDate)
                s = "Start Date cannot be later than End Date";
            break;

        case "EndDate":
            if (StartDate > EndDate)
                s = "End Date cannot be earlier than Start Date";
            break;
    }

    return s;
}

I cannot simply raise a PropertyChange event because I need to validate both fields when either of them changes, so having both of them raise a PropertyChange event for the other will get stuck in an infinite loop.

I also do not like the idea of clearing the Date field if the other date returns a validation error.

Rachel
  • 130,264
  • 66
  • 304
  • 490
  • Just to throw an alternate approach out there, can you make both boxes red when either rule is broken? If so you would only have to raise one propertychanged event. – Josh Aug 19 '11 at 12:56
  • @Josh Would love to, but I don't know how to manually raise the validation event for individual properties – Rachel Aug 19 '11 at 12:59
  • I quit using Validation Events. I've had way too many issues with them where I think they should fire and they don't or they do fire and the border doesn't highlight. I use a Red Border style bound to a boolean representation of valid. I also bind the tooltip to a message property. Then when a value changes I validate it and set the properties appropriately. – Josh Aug 19 '11 at 13:10
  • @Josh Doesn't that mean you either need `PropertyIsValid` and `PropertyErrorMessage` for every single property in the class you're validating? Either that, or your validation occurs on the entire class and not individual properties? – Rachel Aug 19 '11 at 13:17
  • Yes it does. It has the potential to be property overload, but it's reliable and quick to hook up. For me, the trade-off between less headaches and more control trumps having two validation properties per field. – Josh Aug 19 '11 at 13:26
  • 2
    Raising PropertyChanged in GetValidationError method will stuck in the infinite loop.. but raising property changed on the setter of start and end date may solve your problem... – Bathineni Aug 19 '11 at 15:30
  • @bathineni It will still get stuck in a loop since raising a PropertyChanged on StartDate will raise PropertyChanged on EndDate, and triggering PropertyChanged on EndDate will raise PropertyChanged on StartDate again. – Rachel Aug 19 '11 at 16:04
  • @Rachel as far as i know property change will call only getters.. and our property changed will be in setters... so Enddate property changed on startdate setter will call Enddate getter (no propertychange will be raised for startdate in enddate getter). – Bathineni Aug 22 '11 at 08:04
  • @bathineni Oh that's true, I have no clue what I was thinking Friday lol – Rachel Aug 22 '11 at 12:38

4 Answers4

17

The simplest way is to raise a PropertyChanged notification for in the setter for both properties that need to be validated like bathineni suggests

private DateTime StartDate
{
    get { return _startDate; }
    set
    {
        if (_startDate != value)
        {
            _startDate = value;
            RaisePropertyChanged("StartDate");
            RaisePropertyChanged("EndDate");
        }
    }
}

private DateTime EndDate
{
    get { return _endDate; }
    set
    {
        if (_endDate!= value)
        {
            _endDate= value;
            RaisePropertyChanged("StartDate");
            RaisePropertyChanged("EndDate");
        }
    }
}

However if that doesn't work for you, I figured out one way to validate a group of properties together, although your classes have to implement INotifyPropertyChanging in addition to INotifyPropertyChanged (I'm using EntityFramework and by default their classes implement both interfaces)

Extension Method

public static class ValidationGroup
{
    public delegate string ValidationDelegate(string propertyName);
    public delegate void PropertyChangedDelegate(string propertyName);

    public static void AddValidationGroup<T>(this T obj, 
        List<string> validationGroup, bool validationFlag,
        ValidationDelegate validationDelegate, 
        PropertyChangedDelegate propertyChangedDelegate)
        where T : INotifyPropertyChanged, INotifyPropertyChanging
    {

        // This delegate runs before a PropertyChanged event. If the property
        // being changed exists within the Validation Group, check for validation
        // errors on the other fields in the group. If there is an error with one
        // of them, set a flag to true.
        obj.PropertyChanging += delegate(object sender, PropertyChangingEventArgs e)
        {
            if (validationGroup.Contains(e.PropertyName))
            {
                foreach(var property in validationGroup)
                {
                    if (validationDelegate(property) != null)
                    {
                        validationFlag = true;
                        break;
                    }
                }
            }
        };

        // After the Property gets changed, if another field in this group was
        // invalid prior to the change, then raise the PropertyChanged event for 
        // all other fields in the Validation Group to update them.
        // Also turn flag off so it doesn't get stuck in an infinite loop
        obj.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e)
        {
            if (validationGroup.Contains(e.PropertyName))
            {
                if (validationFlag && validationDelegate(e.PropertyName) == null)
                {
                    validationFlag = false;
                    foreach(var property in validationGroup)
                    {
                        propertyChangedDelegate(property);
                    }
                }
            }
        };
    }
}

To use it, add the following call to the constructor of any class that should validate a group of properties together.

this.AddValidationGroup(
    new List<string> { "StartDate", "EndDate" },
    GetValidationError, OnPropertyChanged);

I've tested this with up to 3 properties in a Validation Group and it seems to work OK.

Community
  • 1
  • 1
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • I'm having the exact same issue as you. Did you go with the INotifyPropertyChanging solution or did you find another way? – Björn Nov 24 '11 at 07:14
  • @Björn I went with `INotifyPropertyChanging` in this case because the interface was already implemented by Entity Framework. Had it not been, I probably would have just raised my PropertyChanged events in the `set` methods of the related properties – Rachel Nov 24 '11 at 17:20
  • Thanks for the info. I removed my validation and changed my code so that if StartDate > EndDate i simply set EndDate = StartDate. Maybe not something everyone can do but it suited in my case. – Björn Nov 24 '11 at 18:38
  • 1
    This is a convenient way of doing it, but it seems a code-smell to me. You're raising `PropertyChanged` events on properties that haven't changed, which apart from being wrong in itself could have other knock-on effects. Not that there are easy ways of handling this. – nicodemus13 Nov 26 '12 at 16:06
2

Use this trick, it prevents they call OnPropertyChanged each other :

private bool RPCfromStartDate = false;
private bool RPCfromEndDate = false;

public string this[string columnName]
{
    get 
    {
                string result = null;
                switch (columnName)
                {
                    case "StartDate":
                        if (StartDate.Date >= EndDate.Date)
                        {
                            result = "Start Date cannot be later than End Date";
                        }
                        if (!RPCfromEndDate)
                        {
                            RPCfromStartDate = true;                            
                            OnPropertyChanged("EndDate");
                            RPCfromStartDate = false;
                        } 
                    case "EndDate":
                        if (StartDate.Date >= EndDate.Date)
                        {
                            result = "End Date cannot be earlier than Start Date";
                        }
                        if (!RPCfromStartDate)
                        {
                            RPCfromEndDate = true;                            
                            OnPropertyChanged("StartDate");
                            RPCfromEndDate = false;
                        } 
                        break;
                }
...
JatSing
  • 4,857
  • 16
  • 55
  • 65
0

I generally add all of my validation errors to a dictionary, and have the validation template subscribe to that via the property name. In each property changed event handler, I can check any number of properties and add or remove their validation status as necessary.

Check this answer for how my implementation looks. Sorry it is in VB.NET, but should be fairly straightforward.

Community
  • 1
  • 1
CodeWarrior
  • 7,388
  • 7
  • 51
  • 78
-1

You can also subscribe to the SelectedDateChanged event handler and update needed binding.

BindingExpression expression = datePickerStartDate.GetBindingExpression(DatePicker.SelectedDateProperty);
if (expression != null)
{
    expression.UpdateSource();
}
  • As I said in my question, this does not work because I have two dates and so it will get stuck in an infinite loop. – Rachel Jul 22 '15 at 01:01
  • I am not sure what is confusing you. When startDate is changing, you update EndDate binding (just validation occurs, no change is made). When endDate is changing, you update StartDate binding. No infinite loops should occur. – Paul Solomenchuk Oct 26 '15 at 11:56