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 из комбинированного списка / поз.

У вас есть идеи, как это решить?

Спасибо !

Ответы на вопрос(1)

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

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

Например, здесь есть обычайValidationRule класс, поддерживающий привязываемое свойство:

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

Если свойство на заказ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 собственность будет запущена.

Ваш ответ на вопрос