Detectar errores de validación de WPF

115

En WPF, puede configurar la validación en función de los errores lanzados en su capa de datos durante el enlace de datos utilizando ExceptionValidationRuleo DataErrorValidationRule.

Suponga que tiene un montón de controles configurados de esta manera y tiene un botón Guardar. Cuando el usuario hace clic en el botón Guardar, debe asegurarse de que no haya errores de validación antes de continuar con el guardado. Si hay errores de validación, querrás gritarles.

En WPF, ¿cómo puede saber si alguno de sus controles Data Bound tiene errores de validación configurados?

Kevin Berridge
fuente

Respuestas:

137

Esta publicación fue de gran ayuda. Gracias a todos los que contribuyeron. Aquí hay una versión de LINQ que amarás u odiarás.

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);
}
Decano
fuente
1
¡Me gusta mucho esta solución en particular!
ChristopheD
Acabo de tropezar con este hilo. Pequeña función muy útil. ¡Gracias!
Olav Haugen
¿Hay alguna forma de enumerar solo aquellos DependencyObjects que estaban vinculados a un DataContext en particular? No me gusta la idea de caminar por los árboles. Puede haber una colección de enlaces vinculados a una fuente de datos en particular.
ZAB
5
Me pregunto, ¿cómo se llama a la IsValidfunción? Veo que ha configurado una CanExecuteque supongo que está relacionada con el comando del botón Guardar. ¿Funcionará esto si no estoy usando comandos? ¿Y cómo se relaciona el botón con los otros controles que deben verificarse? Mi único pensamiento sobre cómo usar esto es llamando IsValida cada control que necesita ser validado. Editar: Parece que está validando el senderque espero que sea el botón Guardar. Eso no me parece correcto.
Nicholas Miller
1
@Nick Miller a Windowtambién es un objeto de dependencia. Probablemente lo esté configurando con algún tipo de controlador de eventos en el Window. Alternativamente, puede llamarlo directamente IsValid(this)desde la Windowclase.
Akousmata
47

El siguiente código (del libro Programming WPF de Chris Sell e Ian Griffiths) valida todas las reglas de enlace en un objeto de dependencia y sus hijos:

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

}

Puede llamar a esto en su botón Guardar, haga clic en el controlador de eventos de esta manera en su página / ventana

private void saveButton_Click(object sender, RoutedEventArgs e)
{

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

    ....
   }
}
aogan
fuente
33

El código publicado no me funcionó al usar un ListBox. Lo reescribí y ahora funciona:

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
fuente
1
Vote a favor su solución para trabajar en mi ItemsControl.
Jeff T.
1
Estoy usando esta solución para verificar si mi cuadrícula de datos tiene errores de validación. Sin embargo, este método se llama en mi método canexecute del comando viewmodel, y creo que acceder a los objetos del árbol visual de alguna manera viola el patrón MVVM, ¿no? ¿Alguna alternativa?
Igor Kondrasovas
16

Tuve el mismo problema y probé las soluciones proporcionadas. Una combinación de las soluciones de H-Man2 y skiba_k funcionó casi bien para mí, con una excepción: My Window tiene un TabControl. Y las reglas de validación solo se evalúan para el TabItem que está visible actualmente. Así que reemplacé VisualTreeHelper por LogicalTreeHelper. Ahora funciona.

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

fuente
7

Además de la gran implementación de LINQ de Dean, me divertí encajando el código en una extensión para DependencyObjects:

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

Esto lo hace extremadamente agradable considerando la reutilización.

Matthias Loerke
fuente
2

Ofrecería una pequeña optimización.

Si hace esto muchas veces sobre los mismos controles, puede agregar el código anterior para mantener una lista de controles que realmente tienen reglas de validación. Luego, siempre que necesite verificar la validez, solo revise esos controles, en lugar de todo el árbol visual. Esto resultaría mucho mejor si tiene muchos de estos controles.

duende
fuente
2

Aquí hay una biblioteca para la validación de formularios en WPF. Paquete Nuget aquí .

Muestra:

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

La idea es que definamos un alcance de validación a través de la propiedad adjunta que le indica qué controles de entrada rastrear. Entonces podemos hacer:

<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>
Johan Larsson
fuente
0

Puede iterar sobre todo su árbol de controles de forma recursiva y comprobar la propiedad adjunta Validation.HasErrorProperty, luego centrarse en la primera que encuentre en ella.

También puede utilizar muchas soluciones ya escritas. Puede consultar este hilo para ver un ejemplo y más información.

user21243
fuente
0

Puede que le interese la aplicación de muestra BookLibrary de WPF Application Framework (WAF) . Muestra cómo usar la validación en WPF y cómo controlar el botón Guardar cuando existen errores de validación.

jbe
fuente
0

En forma de respuesta aogan, en lugar de iterar explícitamente a través de las reglas de validación, es mejor invocar 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;
    }
}
Dan está jugando a la luz del fuego
fuente