WPF ValidationRule with dependency property

17,724

Since ValidationRule does not inherit from DependencyObject you cannot create a DependecyProperty in your custom validation class.

However as explained in this link you can have a normal property in your validation class which is of a type that inherits from DependecyObject and create a DependencyProperty in that class.

For example here is a custom ValidationRule class that support bindable property:

[ContentProperty("ComparisonValue")]
public class GreaterThanValidationRule : ValidationRule
{
    public ComparisonValue ComparisonValue { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        string s = value?.ToString();
        int number;

        if (!Int32.TryParse(s, out number))
        {
            return new ValidationResult(false, "Not a valid entry");
        }

        if (number <= ComparisonValue.Value)
        {
            return new ValidationResult(false, $"Number should be greater than {ComparisonValue}");
        }

        return ValidationResult.ValidResult;
    }
}

ComparisonValue is a simple class that inherits from DependencyObject and has a DependencyProperty:

public class ComparisonValue : DependencyObject
{
    public int Value
    {
        get { return (int)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
        nameof(Value),
        typeof(int),
        typeof(ComparisonValue),
        new PropertyMetadata(default(int));

This solves the original problem but unfortunately brings two more problems:

  1. The binding does not work correctly since the ValidationRules is not part of visual tree and therefore cannot get the bound property correctly. For example this naive approach will not work:

    <TextBox Name="TextBoxToValidate">
        <TextBox.Text>
            <Binding Path="ViewModelProperty" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <numbers:GreaterThanValidationRule>
                        <numbers:ComparisonValue Value="{Binding Text, ElementName=TextBoxToValidate}"/>
                    </numbers:GreaterThanValidationRule>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    

    Instead a proxy object should be used as explained in this answer:

    <TextBox Name="TextBoxToValidate">
        <TextBox.Resources>
            <bindingExtensions:BindingProxy x:Key="TargetProxy" Data="{Binding Path=Text, ElementName=TextBoxToValidate}"/>
        </TextBox.Resources>
        <TextBox.Text>
            <Binding Path="ViewModelProperty" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <numbers:GreaterThanValidationRule>
                        <numbers:ComparisonValue Value="{Binding Data, Source={StaticResource TargetProxy}}"/>
                    </numbers:GreaterThanValidationRule>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    

    BindingProxy is a simple class:

    public class BindingProxy : Freezable
    {
        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }
    
        public object Data
        {
            get { return GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }
        public static readonly DependencyProperty DataProperty = DependencyProperty.Register(nameof(Data), typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
    }
    

  1. If the property in custom ValidationRule is bound to another object's property, the validation logic for the original property will not fire when that other object's property changes.

    To solve this problem we should update the binding when the ValidationRule's bound property is updated. First we should bind that property to our ComparisonValue class. Then, we can update the source of the binding when the Value property changes:

    public class ComparisonValue : DependencyObject
    {
        public int Value
        {
            get { return (int)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
            nameof(Value),
            typeof(int),
            typeof(ComparisonValue),
            new PropertyMetadata(default(int), OnValueChanged));
    
        private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ComparisonValue comparisonValue = (ComparisonValue) d;
            BindingExpressionBase bindingExpressionBase = BindingOperations.GetBindingExpressionBase(comparisonValue, BindingToTriggerProperty);
            bindingExpressionBase?.UpdateSource();
        }
    
        public object BindingToTrigger
        {
            get { return GetValue(BindingToTriggerProperty); }
            set { SetValue(BindingToTriggerProperty, value); }
        }
        public static readonly DependencyProperty BindingToTriggerProperty = DependencyProperty.Register(
            nameof(BindingToTrigger),
            typeof(object),
            typeof(ComparisonValue),
            new FrameworkPropertyMetadata(default(object), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    }
    

    The same proxy problem in the first case also exists here. Therefore we should create another proxy object:

    <ItemsControl Name="SomeCollection" ItemsSource="{Binding ViewModelCollectionSource}"/>
    
    <TextBox Name="TextBoxToValidate">
        <TextBox.Resources>
            <bindingExtensions:BindingProxy x:Key="TargetProxy" Data="{Binding Path=Items.Count, ElementName=SomeCollection}"/>
            <bindingExtensions:BindingProxy x:Key="SourceProxy" Data="{Binding Path=Text, ElementName=TextBoxToValidate, Mode=TwoWay}"/>
        </TextBox.Resources>
        <TextBox.Text>
            <Binding Path="ViewModelProperty" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <numbers:GreaterThanValidationRule>
                        <numbers:ComparisonValue Value="{Binding Data, Source={StaticResource TargetProxy}}" BindingToTrigger="{Binding Data, Source={StaticResource SourceProxy}}"/>
                    </numbers:GreaterThanValidationRule>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    

    In this case the Text property of TextBoxToValidate is validated against the Items.Count property of SomeCollection. When the number of items in the list changes, the validation for the Text property will be triggered.

Share:
17,724
theSpyCry
Author by

theSpyCry

Updated on June 18, 2022

Comments

  • theSpyCry
    theSpyCry about 2 years

    Suppose you have a class inheriting from ValidationRule:

    public class MyValidationRule : ValidationRule
    {
        public string ValidationType { get; set; }
        
        public override ValidationResult Validate(object value, CultureInfo cultureInfo) {}
    }
    

    In XAML you are validating like this:

    <ComboBox.SelectedItem>
        <Binding Path="MyPath" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
            <Binding.ValidationRules>
                <qmvalidation:MyValidationRule  ValidationType="notnull"/>
            </Binding.ValidationRules>
        </Binding>
    </ComboBox.SelectedItem>
    

    Which works and everything is ok.

    But suppose now, you want to have ValidationType="{Binding MyBinding}" where MyBinding comes from DataContext.

    For this purpose I would need to make MyValidationRule as a DependencyObject and add a Dependency Property.

    I've tried to write a class that is DependencyObject, and bind it. There are 2 problems though.. the ValidationRule DOES NOT have the DataContext from the Combobox / Item.

    Do you have any ideas, on how to solve that?

  • Admin
    Admin almost 4 years
    Thank you for this great useful answer.