Detectando Erros de Validação WPF

115

No WPF, você pode configurar a validação com base nos erros lançados em sua camada de dados durante a vinculação de dados usando ExceptionValidationRuleou DataErrorValidationRule.

Suponha que você tenha vários controles configurados dessa forma e um botão Salvar. Quando o usuário clica no botão Salvar, você precisa se certificar de que não haja erros de validação antes de prosseguir com o salvamento. Se houver erros de validação, você deve gritar com eles.

No WPF, como você descobre se algum de seus controles Data Bound tem erros de validação configurados?

Kevin berridge
fonte

Respostas:

137

Esta postagem foi extremamente útil. Obrigado a todos que contribuíram. Aqui está uma versão do LINQ que você vai amar ou odiar.

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);
}
reitor
fonte
1
Gosto muito desta solução em particular!
ChristopheD de
Apenas tropecei neste tópico. Pequena função muito útil. Obrigado!
Olav Haugen
Existe alguma maneira de enumerar apenas os DependencyObjects que foram vinculados a um DataContext específico? Não gosto da ideia de treewalk. Pode haver uma coleção de ligações vinculadas a uma fonte de dados específica.
ZAB de
5
Basta saber, como você chama a IsValidfunção? Vejo que você configurou um CanExecuteque eu acho que está relacionado ao comando do botão Salvar. Isso funcionará se eu não estiver usando comandos? E como o botão se relaciona com os outros controles que precisam ser verificados? Meu único pensamento de como usar isso é chamar IsValidcada controle que precisa ser validado. Edit: Parece que você está validando o senderque espero ser o botão Salvar. Isso não parece estar certo para mim.
Nicholas Miller de
1
@Nick Miller a Windowtambém é um objeto de dependência. Ele provavelmente está configurando isso com algum tipo de manipulador de eventos no Window. Como alternativa, você pode ligar diretamente IsValid(this)para a Windowclasse.
akousmata de
47

O código a seguir (do livro Programming WPF de Chris Sell & Ian Griffiths) valida todas as regras de vinculação em um objeto de dependência e seus filhos:

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

}

Você pode chamar isso em seu manipulador de eventos de clique do botão Salvar como este em sua página / janela

private void saveButton_Click(object sender, RoutedEventArgs e)
{

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

    ....
   }
}
aogan
fonte
33

O código postado não funcionou para mim ao usar um ListBox. Eu reescrevi e agora 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
fonte
1
Vote sua solução para trabalhar em meu ItemsControl.
Jeff T.
1
Estou usando esta solução para verificar se meu datagrid tem erros de validação. No entanto, esse método é chamado em meu comando viewmodel canexecute method, e eu acho que acessar objetos de árvore visual de alguma forma viola o padrão MVVM, não é? Quaisquer alternativas?
Igor Kondrasovas
16

Tive o mesmo problema e tentei as soluções fornecidas. Uma combinação de soluções de H-Man2 e skiba_k funcionou quase bem para mim, para uma exceção: My Window has a TabControl. E as regras de validação só são avaliadas para o TabItem que está visível no momento. Então, substituí VisualTreeHelper por LogicalTreeHelper. Agora 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;
    }

fonte
7

Além da ótima implementação LINQ de Dean, me diverti agrupando o código em uma extensão para DependencyObjects:

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

Isso o torna extremamente bom, considerando a reutilização.

Matthias Loerke
fonte
2

Eu ofereceria uma pequena otimização.

Se você fizer isso muitas vezes com os mesmos controles, poderá adicionar o código acima para manter uma lista de controles que realmente têm regras de validação. Então, sempre que você precisar verificar a validade, examine apenas esses controles, em vez de toda a árvore visual. Isso seria muito melhor se você tivesse muitos desses controles.

sprite
fonte
2

Aqui está uma biblioteca para validação de formulário no WPF. Pacote Nuget aqui .

Amostra:

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

A ideia é definir um escopo de validação por meio da propriedade anexada, informando quais controles de entrada rastrear. Então podemos fazer:

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

Você pode iterar em toda a sua árvore de controles recursivamente e verificar a propriedade anexada Validation.HasErrorProperty e, em seguida, focar no primeiro que encontrar nela.

você também pode usar muitas soluções já escritas você pode verificar este tópico para um exemplo e mais informações

usuário21243
fonte
0

Você pode estar interessado no aplicativo de amostra BookLibrary do WPF Application Framework (WAF) . Mostra como usar a validação no WPF e como controlar o botão Salvar quando houver erros de validação.

jbe
fonte
0

Na forma de resposta aogan, em vez de iterar explicitamente por meio de regras de validação, é melhor apenas 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á brincando com a luz do fogo
fonte