Подтвердить что ты не робот

ValidationRule с ValidationStep = "UpdateValue" вызывается с 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() с значением объекта, являющимся значением свойства, которое было установлено (т.е. Строка), я получаю a System.Windows.Data.BindingExpression! Я не вижу ничего в документации Microsoft, которая описывает это поведение.

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

Это расширение для mbmcavoy answer.

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

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

Ответ 3

Это альтернативное расширение для mbmcavoy и адаробного ответа.

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

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

to

object propertyValue = GetPropertyValue(dataItem, propertyName);

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

Ответ 4

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

string strVal = (строка) ((значение BindingExpression)).DataItem