So blockieren Sie den Codefluss, bis ein Ereignis in C # ausgelöst wird

9

Hier haben wir eine Gridmit einem Button. Wenn der Benutzer auf die Schaltfläche klickt, wird eine Methode in einer Utility-Klasse ausgeführt, die die Anwendung zwingt, einen Klick auf Grid zu erhalten. Der Code-Fluss muss hier anhalten und darf erst fortgesetzt werden, wenn der Benutzer auf das geklickt hat Grid.

Ich hatte hier schon einmal eine ähnliche Frage:

Warten Sie, bis der Benutzer auf C # WPF klickt

In dieser Frage habe ich eine Antwort mit async / await erhalten, die funktioniert. Da ich sie jedoch als Teil einer API verwenden werde, möchte ich async / await nicht verwenden, da die Verbraucher dann ihre Methoden mit markieren müssen async was ich nicht will.

Wie schreibe ich eine Utility.PickPoint(Grid grid)Methode, um dieses Ziel zu erreichen?

Ich habe dies gesehen, was vielleicht hilft, aber ich habe es nicht vollständig verstanden, um hier ehrlich zu sein:

Blockieren, bis ein Ereignis abgeschlossen ist

Betrachten Sie es als eine Art Console.ReadKey () -Methode in einer Konsolenanwendung. Wenn wir diese Methode aufrufen, stoppt der Codefluss, bis wir einen Wert eingeben. Der Debugger wird erst fortgesetzt, wenn wir etwas eingeben. Ich möchte das genaue Verhalten für die PickPoint () -Methode. Der Code-Fluss wird angehalten, bis der Benutzer auf das Raster klickt.

<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
quelle
Der offensichtliche Weg ist, Aync/Awaitwie Sie Operation A ausführen und diese Operation STATE speichern. Jetzt möchten Sie, dass der Benutzer auf Grid klickt. Wenn der Benutzer auf Grid klickt, überprüfen Sie den Status, wenn er wahr ist. Führen Sie dann Ihre Operation aus, tun Sie einfach, was Sie wollen.
Rao Hammas Hussain
@RaoHammasHussain Ich habe meine Frage mit einem Link aktualisiert, der helfen kann. Die Dienstprogrammmethode ist Teil einer API, die der Benutzer der API aufruft, wenn er den Endbenutzer auffordern möchte, auf den Bildschirm zu klicken. Betrachten Sie es als ein Eingabeaufforderungsfenster für Text in normalen Windows-Anwendungen oder der Console.Readline () -Methode. In diesen Fällen stoppt der Codefluss, bis der Benutzer etwas eingibt. Jetzt möchte ich genau das, aber diesmal klickt der Benutzer auf den Bildschirm.
Vahid
AutoResetEventist nicht was du willst?
Rao Hammas Hussain
@RaoHammasHussain Ich denke schon, weiß aber wirklich nicht, wie man es hier benutzt.
Vahid
Es ist, als würden Sie WAIT STATE absichtlich implementieren. ist es wirklich erforderlich? Weil du das nicht einfach var point = Utility.PickPoint(Grid grid);in die Grid-Click-Methode einfügen kannst ? eine Operation ausführen und die Antwort zurückgeben?
Rao Hammas Hussain

Antworten:

7

"Wie blockiere ich den Codefluss, bis ein Ereignis ausgelöst wird?"

Ihr Ansatz ist falsch. Ereignisgesteuert bedeutet nicht, ein Ereignis zu blockieren und darauf zu warten. Sie warten nie, zumindest bemühen Sie sich immer, dies zu vermeiden. Warten verschwendet Ressourcen, blockiert Threads und birgt möglicherweise das Risiko eines Deadlocks oder eines Zombiethreads (falls das Signal nie ausgelöst wird).
Es sollte klar sein, dass das Blockieren eines Threads zum Warten auf ein Ereignis ein Anti-Pattern ist, da es der Idee eines Ereignisses widerspricht.

Im Allgemeinen haben Sie zwei (moderne) Optionen: Implementieren einer asynchronen API oder einer ereignisgesteuerten API. Da Sie Ihre API nicht asynchron implementieren möchten, bleibt Ihnen die ereignisgesteuerte API.

Der Schlüssel einer ereignisgesteuerten API besteht darin, dass Sie den Anrufer nicht zwingen müssen, synchron auf ein Ergebnis zu warten oder auf ein Ergebnis abzufragen, sondern den Anrufer fortfahren lassen und ihm eine Benachrichtigung senden, sobald das Ergebnis fertig ist oder der Vorgang abgeschlossen ist. In der Zwischenzeit kann der Anrufer weitere Vorgänge ausführen.

Wenn Sie das Problem aus einer Threading-Perspektive betrachten, ermöglicht die ereignisgesteuerte API dem aufrufenden Thread, z. B. dem UI-Thread, der den Ereignishandler der Schaltfläche ausführt, die Möglichkeit, weiterhin UI-bezogene Vorgänge wie das Rendern von UI-Elementen oder die Behandlung auszuführen Benutzereingaben wie Mausbewegungen und Tastendrücke. Der gleiche Effekt wie bei einer asynchronen API, jedoch weniger praktisch.

Da Sie nicht genügend Details darüber angegeben haben, was Sie wirklich versuchen, was Utility.PickPoint()tatsächlich getan wird und was das Ergebnis der Aufgabe ist oder warum der Benutzer auf das Raster klicken muss, kann ich Ihnen keine bessere Lösung anbieten . Ich kann nur ein allgemeines Muster für die Umsetzung Ihrer Anforderung anbieten.

Ihr Ablauf oder das Ziel ist offensichtlich in mindestens zwei Schritte unterteilt, um eine Abfolge von Vorgängen zu erstellen:

  1. Führen Sie Vorgang 1 aus, wenn der Benutzer auf die Schaltfläche klickt
  2. Führen Sie Operation 2 aus (Fortsetzung / Abschluss von Operation 1), wenn der Benutzer auf die Schaltfläche klickt Grid

mit mindestens zwei Einschränkungen:

  1. Optional: Die Sequenz muss abgeschlossen sein, bevor der API-Client sie wiederholen darf. Eine Sequenz ist abgeschlossen, sobald Operation 2 vollständig ausgeführt wurde.
  2. Operation 1 wird immer vor Operation 2 ausgeführt. Operation 1 startet die Sequenz.
  3. Operation 1 muss abgeschlossen sein, bevor der API-Client Operation 2 ausführen darf

Dies erfordert zwei Benachrichtigungen für den Client der API, um eine nicht blockierende Interaktion zu ermöglichen:

  1. Operation 1 abgeschlossen (oder Interaktion erforderlich)
  2. Operation 2 (oder Ziel) abgeschlossen

Sie sollten Ihre API dieses Verhalten und diese Einschränkungen implementieren lassen, indem Sie zwei öffentliche Methoden und zwei öffentliche Ereignisse verfügbar machen.

Implementieren / Refaktorieren der Utility-API

Utility.cs

class Utility
{
  public event EventHandler InitializePickPointCompleted;
  public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
  private bool IsPickPointInitialized { get; set; }
  private bool IsExecutingSequence { 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.IsExecutingSequence)
    {
      // 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.IsExecutingSequence = 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
    this.IsExecutingSequence = 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 : EventArgs
{
  public Point Result { get; }

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

Verwenden Sie die 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>

Bemerkungen

Ereignisse, die in einem Hintergrundthread ausgelöst werden, führen ihre Handler im selben Thread aus. Für den Zugriff auf ein DispatcherObjectUI-ähnliches Element von einem Handler aus, der in einem Hintergrundthread ausgeführt wird, muss die kritische Operation entweder zur DispatcherVerwendung in die Warteschlange gestellt werden Dispatcher.Invokeoder Dispatcher.InvokeAsyncum threadübergreifende Ausnahmen zu vermeiden.


Einige Gedanken - antworten Sie auf Ihre Kommentare

Da Sie sich an mich gewandt haben, um eine "bessere" Blockierungslösung zu finden, habe ich Sie am Beispiel von Konsolenanwendungen davon überzeugt, dass Ihre Wahrnehmung oder Sichtweise völlig falsch ist.

"Betrachten Sie eine Konsolenanwendung mit diesen beiden Codezeilen.

var str = Console.ReadLine(); 
Console.WriteLine(str);

Was passiert, wenn Sie die Anwendung im Debug-Modus ausführen? Es stoppt in der ersten Codezeile und zwingt Sie, einen Wert in die Konsolen-Benutzeroberfläche einzugeben. Nachdem Sie etwas eingegeben und die Eingabetaste gedrückt haben, wird die nächste Zeile ausgeführt und tatsächlich gedruckt, was Sie eingegeben haben. Ich habe über genau das gleiche Verhalten nachgedacht, aber in der WPF-Anwendung. "

Eine Konsolenanwendung ist etwas völlig anderes. Das Threading-Konzept ist anders. Konsolenanwendungen haben keine grafische Benutzeroberfläche. Nur Eingabe / Ausgabe / Fehlerströme. Sie können die Architektur einer Konsolenanwendung nicht mit einer umfangreichen GUI-Anwendung vergleichen. Das wird nicht funktionieren. Sie müssen dies wirklich verstehen und akzeptieren.

WPF basiert auf einem Rendering-Thread und einem UI-Thread. Diese Threads drehen sich ständig , um mit dem Betriebssystem zu kommunizieren, z. B. um Benutzereingaben zu verarbeiten und die Anwendung ansprechbar zu halten . Sie möchten diesen Thread niemals anhalten / blockieren, da das Framework dadurch keine wesentlichen Hintergrundarbeiten mehr ausführen kann (z. B. das Reagieren auf Mausereignisse - Sie möchten nicht, dass die Maus einfriert):

Warten = Thread blockieren = Unempfindlichkeit = schlechte UX = verärgerte Benutzer / Kunden = Probleme im Büro.

Manchmal muss der Anwendungsfluss auf die Eingabe oder den Abschluss einer Routine warten. Aber wir wollen den Haupt-Thread nicht blockieren.
Aus diesem Grund haben die Leute komplexe asynchrone Programmiermodelle erfunden, um das Warten zu ermöglichen, ohne den Hauptthread zu blockieren und ohne den Entwickler zu zwingen, komplizierten und fehlerhaften Multithreading-Code zu schreiben.

Jedes moderne Anwendungsframework bietet asynchrone Operationen oder ein asynchrones Programmiermodell, um die Entwicklung von einfachem und effizientem Code zu ermöglichen.

Die Tatsache, dass Sie sich bemühen, dem asynchronen Programmiermodell zu widerstehen, zeigt mir ein gewisses Unverständnis. Jeder moderne Entwickler bevorzugt eine asynchrone API gegenüber einer synchronen. Kein seriöser Entwickler möchte das awaitSchlüsselwort verwenden oder seine Methode deklarieren async. Niemand. Sie sind die ersten, denen ich begegne, die sich über asynchrone APIs beschweren und deren Verwendung für sie unpraktisch ist.

Wenn ich Ihr Framework überprüfen würde, das darauf abzielt, Probleme mit der Benutzeroberfläche zu lösen oder Aufgaben im Zusammenhang mit der Benutzeroberfläche zu vereinfachen, würde ich erwarten , dass es asynchron ist - auf ganzer Linie.
UI-bezogene API, die nicht asynchron ist, ist Verschwendung, da sie meinen Programmierstil verkompliziert, daher mein Code, der fehleranfälliger und schwieriger zu warten ist.

Eine andere Perspektive: Wenn Sie anerkennen, dass das Warten den UI-Thread blockiert, was zu einer sehr schlechten und unerwünschten Benutzererfahrung führt, da die Benutzeroberfläche einfriert, bis das Warten vorbei ist. Nachdem Sie dies erkannt haben, warum sollten Sie ein API- oder Plugin-Modell anbieten, das Sie dazu ermutigt? Ein Entwickler, der genau das tut - Warten implementieren?
Sie wissen nicht, was das Plugin eines Drittanbieters tut und wie lange eine Routine dauert, bis sie abgeschlossen ist. Dies ist einfach ein schlechtes API-Design. Wenn Ihre API auf dem UI-Thread ausgeführt wird, muss der Aufrufer Ihrer API in der Lage sein, nicht blockierende Aufrufe an sie zu tätigen.

Wenn Sie die einzige billige oder elegante Lösung ablehnen, verwenden Sie einen ereignisgesteuerten Ansatz, wie in meinem Beispiel gezeigt.
Es macht, was Sie wollen: eine Routine starten - auf Benutzereingaben warten - Ausführung fortsetzen - Ziel erreichen.

Ich habe wirklich mehrmals versucht zu erklären, warum Warten / Blockieren ein schlechtes Anwendungsdesign ist. Auch hier können Sie eine Konsolen-Benutzeroberfläche nicht mit einer umfangreichen grafischen Benutzeroberfläche vergleichen, bei der z. B. die Verarbeitung von Eingaben allein eine Vielzahl komplexer ist als nur das Abhören des Eingabestreams. Ich weiß wirklich nicht, wie viel Erfahrung Sie haben und wo Sie angefangen haben, aber Sie sollten anfangen, das asynchrone Programmiermodell zu akzeptieren. Ich weiß nicht, warum Sie versuchen, es zu vermeiden. Aber es ist überhaupt nicht weise.

Heute werden asynchrone Programmiermodelle überall implementiert, auf jeder Plattform, jedem Compiler, jeder Umgebung, jedem Browser, Server, Desktop, jeder Datenbank - überall. Das ereignisgesteuerte Modell ermöglicht das Erreichen des gleichen Ziels, ist jedoch weniger bequem zu verwenden (Abonnieren / Abbestellen von / von Ereignissen), da Hintergrundthreads verwendet werden. Ereignisgesteuert ist altmodisch und sollte nur verwendet werden, wenn asynchrone Bibliotheken nicht verfügbar oder nicht anwendbar sind.

"Ich habe das genaue Verhalten in Autodesk Revit gesehen."

Das Verhalten (was Sie erleben oder beobachten) unterscheidet sich stark von der Art und Weise, wie diese Erfahrung umgesetzt wird. Zwei verschiedene Dinge. Ihr Autodesk verwendet sehr wahrscheinlich asynchrone Bibliotheken oder Sprachfunktionen oder einen anderen Threading-Mechanismus. Und es ist auch kontextbezogen. Wenn die Methode, die Sie im Kopf haben, in einem Hintergrundthread ausgeführt wird, kann der Entwickler diesen Thread blockieren. Er hat entweder einen sehr guten Grund dafür oder hat einfach eine schlechte Designentscheidung getroffen. Du bist total auf dem falschen Weg;) Blockieren ist nicht gut.
(Ist der Autodesk-Quellcode Open Source? Oder woher wissen Sie, wie er implementiert ist?)

Ich will dich nicht beleidigen, bitte glaub mir. Bitte überdenken Sie jedoch erneut, um Ihre API asynchron zu implementieren. Nur in Ihrem Kopf verwenden Entwickler nicht gerne Async / Warten. Sie haben offensichtlich die falsche Einstellung. Und vergiss das Argument der Konsolenanwendung - es ist Unsinn;)

UI-bezogene API MUSS nach Möglichkeit async / await verwenden. Andernfalls überlassen Sie die gesamte Arbeit dem Schreiben von nicht blockierendem Code auf dem Client Ihrer API. Sie würden mich zwingen, jeden Aufruf Ihrer API in einen Hintergrund-Thread zu packen. Oder um eine weniger komfortable Ereignisbehandlung zu verwenden. Glauben Sie mir - jeder Entwickler schmückt seine Mitglieder lieber mit asyncals mit Event-Handling. Jedes Mal, wenn Sie Ereignisse verwenden, besteht die Gefahr eines potenziellen Speicherverlusts - dies hängt von bestimmten Umständen ab, aber das Risiko ist real und nicht selten, wenn Sie unachtsam programmieren.

Ich hoffe wirklich, dass Sie verstehen, warum Blockieren schlecht ist. Ich hoffe wirklich, dass Sie sich für async / await entscheiden, um eine moderne asynchrone API zu schreiben. Trotzdem habe ich Ihnen einen sehr gebräuchlichen Weg gezeigt, um mithilfe von Ereignissen nicht blockierend zu warten, obwohl ich Sie dringend auffordere, async / await zu verwenden.

"Die API ermöglicht dem Programmierer den Zugriff auf die Benutzeroberfläche usw. Angenommen, der Programmierer möchte ein Add-In entwickeln, bei dem der Endbenutzer beim Klicken auf eine Schaltfläche aufgefordert wird, einen Punkt in der Benutzeroberfläche auszuwählen."

Wenn Sie dem Plugin keinen direkten Zugriff auf UI-Elemente gewähren möchten, sollten Sie eine Schnittstelle zum Delegieren von Ereignissen oder zum Offenlegen interner Komponenten über abstrahierte Objekte bereitstellen.
Die API abonniert intern UI-Ereignisse im Namen des Add-Ins und delegiert das Ereignis dann, indem sie dem API-Client ein entsprechendes "Wrapper" -Ereignis zur Verfügung stellt. Ihre API muss einige Hooks bieten, über die das Add-In eine Verbindung herstellen kann, um auf bestimmte Anwendungskomponenten zuzugreifen. Eine Plugin-API fungiert als Adapter oder Fassade, um externen Benutzern Zugriff auf interne Elemente zu gewähren.
Um ein gewisses Maß an Isolation zu ermöglichen.

Sehen Sie sich an, wie Visual Studio Plugins verwaltet oder wie wir sie implementieren können. Stellen Sie sich vor, Sie möchten ein Plugin für Visual Studio schreiben und recherchieren, wie das geht. Sie werden feststellen, dass Visual Studio seine Interna über eine Schnittstelle oder API verfügbar macht. ZB können Sie den Code-Editor manipulieren oder Informationen über den Inhalt des Editors abrufen, ohne wirklich darauf zugreifen zu müssen.

BionicCode
quelle
Hallo, danke, dass Sie sich der Frage aus einer anderen Perspektive nähern. Entschuldigung, wenn die Frage in Bezug auf Details etwas vage war. Stellen Sie sich eine Konsolenanwendung mit diesen beiden Codezeilen vor. var str = Console.ReadLine(); Console.WriteLine(str);Was passiert, wenn Sie die Anwendung im Debug-Modus ausführen? Es stoppt in der ersten Codezeile und zwingt Sie, einen Wert in die Konsolen-Benutzeroberfläche einzugeben. Nachdem Sie etwas eingegeben und die Eingabetaste gedrückt haben, wird die nächste Zeile ausgeführt und tatsächlich gedruckt, was Sie eingegeben haben. Ich habe über genau das gleiche Verhalten nachgedacht, aber in der WPF-Anwendung.
Vahid
In der CAD-Anwendung, die ich entwickle, sollen die Benutzer sie um Addins / Plugins erweitern können. Die API ermöglicht dem Programmierer den Zugriff auf die Benutzeroberfläche usw. Angenommen, der Programmierer möchte ein Add-In entwickeln, bei dem der Endbenutzer beim Klicken auf eine Schaltfläche aufgefordert wird, einen Punkt in der Benutzeroberfläche auszuwählen, und der Code dann andere Aufgaben ausführt cooles Zeug mit dem gegebenen Punkt. Vielleicht werden sie darum bitten, dass ein anderer Punkt ausgewählt und eine Linie gezogen wird usw.
Vahid,
Ich habe das genaue Verhalten in Autodesk Revit gesehen.
Vahid
1
Ich habe etwas zu Ihrer Anforderung zu sagen. Bitte lesen Sie meine aktualisierte Antwort. Ich habe die Antwort dort gepostet, weil sie irgendwie länger wurde. Ich gebe zu, dass du mich wirklich ausgelöst hast. Bitte denken Sie beim Lesen daran, dass ich Sie nicht beleidigen möchte.
BionicCode
Vielen Dank für Ihre aktualisierte Antwort. Natürlich bin ich nicht beleidigt. Im Gegenteil, ich bin wirklich dankbar für die Zeit und Mühe, die Sie dafür aufgewendet haben.
Vahid
5

Ich persönlich denke, dass dies von allen überkompliziert wird, aber vielleicht verstehe ich den Grund, warum dies auf eine bestimmte Weise getan werden muss, nicht ganz, aber es scheint, als könnte hier ein einfacher Bool-Check verwendet werden.

Machen Sie Ihr Raster in erster Linie durch Festlegen der Eigenschaften Backgroundund testbar IsHitTestVisible, da es sonst nicht einmal Mausklicks erfasst.

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

Erstellen Sie als Nächstes einen Bool-Wert, in dem gespeichert werden kann, ob das Ereignis "GridClick" auftreten soll. Wenn Sie auf das Raster klicken, überprüfen Sie diesen Wert und führen Sie die Ausführung des Rasterklickereignisses aus, wenn es auf den Klick wartet.

Beispiel:

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
quelle
Hallo Tronald, ich denke du hast die Frage falsch verstanden. Ich brauche, damit der Code bei Utility.PickPoint (Ansicht) stoppt und erst fortgesetzt wird, nachdem der Benutzer auf Raster geklickt hat.
Vahid
Oh ja, ich habe es total missverstanden. Ich entschuldige mich, ich wusste nicht, dass du alles brauchst, um tatsächlich aufzuhören. Ich denke nicht, dass dies ohne Multithreading möglich ist, da die gesamte Benutzeroberfläche blockiert wird.
Tronald
Ich bin mir immer noch nicht sicher, ob es nicht möglich ist. Mit async / await ist dies definitiv möglich, was keine Multithread-Lösung ist. Was ich aber brauche, ist eine Alternative zur asynchronen / wartenden Lösung.
Vahid
1
Sicher, aber Sie haben erwähnt, dass Sie async / await nicht verwenden können. Es scheint, als müssten Sie einen Dispatcher und einen Thread verwenden, der vom Hauptthread (der auf der Benutzeroberfläche ausgeführt wird) getrennt ist. Ich hoffe, Sie finden einen anderen Weg, da ich selbst interessiert bin
Tronald
2

Ich habe ein paar Dinge ausprobiert, aber ich kann es nicht ohne schaffen async/await. Denn wenn wir es nicht verwenden, verursacht es DeadLockoder die Benutzeroberfläche ist blockiert und wir können Grid_ClickEingaben vornehmen.

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
quelle
@ danke, dies ist die gleiche Antwort, die ich für meine andere Frage erhalten habe, die async / await verwendet.
Vahid
Oh ja ! Ich habe es jetzt bemerkt, aber ich denke, es ist der einzige Weg, den ich gefunden habe, der funktioniert
Rao Hammas Hussain
2

Sie können asynchron blockieren mit 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();
}

Sie können und möchten den Dispatcher-Thread auch nicht synchron blockieren, da er dann niemals in der Lage ist, den Klick auf zu verarbeiten Grid, dh, er kann nicht gleichzeitig blockiert und Ereignisse behandelt werden.

mm8
quelle
Vielen Dank für eine alternative Antwort. Ich frage mich, wie es zum Beispiel in Console.Readline () gemacht wird. Wenn Sie diese Methode im Debugger erreichen, stoppt sie dort auf magische Weise, es sei denn, wir geben etwas ein? Unterscheidet es sich grundlegend in Konsolenanwendungen? Können wir nicht dasselbe Verhalten in der WinForms / WPF-Anwendung haben? Ich habe dies in der API von Autodesk Revit gesehen. Dort gibt es eine PickPoint () -Methode, die Sie zwingt, einen Punkt auf dem Bildschirm auszuwählen, und ich habe keine asynchrone / wartende Methode gesehen! Ist es zumindest möglich, das Schlüsselwort await auszublenden und es irgendwie aus einer Synchronisierungsmethode heraus aufzurufen?
Vahid
@Vahid: Console.Readline blockiert , dh es kehrt erst zurück, wenn eine Zeile gelesen wurde. Ihre PickPointMethode nicht. Es kehrt sofort zurück. Es könnte möglicherweise blockieren, aber dann können Sie in der Zwischenzeit keine UI-Eingaben verarbeiten, wie ich in meiner Antwort geschrieben habe. Mit anderen Worten, Sie müssten den Klick innerhalb der Methode verarbeiten, um das gleiche Verhalten zu erzielen.
mm8
Die Console.Realine () blockiert, aber gleichzeitig sind KeyPress-Ereignisse zulässig. Können wir hier nicht genau das gleiche Verhalten haben? Blockieren mit PickPoint () und nur MouseEvents zulassen? Ich kann nicht verstehen, warum dies in Console möglich ist, aber nicht in einer UI-basierten Anwendung.
Vahid
Anschließend müssen Sie einen separaten Dispatcher PickPointeinrichten, der die Mausereignisse verarbeitet. Ich kann nicht sehen, wohin du damit gehst?
mm8
1
@Vahind: Machen Sie den Code asynchron und lassen Sie den Benutzer dann auf die Methode warten? Dies ist die API, die ich als UI-Entwickler erwarten würde. Das Aufrufen einer Blockierungsmethode in einer UI-Anwendung macht keinen Sinn.
mm8
2

Technisch ist es mit AutoResetEventund ohne möglich async/await, aber es gibt einen erheblichen Nachteil:

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

Der Nachteil: Wenn Sie diese Methode wie in Ihrem Beispielcode direkt in einem Schaltflächenereignishandler aufrufen, tritt ein Deadlocking auf und Sie sehen, dass die Anwendung nicht mehr reagiert. Da Sie den einzigen UI-Thread verwenden, um auf den Klick des Benutzers zu warten, kann er nicht auf die Aktion eines Benutzers reagieren, einschließlich des Klicks des Benutzers auf das Raster.

Benutzer der Methode sollten sie in einem anderen Thread aufrufen, um Deadlocks zu vermeiden. Wenn es garantiert werden kann, ist das in Ordnung. Andernfalls müssen Sie die Methode wie folgt aufrufen:

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());
}

Dies kann den Verbrauchern Ihrer API mehr Probleme bereiten, außer wenn sie ihre eigenen Threads verwalten. Deshalb async/awaitwurde erfunden.

Ken Hung
quelle
Danke Ken, ist es möglich, dass das Add-In von einem anderen Thread startet und seine Ereignisse dann den Haupt-UI-Thread nicht blockieren?
Vahid
@Vahid Ja und nein. Ja, Sie können die Blockierungsmethode in einem anderen Thread aufrufen und in eine andere Methode einschließen. Die Wrapper-Methode musste jedoch weiterhin in einem anderen Thread als dem UI-Thread aufgerufen werden, um ein Blockieren der UI zu vermeiden. Weil der Wrapper den aufrufenden Thread blockiert , wenn er synchron ist . Obwohl der Wrapper intern einen anderen Thread blockiert, muss er dennoch auf das Ergebnis warten und den aufrufenden Thread blockieren. Wenn der Aufrufer die Wrapper-Methode im UI-Thread aufruft, wird die UI blockiert.
Ken Hung
0

Ich denke, das Problem liegt im Design selbst. Wenn Ihre API für ein bestimmtes Element funktioniert, sollte sie im Ereignishandler dieses Elements verwendet werden, nicht für ein anderes Element.

Wenn wir hier beispielsweise die Position des Klickereignisses im Raster abrufen möchten, muss die API im Ereignishandler verwendet werden, der dem Ereignis im Rasterelement zugeordnet ist, nicht im Schaltflächenelement.

Wenn die Anforderung darin besteht, den Klick auf das Raster erst zu verarbeiten, nachdem wir auf die Schaltfläche geklickt haben, besteht die Verantwortung für die Schaltfläche darin, den Ereignishandler im Raster hinzuzufügen, und das Klickereignis im Raster zeigt das Meldungsfeld an und entfernt es Dieser Ereignishandler wurde über die Schaltfläche hinzugefügt, damit er nach diesem Klick nicht mehr ausgelöst wird ... (der UI-Thread muss nicht blockiert werden)

Um nur zu sagen, wenn Sie den UI-Thread beim Klicken auf die Schaltfläche blockieren, glaube ich nicht, dass der UI-Thread das Klickereignis im Grid danach auslösen kann.

jsami
quelle
0

Erstens kann der UI-Thread nicht wie die Antwort, die Sie von Ihrer frühen Frage erhalten haben, blockiert werden.
Wenn Sie dem zustimmen können, ist es machbar, Async / Warten zu vermeiden, damit Ihr Kunde weniger Änderungen vornimmt, und Sie benötigen sogar kein Multithreading.

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;
    }
}   

Wenn Sie jedoch den UI-Thread oder den "Code-Fluss" blockieren möchten, lautet die Antwort, dass dies unmöglich ist. Denn wenn der UI-Thread blockiert wurde, kann keine weitere Eingabe empfangen werden.
Da Sie etwas über die Konsolen-App erwähnt haben, mache ich nur eine einfache Erklärung.
Wenn Sie eine Konsolen-App ausführen oder AllocConsoleeinen Prozess von einem Prozess aus aufrufen , der keine Verbindung zu einer Konsole (Fenster) hergestellt hat, wird conhost.exe ausgeführt, das eine Konsole (Fenster) bereitstellen kann, und der Konsolen-App- oder Anrufer-Prozess wird an die Konsole angehängt ( Fenster).
Jeder Code, den Sie schreiben, der den Aufrufer-Thread blockieren könnte, z. B. Console.ReadKeyden UI-Thread des Konsolenfensters nicht blockiert. Dies ist der Grund, warum die Konsolen-App auf Ihre Eingabe wartet, aber dennoch auf andere Eingaben wie Mausklicks reagieren kann.

Alex.Wei
quelle