Fluent Validation w WPF
W poprzednim wpisie pokazałem, jak użyć Fluent Validation w WPF. Wspomniałem, że tamta implementacja niestety nie nadaje się, gdy potrzebujemy walidować dane w sposób asynchroniczny – np. sprawdzić w usłudze, czy dane są unikalne. W tamtym wpisie pokazałem również użycie IDataErrorInfo, ponieważ jest on najczęściej wykorzystywany do realizacji walidacji. Dzisiaj natomiast opiszę nową wersję tego interfejsu, dodaną w .NET 4.5 – INotifyDataErrorInfo. Co fajne, nowy interfejs umożliwia realizację walidacji asynchronicznej, która jest dostępna w Fluent Validation.
W tym wpisie bazuję na przykładzie walidacji z wykorzystaniem IDataErrorInfo (dlatego odsyłam do przeczytania w pierwszej kolejności tamtego wpisu) i tutaj skupię się wyłącznie na zmianach, jakie zrobiłem w kodzie, aby wspierać asynchroniczną walidację.
Implementacja view modelu
W pierwszej kolejności usunąłem z ViewModel kod związany z IDataErrorInfo: metodę GetValidationError, właściwość Error oraz indekser.
Metoda SelfValidate zmieniła sygnaturę i teraz jest w wersji asynchronicznej (wraca Task<ValidationResult>):
public virtual Task<ValidationResult> SelfValidate() | |
{ | |
return Task.FromResult(new ValidationResult()); | |
} |
public override Task<ValidationResult> SelfValidate() | |
{ | |
return Validator.ValidateAsync(this); | |
} |
Do ViewModel dodałem dwie pomocnicze właściwości, które będą wykorzystane w innych częściach kodu: IsValidating (czy trwa właśnie walidacja – aby wyświetlić później stosowną informację w widoku) oraz ValidationResult (w niej będzie zapisywany wynik ostatniej walidacji na potrzeby przekazania informacji o błędach do WPF):
private bool _isValidating; | |
public bool IsValidating | |
{ | |
get { return _isValidating; } | |
set | |
{ | |
if (_isValidating != value) | |
{ | |
_isValidating = value; | |
OnPropertyChanged(() => this.IsValidating); | |
} | |
} | |
} | |
private ValidationResult ValidationResult { get; set; } |
Sam interfejs INotifyDataErrorInfo definiuje trzy rzeczy, które trzeba dodać do kodu: zdarzenie ErrorsChanged (za jego pomocą będziemy informować, że zmieniła się informacja o błędach dla właściwości), właściwość HasErrors (czy są błędy) oraz metoda GetErrors (zwraca informacje o błędach dla właściwości). Implementacja tych elementów może wyglądać tak:
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; | |
private void OnErrorsChanged(string propertyName) | |
{ | |
if (ErrorsChanged == null) | |
{ | |
return; | |
} | |
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); | |
} | |
public bool HasErrors => IsValid == false; | |
public IEnumerable GetErrors(string propertyName) | |
{ | |
if (ValidationResult == null) | |
{ | |
return new List<string>(); | |
} | |
return ValidationResult.Errors.Where(e => e.PropertyName == propertyName).Select(e => e.ErrorMessage).ToList(); | |
} |
Zmieniła się również sama metoda Validate. Przede wszystkim jest asynchroniczna, ustawia właściwość IsValidating oraz za pomocą zdarzenia ErrorsChanged (przy pomocą pomocniczej metody) aktualizuje informacje o błędach.
protected async Task<bool> Validate() | |
{ | |
IsValidating = true; | |
ValidationResult = await SelfValidate(); | |
IsValidating = false; | |
IsValid = ValidationResult.IsValid; | |
if (IsValid == false) | |
{ | |
foreach(var propertyName in ValidationResult.Errors.Select(e => e.PropertyName).Distinct()) | |
{ | |
OnErrorsChanged(propertyName); | |
} | |
} | |
return IsValid; | |
} |
W przypadku implementacji z poprzedniego wpisu WPF sam automatycznie, po zmianie danych w kontrolce, korzystał z indeksera, w którym odpalana była metoda SelfValidate. W tym przypadku musimy zrobić to troszeczkę inaczej. Ponieważ sami musimy wywołać metodę Validate, najlepiej zrobić to w metodzie OnPropertyChanged, która w moich aplikacjach jest uruchamiana w każdym seterze właściwości, gdy nastąpiła jest zmiana. Jak widać w poniższym kodzie, na końcu metody sprawdzamy, czy nazwa właściwości jest OK (nie ma sensu wywoływać walidacji dla zmian niektórych właściwości, np. IsValid), i jeśli tak, to wywołujemy metodę Validate. Co istotne, jak widać w powyższym kodzie, metoda Validate informuje WPF tylko w sytuacji, gdy właściwość się nie waliduje. Musimy jeszcze poinformować WPF o tym, że właściwość znowu ma wartość poprawną – i robimy to właśnie w metodzie OnPropertyChanged:
private async void OnPropertyChanged(string propertyName) | |
{ | |
var propertyChanged = PropertyChanged; | |
if (propertyChanged == null) | |
{ | |
return; | |
} | |
propertyChanged(this, new PropertyChangedEventArgs(propertyName)); | |
if (propertyName != nameof(IsValid) && propertyName != nameof(IsValidating)) | |
{ | |
await Validate(); | |
if(ValidationResult.Errors.Any(e => e.PropertyName == propertyName) == false) | |
{ | |
OnErrorsChanged(propertyName); | |
} | |
} | |
} |
Implementacja walidatora oraz widok
Po stronie zmian w bazowym view modelu to już wszystko. W przykładzie zmienił się nieco również sam walidator dla przykładowego widoku. Dodana została dodatkowa reguła sprawdzająca, czy wpisany adres jest unikalny. W prawdziwej aplikacji w tym miejscu prawdopodobnie aplikacja wysłałaby żądanie do usługi, tutaj natomiast symulujemy to wywołaniem metody Task.Delay. Aby walidator wykonał tę walidację asynchronicznie, musimy skorzystać z metody MustAsync, jak to widać poniżej:
public class MainWindowViewModelValidator : AbstractValidator<MainWindowViewModel> | |
{ | |
public MainWindowViewModelValidator() | |
{ | |
RuleFor(u => u.Email) | |
.Cascade(CascadeMode.StopOnFirstFailure) | |
.NotEmpty() | |
.EmailAddress() | |
.MustAsync((email, token) => ValidateEmail(email, token)) | |
.WithMessage("Email must be unique"); | |
} | |
private async Task<bool> ValidateEmail(string email, CancellationToken token) | |
{ | |
await Task.Delay(2000); | |
return email != "daniel@plawgo.pl"; | |
} | |
} |
Ostatnią rzeczą, która trzeba jeszcze zmienić, jest definicja bindingu w samym widoku. Zamieniamy ValidatesOnDataErrors na ValidatesOnNotifyDataErrors:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}" Name="Email"/>
Podsumowanie
Po tych wszystkich zmianach możemy się cieszyć asynchroniczną walidacją w aplikacji WPF z wykorzystaniem Fluent Validation. Dzięki temu rozwiązaniu możemy bez blokowania interfejsu użytkownika walidować dane po stronie serwera. Od jakiegoś czasu przestawiłem się w swoich aplikacjach z walidacji z wykorzystaniem IDataErrorInfo na INotifyDataErrorInfo, do czego również zachęcam i Ciebie, drogi Czytelniku. 🙂
Jak poprzednio, cały działający przykład znajduje się na githubie.
1 thought on “Integracja Fluent Validation z WPF – wersja async”