WPF ValidationRule с свойством зависимостей

Предположим, что у вас есть класс, наследующий от ValidationRule:

public class MyValidationRule : ValidationRule
{
    public string ValidationType { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {}
}

в XAML, который вы проверяете следующим образом:

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

Что работает, и все в порядке.

Но предположим, теперь вы хотите иметь ValidationType="{Binding MyBinding}", где MyBinding происходит от DataContext.

Для этого мне нужно сделать MyValidationRule как DependencyObject и добавить Свойство зависимостей.

Я попытался написать класс DependencyObject и связать его. Есть 2 проблемы, хотя.. ValidationRule НЕ имеет DataContext из Combobox/Item.

Есть ли у вас какие-либо идеи, как это решить?

Спасибо!

Ответы

Ответ 1

Так как ValidationRule не наследует от DependencyObject, вы не можете создать DependecyProperty в своем настраиваемом классе проверки.

Однако, как описано в этой ссылке, вы можете иметь нормальное свойство в своем классе проверки, который имеет тип, который наследуется от DependecyObject и создайте DependencyProperty в этом классе.

Например, это пользовательский класс ValidationRule, который поддерживает свойство bindable:

[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 - это простой класс, наследующий от DependencyObject и имеющий 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));

Это решает исходную проблему, но, к сожалению, вызывает еще две проблемы:

  • Связывание работает неправильно, поскольку ValidationRules не является частью визуального дерева и поэтому не может правильно получить связанное свойство. Например, этот наивный подход не будет работать:

    <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>
    

    Вместо этого следует использовать прокси-объект, как описано в этом:

    <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 - простой класс:

    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. Если свойство в пользовательском ValidationRule связано с другим свойством объекта, логика проверки для исходного свойства не будет срабатывать при изменении этого другого свойства объекта.

    Чтобы решить эту проблему, мы должны обновить привязку при обновлении свойства привязки ValidationRule. Сначала мы должны привязать это свойство к нашему классу ComparisonValue. Затем мы можем обновить источник привязки при изменении свойства Value:

    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));
    }
    

    В первом случае существует такая же проблема с прокси-сервером. Поэтому мы должны создать еще один прокси-объект:

    <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>
    

    В этом случае свойство Text TextBoxToValidate проверяется на свойство Items.Count SomeCollection. Когда количество элементов в списке изменится, будет активирована проверка для свойства Text.