Integracja Fluent Validation z WPF – wersja async

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());
}
view raw ViewModel1.cs hosted with ❤ by GitHub
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; }
view raw ViewModel2.cs hosted with ❤ by GitHub

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();
}
view raw ViewModel3.cs hosted with ❤ by GitHub

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;
}
view raw ViewModel4.cs hosted with ❤ by GitHub

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);
}
}
}
view raw ViewModel5.cs hosted with ❤ by GitHub

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

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:

<TextBox
Text="{Binding Email,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True}"
Name="Email"/>
view raw MainWindow.xaml hosted with ❤ by GitHub

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

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *