"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:
- Führen Sie Vorgang 1 aus, wenn der Benutzer auf die Schaltfläche klickt
- 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:
- 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.
- Operation 1 wird immer vor Operation 2 ausgeführt. Operation 1 startet die Sequenz.
- 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:
- Operation 1 abgeschlossen (oder Interaktion erforderlich)
- 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 DispatcherObject
UI-ähnliches Element von einem Handler aus, der in einem Hintergrundthread ausgeführt wird, muss die kritische Operation entweder zur Dispatcher
Verwendung in die Warteschlange gestellt werden Dispatcher.Invoke
oder Dispatcher.InvokeAsync
um 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 await
Schlü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 async
als 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.
Aync/Await
wie 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.