Hier haben wir eine Grid
mit 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)
{
}
}
quelle
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.AutoResetEvent
ist nicht was du willst?var point = Utility.PickPoint(Grid grid);
in die Grid-Click-Methode einfügen kannst ? eine Operation ausführen und die Antwort zurückgeben?Antworten:
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:
Grid
mit mindestens zwei Einschränkungen:
Dies erfordert zwei Benachrichtigungen für den Client der API, um eine nicht blockierende Interaktion zu ermöglichen:
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
PickPointCompletedEventArgs.cs
Verwenden Sie die API
MainWindow.xaml.cs
MainWindow.xaml
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 zurDispatcher
Verwendung in die Warteschlange gestellt werdenDispatcher.Invoke
oderDispatcher.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.
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 deklarierenasync
. 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.
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.
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.
quelle
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.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
Background
und testbarIsHitTestVisible
, da es sonst nicht einmal Mausklicks erfasst.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:
quelle
Ich habe ein paar Dinge ausprobiert, aber ich kann es nicht ohne schaffen
async/await
. Denn wenn wir es nicht verwenden, verursacht esDeadLock
oder die Benutzeroberfläche ist blockiert und wir könnenGrid_Click
Eingaben vornehmen.quelle
Sie können asynchron blockieren mit
SemaphoreSlim
: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.quelle
Console.Readline
blockiert , dh es kehrt erst zurück, wenn eine Zeile gelesen wurde. IhrePickPoint
Methode 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.PickPoint
einrichten, der die Mausereignisse verarbeitet. Ich kann nicht sehen, wohin du damit gehst?Technisch ist es mit
AutoResetEvent
und ohne möglichasync/await
, aber es gibt einen erheblichen Nachteil: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:
Dies kann den Verbrauchern Ihrer API mehr Probleme bereiten, außer wenn sie ihre eigenen Threads verwalten. Deshalb
async/await
wurde erfunden.quelle
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.
quelle
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.
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
AllocConsole
einen 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.ReadKey
den 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.quelle