Właściwa walidacja za pomocą MVVM

Ostrzeżenie: bardzo długi i szczegółowy post.

Dobra, walidacja w WPF podczas używania MVVM. Czytałem już wiele rzeczy, spojrzałem na wiele pytań SO i próbowałemwiele zbliża się, ale w pewnym momencie wszystko wydaje się nieco hacky i naprawdę nie jestem pewien, jak to zrobićwłaściwy sposób™.

W idealnej sytuacji chcę, aby wszystkie sprawdzenie poprawności odbyło się w modelu widoku przy użyciuIDataErrorInfo; tak właśnie zrobiłem. Istnieją jednak różne aspekty, które sprawiają, że to rozwiązanie nie jest kompletnym rozwiązaniem dla całego tematu walidacji.

Sytuacja

Weźmy następującą prostą formę. Jak widać, to nic nadzwyczajnego. Mamy tylko dwa pola tekstowe, które wiążą się z astring iint właściwość w modelu widoku każdy. Ponadto mamy przycisk związany zICommand.

Tak więc dla walidacji mamy teraz dwie możliwości:

Możemy uruchomić walidację automatycznie, gdy zmieni się wartość pola tekstowego. Jako taki użytkownik otrzymuje natychmiastową odpowiedź, gdy wpisał coś nieprawidłowego.Możemy zrobić krok dalej, aby wyłączyć przycisk, gdy wystąpią jakiekolwiek błędy.Lub możemy uruchomić sprawdzanie poprawności tylko wtedy, gdy przycisk jest wciśnięty, a następnie wyświetlać wszystkie błędy, jeśli dotyczy. Oczywiście nie możemy tutaj wyłączyć przycisku błędów.

Idealnie chciałbym zaimplementować wybór 1. Dla normalnych powiązań danych z aktywowanymValidatesOnDataErrors jest to zachowanie domyślne. Więc gdy tekst się zmienia, powiązanie aktualizuje źródło i uruchamiaIDataErrorInfo walidacja dla tej nieruchomości; błędy są zgłaszane do widoku. Jak na razie dobrze.

Stan sprawdzania poprawności w modelu widoku

Ciekawostką jest, aby model widoku lub przycisk w tym przypadku wiedział, czy są jakieś błędy. DrogaIDataErrorInfo działa, głównie do zgłaszania błędów do widoku. Widok może więc łatwo sprawdzić, czy są jakieś błędy, wyświetlać je, a nawet wyświetlać adnotacjeValidation.Errors. Co więcej, walidacja zawsze odbywa się, patrząc na jedną właściwość.

Zatem posiadanie modelu widoku wie, kiedy występują jakiekolwiek błędy lub czy weryfikacja się powiodła, jest skomplikowane. Powszechnym rozwiązaniem jest po prostu wyzwalanieIDataErrorInfo sprawdzanie poprawności wszystkich właściwości w samym modelu widoku. Jest to często wykonywane przy użyciu oddzielnegoIsValid własność. Korzyścią jest to, że można go również łatwo użyć do wyłączenia polecenia. Wadą jest to, że może to spowodować zbyt częste sprawdzanie poprawności wszystkich właściwości, ale większość walidacji powinna być wystarczająca, aby nie zaszkodzić wydajności. Innym rozwiązaniem byłoby zapamiętanie, które właściwości spowodowały błędy podczas sprawdzania poprawności i sprawdzają tylko te, ale wydaje się to nieco skomplikowane i niepotrzebne przez większość czasu.

Najważniejsze jest to, że może to działać dobrze.IDataErrorInfo zapewnia walidację wszystkich właściwości i możemy po prostu użyć tego interfejsu w samym modelu widoku, aby uruchomić tam sprawdzanie poprawności dla całego obiektu. Przedstawienie problemu:

Wiążące wyjątki

Model widoku używa rzeczywistych typów dla swoich właściwości. W naszym przykładzie właściwość całkowita jest rzeczywistaint. Pole tekstowe używane w widoku jednak natywnie obsługujetekst. Więc kiedy wiążę się zint w modelu widoku silnik powiązania danych automatycznie przeprowadzi konwersje typów - a przynajmniej spróbuje. Jeśli możesz wprowadzić tekst w polu tekstowym przeznaczonym dla liczb, istnieje duże prawdopodobieństwo, że nie zawsze będą w nim prawidłowe liczby: więc mechanizm powiązania danych nie będzie mógł przekonwertować i rzucićFormatException.

Po stronie widzimy to łatwo. Wyjątki od silnika powiązań są automatycznie przechwytywane przez WPF i są wyświetlane jako błędy - nie ma nawet potrzeby włączaniaBinding.ValidatesOnExceptions które byłyby wymagane dla wyjątków rzucanych w seter. Komunikaty o błędach mają jednak ogólny tekst, więc może to stanowić problem. Rozwiązałem to dla siebie za pomocąBinding.UpdateSourceExceptionFilter handler, sprawdzając zgłoszony wyjątek i przeglądając właściwość source, a następnie generując mniej ogólny komunikat o błędzie. Wszystko to wywróciło się do mojego własnego rozszerzenia znaczników Binding, więc mogę mieć wszystkie potrzebne mi ustawienia domyślne.

Więc widok jest w porządku. Użytkownik popełnia błąd, widzi informacje zwrotne o błędzie i może je poprawić. Model widoku jednakzgubiony. Gdy silnik powiązań rzucił wyjątek, źródło nigdy nie zostało zaktualizowane. Tak więc model widoku nadal znajduje się na starej wartości, co nie jest tym, co jest wyświetlane użytkownikowi iIDataErrorInfo walidacja oczywiście nie ma zastosowania.

Co gorsza, nie ma dobrego sposobu, aby model widoku wiedział o tym. Przynajmniej nie znalazłem jeszcze dobrego rozwiązania tego problemu. Możliwe, że raport z widoku powróci do modelu widoku, że wystąpił błąd. Można to zrobić przez powiązanie danych zValidation.HasError z powrotem do modelu widoku (co nie jest możliwe bezpośrednio), więc model widoku może najpierw sprawdzić stan widoku.

Inną opcją byłoby przekazanie obsługiwanego wyjątkuBinding.UpdateSourceExceptionFilter do modelu widoku, więc zostanie o tym powiadomiony. Model widoku może nawet zapewniać pewien interfejs dla powiązania, aby raportować te rzeczy, umożliwiając niestandardowe komunikaty o błędach zamiast ogólnych dla poszczególnych typów. Ale to spowodowałoby silniejsze sprzężenie od widoku do modelu widoku, którego generalnie chcę uniknąć.

Innym „rozwiązaniem” byłoby pozbycie się wszystkich właściwości wpisanych, użyj zwykłegostring właściwości i zamiast tego wykonaj konwersję w modelu widoku. To oczywiście spowodowałoby przeniesienie wszystkich walidacji do modelu widoku, ale także oznaczałoby niesamowitą ilość duplikacji rzeczy, którymi zajmuje się zazwyczaj silnik powiązania danych. Ponadto zmieniłoby semantykę modelu widoku. Dla mnie widok jest zbudowany dla modelu widoku, a nie odwrotnie - oczywiście projekt modelu widoku zależy od tego, co wyobrażamy sobie, co zrobić, ale nadal istnieje ogólna swoboda tego, jak to robi widok. Tak więc model widoku definiujeint właściwość, ponieważ istnieje liczba; widok może teraz używać pola tekstowego (zezwalając na wszystkie te problemy) lub użyć czegoś, co natywnie działa z liczbami. Więc nie, zmieniając typy właściwości nastring nie jest dla mnie opcją.

W końcu jest to problem z widokiem. Widok (i jego silnik powiązania danych) jest odpowiedzialny za nadanie modelowi widoku właściwych wartości do pracy. Ale w tym przypadku wydaje się, że nie ma dobrego sposobu, aby powiedzieć modelowi widoku, że powinien unieważnić starą wartość właściwości.

BindingGroups

Grupy wiążące to jeden ze sposobów, w jaki próbowałem to rozwiązać. Grupy wiążące mają możliwość grupowania wszystkich walidacji, w tymIDataErrorInfo i wyrzucone wyjątki. Jeśli są dostępne dla modelu widoku, mają nawet sposób na sprawdzenie statusu walidacji dlawszystko tych źródeł walidacji, na przykład przy użyciuCommitEdit.

Domyślnie grupy wiążące implementują wybór 2 z góry. Sprawiają, że powiązania aktualizują się jawnie, zasadniczo dodając dodatkoweniezaangażowany stan. Po kliknięciu przycisku polecenie możepopełnić te zmiany, uruchamiają aktualizacje źródłowe i wszystkie walidacje i uzyskują pojedynczy wynik, jeśli się powiedzie. Tak więc akcja polecenia może być taka:

 if (bindingGroup.CommitEdit())
     SaveEverything();

CommitEdit zwróci prawdę tylko jeśliwszystko walidacja się powiodła. To zajmieIDataErrorInfo pod uwagę, a także sprawdzić wiążące wyjątki. Wydaje się, że jest to idealne rozwiązanie do wyboru 2. Jedyne, co jest trochę kłopotliwe, to zarządzanie grupą powiązań za pomocą powiązań, ale zbudowałem sobie coś, co w większości się tym zajmuje (związane z).

Jeśli grupa powiązań jest obecna dla powiązania, powiązanie będzie domyślnie jawneUpdateSourceTrigger. Aby zaimplementować wybór 1 z góry przy użyciu grup wiążących, musimy w zasadzie zmienić wyzwalacz. Ponieważ i tak mam niestandardowe rozszerzenie powiązania, jest to dość proste, po prostu ustawiłem toLostFocus dla wszystkich.

Teraz powiązania będą nadal aktualizowane po zmianie pola tekstowego. Jeśli źródło może zostać zaktualizowane (silnik wiążący nie zgłasza wyjątku), toIDataErrorInfo będzie działać jak zwykle. Jeśli nie można go zaktualizować, widok nadal będzie mógł go zobaczyć. A jeśli klikniemy nasz przycisk, podstawowe polecenie może zadzwonićCommitEdit (chociaż nie trzeba niczego zatwierdzać) i uzyskaj całkowity wynik walidacji, aby sprawdzić, czy można go kontynuować.

W ten sposób możemy nie być w stanie łatwo wyłączyć tego przycisku. Przynajmniej nie z modelu widoku. Sprawdzanie sprawdzania poprawności w kółko nie jest dobrym pomysłem tylko po to, aby zaktualizować status polecenia, a model widoku nie jest powiadamiany, jeśli mimo wszystko zostanie zgłoszony wyjątek mechanizmu powiązania (który powinien wtedy wyłączyć przycisk) - lub gdy odchodzi do włącz ponownie przycisk. Nadal możemy dodać wyzwalacz, aby wyłączyć przycisk w widoku za pomocąValidation.HasError więc nie jest to niemożliwe.

Rozwiązanie?

Ogólnie wydaje się, że jest to idealne rozwiązanie. Jaki jest z tym problem? Szczerze mówiąc, nie jestem do końca pewien. Grupy wiążące są złożoną rzeczą, która wydaje się być zwykle używana w mniejszych grupach, prawdopodobnie z wieloma grupami wiążącymi w jednym widoku. Używając jednej dużej grupy powiązań dla całego widoku, aby zapewnić moją walidację, czuję się tak, jakbym ją nadużywał. I ciągle myślę, że musi istnieć lepszy sposób na rozwiązanie tej całej sytuacji, ponieważ z pewnością nie mogę być jedynym, który ma te problemy. I do tej pory nie widziałem, żeby wiele osób używało grup wiążących do sprawdzania poprawności z MVVM, więc wydaje się dziwne.

Jaki jest właściwy sposób sprawdzania poprawności w WPF za pomocą MVVM, a jednocześnie sprawdzania, czy istnieją wyjątki wiążące silnik?

Moje rozwiązanie (/ hack)

Przede wszystkim dzięki za Twój wkład! Jak napisałem powyżej, używamIDataErrorInfo już sprawdzam poprawność danych i osobiście uważam, że jest to najwygodniejsze narzędzie do wykonywania zadania sprawdzania poprawności. Używam narzędzi podobnych do tego, które zasugerował Sheridan w swojej odpowiedzi poniżej, więc utrzymywanie działa również dobrze.

W końcu mój problem sprowadzał się do wiążącego problemu wyjątku, w którym model widoku nie wiedziałby o tym, kiedy to się stało. Chociaż mogłem sobie z tym poradzić z grupami wiążącymi, jak opisano powyżej, nadal się z tym nie zgadzałem, ponieważ po prostu nie czułem się z tym dobrze. Co zatem zrobiłem?

Jak wspomniałem powyżej, wykrywam wiążące wyjątki po stronie widoku, słuchając wiązańUpdateSourceExceptionFilter. Tam mogę uzyskać odwołanie do modelu widoku z wyrażenia wiążącegoDataItem. Mam wtedy interfejsIReceivesBindingErrorInformation który rejestruje model widoku jako możliwy odbiornik w celu uzyskania informacji o błędach wiązania. Następnie używam tego do przekazania ścieżki wiązania i wyjątku od modelu widoku:

object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
{
    BindingExpression expr = (bindExpression as BindingExpression);
    if (expr.DataItem is IReceivesBindingErrorInformation)
    {
        ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
    }

    // check for FormatException and produce a nicer error
    // ...
 }

W modelu widoku pamiętam wtedy, gdy otrzymuję powiadomienie o wyrażeniu wiązania ścieżki:

HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
    bindingErrors.Add(path);
}

I ilekroćIDataErrorInfo ponownie zatwierdza nieruchomość, wiem, że wiązanie zadziałało i mogę usunąć właściwość z zestawu mieszania.

W modelu widoku mogę następnie sprawdzić, czy zestaw skrótów zawiera jakiekolwiek elementy i przerwać działanie, które wymaga pełnego sprawdzenia poprawności danych. Może nie jest to najładniejsze rozwiązanie ze względu na sprzężenie z widoku do modelu widoku, ale użycie tego interfejsu jest co najmniej nieco mniej problematyczne.

questionAnswers(5)

yourAnswerToTheQuestion