Kategorie szkoleń | Egzaminy | Kontakt
  • 2
  • 1
  • 596

Odpowiedzi (2)

  • 3

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:

  • nazwy właściwości są wpisywane ręcznie, więc łatwo o pomyłkę, a w konsekwencji błąd w aplikacji
  • wszystkie walidacje są w jednym miejscu, więc przy skomplikowanych regułach można się pogubić

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>


   
  • Odpowiedział
  • @ | 03.09.2014
  • TRENER ALTKOM AKADEMII
  • 2

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 { getset; }
		public int? MaxValue { getset; }
		public string ErrorMessage { getset; }
 
		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

  • 7z

    WalidacjaPrzyklad.7z ( 7K )
  • Odpowiedział
  • @ | 10.01.2015
  • TRENER ALTKOM AKADEMII
Komentarze
Owszem, to kolejny sposób walidacji, ale działa tylko po stronie interfejsu użytkownika (View). Przykłady, które zaprezentowałem działają na "głębszych warstwach" i mogą być również zastosowane poza WPF, np. w aplikacji webowej MVC.

Uważam, że ValidationRule świetnie nadaje się w przypadkach, gdy chcemy zapewnić poprawność kodu pocztowego, formatu daty, walidacja nip, regon itd.
Natomiast nie powinien być stosowany do walidacji logiki biznesowej bo widok powinien być od tego odseparowany.

Reasumując, mechanizmy się nie wykluczają lecz uzupełniają.






Skomentował : @ TRENER ALTKOM AKADEMII ,01.02.2018