How can I validate multiple properties when any of them changes?
Asked Answered
H

4

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.

Handfasting answered 19/8, 2011 at 12:53 Comment(9)
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.Byyourleave
@Byyourleave Would love to, but I don't know how to manually raise the validation event for individual propertiesHandfasting
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.Byyourleave
@Byyourleave 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?Handfasting
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.Byyourleave
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...Hollenbeck
@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.Handfasting
@Handfasting 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).Hollenbeck
@bathineni Oh that's true, I have no clue what I was thinking Friday lolHandfasting
H
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.

Handfasting answered 19/8, 2011 at 14:36 Comment(4)
I'm having the exact same issue as you. Did you go with the INotifyPropertyChanging solution or did you find another way?Infecund
@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 propertiesHandfasting
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.Infecund
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.Black
B
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;
                }
...
Brann answered 9/11, 2011 at 8:48 Comment(0)
L
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.

Lampe answered 19/8, 2011 at 14:29 Comment(0)
S
-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();
}
Sailor answered 21/7, 2015 at 15:14 Comment(2)
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.Handfasting
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.Sailor

© 2022 - 2024 — McMap. All rights reserved.