Wie kann die erneute Eingabe des WPF-Ereignishandlers während des ActiveX-Methodenaufrufs verhindert werden?

9

Wir rufen Methoden in einer ActiveX-Komponente innerhalb einer WPF- und STA-Anwendung auf. Dieser Aufruf wird spät gebunden über:

ausgeführt %Vor%

... wobei ocx das ActiveX-Objekt ist, das mit der Methode System.Windows.Forms.AxHost.GetOcx () abgerufen wurde.

Dieser Aufruf wird innerhalb eines WPF-Ereignishandlers ausgeführt, sagen wir "Mausklick".

Jetzt das Problem. Wenn wir doppelklicken, wird das Ereignis "Mausklick" ausgelöst und InvokeMember () ausgeführt. Während während dieses Aufrufs sehen wir jedoch, dass das Ereignis "Mausklick" erneut eingegeben wird. Im selben Thread sehen wir den Event-Handler zweimal auf dem Aufruf-Stack. Das ist sehr unerwartet und wir versuchen das zu verhindern. Wie können wir das verhindern?

Der einzige Grund, warum wir uns vorstellen können, warum es passiert, ist:

  • Das COM-Objekt wird in einer anderen STA erstellt. Daher führen wir einen Cross-STA-Aufruf durch, der gemarshallt werden muss
  • Threadübergreifende STA-Aufrufe verwenden eine Windows-Nachricht zum Senden einer RPC-Anforderung an die COM-Komponente
  • Threadübergreifende STA-Aufrufe verwenden die Windows Message-Pumpe, um die RPC-Antwort
  • zu empfangen
  • Während des Wartens kommt ein anderer Ereignistyp (wie 'Mausklick'), und dies wird behandelt, bevor die RPC-Antwort behandelt wird.
  • Diese RPC-Antwort wird behandelt

Dinge, die wir versucht haben, das Problem zu beheben:

  • Verwenden Sie lock () in allen Event-Handlern. Dies funktioniert nicht, da lock () einen Thread sperrt, und in diesem Fall ist es derselbe Thread, der wieder in den Event-Handler eintritt.
  • benutze benutzerdefinierte Sperren wie 'bool locked = false; if (! locked) {locked = true; InvokeMethod (); ...; gesperrt = falsch; } '. Das funktioniert teilweise: Es verwirft die Ereignisse, anstatt sie für später in die Warteschlange zu stellen, und benötigt eine umfangreiche Änderung an unseren all Event-Handlern, was nicht angenehm ist.
  • Verwenden Sie Dispatcher.DisableProcessing, um zu verhindern, dass (andere) Nachrichten verarbeitet werden. Das hilft nicht: Es wird eine Exception ausgelöst, weil Nachrichten sowieso verarbeitet werden.
  • Erstellen Sie einen zweiten Dispatcher in einem neuen Thread, und führen Sie ocx.InvokeMethod () über Dispatcher.Invoke () aus, damit es von einem anderen Thread behandelt wird. Dies gibt 'Ein Ereignis konnte keinen der Abonnenten aufrufen (Ausnahme von HRESULT: 0x80040201)' (Ja, wir haben auch COM-Ereignisse des ActiveX-Objekts abonniert).
  • Verwenden Sie Dispatcher.PushFrame (), um die Ereignisbehandlung zu stoppen. Dies schlägt auch fehl.

Eine witzige Idee, die funktionieren könnte, aber nicht wissen, wie dies implementiert werden könnte, wäre das Erstellen einer neuen Nachrichtenpumpe als WPF-Nachrichtenpumpe, die so konfiguriert werden kann, dass sie nur RPC-Aufrufe temporär behandelt. Dies ist in der Art von Ссылка , aber immer noch etwas anders aus dieser Situation.

Also, die Frage läuft darauf hinaus, wie wir ActiveX-Aufruf synchron machen können, wie wir es erwartet haben, anstatt async zu sein?

Aktualisieren

Um zu verdeutlichen, dass es sich bei dem beteiligten Mechanismus nicht nur um Mausereignisse handelt, sondern um das allgemeinere Problem, dass ein neues Ereignis während der Ausführung des alten Ereignisses behandelt wird, werde ich ein weiteres Beispiel mit einem Stack-Trace geben :

Kontext: Wir haben ein WPF-Gitter, auf dem wir einen Mausklick bekommen (Grid_MouseDown), wir haben ein ActiveX-Objekt, auf dem wir die Methode 'CloseShelf' ausführen. Das Öffnen des Regals dauert einige Zeit, daher abonnieren wir das Ereignis 'EventShelfClosed', das im Event-Handler von EventShelfClosed 'ListShelf' aufruft, um zu wissen, welche Regale übrig sind.

So sieht der verwaltete Stack-Trace aus (Hans hat nach einem nicht verwalteten Stacktrace gefragt, aber ich weiß nicht, wie ich einen bekomme):

%Vor%

Was passiert, ist, dass die Methode 'CloseShelf' das Regal schließt, aber in diesem Fall ist 'CloseShelf' so schnell, dass das Ereignis 'EventShelfClosed' ausgegeben wird und während der Aufruf von CloseShelf gehandhabt wird. Nun ruft CloseShelf ListShelfs auf, aber ListShelfs schlägt fehl und gibt null zurück, da die ActiveX-Komponente durch den noch aktiven Call 'CloseShelf' gesperrt ist.

Warum ist das ein Problem? Weil der Programmierer nicht erwartet, dass ein Methodenaufruf asynchron ist. Dies traf uns nach dem Erstellen eines großen Programms, das jetzt bedeutet, alle Aufrufe für unerwartetes Verhalten zu auditieren.

Was möchten wir in diesem Fall sehen? Wir würden gerne sehen, dass "CloseShelf" zurückkehrt, ohne andere Ereignisse während des Anrufs zu behandeln. Die Methode sollte synchron sein und alle ausstehenden Ereignisse während der (nicht-rekursiven) Hauptnachrichtenschleife behandelt werden.

Eine boolesche 'Lock'-Art wird hier nicht helfen, da uns hier Ereignisse fehlen würden, die die Anwendung blockieren.

    
Rutger Nijlunsing 29.11.2011, 10:20
quelle

5 Antworten

2

& lt; tldr & gt; Probieren Sie den folgenden Code in Versuch # 3. Es gab nichts im Fernsehen, als ich mich hinsetzte, um das zu schreiben.

Danke für die Klarstellung! Ich sehe, dass es hier nur einen Thread gibt; Da sich der CloseShelf-Frame immer noch auf dem Stapel befindet, sieht es so aus, als ob der COM-Aufruf tatsächlich blockiert.

Von der Stack-Ablaufverfolgung aus sieht es so aus, als ob das com-Objekt GetMessage oder PeekMessage API (oder VB6, DoEvents o.ä.) aufruft, die die Nachrichtenwarteschlange und PROCESS-Nachrichten darauf prüft - unabhängig davon, ob es Einweihung. AKA "pumpt die Nachrichtenwarteschlange" - wenn sie jedoch peekmessage verwendet, blockiert sie nicht, wenn keine Nachrichten vorhanden sind, führt jedoch die Nachrichten weiterhin aus. In Ihrem Stacktrack könnten diese Aufrufe im unsichtbaren nativen Teil verborgen sein. Der Stack-Trace beweist tatsächlich, dass der COM-Aufruf irgendwie zurück in Ihren Code ruft! Es sieht so aus als ob du dir dessen bewusst bist. Wenn es für einen Leser ein bisschen neblig ist, sind hier ein paar Links:

Ссылка
Ссылка

Kooperatives Multitasking (eine Meldungsschleife für das gesamte Betriebssystem wie win3.0): Ссылка

Das Zitat "freiwillig abgetretene Zeit ..." wird tatsächlich durch diese Anrufe erledigt. Und es passiert immer noch im GUI-Thread jeder Windows-App!

Da der tatsächliche Aufruf in der COM ist, wenn Sie ihn nicht bearbeiten können, müssen Sie ihn noch codieren. Es ist wahrscheinlich nur diese eine Methode (hoffentlich).

Manchmal überprüfen Programme und Komponenten die Nachrichtenschleife absichtlich, um Zeiten zu ermöglichen, damit Dinge reagieren können, oder den Bildschirm neu zu streichen. Genau wie dieses Poster versucht zu tun:

Gibt es eine Funktion wie PeekMessage, die keine Nachrichten verarbeitet?

Das Zitat über 'Das System kann auch interne Ereignisse verarbeiten' führt Sie hier hin.

Beachten Sie, wo Sie sagen "der Programmierer erwartet nicht, dass ein Methodenaufruf asynchron ist" - er ist nicht asynchron oder die Stack-Ablaufverfolgung würde anders aussehen. es kehrt in deinen Code zurück. Als ein alter Win3.1-Programmierer war das die Hölle, mit der wir für JEDE App im System fertig wurden!

Ich habe festgestellt, dass der Link googlen, um die Nachrichtenwarteschlangen-Peekmessage zu prüfen, während versucht wird, zu sehen, ob Get / Peekmessage usw. daran gehindert werden kann, Nachrichten zu verarbeiten. Sie können aus dieser Dokumentation sehen ...

Ссылка

... das Senden von etwas wie PM_QS_INPUT | PM_QS_PAINT würde das Problem verhindern. Da Sie es nicht anrufen, können Sie es leider nicht ändern! Und ich habe keine Möglichkeit gesehen, eine Markierung zu setzen, um nachfolgende Nachrichtenverarbeitung von späteren Anrufen zu steuern.

Wenn ein Leser immer noch verwirrt ist (wenn nicht, überspringen Sie diesen Code) ... Für ein einfaches Beispiel, machen Sie eine VB Winforms App und machen Sie eine Taste und doppelklicken Sie darauf und fügen Sie diesen Code ein - (ich sage VB weil Anwendung. doevents ist der einfachste Weg, um diese scheußliche Nachrichtenwarteschlange aufzurufen):

%Vor%

Klicken Sie nun auf die Schaltfläche. Beachten Sie, dass Sie das Fenster und die Hintergrund-Repaints vergrößern können - da die Ereignisse diese Ereignisse passieren, indem Sie die Nachrichtenwarteschlange überprüfen (REM-Out der Doevents und warten bis die Zählung beendet ist). Versuchen Sie 3x zu klicken und Sie erhalten 3 zählt in einer Reihe).

JETZT ... der Kicker. Klicken Sie auf die Schaltfläche w / Doevents nicht auskommentiert. Klicken Sie dreimal auf die Schaltfläche - die Countdowns unterbrechen sich gegenseitig und werden dann fortgesetzt, sobald die vorherige beendet ist. Sie können die IDE anhalten und 3 Klick-Callstacks sehen.

Lecker!

Ich sehe auch, dass Sie einige Dinge ausprobiert haben, um die Verarbeitung von Nachrichten oder Ereignissen zu verhindern. Wenn der Code, der das EventShelfClosed auslöst, von der Re-entrance aufgerufen wird, die durch eine PeekMessage oder "Doevents" verursacht wird, wirkt sich das jedoch möglicherweise nicht aus.

Beachten Sie, dass diese Übung ihre Gegner ist: Ссылка

Am besten wäre es, die COM zu ändern, damit keine API-Aufrufe die Nachrichtenschleife überprüfen.

Viel Glück damit!

Eine andere Möglichkeit, dies zu ändern, wäre, von Ereignissen zu wechseln, die EventShelfClosed steuern, und sie explizit aufzurufen, nachdem der Aufruf von CloseShelf beendet wurde (da der Com-Aufruf tatsächlich stattfindet). Leider wird die Architektur Ihres Programms dies wahrscheinlich nicht zulassen ohne größere Änderungen und / oder erhöhten Zusammenhalt und andere Dinge, die hübsche Modelle verschmutzen (nicht Modemodelle beachten Sie: -).

Ein anderer Weg wäre, ein neues Thread-Objekt auf eine Funktion zu zeigen, die das com aufruft, dann startet es und fügt es dann hinzu, in der Hoffnung, dass irgendetwas wie PeekMessage keinen Nachrichten-Pump auf dem neuen Thread findet nicht in die Dinge eingreifen.Es sieht so aus, als ob ein paar von euch diese Art von Dingen involviert hätten. Leider, wenn der COM nach Nachrichten guckt, und es gibt keine Nachrichtenpumpe auf dem Thread, Kaboom. Es wird wahrscheinlich explodieren, anstatt nur Dinge zu ignorieren. Klingt so, als wäre das passiert. Wenn die COM auf anderen Elementen basiert, auf die nur über den GUI / messagepump -Thread zugegriffen werden soll, haben Sie Probleme mit Thread-übergreifenden Aufrufen (was sicherlich der Fall wäre, wenn die COM mit der Benutzeroberfläche oder anderen UI-Objekten interagiert) ).

Wenn Sie nicht verhindern können, dass die Nachrichtenwarteschlange überprüft wird, oder verhindern, dass EventShelfClosed bis zu einem späteren Zeitpunkt ausgelöst wird, haben Sie keine andere Wahl, als EventShelfClosed aufgerufen zu bekommen. Aber was du tun kannst, ist, es warten zu lassen und dann zu fallen, wenn das CloseShelf beendet ist.

Sie müssen also noch ein boolesches Feld auf Klassenebene von CloseShelf setzen / nicht setzen, damit EventShelfClosed erkennt, dass es läuft.

Wenn Sie dies nur in einer While-Schleife überprüfen, wird der einzelne Thread, den Sie haben, blockiert und die App eingefroren. Sie können einfach versuchen, EventShelfClosed erneut aufzusteigen und die Funktion zu verlassen, solange bool gesetzt ist. Da RaiseEvent jedoch in verwaltet verwaltet bleibt, wird der Code sofort ausgeführt, und die Nachrichtenwarteschlange wird nicht überprüft, wenn sie mit einem Stackoverflow abstürzt. Wenn Sie herausfinden können, wie EventShelfClosed durch Aufrufen der PostMessage-API (nicht von SendMessage, die es sofort ausführt) erneut ausgelöst wird, wird die Nachrichtenwarteschlange des GUI-Threads so oft weitergeführt, wie Windows durch den COM-Aufruf überprüft wird. Es sei denn, COM wartet aus einem dummen Grund darauf, dass die Warteschlange leer ist - eine weitere Sperre. Nicht zu empfehlen.

Soo ... Du kannst Feuer mit Feuer bekämpfen. Hier implementiere ich eine weitere Nachrichtenüberprüfungsschleife, damit Ihre Ereignisse passieren können, während Sie darauf warten, dass das Bool gelöscht wird.

Beachten Sie, dass dies nur ein Problem in diesem Fall beheben wird. Alle Anrufe überwachen ... das ist kein Wundermittel. Meine Vermutung ist, dass es keine gibt. Sehr chaotisch und das ist ein totaler Hack.

Versuch # 3

Es ist nicht wirklich Versuch # 3, es ist mehr wie Möglichkeit # 8. Aber ich habe dies in meiner alten Antwort erwähnt und bin zu faul, um das zu bearbeiten.

%Vor%

Nun gibt es natürlich keine DoEvents mehr in WPF, sondern in winforms '' application ''. Dieser Blog hat eine Implementierung

Ссылка

%Vor%

Nicht getestet, natürlich! Beachten Sie dies von der Korrektur in den Kommentaren:

%Vor%

Oder Sie könnten immer auf WinForms verweisen und Application.DoEvents aufrufen.

Ich schätze, Sie wissen das schon, aber Sie sind gerade in einer schlechten Lage. Wenn das nicht klappt, Viel Glück! Wenn Sie einen besseren Weg finden, aktualisieren Sie bitte den Beitrag, weil, nun, böse Hacks wie diese saugen, aber jetzt können Sie sehen, warum Leute sagen, die Nachrichtenwarteschlange sparsam zu überprüfen!

    
FastAl 03.12.2011 04:10
quelle
1

Ich habe mich in der Vergangenheit mit ähnlichen Problemen beschäftigt, aber nicht mit WPF.

In einer Win32-Anwendung wurde empfohlen, IMessageFilter :: MessagePending zu verwenden - dies könnte konfiguriert werden, um zu sagen, welche Arten von Nachrichten verarbeitet werden dürfen, wenn ein ausgehende STA-Aufruf bereits ausgeführt wurde. Hier mussten Sie darauf achten, dass Callbacks von Ihrem Angerufenen akzeptiert und bearbeitet wurden.

Ссылка

In WPF ist dies nicht verfügbar. Ich denke, Ihr Ansatz, einen anderen Thread zu verwenden, ist der richtige Weg.

Im Prinzip möchten Sie, dass Ihr Haupt-Thread auf einem untergeordneten Thread blockiert. Der untergeordnete Thread kann dann den ausgehenden COM-Anruf tätigen. Wahrscheinlich möchten Sie den untergeordneten Thread zu einer STA machen, um andere unbekannte Probleme zu vermeiden. Es ist wichtig, dass Nachrichten auf den untergeordneten Thread gepumpt werden und dass alle Zeiger oder Typen ordnungsgemäß verwaltet werden, da sich der untergeordnete Thread in einer anderen COM-Appliance befindet. Reentrancy wird vermieden, da Callbacks das einzige sind, was versucht, den Thread, der pumpt, zu melden.

In WPF glaube ich, dass ein Dispatcher alle Funktionen bereitstellen sollte, die Sie benötigen.

Ich bin nicht sicher, warum Ihr Versuch, dies mit einem Dispatcher zu tun, fehlgeschlagen ist - es könnte mit diesem bekannten Problem zusammenhängen: Ссылка

    
morechilli 02.12.2011 18:40
quelle
1

Sie haben die Antwort!

Ist es nur ein alter Hack? Nein, das ist es nicht, es ist Standard-Betriebsverfahren, wenn irgendeine Art von Wiedereintritt beteiligt ist. Das hat für mich in mehr Fällen fehlerfrei funktioniert, als ich mich erinnern kann, von einfachen VB3-Crud-Popups für einzelne Panels bis hin zu riesigen MVVM / DDD-Enterprise-Management-Apps.

Es ist das, was Sie erwähnt haben: Verwenden Sie benutzerdefinierte Sperren wie 'static bool locked = false; if (! locked) {locked = true; InvokeMethod (); ...; gesperrt = falsch; } '.

BEARBEITEN

Beachten Sie die Kommentare des OP. OK, damit das Problem nicht gelöst wird! Das 2. Ereignis ist kein unechtes Klicken; Es ist ein anderes Ereignis, das für den ordnungsgemäßen Betrieb des Systems entscheidend ist.

Bitte sehen Sie meine nächste Antwort für ein paar weitere Versuche. # 3 ist das hässlichste aber sollte funktionieren.

    
FastAl 01.12.2011 17:56
quelle
0

Wenn Sie bereits asynchrones Verhalten haben, würde ich Jeffrey Richters PowerThreading -Bibliothek ausprobieren. Es hat AsyncEnumerator , um die asynchrone Programmierung zu vereinfachen. Und es hat auch Sperrgrundelement, das Ihnen helfen kann, Ihr Szenario zu implementieren. Soweit ich weiß, unterscheidet sich dieses Primitiv von der regulären Monitor -Klasse auf diese Weise, so dass es nicht möglich ist, den Code auch im selben Thread erneut einzugeben, damit es Ihnen helfen kann. Leider habe ich dieses Primitiv nicht ausprobiert und kann daher nicht viel hinzufügen.

Hier ist ein Artikel über dieses Primitiv: Ссылка

    
Snowbear 02.12.2011 17:49
quelle
0

Ich weiß, dass dies keine vollständige Antwort ist, aber ich werde gerne erklären, wie Ereignisse funktionieren. Dies wird Ihnen helfen zu verstehen, dass der Wiedereintritt kein Problem ist und wie die Ereignisse funktionieren.

  1. Envent-Handler-Aufrufe sind immer synchron, aber es bedeutet nicht, dass keine anderen Ereignisse im Stapel vorhanden sind.
  2. Nehmen wir an, Sie klicken auf eine Schaltfläche in einem Listenfeld, und Sie rufen ein Ereignis mit geänderter Liste manuell auf, und irgendwo wird tatsächlich ein Aufruf aufgerufen.

Ihr typischer Event-Raifer-Aufruf sieht so aus,

%Vor%

Beachten Sie, dass der OnClick-Aufruf immer noch auf dem Stapel liegt, während das SelectionChanged-Ereignis ausgelöst wird. OnClick wird nicht aus dem Stapel genommen, bis der SelectionChanged-Aufruf abgeschlossen ist.

  

Was möchten wir in diesem Fall sehen? Das würden wir gerne sehen   'CloseShelf' kehrt zurück, ohne andere Ereignisse während des Anrufs zu behandeln.   Die Methode sollte synchron sein und alle ausstehenden Ereignisse behandelt werden   während der Haupt (nicht-rekursiven) Nachrichtenschleife.

Wie ist das möglich, wenn Sie Events innerhalb von CloseShelf auslösen?

Sie können dies nur tun, indem Sie den Ereignishandler wie in

einreihen %Vor%

Dadurch wird das Ereignis ausgelöst, nachdem OnClick finish ist. In diesem Fall wird OnClick on stack nicht angezeigt, während SelectionChanged ausgeführt wird.

    
Akash Kava 03.12.2011 06:41
quelle

Tags und Links