Rozwiązania do ponownego wejścia asynchronicznego w języku C # 5

16

Więc coś mnie denerwuje w związku z nową obsługą asynchronizacji w C # 5:

Użytkownik naciska przycisk, który rozpoczyna operację asynchroniczną. Połączenie natychmiast wraca, a pompa komunikatów zaczyna działać ponownie - o to właśnie chodzi.

Aby użytkownik mógł ponownie nacisnąć przycisk - powodując ponowne wejście. Co jeśli jest to problem?

W pokazach, które widziałem, wyłączają przycisk przed awaitpołączeniem i włączają go ponownie później. Wydaje mi się, że jest to bardzo delikatne rozwiązanie w rzeczywistej aplikacji.

Czy powinniśmy kodować jakiś automat stanów, który określa, które kontrolki muszą być wyłączone dla danego zestawu uruchomionych operacji? Czy jest jakiś lepszy sposób?

Kusi mnie, aby po prostu pokazać modalne okno dialogowe na czas trwania operacji, ale to trochę przypomina używanie młota.

Czy ktoś ma jakieś jasne pomysły?


EDYTOWAĆ:

Myślę, że wyłączenie kontrolek, których nie należy używać podczas wykonywania operacji, jest delikatne, ponieważ myślę, że szybko się skomplikuje, gdy pojawi się okno z wieloma kontrolkami. Lubię uprościć sprawę, ponieważ zmniejsza ryzyko błędów, zarówno podczas początkowego kodowania, jak i późniejszej konserwacji.

Co się stanie, jeśli istnieje zbiór elementów sterujących, które należy wyłączyć dla określonej operacji? A jeśli wiele operacji działa jednocześnie?

Nicholas Butler
źródło
Co w tym jest kruche? Wskazuje użytkownikowi, że coś jest przetwarzane. Dodaj trochę dynamicznego wskazania, które jest przetwarzane i wydaje mi się, że jest to całkiem dobry interfejs użytkownika.
Steven Jeuris,
PS: Czy C # .NET 4.5 nazywa się C # 5?
Steven Jeuris,
1
Ciekawe, dlaczego to pytanie jest tutaj, a nie SO? Zdecydowanie chodzi o kod, więc myślę, że powinien tam być. Myślę, że ...
C. Ross,
@ C.Ross, To jest bardziej pytanie dotyczące projektu / interfejsu użytkownika. Tak naprawdę nie ma tu wyjaśnienia technicznego.
Morgan Herlocker,
Mamy podobny problem w formularzach ajax HTML, w których użytkownik naciska przycisk wysyłania, wysyłane jest żądanie ajax, a następnie chcemy zablokować użytkownikowi ponowne kliknięcie przycisku wysyłania, wyłączenie przycisku jest bardzo częstym rozwiązaniem w tej sytuacji
Raynos

Odpowiedzi:

14

Więc coś mnie denerwuje w związku z nową obsługą asynchronizacji w C # 5: Użytkownik naciska przycisk, który rozpoczyna operację asynchroniczną. Połączenie natychmiast wraca, a pompa komunikatów zaczyna działać ponownie - o to właśnie chodzi. Aby użytkownik mógł ponownie nacisnąć przycisk - powodując ponowne wejście. Co jeśli jest to problem?

Zacznijmy od zauważenia, że ​​jest to już problem, nawet bez wsparcia asynchronicznego w języku. Wiele rzeczy może powodować, że wiadomości są usuwane z kolejki podczas obsługi zdarzenia. W tej sytuacji musisz już kodować obronnie; nowa funkcja języka czyni to bardziej oczywistym , czyli dobrocią.

Najczęstszym źródłem pętli komunikatów działającej podczas zdarzenia powodującego niechciane ponowne wejście jest DoEvents. Zaletą awaitw przeciwieństwie do DoEventsjest to, że awaitsprawia, że jest bardziej prawdopodobne, że nowe zadania nie idą do „zagłodzić” aktualnie uruchomionych zadań. DoEventspompuje pętlę komunikatów, a następnie synchronicznie wywołuje procedurę obsługi zdarzenia ponownie uczestnika, podczas gdy awaitzwykle kolejkuje nowe zadanie, aby mogło zostać uruchomione w pewnym momencie w przyszłości.

W pokazach, które widziałem, wyłączają przycisk przed oczekującym połączeniem i włączają go ponownie później. Myślę, że wyłączenie elementów sterujących, których nie należy używać podczas wykonywania operacji, jest delikatne, ponieważ myślę, że szybko się skomplikuje, gdy pojawi się okno z wieloma elementami sterującymi. Co się stanie, jeśli istnieje zbiór elementów sterujących, które należy wyłączyć dla określonej operacji? A jeśli wiele operacji działa jednocześnie?

Możesz zarządzać tą złożonością w taki sam sposób, jak każdą inną złożonością w języku OO: dzielisz mechanizmy na klasę i czynisz klasę odpowiedzialną za prawidłowe wdrożenie polityki . (Staraj się logicznie odróżniać mechanizmy od polityki; powinna istnieć możliwość zmiany polityki bez ingerowania w mechanizmy).

Jeśli masz formularz z wieloma kontrolkami, które działają w interesujący sposób , żyjesz w świecie złożoności własnego tworzenia . Musisz napisać kod, aby zarządzać tą złożonością; jeśli ci się to nie podoba, uprość formularz, aby nie był tak skomplikowany.

Kusi mnie, aby po prostu pokazać modalne okno dialogowe na czas trwania operacji, ale to trochę przypomina używanie młota.

Użytkownicy tego nie znoszą.

Eric Lippert
źródło
Dzięki, Eric, to chyba ostateczna odpowiedź - to zależy od twórcy aplikacji.
Nicholas Butler,
6

Jak myślisz, dlaczego wyłączenie przycisku przed, awaita następnie ponowne włączenie go po zakończeniu połączenia jest delikatne? Czy to dlatego, że obawiasz się, że przycisk nigdy nie zostanie ponownie włączony?

Cóż, jeśli połączenie nie powróci, nie będzie można go ponownie nawiązać, więc wygląda na to, że takie zachowanie jest pożądane. Wskazuje to na błąd, który należy naprawić. Jeśli upłynie limit czasu połączenia asynchronicznego, nadal może to pozostawić aplikację w nieokreślonym stanie - ponownie taki, w którym włączenie przycisku może być niebezpieczne.

Jedyny raz może to się popsuć, jeśli istnieje kilka operacji, które wpływają na stan przycisku, ale myślę, że takie sytuacje powinny być bardzo rzadkie.

ChrisF
źródło
Oczywiście musisz sobie poradzić z błędami - zaktualizowałem swoje pytanie
Nicholas Butler,
5

Nie jestem pewien, czy dotyczy to ciebie, ponieważ zwykle używam WPF, ale widzę, że odwołujesz się do ViewModelniego, więc może.

Zamiast wyłączać przycisk, ustaw IsLoadingflagę na true. Następnie dowolny element interfejsu użytkownika, który chcesz wyłączyć, można powiązać z flagą. Efektem końcowym jest to, że zadaniem interfejsu użytkownika jest uporządkowanie własnego stanu włączenia, a nie logiki biznesowej.

Oprócz tego większość przycisków w WPF jest powiązana z ICommand, i zwykle ustawiam wartość ICommand.CanExecuterówną !IsLoading, więc automatycznie zapobiega wykonywaniu polecenia, gdy IsLoading ma wartość true (wyłącza również dla mnie przycisk)

Rachel
źródło
3

W pokazach, które widziałem, wyłączają przycisk przed oczekującym połączeniem i włączają go ponownie później. Wydaje mi się, że jest to bardzo delikatne rozwiązanie w rzeczywistej aplikacji.

Nie ma w tym nic kruchego i tego oczekiwałby użytkownik. Jeśli użytkownik musi zaczekać przed ponownym naciśnięciem przycisku, należy poczekać.

Widzę, jak pewne scenariusze sterowania mogą sprawić, że podejmowanie decyzji, które elementy sterujące mają zostać wyłączone / włączone w danym momencie, jest dość skomplikowane, ale czy zaskakujące jest to, że zarządzanie złożonym interfejsem użytkownika byłoby skomplikowane? Jeśli masz zbyt wiele przerażających efektów ubocznych z określonej kontroli, zawsze możesz po prostu wyłączyć całą formę i rzucić „ładowanie” gigantycznego koncertu na całość. Jeśli tak jest w przypadku każdej kontroli, twój interfejs użytkownika prawdopodobnie nie jest dobrze zaprojektowany (ścisłe połączenie, słabe grupowanie kontroli itp.).

Morgan Herlocker
źródło
Dzięki - ale jak zarządzać złożonością?
Nicholas Butler,
Jedną z opcji byłoby umieszczenie powiązanych elementów sterujących w kontenerze. Dezaktywuj / włącz przez kontrolę kontenera, a nie przez kontrolę. ie: EditorControls.Foreach (c => c.Enabled = false);
Morgan Herlocker,
2

Wyłączenie widgetu jest najmniej złożoną i podatną na błędy opcją. Wyłączasz go w module obsługi przed rozpoczęciem operacji asynchronicznej i włączasz ponownie na końcu operacji. Dlatego wyłączanie i włączanie dla każdej kontrolki są lokalne dla obsługi tej kontroli.

Możesz nawet utworzyć opakowanie, które przejmie przekazanie i kontrolę (lub więcej z nich), wyłączy kontrolę i asynchronicznie uruchomi coś, co wykona przekazanie i ponowne włączenie kontroli. Powinien zająć się wyjątkami (w końcu włączyć), więc nadal działa niezawodnie, jeśli operacja się nie powiedzie. Możesz to połączyć z dodaniem wiadomości na pasku stanu, aby użytkownik wiedział, na co czeka.

Możesz dodać logikę do liczenia wyłączeń, więc system działa dalej, jeśli masz dwie operacje, które powinny wyłączyć trzecią, ale nie wykluczają się nawzajem. Kontrolę wyłączasz dwa razy, więc zostanie ona ponownie włączona tylko wtedy, gdy ponownie ją włączysz. To powinno obejmować większość przypadków, jednocześnie utrzymując je oddzielnie, tak bardzo, jak to możliwe.

Z drugiej strony wszystko takie jak maszyna stanowa stanie się złożone. Z mojego doświadczenia wynika, że ​​wszystkie maszyny stanowe, które widziałem, były trudne w utrzymaniu, a twoja maszyna stanowa musiałaby obejmować cały dialog, wiążąc wszystko ze wszystkim.

Jan Hudec
źródło
Dzięki - podoba mi się ten pomysł. Czy miałbyś zatem obiekt menedżera dla swojego okna, który zlicza liczbę żądań wyłączania i włączania dla każdej kontrolki?
Nicholas Butler,
@NickButler: Cóż, masz już obiekt menedżera dla każdego okna - formularz. Miałem więc klasę implementującą żądania i liczącą, i po prostu dodawałem instancje do odpowiednich formularzy.
Jan Hudec
1

Chociaż Jan Hudec i Rachel wyrazili pokrewne pomysły, sugerowałbym użycie czegoś takiego jak architektura Model-View - wyrażenie stanu formy w obiekcie i powiązanie elementów formularza z tym stanem. Spowodowałoby to wyłączenie przycisku w sposób, który można skalować w przypadku bardziej złożonych scenariuszy.

psr
źródło