Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to trigger validation on one property when another property has been validated, using custom ValidationAttribute and INotifyDataErrorInfo

Up until recently, I have used a custom extended version of the IDataErrorInfo interface. My extension enables me to work with multiple errors simultaneously and so far, it's served me very well. However, with the introduction of the INotifyDataErrorInfo interface, I thought I'd experiment with it to see if there was any improvement.

After following some online tutorials, I got it working with the various ValidationAttributes from the System.ComponentModel.DataAnnotations namespace. Using these Attributes let you provide basic validation rules like this:

[MinLength(3, ErrorMessage = "Name must be longer than 3 characters.")]
public string Name
{
    get { return name; }
    set { name = value; NotifyPropertyChanged("Name"); Validate("Name", name); }
}

Initially, it seemed pretty good, as the error messages plug right into the Valaidation.Errors collection available in the applied ErrorTemplates. However, most of the built in validation rules are really basic and I'm used to having to implement complicated validation rules that involve other property values.

So I set out to find a way to create a simple validation rule that involved multiple properties: A rule that one of two or more fields must be set. So I declared a class that extended the ValidationAttribute and after searching online, found a way to access the other property values.

I knocked up a basic UI with a custom ErrorTemplate applied to each TextBox, that displayed the Validation.Errors collection for the data bound property:

<ControlTemplate x:Key="ErrorTemplate">
    <StackPanel Orientation="Horizontal">
        <Border BorderBrush="#4FFF0000" BorderThickness="1" Margin="0,10">
            <AdornedElementPlaceholder />
        </Border>
        <Image Name="WarningImage" Source="pack://application:,,,/WpfApplication1;component/Images/Warning_16.png" Margin="5,0,0,0" Tag="{Binding}" />
        <Popup PlacementTarget="{Binding ElementName=WarningImage}" Placement="Right" Margin="5,0,0,0" AllowsTransparency="True" IsOpen="True">
            <Border BorderThickness="1" BorderBrush="#4FFF0000" CornerRadius="5" Background="White" Padding="5" Margin="10">
                <Border.Effect>
                    <DropShadowEffect Color="Red" Opacity="0.5" BlurRadius="15" ShadowDepth="0" />
                </Border.Effect>
                <ItemsControl ItemsSource="{Binding}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ErrorContent}" />
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </Border>
        </Popup>
    </StackPanel>
</ControlTemplate>

With my custom Attribute set on the Name property, I managed to add a ValidationResult into the Validation.Errors collection through the interface when neither property was set, but here's the problem: If I added a value into one of the other TextBoxes data bound to the other required properties, the error message in the first TextBox would stay there.

If I went back to the first TextBox and typed something, then the validation would work, so even if I deleted the value, it still knew that one of the required properties was set. So the validation code works, but the problem is that property changes to the other required properties do not trigger the validation in the Name property.

Even when I applied the same custom Attribute to the other required properties, the same thing happened... each validation error would only clear when typing in its related TextBox. I also tried the built in CustomValidationAttribute which enables us to call a method in the class to validate with, but the end result was the same.

The validation code works, but is just not triggered from the other required property changes. I even tried calling the Validate method, passing in the names of the other properties, but that ended in a continuous loop. So the question is, how can I trigger a validation on one property when another property has been validated?

like image 428
Sheridan Avatar asked Nov 01 '22 20:11

Sheridan


1 Answers

Here's what I did, in a class containing From and To properties. I wanted to validate that From is less than or equal to To.

The validation logic is applied using CustomValidationAttribute, which is easier than creating your own validation attribute classes. You simply tell it the type of your class, and the name of the method to call that contains your validation logic (the method must have a specific signature though). Here is my relevant code:-

    [CustomValidation(typeof(MyModel), "ValidateRange")]
    public double From
    {
        get
        {
            return _from;
        }
        set
        {
            if (_from != value)
            {
                _from = value;
                OnPropertyChanged("From");

                // Validate the other side
                ValidateProperty("To", _to);
            }
        }
    }

    [CustomValidation(typeof(MyModel), "ValidateRange")]
    public double To
    {
        get
        {
            return _to;
        }
        set
        {
            if (_to != value)
            {
                _to = value;
                OnPropertyChanged("To");

                // Validate the other side
                ValidateProperty("From", _from);
            }
        }
    }

    private static ValidationResult ValidateRange(ValidationContext validationContext)
    {
        var model = validationContext.ObjectInstance as MyModel;

        if (model.From > model.To)
        {
            return new ValidationResult("Invalid range");
        }

        return null;
    }

As you can see, the code in one property setter forces validation of the "other" property, exactly as you mentioned in your last paragraph. There's no reason why it should go into an endless loop, unless your validation code is trying to set one of the properties, which would trigger another call to Validate(), and so on, and so on.

like image 65
Andrew Stephens Avatar answered Nov 15 '22 06:11

Andrew Stephens