WPF – walidacja danych

2012-12-01

Jedną z podstawowych kwestii przy budowie interfejsu użytkownika jest walidacja wprowadzanych danych. WPF w tym obszarze oferuje kilka mechanizmów. Dzisiaj zajmę się omówieniem trzech rozwiązań: walidacja poprzez implementację interfejsu IDataErrorInfo (DataErrorValidationRule), walidacja za pomocą wyjątków (ExceptionValidationRule) oraz walidacja przy wykorzystaniu własnych obiektów reguł (ValidationRule).
W jaki sposób działa weryfikacja wprowadzanych danych? Jeżeli kontrolka ma ustawione reguły walidacji mechanizm wiązania uruchamia je w momencie przesyłania danych z kontrolki do podpiętego źródła. Gdy reguła walidacji zwróci błąd tworzony jest obiekt ValidationError i dodawany do kolekcji Validation.Errors dostępnej jako właściwość kontrolki. Jeżeli kolekcja ta nie jest pusta właściwość Validation.HasError kontrolki ustawiana jest na true. Dodatkowo gdy wiązanie ma ustawioną właściwość NotifyOnValidationError w przypadku błędu wywołane zostanie zdarzenie Validation.Error na danej kontrolce. Możemy także dla danej reguły walidacji ustawić właściwość ValidatesOnTargetUpdated określającą czy walidacja ma być uruchamiana przy zmianie wartości źródła spoza kontrolki.
Zacznijmy od przygotowania prostego projektu zawierającego model widoku (ViewModel) z trzema właściwościami oraz widok (View) posiadający kontrolki zbindowane do tych właściwości:

ViewModel:

namespace WPFValidation
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        virtual protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private string firstName;
        private string lastName;
        private int age;

        public string FirstName
        {
            get { return firstName; }
            set
            {
                firstName = value;
                OnPropertyChanged("FirstName");
            }
        }

        public string LastName
        {
            get { return lastName; }
            set
            {
                lastName = value;
                OnPropertyChanged("LastName");
            }
        }

        public int Age
        {
            get { return age; }
            set
            {
                age = value;
                OnPropertyChanged("Age");
            }
        }
    }
}

View:

<Window x:Class="WPFValidation.View"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WPFValidation"
        Title="View" Height="279" Width="300" WindowStartupLocation="CenterScreen">
    <Window.DataContext>
        <l:ViewModel />
    </Window.DataContext>
    <Grid>
        <Label Content="Wiek:" HorizontalAlignment="Left"
               VerticalAlignment="Top" Margin="42,119,0,0"/>
        <Label Content="Nazwisko:" HorizontalAlignment="Left"
               VerticalAlignment="Top" Margin="18,76,0,0"/>
        <Label Content="Imię:" HorizontalAlignment="Left"
               VerticalAlignment="Top" Margin="46,33,0,0"/>
        <TextBox Height="23" HorizontalAlignment="Left" Margin="81,33,0,0"
                 Name="tbFirstName" VerticalAlignment="Top" Width="155"
                 Text="{Binding FirstName}" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="81,76,0,0"
                 Name="tbLastName" VerticalAlignment="Top" Width="155"
                 Text="{Binding LastName}" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="81,119,0,0"
                 Name="tbAge" VerticalAlignment="Top" Width="93"
                 Text="{Binding Age}" />
        <Button Content="Zapisz" Height="23" HorizontalAlignment="Left"
                Margin="105,188,0,0"
                Name="btSave" VerticalAlignment="Top" Width="75" />
    </Grid>
</Window>

View

Prezentacja błędów walidacji

Na początku opracujemy sposób prezentacji informacji o nieprawidłowych danych. Załóżmy, że w przypadku błędu walidacji dana kontrolka zostanie otoczona czerwoną ramką, obok niej pojawi się ikona błędu, natomiast informacja o błędzie prezentowana będzie po najechaniu myszą na kontrolkę lub ikonę:

walidacja

Oto implementacja powyższego rozwiązania:

<Window.Resources>
    <ControlTemplate x:Key="errorTemplate">
        <DockPanel LastChildFill="true">
            <Border Background="OrangeRed" DockPanel.Dock="right"
                    Margin="3,0,0,0" Width="20" Height="20" CornerRadius="5"
                    ToolTip="{Binding ElementName=adoner,
                        Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                <TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center"
                           FontWeight="Bold" Foreground="white" />
            </Border>
            <AdornedElementPlaceholder Name="adoner" VerticalAlignment="Center">
                <Border BorderBrush="OrangeRed" BorderThickness="1" />
            </AdornedElementPlaceholder>
        </DockPanel>
    </ControlTemplate>
    <Style x:Key="textBoxError" TargetType="TextBox">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors)[0].ErrorContent}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

W zasobach widoku umieszczony został obiekt ControlTemplate odpowiedzialny za utworzenie ikony błędu oraz ramki wokół kontrolki, a także styl ustawiający ToolTip kontrolki w przypadku błędu.

Walidacja poprzez implementację interfejsu IDataErrorInfo (DataErrorValidationRule)

Pierwszym mechanizmem walidacji jaki zastosujemy jest implementacja interfejsu IDataErrorInfo w modelu widoku (ViewModel). Interfejs ten wymaga metody Error (nie jest ona wykorzystywana w WPF) oraz indeksatora, do którego przekazywane są nazwy właściwości po zmianie ich wartości. Indeksator zwraca null w przypadku poprawnej walidacji lub komunikat w przypadku błędu. To rozwiązanie zastosujemy dla właściwości FirstName. Gdy wprowadzona wartość będzie pusta zgłoszony zostanie błąd:

public string Error
{
    get { return String.Empty; }
}

public string this[string fieldName]
{
    get
    {
        string result = null;
        if (fieldName == "FirstName")
        {
            if (string.IsNullOrEmpty(FirstName))
                result = "Imię nie może być puste!";
        }
        return result;
    }
}

W kontrolce odpowiedzialnej za wprowadzanie imienia musimy dodać obsługę tego typu walidacji:

<TextBox Height="23" HorizontalAlignment="Left" Margin="81,33,0,0"
         Name="tbFirstName" VerticalAlignment="Top" Width="155"
         Validation.ErrorTemplate="{StaticResource errorTemplate}"
         Style="{StaticResource textBoxError}"
         Text="{Binding FirstName, UpdateSourceTrigger=LostFocus,
            ValidatesOnDataErrors=True}" />

Pojawiły się w niej cztery nowe elementy:

  • Validation.ErrorTemplate – szablon ustawiany w przypadku błędu walidacji (przypisujemy obiekt zdefiniowany wcześniej w zasobach widoku)
  • Style – przypisanie stylu zdefiniowanego w zasobach widoku
  • UpdateSourceTrigger – opcja pozwala na określenie, w którym momencie następuje aktualizacja źródła i uruchomienie walidacji
  • ValidatesOnDataErrors – włączenie walidacji dla interfejsu IDataErrorInfo, jest to skrócona forma następującego kodu:
<Binding.ValidationRules>
    <DataErrorValidationRule />
</Binding.ValidationRules>

Należy pamiętać, że w przypadku tego rozwiązania walidacja uruchamiana jest dopiero po przypisaniu wartości do danej właściwości. Oznacza to, że jeżeli operacja ta nie powiedzie się (wystąpi np. błąd konwersji) walidacja nie zostanie uruchomiona. Ma to duże znaczenie przy typach liczbowych – gdy do kontrolki wprowadzone zostaną litery ten rodzaj walidacji nie wykryje tego faktu (w kontrolce będzie inna wartość niż we właściwości źródłowej).

Walidacja za pomocą wyjątków (ExceptionValidationRule)

Kolejnym typem walidacji jaki zastosujemy jest walidacja poprzez zgłoszenie wyjątku. Tego rozwiązania użyjemy dla właściwości LastName. W przypadku próby wprowadzenia pustego ciągu znaków zgłoszony zostanie wyjątek:

public string LastName
{
    get { return lastName; }
    set
    {
        if (string.IsNullOrEmpty(value))
            throw new ArgumentException("Nazwisko nie może być puste!");
        lastName = value;
        OnPropertyChanged("LastName");
    }
}

W kontrolce odpowiedzialnej za wprowadzanie nazwiska musimy dodać obsługę tego typu walidacji:

<TextBox Height="23" HorizontalAlignment="Left" Margin="81,76,0,0"
         Name="tbLastName" VerticalAlignment="Top" Width="155"
         Validation.ErrorTemplate="{StaticResource errorTemplate}"
         Style="{StaticResource textBoxError}"
         Text="{Binding LastName, UpdateSourceTrigger=LostFocus,
            ValidatesOnExceptions=True}" />

Pojawiły się w niej cztery nowe elementy (oprócz omówionych wcześniej):

  • Validation.ErrorTemplate – szablon ustawiany w przypadku błędu walidacji (przypisujemy obiekt zdefiniowany wcześniej w zasobach widoku)
  • Style – przypisanie stylu zdefiniowanego w zasobach widoku
  • UpdateSourceTrigger – opcja pozwala na określenie, w którym momencie następuje aktualizacja źródła i uruchomienie walidacji
  • ValidatesOnExceptions – włączenie walidacji na podstawie wyjątków, jest to skrócona forma następującego kodu:
<Binding.ValidationRules>
    <ExceptionValidationRule />
</Binding.ValidationRules>

Jeżeli chcemy za pomocą tego typu walidacji wykrywać błędy konwersji wartości pomiędzy kontrolką a właściwością źródłową musimy zastosować pewne mało eleganckie rozwiązanie. Mianowicie, mając pole prywatne typu int tworzymy do niego właściwość typu string, w której podczas przypisywania wartości sprawdzamy możliwość jej przekonwertowania na typ int i ewentualnie zgłaszamy wyjątek.

Walidacja przy wykorzystaniu własnych obiektów reguł (ValidationRule)

Ostatnim rozwiązaniem jest stworzenie własnej klasy implementującej reguły walidacji. Klasa taka musi dziedziczyć po klasie ValidationRule. Za przeprowadzenie walidacji odpowiada metoda Validate zwracająca wynik walidacji w postaci obiektu ValidationResult. W celu walidacji pól liczbowych stworzyłem klasę IntegerValidation, w której umieściłem właściwości MinValue, MaxValue oraz ErrorMessage umożliwiające sterowanie procesem walidacji:

public class IntegerValidation : ValidationRule
{
    public int? MinValue { get; set; }
    public int? MaxValue { get; set; }
    public string ErrorMessage { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        int i;
        if (!int.TryParse(value.ToString(), out i))
            return new ValidationResult(false, "Podana wartość nie jest liczbą!");
        else
            if (i < (MinValue ?? i) || i > (MaxValue ?? i))
                return new ValidationResult(false, ErrorMessage);
            else
                return ValidationResult.ValidResult;
    }
}

Obiektu klasy IntegerValidation użyjemy w kontrolce powiązanej z właściwością Age:

<TextBox Height="23" HorizontalAlignment="Left" Margin="81,119,0,0"
         Name="tbAge" VerticalAlignment="Top" Width="93"
         Validation.ErrorTemplate="{StaticResource errorTemplate}"
         Style="{StaticResource textBoxError}">
    <TextBox.Text>
        <Binding Path="Age" UpdateSourceTrigger="PropertyChanged" >
            <Binding.ValidationRules>
                <l:IntegerValidation ValidatesOnTargetUpdated="True"
                                     ValidationStep="RawProposedValue"
                                     MinValue="18"
                                     ErrorMessage="Osoba musi być pełnoletnia!" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

Pojawiły się w niej cztery nowe elementy:

  • Validation.ErrorTemplate – szablon ustawiany w przypadku błędu walidacji (przypisujemy obiekt zdefiniowany wcześniej w zasobach widoku)
  • Style – przypisanie stylu zdefiniowanego w zasobach widoku
  • UpdateSourceTrigger – opcja pozwala na określenie, w którym momencie następuje aktualizacja źródła i uruchomienie walidacji
  • Binding.ValidationRules – użycie obiektu IntegerValidation w celu walidacji danych

Dodatkowo za pomocą właściwości ValidationStep reguły walidacji możemy określić, w którym momencie walidacja będzie uruchamiana:

  • RawProposedValue – przed konwersją wartości na docelowy typ i przed ustawieniem właściwości (parametr value metody Validate zawiera wartość przed konwersją)
  • ConvertedProposedValue – po konwersji wartości na docelowy typ, przed ustawieniem właściwości (parametr value metody Validate zawiera skonwertowaną wartość)
  • UpdatedValue – po ustawieniu właściwości (parametr value metody Validate udostępnia obiekt BindingExpression gdzie DataItem/ResolvedSource to referencja do obiektu zawierającego modyfikowaną właściwość, ResolvedSourcePropertyName to nazwa tej właściwości)
  • CommittedValue – po zatwierdzeniu wartości we właściwości (parametr value metody Validate udostępnia obiekt BindingExpression gdzie DataItem/ResolvedSource to referencja do obiektu zawierającego modyfikowaną właściwość, ResolvedSourcePropertyName to nazwa tej właściwości)

Dwie pierwsze opcje pozwalają walidować wartości przed przypisaniem ich do docelowej właściwości ale nie dają możliwości odwołania się do obiektu zawierającego modyfikowaną właściwość (ViewModel). Z kolei dwie pozostałe opcje nie pozwalają na wykrycie błędów konwersji ponieważ walidacja uruchamiana jest już po przypisaniu wartości do źródła.

Blokowanie wybranych kontrolek przy błędach walidacji

Jeżeli mamy już zaimplementowane mechanizmy walidacji danych, w prosty sposób możemy wpływać na możliwość wykonania pewnych akcji na widoku. W naszym oknie zablokujemy przycisk Zapisz w przypadku błędnie wprowadzonych danych. Do tego celu możemy użyć wyzwalaczy operujących na wielu warunkach. Na podstawie właściwości Validation.HasError poszczególnych kontrolek ustawiamy właściwość IsEnabled przycisku:

<Button Content="Zapisz" Height="23" HorizontalAlignment="Left"
        Margin="105,188,0,0" Name="btSave" VerticalAlignment="Top" Width="75">
    <Button.Style>
        <Style TargetType="Button">
            <Setter Property="IsEnabled" Value="false" />
            <Style.Triggers>
                <MultiDataTrigger>
                    <Setter Property="IsEnabled" Value="true" />
                    <MultiDataTrigger.Conditions>
                        <Condition Binding="{Binding ElementName=tbFirstName,
                            Path=(Validation.HasError)}" Value="false" />
                        <Condition Binding="{Binding ElementName=tbLastName,
                            Path=(Validation.HasError)}" Value="false" />
                        <Condition Binding="{Binding ElementName=tbAge,
                            Path=(Validation.HasError)}" Value="false" />
                    </MultiDataTrigger.Conditions>
                </MultiDataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>

Oto efekt końcowy:

View

Podsumowanie

Jak widać mamy sporo możliwości jeżeli chodzi o weryfikację poprawności wprowadzanych danych. Opisane przeze mnie mechanizmy pozwalają na tworzenie dowolnych kombinacji reguł walidacyjnych. Co więcej, w jednej kontrolce możemy stosować wiele reguł jednocześnie. Osobiście preferuję podejście, w którym za wykrywanie błędów konwersji lub wymuszenie formatu odpowiadają własne obiekty reguł (ValidationRule), natomiast za biznesową poprawność danych odpowiadają obiekty logiki biznesowej wykorzystywane poprzez implementację interfejsu IDataErrorInfo w modelu widoku.

Więcej informacji można znaleźć na MSDN: Validation Class, ValidationRule ClassIDataErrorInfo Interface, Binding.ValidatesOnDataErrors Property, Binding.ValidatesOnExceptions Property.

Reklamy

Posted on 2012-12-01, in .NET/C# and tagged , , , , . Bookmark the permalink. 3 komentarze.

  1. Przydałoby się źródło projektu, ponieważ pierwszy sposób tutaj podany nie bardzo chce działać.

    • Co znaczy nie chce działać? Jaki jest komunikat błędu i w którym miejscu? Aby zadziałał pierwszy sposób w klasie ViewModel musimy dodać implementację interfejsu IDataErrorInfo:

      public class ViewModel : INotifyPropertyChanged, IDataErrorInfo

      • Nie wiem, czy nie doczytałem czy jak, ale faktycznie dopisanie implementacji IDataErrorInfo do definicji klasy spowodowało że teraz działa jak należy. Dzięki.

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

%d blogerów lubi to: