Обнаружение ошибок валидации WPF

В WPF вы можете настроить проверку на основе ошибок, возникших в вашем уровне данных во время привязки данных, используя ExceptionValidationRule или же DataErrorValidationRule,

Предположим, у вас есть несколько элементов управления, настроенных таким образом, и у вас есть кнопка Сохранить. Когда пользователь нажимает кнопку "Сохранить", необходимо убедиться, что нет ошибок проверки, прежде чем продолжить сохранение. Если есть ошибки валидации, вы хотите их исправить.

В WPF, как вы узнаете, есть ли в ваших элементах управления привязкой данных установленные ошибки проверки?

11 ответов

Решение

Этот пост был чрезвычайно полезным. Спасибо всем, кто внес свой вклад. Вот версия LINQ, которую вы будете любить или ненавидеть.

private void CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = IsValid(sender as DependencyObject);
}

private bool IsValid(DependencyObject obj)
{
    // The dependency object is valid if it has no errors and all
    // of its children (that are dependency objects) are error-free.
    return !Validation.GetHasError(obj) &&
    LogicalTreeHelper.GetChildren(obj)
    .OfType<DependencyObject>()
    .All(IsValid);
}

Следующий код (из книги "Программирование WPF" Криса Села и Иана Гриффитса) проверяет все правила связывания для объекта зависимости и его дочерних элементов:

public static class Validator
{

    public static bool IsValid(DependencyObject parent)
    {
        // Validate all the bindings on the parent
        bool valid = true;
        LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
        while (localValues.MoveNext())
        {
            LocalValueEntry entry = localValues.Current;
            if (BindingOperations.IsDataBound(parent, entry.Property))
            {
                Binding binding = BindingOperations.GetBinding(parent, entry.Property);
                foreach (ValidationRule rule in binding.ValidationRules)
                {
                    ValidationResult result = rule.Validate(parent.GetValue(entry.Property), null);
                    if (!result.IsValid)
                    {
                        BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
                        System.Windows.Controls.Validation.MarkInvalid(expression, new ValidationError(rule, expression, result.ErrorContent, null));
                        valid = false;
                    }
                }
            }
        }

        // Validate all the bindings on the children
        for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
        {
            DependencyObject child = VisualTreeHelper.GetChild(parent, i);
            if (!IsValid(child)) { valid = false; }
        }

        return valid;
    }

}

Вы можете вызвать это в вашем обработчике нажатия кнопки "Сохранить", как это происходит на вашей странице / в окне.

private void saveButton_Click(object sender, RoutedEventArgs e)
{

  if (Validator.IsValid(this)) // is valid
   {

    ....
   }
}

Размещенный код не работает для меня при использовании ListBox. Я переписал его, и теперь он работает:

public static bool IsValid(DependencyObject parent)
{
    if (Validation.GetHasError(parent))
        return false;

    // Validate all the bindings on the children
    for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
    {
        DependencyObject child = VisualTreeHelper.GetChild(parent, i);
        if (!IsValid(child)) { return false; }
    }

    return true;
}

Имел ту же проблему и попробовал предоставленные решения. Комбинация решений H-Man2 и skiba_k для меня почти сработала, за одним исключением: в моем окне есть TabControl. А правила проверки оцениваются только для того элемента TabItem, который в данный момент виден. Поэтому я заменил VisualTreeHelper на LogicalTreeHelper. Теперь это работает.

    public static bool IsValid(DependencyObject parent)
    {
        // Validate all the bindings on the parent
        bool valid = true;
        LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
        while (localValues.MoveNext())
        {
            LocalValueEntry entry = localValues.Current;
            if (BindingOperations.IsDataBound(parent, entry.Property))
            {
                Binding binding = BindingOperations.GetBinding(parent, entry.Property);
                if (binding.ValidationRules.Count > 0)
                {
                    BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
                    expression.UpdateSource();

                    if (expression.HasError)
                    {
                        valid = false;
                    }
                }
            }
        }

        // Validate all the bindings on the children
        System.Collections.IEnumerable children = LogicalTreeHelper.GetChildren(parent);
        foreach (object obj in children)
        {
            if (obj is DependencyObject)
            {
                DependencyObject child = (DependencyObject)obj;
                if (!IsValid(child)) { valid = false; }
            }
        }
        return valid;
    }

В дополнение к замечательной LINQ-реализации Дина, я с удовольствием упаковал код в расширение для DependencyObjects:

public static bool IsValid(this DependencyObject instance)
{
   // Validate recursivly
   return !Validation.GetHasError(instance) &&  LogicalTreeHelper.GetChildren(instance).OfType<DependencyObject>().All(child => child.IsValid());
}

Это делает его очень приятным, учитывая возможность повторного использования.

Я бы предложил небольшую оптимизацию.

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

Вот библиотека для проверки формы в WPF. Пакет Nuget здесь.

Образец:

<Border BorderBrush="{Binding Path=(validationScope:Scope.HasErrors),
                              Converter={local:BoolToBrushConverter},
                              ElementName=Form}"
        BorderThickness="1">
    <StackPanel x:Name="Form" validationScope:Scope.ForInputTypes="{x:Static validationScope:InputTypeCollection.Default}">
        <TextBox Text="{Binding SomeProperty}" />
        <TextBox Text="{Binding SomeOtherProperty}" />
    </StackPanel>
</Border>

Идея состоит в том, что мы определяем область проверки с помощью присоединенного свойства, сообщая ему, какие элементы управления вводом следует отслеживать. Тогда мы можем сделать:

<ItemsControl ItemsSource="{Binding Path=(validationScope:Scope.Errors),
                                    ElementName=Form}">
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type ValidationError}">
            <TextBlock Foreground="Red"
                       Text="{Binding ErrorContent}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Вы можете рекурсивно перебрать все дерево элементов управления и проверить вложенное свойство Validation.HasErrorProperty, а затем сосредоточиться на первом, которое вы найдете в нем.

вы также можете использовать множество уже написанных решений, вы можете проверить эту ветку для примера и дополнительной информации

В форме ответа aogan вместо явной итерации правил валидации лучше всего вызвать expression.UpdateSource():

if (BindingOperations.IsDataBound(parent, entry.Property))
{
    Binding binding = BindingOperations.GetBinding(parent, entry.Property);
    if (binding.ValidationRules.Count > 0)
    {
        BindingExpression expression 
            = BindingOperations.GetBindingExpression(parent, entry.Property);
        expression.UpdateSource();

        if (expression.HasError) valid = false;
    }
}

Я использую DataGrid, и приведенный выше обычный код не находил ошибок, пока сам DataGrid не потерял фокус. Даже с приведенным ниже кодом он по-прежнему не «видит» ошибку, пока строка не потеряет фокус, но это по крайней мере лучше, чем ждать, пока сетка не потеряет фокус.

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

      public static List<string> Errors { get; set; } = new();

public static bool IsValid(this DependencyObject parent)
{
    Errors.Clear();

    return IsValidInternal(parent);
}

private static bool IsValidInternal(DependencyObject parent)
{
    // Validate all the bindings on this instance
    bool valid = true;

    if (Validation.GetHasError(parent) ||
        GetRowsHasError(parent))
    {
        valid = false;

        /*
         * Find the error message and log it in the Errors list.
         */
        foreach (var error in Validation.GetErrors(parent))
        {
            if (error.ErrorContent is string errorMessage)
            {
                Errors.Add(errorMessage);
            }
            else
            {
                if (parent is Control control)
                {
                    Errors.Add($"<unknow error> on field `{control.Name}`");
                }
                else
                {
                    Errors.Add("<unknow error>");
                }
            }
        }
    }

    // Validate all the bindings on the children
    for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); i++)
    {
        var child = VisualTreeHelper.GetChild(parent, i);
        if (IsValidInternal(child) == false)
        {
            valid = false;
        }
    }

    return valid;
}

private static bool GetRowsHasError(DependencyObject parent)
{
    DataGridRow dataGridRow;

    if (parent is not DataGrid dataGrid)
    {
        /*
         * This is not a DataGrid, so return and say we do not have an error.
         * Errors for this object will be checked by the normal check instead.
         */
        return false;
    }

    foreach (var item in dataGrid.Items)
    {
        /*
         * Not sure why, but under some conditions I was returned a null dataGridRow
         * so I had to test for it.
         */
        dataGridRow = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(item);
        if (dataGridRow != null &&
            Validation.GetHasError(dataGridRow))
        {
            return true;
        }
    }
    return false;
}

Возможно, вас заинтересует пример приложения BookLibrary WPF Application Framework (WAF). В нем показано, как использовать проверку в WPF и как управлять кнопкой "Сохранить" при наличии ошибок проверки.

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