Jak zablokować przepływ kodu do momentu uruchomienia zdarzenia w C #

11

Tutaj mamy Gridz 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)
    {

    }
}
Vahid
źródło
Oczywistym sposobem jest Aync/Awaitwykonanie 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?
Rao Hammas Hussain
@RaoHammasHussain Zaktualizowałem moje pytanie za pomocą linku, który może pomóc. Metoda narzędziowa będzie częścią interfejsu API, do którego wywoła użytkownik interfejsu API, ilekroć chce poprosić użytkownika końcowego o kliknięcie na ekranie. Rozważ to jak okno zachęty dla tekstu w normalnych aplikacjach Windows lub w Console.Readline (). W takich przypadkach przepływ kodu zostaje zatrzymany, dopóki użytkownik czegoś nie wprowadzi. Teraz chcę dokładnie to, ale tym razem użytkownik klika ekran.
Vahid
AutoResetEventnie jest tym czego chcesz?
Rao Hammas Hussain
@RaoHammasHussain Myślę, że tak, ale naprawdę nie wiem, jak go tutaj użyć.
Vahid
To tak, jakbyś celowo wdrażał WAIT STATE. czy to naprawdę wymagane? bo nie możesz tego po prostu zastosować var point = Utility.PickPoint(Grid grid);w metodzie Grid Click? wykonać operację i zwrócić odpowiedź?
Rao Hammas Hussain

Odpowiedzi:

9

„Jak zablokować przepływ kodu do momentu uruchomienia zdarzenia?”

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:

  1. Wykonaj operację 1, gdy użytkownik kliknie przycisk
  2. Wykonaj operację 2 (kontynuuj / zakończ operację 1), gdy użytkownik kliknie na Grid

z co najmniej dwoma ograniczeniami:

  1. Opcjonalnie: sekwencja musi zostać zakończona, zanim klient API będzie mógł ją powtórzyć. Sekwencja jest zakończona po zakończeniu operacji 2.
  2. Operacja 1 jest zawsze wykonywana przed operacją 2. Operacja 1 rozpoczyna sekwencję.
  3. Operacja 1 musi zostać zakończona, zanim klient API będzie mógł wykonać operację 2

Wymaga to dwóch powiadomień (zdarzeń) dla klienta interfejsu API, aby umożliwić interakcję nieblokującą:

  1. Operacja 1 zakończona (lub wymagana interakcja)
  2. Operacja 2 (lub bramka) zakończona

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 IsBusywł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

class Utility
{
  public event EventHandler InitializePickPointCompleted;
  public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
  public bool IsBusy { get; set; }
  private bool IsPickPointInitialized { get; set; }

  // The prefix 'Begin' signals the caller or client of the API, 
  // that he also has to end the sequence explicitly
  public void BeginPickPoint(param)
  {
    // Implement constraint 1
    if (this.IsBusy)
    {
      // Alternatively just return or use Try-do pattern
      throw new InvalidOperationException("BeginPickPoint is already executing. Call EndPickPoint before starting another sequence.");
    }

    // Set the flag that a current sequence is in progress
    this.IsBusy = true;

    // Execute operation until caller interaction is required.
    // Execute in background thread to allow API caller to proceed with execution.
    Task.Run(() => StartOperationNonBlocking(param));
  }

  public void EndPickPoint(param)
  {
    // Implement constraint 2 and 3
    if (!this.IsPickPointInitialized)
    {
      // Alternatively just return or use Try-do pattern
      throw new InvalidOperationException("BeginPickPoint must have completed execution before calling EndPickPoint.");
    }

    // Execute operation until caller interaction is required.
    // Execute in background thread to allow API caller to proceed with execution.
    Task.Run(() => CompleteOperationNonBlocking(param));
  }

  private void StartOperationNonBlocking(param)
  {
    ... // Do something

    // Flag the completion of the first step of the sequence (to guarantee constraint 2)
    this.IsPickPointInitialized = true;

    // Request caller interaction to kick off EndPickPoint() execution
    OnInitializePickPointCompleted();
  }

  private void CompleteOperationNonBlocking(param)
  {
    // Execute goal and get the result of the completed task
    Point result = ExecuteGoal();

    // Reset API sequence (allow next client invocation)
    this.IsBusy = false;
    this.IsPickPointInitialized = false;

    // Notify caller that execution has completed and the result is available
    OnPickPointCompleted(result);
  }

  private void OnInitializePickPointCompleted()
  {
    // Set the result of the task
    this.InitializePickPointCompleted?.Invoke(this, EventArgs.Empty);
  }

  private void OnPickPointCompleted(Point result)
  {
    // Set the result of the task
    this.PickPointCompleted?.Invoke(this, new PickPointCompletedEventArgs(result));
  }
}

PickPointCompletedEventArgs.cs

class PickPointCompletedEventArgs : AsyncCompletedEventArgs 
{
  public Point Result { get; }

  public PickPointCompletedEventArgs(Point result)
  {
    this.Result = result;
  }
}

Użyj interfejsu API

MainWindow.xaml.cs

partial class MainWindow : Window
{
  private Utility Api { get; set; }

  public MainWindow()
  {
    InitializeComponent();

    this.Api = new Utility();
  }

  private void StartPickPoint_OnButtonClick(object sender, RoutedEventArgs e)
  {
    this.Api.InitializePickPointCompleted += RequestUserInput_OnInitializePickPointCompleted;

    // Invoke API and continue to do something until the first step has completed.
    // This is possible because the API will execute the operation on a background thread.
    this.Api.BeginPickPoint();
  }

  private void RequestUserInput_OnInitializePickPointCompleted(object sender, EventArgs e)
  {
    // Cleanup
    this.Api.InitializePickPointCompleted -= RequestUserInput_OnInitializePickPointCompleted;

    // Communicate to the UI user that you are waiting for him to click on the screen
    // e.g. by showing a Popup, dimming the screen or showing a dialog.
    // Once the input is received the input event handler will invoke the API to complete the goal   
    MessageBox.Show("Please click the screen");  
  }

  private void FinishPickPoint_OnGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  {
    this.Api.PickPointCompleted += ShowPoint_OnPickPointCompleted;

    // Invoke API to complete the goal
    // and continue to do something until the last step has completed
    this.Api.EndPickPoint();
  }

  private void ShowPoint_OnPickPointCompleted(object sender, PickPointCompletedEventArgs e)
  {
    // Cleanup
    this.Api.PickPointCompleted -= ShowPoint_OnPickPointCompleted;

    // Get the result from the PickPointCompletedEventArgs instance
    Point point = e.Result;

    // Handle the result
    MessageBox.Show(point.ToString());
  }
}

MainWindow.xaml

<Window>
  <Grid MouseLeftButtonUp="FinishPickPoint_OnGridMouseLeftButtonUp">
    <Button Click="StartPickPoint_OnButtonClick" />
  </Grid>
</Window>

Uwagi

Zdarzenia wywołane w wątku w tle wykonają swoje procedury obsługi w tym samym wątku. Dostęp do DispatcherObjectpodobnego elementu interfejsu użytkownika z modułu obsługi, który jest wykonywany w wątku w tle, wymaga kolejkowania operacji krytycznej przy Dispatcherużyciu jednego z nich Dispatcher.Invokelub Dispatcher.InvokeAsyncuniknięcia wyjątków między wątkami.
Przeczytaj uwagi o tym, DispatcherObjectaby 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 SynchronizationContextlub używając AsyncOperation(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życiu Progress<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.

„Rozważ aplikację konsolową z tymi dwoma wierszami kodu.

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 ”.

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:

public virtual String ReadLine() 
{
  StringBuilder sb = new StringBuilder();
  while (true) 
  {
    int ch = Read();
    if (ch == -1) 
      break;
    if (ch == '\r' || ch == '\n') 
    {
      if (ch == '\r' && Peek() == '\n') 
        Read();
      return sb.ToString();
    }
    sb.Append((char)ch);
  }
  if (sb.Length > 0) 
    return sb.ToString();
  return null;
}

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ć awaitsłowa kluczowego ani deklarować swojej metody async. 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.

„Widziałem dokładne zachowanie w programie Autodesk Revit”.

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.

„Interfejs API pozwoli programiście na dostęp do interfejsu użytkownika itp. Załóżmy teraz, że programista chce opracować dodatek, który po kliknięciu przycisku użytkownik końcowy zostanie poproszony o wybranie punktu w interfejsie użytkownika”

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.

BionicCode
źródło
Cześć, dziękuję za podejście do pytania z innej perspektywy. Przepraszam, jeśli pytanie było nieco niejasne w szczegółach. Rozważ aplikację konsolową z tymi dwoma wierszami kodu. 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.
Vahid
W opracowywanej przeze mnie aplikacji CAD użytkownicy powinni mieć możliwość jej rozszerzenia o dodatki / wtyczki. Interfejs API pozwoli programiście na dostęp do interfejsu użytkownika itp. Załóżmy teraz, że programista chce opracować dodatek, który po kliknięciu przycisku zostanie poproszony o wybranie punktu w interfejsie użytkownika, a następnie kod wykona inne czynności fajne rzeczy z danym punktem. Może
poprosą
Widziałem dokładne zachowanie w programie Autodesk Revit.
Vahid
1
Mam coś do powiedzenia na temat twoich wymagań. Proszę przeczytać moją zaktualizowaną odpowiedź. Wysłałem tam odpowiedź, ponieważ stała się jakoś dłuższa. Przyznaję, że naprawdę mnie wyzwoliłeś. Podczas czytania pamiętaj, że nie chcę cię obrażać.
BionicCode
Dziękujemy za zaktualizowaną odpowiedź. Oczywiście nie obrażam się. Wręcz przeciwnie, jestem naprawdę wdzięczny za poświęcony czas i wysiłek.
Vahid
5

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 Backgroundi IsHitTestVisible, w przeciwnym razie nie będzie nawet rejestrować kliknięć myszką.

<grid MouseLeftButtonUp="Grid_MouseLeftButtonUp" IsHitTestVisible="True" Background="Transparent">

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:

bool awaitingClick = false;


private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
   awaitingClick=true;
}

private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{     
     //Stop here if the program shouldn't do anything when grid is clicked
     if (!awaitingClick) { return; } 

     //Run event
     var point = Utility.PickPoint(View);
     MessageBox.Show(point.ToString());

     awaitingClick=false;//Reset
}
Tronald
źródło
Cześć Tronald, myślę, że źle zrozumiałeś pytanie. Potrzebuję, aby kod zatrzymał się w Utility.PickPoint (widok) i kontynuował dopiero po kliknięciu przez użytkownika siatki.
Vahid
O tak, zupełnie nie zrozumiałem. Przepraszam, nie zdawałem sobie sprawy, że potrzebujesz wszystkiego, aby się zatrzymać. Nie sądzę, że jest to możliwe bez wielowątkowości, ponieważ cały interfejs użytkownika zostanie zablokowany.
Tronald
Nadal nie jestem pewien, czy nie jest to możliwe. Jest to zdecydowanie możliwe w przypadku asynchronizacji / oczekiwania, co nie jest rozwiązaniem wielowątkowym. Ale potrzebuję alternatywy dla rozwiązania asynchronicznego / oczekującego.
Vahid
1
Jasne, ale wspomniałeś, że nie możesz używać asynchronizacji / oczekiwania. Wygląda na to, że będziesz musiał skorzystać z programu rozsyłającego i wątku oddzielonego od głównego wątku (który wykonuje się w interfejsie użytkownika). Mam nadzieję, że znajdziesz inny sposób, ponieważ jestem zainteresowany
Tronald
2

Próbowałem kilku rzeczy, ale nie jestem w stanie tego zrobić bez async/await. Ponieważ jeśli go nie użyjemy, spowoduje DeadLockto zablokowanie interfejsu użytkownika, a następnie będziemy mogli przyjmować Grid_Clickdane wejściowe.

private async void ToolBtn_OnClick(object sender, RoutedEventArgs e)
{
    var senderBtn = sender as Button;
    senderBtn.IsEnabled = false;

    var response = await Utility.PickPoint(myGrid);
    MessageBox.Show(response.ToString());
    senderBtn.IsEnabled = true;
}  

public static class Utility
{
    private static TaskCompletionSource<bool> tcs;
    private static Point _point = new Point();

    public static async Task<Point> PickPoint(Grid grid)
    {
        tcs = new TaskCompletionSource<bool>();
        _point = new Point();

        grid.MouseLeftButtonUp += GridOnMouseLeftButtonUp;


        await tcs.Task;

        grid.MouseLeftButtonUp -= GridOnMouseLeftButtonUp;
        return _point;
    }


    private static void GridOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {

        // do something here ....
        _point = new Point { X = 23, Y = 34 };
        // do something here ....

        tcs.SetResult(true); // as soon its set it will go back

    }
}
Rao Hammas Hussain
źródło
@ dzięki, to jest ta sama odpowiedź, którą otrzymałem na moje inne pytanie, które używa asynchronizacji / oczekiwania.
Vahid
o tak ! zauważyłem to teraz, ale myślę, że to jedyny sposób, w jaki
działam
2

Możesz zablokować asynchronicznie, używając SemaphoreSlim:

public partial class MainWindow : Window, IDisposable
{
    private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(0, 1);

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        var point = Utility.PickPoint(View);

        // 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.
        await _semaphoreSlim.WaitAsync();

        MessageBox.Show(point.ToString());
    }

    private void View_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        //click on grid detected....
        _semaphoreSlim.Release();
    }

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);
        Dispose();
    }

    public void Dispose() => _semaphoreSlim.Dispose();
}

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.

mm8
źródło
Dziękuję za alternatywną odpowiedź. Zastanawiam się, jak to się robi na przykład w Console.Readline ()? Kiedy osiągniesz tę metodę w debuggerze, magicznie się tam zatrzyma, chyba że coś wejdziemy? Czy zasadniczo różni się w aplikacjach konsolowych? Czy nie możemy mieć takiego samego zachowania w aplikacji WinForms / WPF? Widziałem to w interfejsie API programu Autodesk Revit, istnieje tam metoda PickPoint (), która zmusza do wybrania punktu na ekranie i nie widziałem, aby użyto asynchronizacji / oczekiwania! Czy przynajmniej możliwe jest ukrycie słowa kluczowego oczekującego i wywołanie go w jakiś sposób przed metodą synchronizacji?
Vahid
@Vahid: Console.Readline bloki , tzn. Nie zwraca, dopóki nie zostanie odczytany wiersz. Twoja PickPointmetoda 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.
mm8
Console.Realine () blokuje, ale jednocześnie dozwolone są zdarzenia KeyPress. Czy nie możemy mieć dokładnie takiego samego zachowania? Blokujesz przez PickPoint () i zezwalasz tylko na MouseEvents? Nie rozumiem, dlaczego jest to możliwe w konsoli, ale nie w aplikacji opartej na interfejsie użytkownika.
Vahid
Następnie musisz skonfigurować osobny dyspozytor, PickPointktóry obsługuje zdarzenia myszy. Nie widzę, gdzie idziesz z tym?
mm8
1
@Vahind: Czy asynchronizuje kod i pozwala użytkownikowi czekać na metodę? To jest interfejs API, którego oczekiwałbym jako programista interfejsu użytkownika. Wywołanie metody blokowania w aplikacji interfejsu użytkownika nie ma żadnego sensu.
mm8
2

Technicznie jest to możliwe zi AutoResetEventbez async/await, ale istnieje znacząca wada:

public static Point PickPoint(Grid grid)
{
    var pointPicked = new AutoResetEvent(false);
    grid.MouseLeftButtonUp += (s, e) => 
    {
        // do whatever after the grid is clicked

        // signal the end of waiting
        pointPicked.Set();
    };

    // code flow will stop here and wait until the grid is clicked
    pointPicked.WaitOne();
    // return something...
}

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ę:

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    // here I used ThreadPool, but you may use other means to run on another thread
    ThreadPool.QueueUserWorkItem(new WaitCallback(Capture));
}

private void Capture(object state)
{
    // 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());
}

Może to przysporzyć kłopotów konsumentom interfejsu API, chyba że używali oni do zarządzania własnymi wątkami. Dlategoasync/await został wymyślony.

Ken Hung
źródło
Dziękuję Ken, czy jest możliwe, że dodatek zaczyna się od innego wątku, a następnie jego zdarzenia nie blokują głównego wątku interfejsu użytkownika?
Vahid
@Vahid Tak i nie. Tak, możesz wywołać metodę blokowania w innym wątku i zawinąć ją w inną metodę. Jednak metoda otoki nadal wymagała wywołania w innym wątku innym niż wątek interfejsu użytkownika, aby uniknąć blokowania interfejsu użytkownika. Ponieważ opakowanie zablokuje wątek wywołujący, jeśli jest synchroniczny . Chociaż wewnętrznie opakowanie blokuje inny wątek, nadal musi czekać na wynik i blokuje wywołanie wątku. Jeśli wywołujący wywołuje metodę otoki w wątku interfejsu użytkownika, interfejs użytkownika jest blokowany.
Ken Hung
0

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.

jsami
źródło
0

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.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        Utility.PickPoint(View, (x) => MessageBox.Show(x.ToString()));
    }
}

public static class Utility
{
    private static Action<Point> work;

    public static void PickPoint(Grid grid, Action<Point> work)
    {
        if (Utility.work == null)
        {
            grid.PreviewMouseLeftButtonUp += Grid_PreviewMouseLeftButtonUp;
            Utility.work = work;
        }
    }

    private static void Grid_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        var grid = (Grid)sender;
        work.Invoke(e.GetPosition(grid));
        grid.PreviewMouseLeftButtonUp -= Grid_PreviewMouseLeftButtonUp;
        Utility.work = null;
    }
}   

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 AllocConsolez 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.ReadKeynie 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ą.

Alex.Wei
źródło