Detecting WPF Validation Errors
Asked Answered
F

11

123

In WPF you can setup validation based on errors thrown in your Data Layer during Data Binding using the ExceptionValidationRule or DataErrorValidationRule.

Suppose you had a bunch of controls set up this way and you had a Save button. When the user clicks the Save button, you need to make sure there are no validation errors before proceeding with the save. If there are validation errors, you want to holler at them.

In WPF, how do you find out if any of your Data Bound controls have validation errors set?

Front answered 24/9, 2008 at 14:22 Comment(0)
I
147

This post was extremely helpful. Thanks to all who contributed. Here is a LINQ version that you will either love or hate.

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);
}
Ingenuous answered 10/1, 2011 at 18:59 Comment(6)
I like this particular solution a lot !Kleenex
Just stumbled over this thread. Very useful little function. Thanks!Sanative
Is there any way to enum only those DependencyObjects who were bound to a particular DataContext? I do not like the idea of treewalk. There may be a collection of bindings linked to a particular data source.Gnosticism
Just wondering, how do you call the IsValid function? I see you have set up a CanExecute which I would guess is related to the Save button's command. Will this work if I'm not using commands? And how is the button related to the other controls which need to be checked? My only thought of how to use this is by calling IsValid for each control that needs to be validated. Edit: It seems like you are validating the sender which I expect to be the save button. That doesn't seem to be right to me.Minna
@Nick Miller a Window is also a dependency object. I he is probably setting it up with some sort of event handler on the Window. Alternatively, you could just call it directly with IsValid(this) from the Window class.Rosenfeld
def loving it!!Fries
H
50

The following code (from Programming WPF book by Chris Sell & Ian Griffiths) validates all binding rules on a dependency object and its children:

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

}

You can call this in your save button click event handler like this in your page/window

private void saveButton_Click(object sender, RoutedEventArgs e)
{

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

    ....
   }
}
Hog answered 24/9, 2008 at 16:56 Comment(1)
I like this answer because the code also validates untouched bindings.Dilettantism
D
35

The posted code did not work for me when using a ListBox. I rewrote it and now it works:

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;
}
Divebomb answered 19/2, 2009 at 14:31 Comment(3)
Up vote your solution for working on my ItemsControl.Godbeare
I am using this solution to check if my datagrid has validation errors. However, this method is called on my viewmodel command canexecute method, and I think accessing visual tree objects somehow violates MVVM patter, don't you? Any Alternatives?Sub
Works with WPF's DataGrid. I couldn't find a way of getting aogan's to work with the DataGrid.Stakeout
R
16

Had the same problem and tried the provided solutions. A combination of H-Man2's and skiba_k's solutions worked almost fine for me, for one exception: My Window has a TabControl. And the validation rules only get evaluated for the TabItem that is currently visible. So I replaced VisualTreeHelper by LogicalTreeHelper. Now it works.

    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;
    }
Resume answered 23/10, 2009 at 12:6 Comment(0)
I
7

In addition to the great LINQ-implementation of Dean, I had fun wrapping the code into an extension for DependencyObjects:

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

This makes it extremely nice considering reuseablity.

Innovate answered 31/10, 2012 at 22:6 Comment(0)
L
2

I would offer a small optimization.

If you do this many times over the same controls, you can add the above code to keep a list of controls that actually have validation rules. Then whenever you need to check for validity, only go over those controls, instead of the whole visual tree. This would prove to be much better if you have many such controls.

Leyva answered 1/7, 2010 at 8:40 Comment(0)
D
2

Here is a library for form validation in WPF. Nuget package here.

Sample:

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

The idea is that we define a validation scope via the attached property telling it what input controls to track. Then we can do:

<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>
Diehard answered 10/2, 2016 at 11:19 Comment(0)
E
0

You can iterate over all your controls tree recursively and check the attached property Validation.HasErrorProperty, then focus on the first one you find in it.

you can also use many already-written solutions you can check this thread for an example and more information

Exterritorial answered 24/9, 2008 at 14:29 Comment(0)
B
0

In answer form aogan, instead of explicitly iterate through validation rules, better just invoke 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;
    }
}
Blen answered 6/7, 2009 at 6:42 Comment(0)
D
0

You might be interested in the BookLibrary sample application of the WPF Application Framework (WAF). It shows how to use validation in WPF and how to control the Save button when validation errors exists.

Dade answered 16/8, 2010 at 17:19 Comment(0)
S
0

I am using a DataGrid, and the normal code above did not find errors until the DataGrid itself lost focus. Even with the code below, it still doesn't "see" an error until the row loses focus, but that's at least better than waiting until the grid loses focus.

This version also tracks all errors in a string list. Most of the other version in this post do not do that, so they can stop on the first error.

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;
}
Stakeout answered 1/9, 2022 at 19:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.