Wykrywanie błędów walidacji WPF

115

W WPF można skonfigurować walidację na podstawie błędów zgłaszanych w warstwie danych podczas wiązania danych przy użyciu ExceptionValidationRulelub DataErrorValidationRule.

Załóżmy, że masz skonfigurowaną w ten sposób grupę kontrolek i masz przycisk Zapisz. Gdy użytkownik kliknie przycisk Zapisz, przed kontynuowaniem zapisywania upewnij się, że nie ma żadnych błędów sprawdzania poprawności. Jeśli występują błędy walidacji, chcesz je wykrzyczeć.

W WPF, jak sprawdzić, czy którykolwiek z formantów związanych z danymi ma ustawione błędy walidacji?

Kevin Berridge
źródło

Odpowiedzi:

137

Ten post był niezwykle pomocny. Dziękuję wszystkim, którzy wnieśli swój wkład. Oto wersja LINQ, którą pokochasz lub znienawidzisz.

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);
}
Dziekan
źródło
1
Bardzo podoba mi się to rozwiązanie!
ChristopheD
Właśnie natknąłem się na ten wątek. Bardzo przydatna mała funkcja. Dzięki!
Olav Haugen
Czy istnieje sposób wyliczenia tylko tych DependencyObjects, które były powiązane z określonym DataContext? Nie podoba mi się pomysł treewalk. Może istnieć kolekcja powiązań połączona z określonym źródłem danych.
ZAB
5
Zastanawiasz się tylko, jak wywołać tę IsValidfunkcję? Widzę, że skonfigurowałeś polecenie, CanExecutektóre, jak sądzę, jest powiązane z poleceniem przycisku Zapisz. Czy to zadziała, jeśli nie używam poleceń? A jaki jest związek przycisku z innymi kontrolkami, które należy sprawdzić? Moją jedyną myślą o tym, jak tego użyć, jest wywołanie IsValidkażdej kontrolki, która musi zostać zweryfikowana. Edycja: Wygląda na to, że sprawdzasz, senderco, jak spodziewam się, będzie przyciskiem zapisywania. Nie wydaje mi się to w porządku.
Nicholas Miller
1
@Nick Miller a Windowjest również obiektem zależności. Prawdopodobnie konfiguruje go za pomocą jakiegoś programu obsługi zdarzeń w Window. Alternatywnie, można po prostu nazwać to bezpośrednio IsValid(this)z Windowklasy.
akousmata
47

Poniższy kod (z książki Programming WPF autorstwa Chrisa Sell'a i Iana Griffithsa) sprawdza poprawność wszystkich reguł wiążących dla obiektu zależności i jego elementów podrzędnych:

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

}

Możesz to wywołać w programie obsługi zdarzeń kliknięcia przycisku zapisywania, w ten sposób w swojej stronie / oknie

private void saveButton_Click(object sender, RoutedEventArgs e)
{

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

    ....
   }
}
aogan
źródło
33

Wysłany kod nie działał dla mnie podczas korzystania z ListBox. Przepisałem to i teraz działa:

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
źródło
1
Głosuj na swoje rozwiązanie do pracy nad moim ItemsControl.
Jeff T.
1
Używam tego rozwiązania, aby sprawdzić, czy mój datagrid ma błędy walidacji. Jednak ta metoda jest wywoływana w mojej metodzie polecenia viewmodel canexecute i myślę, że dostęp do wizualnych obiektów drzewa w jakiś sposób narusza wzór MVVM, prawda? Jakieś alternatywy?
Igor Kondrasovas
16

Miałem ten sam problem i wypróbowałem dostarczone rozwiązania. Połączenie rozwiązań H-Man2 i skiba_k działało dla mnie prawie dobrze, z jednym wyjątkiem: Moje okno ma TabControl. Reguły walidacji są oceniane tylko dla TabItem, który jest obecnie widoczny. Więc zamieniłem VisualTreeHelper na LogicalTreeHelper. Teraz działa.

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

źródło
7

Oprócz świetnej implementacji LINQ Dean, dobrze się bawiłem pakując kod w rozszerzenie dla DependencyObjects:

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

To sprawia, że ​​jest niezwykle przyjemny, biorąc pod uwagę możliwość ponownego użycia.

Matthias Loerke
źródło
2

Zaproponowałbym małą optymalizację.

Jeśli zrobisz to wiele razy dla tych samych kontrolek, możesz dodać powyższy kod, aby zachować listę kontrolek, które faktycznie mają reguły walidacji. Następnie, ilekroć chcesz sprawdzić poprawność, przejrzyj tylko te kontrolki, a nie całe drzewo wizualne. Byłoby to znacznie lepsze, gdybyś miał wiele takich kontroli.

krasnoludek
źródło
2

Oto biblioteka do sprawdzania poprawności formularzy w WPF. Pakiet Nuget tutaj .

Próba:

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

Chodzi o to, że definiujemy zakres walidacji za pomocą dołączonej właściwości, informując ją, jakie kontrolki wejściowe mają śledzić. Wtedy możemy:

<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
źródło
0

Możesz iterować po wszystkich swoich drzewach kontrolek rekurencyjnie i sprawdzić załączoną właściwość Validation.HasErrorProperty, a następnie skupić się na pierwszej, którą w nim znajdziesz.

możesz też skorzystać z wielu już napisanych rozwiązań możesz sprawdzić w tym wątku przykład i więcej informacji

user21243
źródło
0

Może Cię zainteresować przykładowa aplikacja BookLibrary platformy WPF Application Framework (WAF) . Pokazuje, jak używać walidacji w WPF i jak kontrolować przycisk Zapisz, gdy występują błędy walidacji.

jbe
źródło
0

W formularzu odpowiedzi aogan, zamiast jawnie iterować przez reguły walidacji, lepiej po prostu wywołaj 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 bawi się przy ognisku
źródło