Tutaj mamy Grid
z Button
. Gdy użytkownik kliknie ten przycisk, wykonywana jest metoda z klasy Utility, która zmusza aplikację do kliknięcia na siatce. Przepływ kodu musi się tutaj zatrzymać i nie kontynuować, dopóki użytkownik nie kliknie przycisku Grid
.
Miałem wcześniej podobne pytanie:
Poczekaj, aż użytkownik kliknie C # WPF
W tym pytaniu otrzymałem odpowiedź za pomocą asynchronizacji / czekania, która działa, ale ponieważ zamierzam użyć jej jako części interfejsu API, nie chcę używać asynchronizacji / oczekiwania, ponieważ konsumenci będą musieli następnie oznaczyć swoje metody za pomocą asynchronizacja, której nie chcę.
Jak napisać Utility.PickPoint(Grid grid)
metodę osiągnięcia tego celu?
Widziałem to, co może pomóc, ale nie do końca zrozumiałem, aby zastosować tutaj szczerze:
Blokowanie do czasu zakończenia zdarzenia
Rozważ to jako metodę Console.ReadKey () w aplikacji Console. Po wywołaniu tej metody przepływ kodu zatrzymuje się, dopóki nie wprowadzimy wartości. Debuger nie będzie kontynuowany, dopóki czegoś nie wejdziemy. Chcę dokładnego zachowania metody PickPoint (). Przepływ kodu zostanie zatrzymany, dopóki użytkownik nie kliknie siatki.
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="3*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid x:Name="View" Background="Green"/>
<Button Grid.Row="1" Content="Pick" Click="ButtonBase_OnClick"/>
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
// do not continue the code flow until the user has clicked on the grid.
// so when we debug, the code flow will literally stop here.
var point = Utility.PickPoint(View);
MessageBox.Show(point.ToString());
}
}
public static class Utility
{
public static Point PickPoint(Grid grid)
{
}
}
źródło
Aync/Await
wykonanie operacji A i zapisanie tej operacji STAN, teraz chcesz, aby użytkownik powinien kliknąć opcję Siatka. Więc jeśli użytkownik kliknie opcję Siatka, sprawdź stan, jeśli jest prawdziwy, a następnie wykonaj swoją operację, po prostu rób co chcesz?AutoResetEvent
nie jest tym czego chcesz?var point = Utility.PickPoint(Grid grid);
w metodzie Grid Click? wykonać operację i zwrócić odpowiedź?Odpowiedzi:
Twoje podejście jest złe. Sterowane zdarzeniem nie oznacza blokowania i czekania na zdarzenie. Nigdy nie czekaj, przynajmniej zawsze starasz się tego uniknąć. Oczekiwanie marnuje zasoby, blokuje wątki i może wprowadzić ryzyko impasu lub wątku zombie (w przypadku, gdy sygnał zwolnienia nigdy nie zostanie podniesiony).
Powinno być jasne, że blokowanie wątku w celu oczekiwania na zdarzenie jest anty-wzorcem, ponieważ jest sprzeczne z ideą zdarzenia.
Zasadniczo masz dwie (nowoczesne) opcje: zaimplementuj asynchroniczny interfejs API lub interfejs API sterowany zdarzeniami. Ponieważ nie chcesz implementować interfejsu API asynchronicznie, pozostaje ci interfejs API sterowany zdarzeniami.
Kluczem API sterowanego zdarzeniami jest to, że zamiast zmuszać dzwoniącego do synchronicznego oczekiwania na wynik lub odpytywania o wynik, pozwalasz dzwoniącemu kontynuować i wysyłać mu powiadomienie, gdy wynik jest gotowy lub operacja jest zakończona. W międzyczasie osoba dzwoniąca może kontynuować wykonywanie innych operacji.
Patrząc na problem z perspektywy wątków, API sterowane zdarzeniami pozwala wątkowi wywołującemu, np. Wątkowi interfejsu użytkownika, który wykonuje procedurę obsługi przycisku, mieć swobodę kontynuowania obsługi np. Innych operacji związanych z interfejsem użytkownika, takich jak renderowanie elementów interfejsu użytkownika lub obsługi danych wejściowych użytkownika, takich jak ruchy myszy i naciśnięcia klawiszy. Interfejs API sterowany zdarzeniami ma taki sam efekt lub cel jak interfejs API asynchroniczny, chociaż jest znacznie mniej wygodny.
Ponieważ nie podałeś wystarczająco dużo szczegółów na temat tego, co naprawdę próbujesz zrobić, co
Utility.PickPoint()
faktycznie robi i jaki jest wynik zadania lub dlaczego użytkownik musi kliknąć opcję `Siatka, nie mogę zaoferować lepszego rozwiązania . Mogę tylko przedstawić ogólny wzorzec, w jaki sposób spełnić twoje wymagania.Twój przepływ lub cel jest oczywiście podzielony na co najmniej dwa kroki, aby uczynić z niego sekwencję operacji:
Grid
z co najmniej dwoma ograniczeniami:
Wymaga to dwóch powiadomień (zdarzeń) dla klienta interfejsu API, aby umożliwić interakcję nieblokującą:
Powinieneś pozwolić swojemu API zaimplementować to zachowanie i ograniczenia, ujawniając dwie metody publiczne i dwa zdarzenia publiczne.
Ponieważ ta implementacja zezwala tylko na pojedyncze (nie współbieżne) wywołanie interfejsu API, zaleca się również ujawnienie
IsBusy
właściwości wskazującej działającą sekwencję. Umożliwia to odpytywanie bieżącego stanu przed rozpoczęciem nowej sekwencji, chociaż zaleca się czekanie na zakończenie zdarzenia w celu wykonania kolejnych wywołań.Implementuj / refaktoryzuj API narzędzia
Utility.cs
PickPointCompletedEventArgs.cs
Użyj interfejsu API
MainWindow.xaml.cs
MainWindow.xaml
Uwagi
Zdarzenia wywołane w wątku w tle wykonają swoje procedury obsługi w tym samym wątku. Dostęp do
DispatcherObject
podobnego elementu interfejsu użytkownika z modułu obsługi, który jest wykonywany w wątku w tle, wymaga kolejkowania operacji krytycznej przyDispatcher
użyciu jednego z nichDispatcher.Invoke
lubDispatcher.InvokeAsync
uniknięcia wyjątków między wątkami.Przeczytaj uwagi o tym,
DispatcherObject
aby dowiedzieć się więcej o tym zjawisku zwanym powinowactwem dyspozytora lub powinowactwem do wątku.Dla wygodnego użycia interfejsu API sugeruję, aby wszystkie zdarzenia przenieść do pierwotnego kontekstu dzwoniącego, przechwytując go i używając go
SynchronizationContext
lub używającAsyncOperation
(lubAsyncOperationManager
).Powyższy przykład można łatwo ulepszyć, zapewniając anulowanie (zalecane) np. Przez ujawnienie
Cancel()
metody np.PickPointCancel()
I raportowanie postępów (najlepiej przy użyciuProgress<T>
).Kilka przemyśleń - odpowiedz na swoje komentarze
Ponieważ zbliżałeś się do mnie, by znaleźć „lepsze” rozwiązanie blokujące, biorąc pod uwagę przykład aplikacji konsolowych, przekonałem cię, że twoje postrzeganie lub punkt widzenia są całkowicie błędne.
Aplikacja konsolowa to coś zupełnie innego. Koncepcja wątków jest nieco inna. Aplikacje konsolowe nie mają GUI. Tylko strumienie wejściowe / wyjściowe / błędów. Nie można porównać architektury aplikacji konsoli do bogatej aplikacji GUI. To nie zadziała. Naprawdę musisz to zrozumieć i zaakceptować.
Również nie daj się zwieść wyglądowi . Czy wiesz, co się dzieje w środku
Console.ReadLine
? Jak to jest realizowane ? Czy blokuje główny wątek i równolegle czyta wejście? A może to tylko ankieta?Oto oryginalne wdrożenie
Console.ReadLine
:Jak widać, jest to prosta synchroniczna operacja. Przeszukuje dane wejściowe użytkownika w „nieskończonej” pętli. Bez magicznego bloku i kontynuuj.
WPF jest zbudowany wokół wątku renderującego i wątku interfejsu użytkownika. Nici te wciąż zawsze przędzenia w celu komunikowania się z systemem operacyjnym jak przenoszenia danych wprowadzanych przez użytkownika - utrzymanie aplikacji reaguje . Nigdy nie chcesz wstrzymywać / blokować tego wątku, ponieważ zatrzyma on szkielet w wykonywaniu niezbędnych prac w tle, takich jak reagowanie na zdarzenia myszy - nie chcesz, aby mysz się zawieszała:
czekanie = blokowanie wątków = brak reakcji = złe UX = zirytowani użytkownicy / klienci = problemy w biurze.
Czasami przepływ aplikacji wymaga oczekiwania na wejście lub procedurę. Ale nie chcemy blokować głównego wątku.
Dlatego ludzie wymyślili złożone modele programowania asynchronicznego, aby umożliwić czekanie bez blokowania głównego wątku i bez zmuszania programisty do pisania skomplikowanego i błędnego wielowątkowego kodu.
Każda nowoczesna struktura aplikacji oferuje operacje asynchroniczne lub asynchroniczny model programowania, aby umożliwić opracowanie prostego i wydajnego kodu.
Fakt, że usiłujesz się oprzeć asynchronicznemu modelowi programowania, pokazuje mi pewien brak zrozumienia. Każdy współczesny programista woli interfejs asynchroniczny niż interfejs synchroniczny. Żaden poważny programista nie chce używać
await
słowa kluczowego ani deklarować swojej metodyasync
. Nikt. Jesteś pierwszym, z którym się spotykam, który skarży się na asynchroniczne interfejsy API i który uważa, że korzystanie z nich jest niewygodne.Gdybym sprawdził twoje środowisko, które ma na celu rozwiązanie problemów związanych z interfejsem użytkownika lub ułatwiło zadania związane z interfejsem użytkownika, oczekiwałbym, że będzie on asynchroniczny - do końca.
Interfejs API związany z interfejsem użytkownika, który nie jest asynchroniczny, jest marnotrawstwem, ponieważ komplikuje mój styl programowania, dlatego mój kod staje się bardziej podatny na błędy i trudny do utrzymania.
Z innej perspektywy: kiedy przyznajesz, że czekanie blokuje wątek interfejsu użytkownika, powoduje bardzo złe i niepożądane wrażenia użytkownika, ponieważ interfejs użytkownika zawiesza się, aż do zakończenia oczekiwania, teraz, gdy zdajesz sobie z tego sprawę, dlaczego oferujesz interfejs API lub model wtyczki, który zachęca programistę do zrobienia dokładnie tego - wdrożyć czekanie?
Nie wiesz, co zrobi wtyczka innej firmy i ile czasu zajmie procedura do jej zakończenia. To po prostu zły projekt API. Gdy interfejs API działa w wątku interfejsu użytkownika, osoba wywołująca interfejs API musi być w stanie wykonywać nieblokujące wywołania do niego.
Jeśli odrzucasz jedyne tanie lub pełne wdzięku rozwiązanie, zastosuj podejście oparte na zdarzeniach, jak pokazano w moim przykładzie.
Robi to, co chcesz: uruchomić procedurę - czekać na dane wejściowe użytkownika - kontynuować wykonywanie - osiągnąć cel.
Naprawdę kilkakrotnie próbowałem wyjaśnić, dlaczego oczekiwanie / blokowanie jest złym projektem aplikacji. Ponownie nie można porównać interfejsu konsoli do bogatego interfejsu graficznego, w którym np. Sama obsługa danych wejściowych jest o wiele bardziej złożona niż tylko słuchanie strumienia wejściowego. Naprawdę nie znam twojego poziomu doświadczenia i miejsca, w którym zacząłeś, ale powinieneś zacząć stosować model programowania asynchronicznego. Nie znam powodu, dla którego próbujesz tego uniknąć. Ale to wcale nie jest mądre.
Obecnie modele programowania asynchronicznego są wdrażane wszędzie, na każdej platformie, kompilatorze, każdym środowisku, przeglądarce, serwerze, komputerze stacjonarnym, bazie danych - wszędzie. Model sterowany zdarzeniami pozwala osiągnąć ten sam cel, ale jest mniej wygodny w użyciu (subskrybuj / wypisz się do / ze zdarzeń, czytaj dokumenty (gdy są dokumenty), aby dowiedzieć się o zdarzeniach), opierając się na wątkach w tle. Sterowane zdarzeniami są przestarzałe i powinny być używane tylko wtedy, gdy biblioteki asynchroniczne są niedostępne lub nie mają zastosowania.
Na marginesie: Framwork .NET (.NET Standard) oferuje
TaskCompletionSource
(między innymi) prosty sposób na konwersję istniejącego API sterowanego parzystym na asynchroniczny API.Zachowanie (to, czego doświadczasz lub obserwujesz) znacznie różni się od sposobu implementacji tego doświadczenia. Dwie różne rzeczy. Twój Autodesk najprawdopodobniej używa asynchronicznych bibliotek lub funkcji językowych lub innego mechanizmu wątków. I jest również związany z kontekstem. Gdy metoda, o której myślisz, jest wykonywana w wątku w tle, programista może zablokować ten wątek. Ma albo bardzo dobry powód, aby to zrobić, albo po prostu dokonał złego wyboru projektu. Jesteś całkowicie na złym torze;) Blokowanie nie jest dobre.
(Czy kod źródłowy Autodesk jest oprogramowaniem typu open source? Lub skąd wiesz, jak jest on wdrażany?)
Nie chcę cię obrażać, proszę uwierz mi. Zastanów się jednak, czy zaimplementować interfejs API asynchronicznie. Tylko w twojej głowie programiści nie lubią używać asynchronizacji / czekania. Oczywiście masz niewłaściwy sposób myślenia. I zapomnij o argumencie aplikacji konsolowej - to nonsens;)
Interfejs API związany z interfejsem użytkownika MUSI używać asynchronicznie / oczekuj, gdy tylko jest to możliwe. W przeciwnym razie pozostawiasz całą pracę, aby napisać nieblokujący kod do klienta interfejsu API. Zmusiłbyś mnie do zawinięcia każdego wywołania twojego API w wątek w tle. Lub użyć mniej wygodnej obsługi zdarzeń. Uwierz mi - każdy programista raczej ozdabia swoich członków
async
, niż zajmuje się obsługą zdarzeń. Za każdym razem, gdy korzystasz ze zdarzeń, możesz ryzykować wyciek pamięci - zależy od pewnych okoliczności, ale ryzyko jest realne i nierzadkie przy programowaniu beztroskim.Naprawdę mam nadzieję, że rozumiesz, dlaczego blokowanie jest złe. Naprawdę mam nadzieję, że zdecydujesz się użyć asynchronicznego / oczekuj na napisanie nowoczesnego asynchronicznego API. Niemniej jednak pokazałem ci bardzo częsty sposób oczekiwania na nieblokowanie przy użyciu zdarzeń, chociaż zachęcam do użycia asynchronizacji / oczekiwania.
Jeśli nie chcesz zezwalać wtyczce na bezpośredni dostęp do elementów interfejsu użytkownika, powinieneś udostępnić interfejs do delegowania zdarzeń lub ujawnienia komponentów wewnętrznych poprzez abstrakcyjne obiekty.
Interfejs API wewnętrznie subskrybuje zdarzenia interfejsu użytkownika w imieniu dodatku, a następnie deleguje je, udostępniając odpowiednie zdarzenie „otoki” klientowi interfejsu API. Twój interfejs API musi oferować pewne zaczepy, w których dodatek może się łączyć, aby uzyskać dostęp do określonych składników aplikacji. Wtyczka API działa jak adapter lub fasada, zapewniając zewnętrznym dostęp do wewnętrznych.
Aby umożliwić pewien stopień izolacji.
Zobacz, jak Visual Studio zarządza wtyczkami lub pozwala nam je implementować. Udawaj, że chcesz napisać wtyczkę do programu Visual Studio i poszukaj informacji, jak to zrobić. Uświadomisz sobie, że Visual Studio udostępnia swoje elementy wewnętrzne za pośrednictwem interfejsu lub interfejsu API. Np. Możesz manipulować edytorem kodu lub uzyskiwać informacje o treści edytora bez rzeczywistego dostępu do niego.
źródło
var str = Console.ReadLine(); Console.WriteLine(str);
Co się stanie, gdy uruchomisz aplikację w trybie debugowania. Zatrzyma się w pierwszym wierszu kodu i zmusi cię do wprowadzenia wartości w interfejsie użytkownika konsoli, a następnie po wprowadzeniu czegoś i naciśnięciu klawisza Enter wykona następny wiersz i wydrukuje to, co wpisałeś. Myślałem o tym samym zachowaniu, ale w aplikacji WPF.Osobiście uważam, że jest to zbyt skomplikowane przez wszystkich, ale może nie do końca rozumiem powód, dla którego należy to zrobić w określony sposób, ale wydaje się, że można tutaj zastosować prostą kontrolę bool.
Przede wszystkim spraw, aby twoja siatka była sprawdzalna pod względem trafności, ustawiając właściwości
Background
iIsHitTestVisible
, w przeciwnym razie nie będzie nawet rejestrować kliknięć myszką.Następnie utwórz wartość bool, w której można zapisać, czy powinno wystąpić zdarzenie „GridClick”. Po kliknięciu siatki sprawdź tę wartość i wykonaj wykonanie ze zdarzenia kliknięcia siatki, jeśli oczekuje ono na kliknięcie.
Przykład:
źródło
Próbowałem kilku rzeczy, ale nie jestem w stanie tego zrobić bez
async/await
. Ponieważ jeśli go nie użyjemy, spowodujeDeadLock
to zablokowanie interfejsu użytkownika, a następnie będziemy mogli przyjmowaćGrid_Click
dane wejściowe.źródło
Możesz zablokować asynchronicznie, używając
SemaphoreSlim
:Nie możesz i nie chcesz blokować wątku dyspozytora synchronicznie, ponieważ wtedy nigdy nie będzie on w stanie obsłużyć kliknięcia na
Grid
, tzn. Nie może być zarówno zablokowany, jak i obsługiwać zdarzeń jednocześnie.źródło
Console.Readline
bloki , tzn. Nie zwraca, dopóki nie zostanie odczytany wiersz. TwojaPickPoint
metoda nie. Zwraca natychmiast. Może to potencjalnie zablokować, ale w międzyczasie nie będziesz w stanie obsłużyć interfejsu użytkownika, jak napisałem w mojej odpowiedzi. Innymi słowy, musisz uzyskać kliknięcie w metodzie, aby uzyskać takie samo zachowanie.PickPoint
który obsługuje zdarzenia myszy. Nie widzę, gdzie idziesz z tym?Technicznie jest to możliwe zi
AutoResetEvent
bezasync/await
, ale istnieje znacząca wada:Wada: jeśli wywołasz tę metodę bezpośrednio w procedurze obsługi zdarzenia przycisku, tak jak robi to twój przykładowy kod, nastąpi zakleszczenie i zobaczysz, że aplikacja przestaje odpowiadać. Ponieważ używasz jedynego wątku interfejsu użytkownika do oczekiwania na kliknięcie użytkownika, nie może on zareagować na żadną akcję użytkownika, w tym kliknięcie użytkownika w siatkę.
Konsumenci metody powinni wywołać ją w innym wątku, aby zapobiec zakleszczeniom. Jeśli można to zagwarantować, w porządku. W przeciwnym razie musisz wywołać następującą metodę:
Może to przysporzyć kłopotów konsumentom interfejsu API, chyba że używali oni do zarządzania własnymi wątkami. Dlatego
async/await
został wymyślony.źródło
Myślę, że problem dotyczy samego projektu. Jeśli interfejs API działa na określonym elemencie, należy go użyć w module obsługi zdarzeń tego samego elementu, a nie na innym elemencie.
Na przykład tutaj chcemy uzyskać pozycję zdarzenia kliknięcia w siatce, interfejs API musi być używany w procedurze obsługi zdarzeń powiązanej ze zdarzeniem w elemencie siatki, a nie w elemencie przycisku.
Teraz, jeśli wymaganiem jest obsługa kliknięcia w siatce dopiero po kliknięciu przycisku, wówczas odpowiedzialnością przycisku będzie dodanie procedury obsługi zdarzeń w siatce, a zdarzenie kliknięcia w siatce wyświetli okno komunikatu i usunie ten moduł obsługi zdarzeń dodany przez przycisk, aby nie uruchamiał się po tym kliknięciu ... (nie trzeba blokować wątku interfejsu użytkownika)
Wystarczy powiedzieć, że jeśli zablokujesz wątek interfejsu użytkownika po kliknięciu przycisku, nie sądzę, że wątek interfejsu będzie mógł później wywołać zdarzenie kliknięcia w siatce.
źródło
Po pierwsze, wątek interfejsu użytkownika nie może być blokowany tak jak odpowiedź na twoje pierwsze pytanie.
Jeśli możesz się z tym zgodzić, unikanie asynchronizacji / czekania, aby zmusić klienta do zrobienia mniej modyfikacji, jest wykonalne, a nawet nie wymaga wielowątkowości.
Ale jeśli chcesz zablokować wątek interfejsu użytkownika lub „przepływ kodu”, odpowiedzią będzie, że jest to niemożliwe. Ponieważ jeśli wątek interfejsu użytkownika został zablokowany, nie można odbierać dalszych danych wejściowych.
Ponieważ wspomniałeś o aplikacji konsolowej, zrobię tylko proste wyjaśnienie.
Po uruchomieniu aplikacji konsoli lub wywołaniu
AllocConsole
z procesu, który nie został podłączony do żadnej konsoli (okna), conhost.exe, który może zapewnić konsolę (okno), zostanie wykonany, a aplikacja konsoli lub proces wywołujący zostanie dołączony do konsoli ( okno).Tak więc każdy napisany kod, który mógłby blokować wątek dzwoniącego, taki jak
Console.ReadKey
nie blokuje wątku interfejsu użytkownika okna konsoli, jest to powód, dla którego aplikacja konsoli oczekuje na dane wejściowe, ale nadal może reagować na inne dane wejściowe, takie jak kliknięcie myszą.źródło