ValidationRule with ValidationStep="updatedValue" вызывается с BindingExpression вместо обновленного значения

Я начинаю использовать ValidationRules в своем приложении WPF, но я в замешательстве.

У меня есть следующее простое правило:

class RequiredRule : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if (String.IsNullOrWhiteSpace(value as string))
        {
            return new ValidationResult(false, "Must not be empty");
        }
        else
        {
            return new ValidationResult(true, null);
        }

    }
}

Используется в XAML следующим образом:

<TextBox>
    <TextBox.Text>
        <Binding Path="Identity.Name">
            <Binding.ValidationRules>
                <validation:RequiredRule/>
            </Binding.ValidationRules>
         </Binding>
     </TextBox.Text>
</TextBox>

Это в основном работает, как я ожидал. Я был удивлен, увидев, что мой источник собственности (Identity.Name) не был установлен; У меня есть функция отмены, которая никогда не видит изменения, и нет никакого способа отменить значение, кроме его повторного ввода (не хорошо).

Обзор привязки данных Microsoft описывает процесс проверки в нижней части, который очень хорошо объясняет это поведение. Исходя из этого, я хотел бы иметь ValidationStep установлен в UpdatedValue,

<validation:RequiredRule ValidationStep="UpdatedValue"/>

Вот где все становится странным для меня. Вместо вызова Validate() со значением объекта, являющимся значением свойства, которое было установлено (т. Е. Строка), я получаю System.Windows.Data.BindingExpression! Я не вижу ничего в документации Microsoft, которая описывает это поведение.

В отладчике я вижу исходный объект (DataContext из TextBox), найдите путь к свойству и убедитесь, что значение установлено. Тем не менее, я не вижу хорошего способа добраться до нужного свойства в правиле валидации.

Примечание: с ValidationStep как ConvertedProposedValueЯ получаю введенную строку (конвертер не используется), но он также блокирует обновление свойства источника при сбое проверки, как и ожидалось. С CommittedValueЯ получаю BindingExpression вместо строки.

Здесь есть несколько вопросов:

  1. Почему я получаю несогласованный тип аргумента, который передается в Validate() на основе параметра ValidationStep?

  2. Как я могу получить фактическое значение из BindingExpression?

  3. С другой стороны, есть ли хороший способ позволить пользователю вернуть TextBox к предыдущему (действительному) состоянию? (Как я уже говорил, моя собственная функция отмены никогда не видит изменений.)

4 ответа

Решение

Я решил проблему извлечения значения из BindingExpressionс небольшим ограничением.

Сначала немного более полного XAML:

<Window x:Class="ValidationRuleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ValidationRuleTest"
        Title="MainWindow" Height="100" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="String 1"/>
        <TextBox Grid.Column="1">
            <TextBox.Text>
                <Binding Path="String1" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:RequiredRule ValidationStep="RawProposedValue"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBlock Text="String 2" Grid.Row="1"/>
        <TextBox Grid.Column="1" Grid.Row="1">
            <TextBox.Text>
                <Binding Path="String2" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:RequiredRule ValidationStep="UpdatedValue"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </Grid>
</Window>

Обратите внимание, что первый TextBox использует ValidationStep="RawProposedValue" (по умолчанию), а второй использует ValidationStep="UpdatedValue", но оба используют одно и то же правило проверки.

Простая ViewModel (без учета INPC и других полезных вещей):

class MainWindowViewModel
{
    public string String1
    { get; set; }

    public string String2
    { get; set; }
}

И наконец, новый RequiredRule:

class RequiredRule : ValidationRule
{
    public override ValidationResult Validate(object value,
        System.Globalization.CultureInfo cultureInfo)
    {
        // Get and convert the value
        string stringValue = GetBoundValue(value) as string;

        // Specific ValidationRule implementation...
        if (String.IsNullOrWhiteSpace(stringValue))
        {
            return new ValidationResult(false, "Must not be empty"); 
        }
        else
        {
            return new ValidationResult(true, null); 
        }
    }

    private object GetBoundValue(object value)
    {
        if (value is BindingExpression)
        {
            // ValidationStep was UpdatedValue or CommittedValue (Validate after setting)
            // Need to pull the value out of the BindingExpression.
            BindingExpression binding = (BindingExpression)value;

            // Get the bound object and name of the property
            object dataItem = binding.DataItem;
            string propertyName = binding.ParentBinding.Path.Path;

            // Extract the value of the property.
            object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null);

            // This is what we want.
            return propertyValue;
        }
        else
        {
            // ValidationStep was RawProposedValue or ConvertedProposedValue
            // The argument is already what we want!
            return value;
        }
    }
}

GetBoundValue() Метод найдет интересующее меня значение, если получит BindingExpression, или просто отбросит аргумент, если это не так. Реальный ключ находил "Путь", а затем использовал его, чтобы получить свойство и его значение.

Ограничение: в моем первоначальном вопросе мой переплет Path="Identity.Name", как я копался в подобъектах моей ViewModel. Это не будет работать, так как в приведенном выше коде ожидается, что путь будет напрямую к свойству связанного объекта. К счастью, я уже сплющил свою ViewModel, так что это больше не так, но можно было бы обойти эту проблему, чтобы сначала установить в качестве подобъекта объектный текст элемента управления.

Я хотел бы отдать должное Эдуардо Бритесу, так как его ответ и обсуждение вернули меня к размышлениям об этом и дали часть его головоломки. Кроме того, в то время как я собирался полностью отказаться от ValidationRules и использовать вместо этого IDataErrorInfo, мне нравится его предложение об их совместном использовании для различных типов и сложностей проверки.

Это продолжение ответа mbmcavoy.

Я изменил GetBoundValue метод для того, чтобы снять ограничение для путей привязки. BindingExpression удобно имеет свойства ResolvedSource и ResolvedSourcePropertyName, которые видны в отладчике, но не доступны через обычный код. Получить их с помощью рефлексии не проблема, и это решение должно работать с любым связующим путем.

private object GetBoundValue(object value)
{
    if (value is BindingExpression)
    {
        // ValidationStep was UpdatedValue or CommittedValue (validate after setting)
        // Need to pull the value out of the BindingExpression.
        BindingExpression binding = (BindingExpression)value;

        // Get the bound object and name of the property
        string resolvedPropertyName = binding.GetType().GetProperty("ResolvedSourcePropertyName", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null).ToString();
        object resolvedSource = binding.GetType().GetProperty("ResolvedSource", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null);

        // Extract the value of the property
        object propertyValue = resolvedSource.GetType().GetProperty(resolvedPropertyName).GetValue(resolvedSource, null);

        return propertyValue;
    }
    else
    {
        return value;
    }
}

Это альтернативное расширение ответа mbmcavoy и adabyron.

Чтобы снять ограничение на пути привязки, я получаю значение свойства, используя такой метод:

public static object GetPropertyValue(object obj, string propertyName)
{
    foreach (String part in propertyName.Split('.'))
    {
        if (obj == null) { return null; }

        Type type = obj.GetType();
        PropertyInfo info = type.GetProperty(part);
        if (info == null) { return null; }

        obj = info.GetValue(obj, null);
    }

    return obj;
}

Теперь просто поменяй

object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null);

в

object propertyValue = GetPropertyValue(dataItem, propertyName);

Связанный пост: Получить значение свойства из строки, используя отражение в C#

Чтобы ответить на ваш вопрос 2:

string strVal = (string) ((BindingExpression) value).DataItem

Другие вопросы по тегам