W jaki sposób sprawdzać poprawność wprowadzanych danych przez użytkownika i sygnalizować błędy?
W jaki sposób sprawdzać poprawność wprowadzanych danych przez użytkownika i sygnalizować błędy?
Najlepszym rozwiązaniem jest walidacja na poziomie klas modelu danych, a nie kontrolek. Dzięki temu raz utworzone reguły będą sprawdzane w każdym miejscu aplikacji, gdy tylko użytkownik będzie wprowadzał dane.
W tym celu możemy zaimplementować interfejs IDataErrorInfo o następującej strukturze:
// Summary: // Provides the functionality to offer custom error information that a user // interface can bind to. public interface IDataErrorInfo { // Summary: // Gets an error message indicating what is wrong with this object. // // Returns: // An error message indicating what is wrong with this object. The default is // an empty string (""). string Error { get; } // Summary: // Gets the error message for the property with the given name. // // Parameters: // columnName: // The name of the property whose error message to get. // // Returns: // The error message for the property. The default is an empty string (""). string this[string columnName] { get; } }
Przykład implementacji:
public class Customer : IDataErrorInfo { public string Name{get;set;} public string Email { get; set; } public decimal Balance{get;set; string IDataErrorInfo.this[string propertyName] { get { if(propertyName=="Name") { if(String.IsNullOrEmpty(Name)) { return "Name is required."; } } return null; } } string IDataErrorInfo.Error { get { if(Balance < 100 || Balance > 300) { return "Balance must be beetween 100 and 300."; } return null; } } }
Jednak ten sposób implementacji ma kilka wad:
Aby uniknąć tych problemów, proponuję zastosować świetną bibliotekę FluentValidation, dostępną w postaci paczki nuget.
Zanim jednak ruszymy do dzieła, przygotowałem implementację interfejsu IDataErrorInfo w klasie bazowej:
public abstract class Base : IDataErrorInfo, INotifyPropertyChanged { protected IValidator validator = null; public string Error { get { if (validator!=null) { var results = validator.Validate(this); return GetErrors(results); } return string.Empty; } } private static string GetErrors(FluentValidation.Results.ValidationResult results) { if (results != null && results.Errors.Any()) { var errors = string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage).ToArray()); return errors; } else return string.Empty; } public string this[string columnName] { get { return InputValidation(columnName); } } private string InputValidation(string propertyName) { List<string> properties = new List<string>(); properties.Add(propertyName); if (validator != null) { var results = validator.Validate (new ValidationContext(this, new PropertyChain(), new MemberNameValidatorSelector(properties))); return GetErrors(results); } return string.Empty; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } }
Następnie musimy utworzyć klasę walidacji:
public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { RuleFor(customer => customer.Name) .NotEmpty(); RuleFor(customer => customer.Email) .NotEmpty(); RuleFor(customer => customer.Balance) .InclusiveBetween(100, 300); } }
I przekazać jej obiekt w konstruktorze klasy Customer:
public class Customer : Base { public Customer() { validator = new CustomerValidator(); } private string _email; private string _name; private decimal _balance; public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } } public string Email { get { return _email; } set { _email = value; OnPropertyChanged("Email"); } } public decimal Balance { get { return _balance; } set { _balance = value; OnPropertyChanged("Balance"); } } }
Pozostało nam jeszcze podpięcie mechanizmu walidacji w WPF:
<Window x:Class="ValidationDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ValidationDemo.ViewModels" Title="MainWindow" Height="350" Width="700"> <Window.DataContext> <vm:CustomerViewModel /> </Window.DataContext> <Window.Resources> <ControlTemplate x:Key="ErrorTemplate"> <StackPanel Orientation="Horizontal"> <AdornedElementPlaceholder /> <TextBlock Margin="0" Text="{Binding [0].ErrorContent}" Foreground="Red"/> </StackPanel> </ControlTemplate> <Style TargetType="TextBox"> <Setter Property="Width" Value="200"/> <Setter Property="Margin" Value="20"/> <Setter Property="Validation.ErrorTemplate" Value="{StaticResource ErrorTemplate}" /> </Style> </Window.Resources> <Grid> <StackPanel> <TextBox Text="{Binding Customer.Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> <TextBox Text="{Binding Customer.Email, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> <TextBox Text="{Binding Customer.Balance, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> </StackPanel> </Grid> </Window>
Moim zdaniem znacznie lepszym pomysłem jest tworzenie własnej klasy walidacji dziedziczącej po ValidationRule. Przykładowo mając formularz, w którym mamy do sprawdzenia liczbę, tworzymy następującą klasę walidującą dane typu int:
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; } }
Warto dodatkowo (choć nie jest to wymagane) stworzyć styl, który ładnie zaprezentuje nam błędną wartość. Można go umieścić przykładowo w zasobach okna:
<Window.Resources> <ControlTemplate x:Key="errorTemplate"> <DockPanel LastChildFill="true"> <Border Width="20" Height="20" Margin="3,0,0,0" Background="OrangeRed" CornerRadius="5" DockPanel.Dock="right" ToolTip="{Binding ElementName=adoner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"> <TextBlock HorizontalAlignment="center" VerticalAlignment="center" FontWeight="Bold" Foreground="white" Text="!" /> </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>
Ostatnią czynnością jest rozszerzenie definicji bindingu właściwości Text naszego TextBoxa. W całej okazałości prezentuje się to następująco:
<TextBox Name="tbAge" Width="93" Height="23" Margin="81,119,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Style="{StaticResource textBoxError}" Validation.ErrorTemplate="{StaticResource errorTemplate}"> <TextBox.Text> <Binding Path="Age" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <l:IntegerValidation ErrorMessage="Osoba musi być pełnoletnia!" MaxValue="67" MinValue="18" ValidatesOnTargetUpdated="True" ValidationStep="RawProposedValue" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
Trzeba pamiętać, aby dodać przestrzeń nazw do miejsca, gdzie znajduje się klasa walidacji:
xmlns:l="clr-namespace:WpfProject1"
I to już wszystko. Powyższy przykład nadaje się świetnie, jeśli korzystamy z wzorca projektowego MVVM. Ale działa również, gdy stosujemy podstawowy binding z istniejącego CodeBehind. Taki sposób daje nam możliwość walidacji znacznie bardziej skomplikowanych wartości niż tylko liczby całkowite z zakresem. Polecam jego stosowanie również dlatego, że jest czytelny dla samego projektu.
Załączniki