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.

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


person PaN1C_Showt1Me    schedule 05.10.2010    source источник


Ответы (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));

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

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

    Чтобы решить эту проблему, мы должны обновить привязку при обновлении свойства bound 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.

person Yusuf Tarık Günaydın    schedule 13.07.2016
comment
Спасибо за отличный полезный ответ. - person ; 17.07.2020