major
#29460
IllegalStateException "Control has no id!" beim Öffnen eines Dialogs bei offenem Popup-Dialog
Symptom
Beim Ausführen eines Gotos (bzw. allgemein beim Öffnen eines Dialogs) tritt sporadisch folgender Fehler auf:
com.top_logic.util.error.TopLogicException: Internal error at com.top_logic.mig.html.layout.CommandDispatcher.internalDispatchCommand(CommandDispatcher.java:218) ... Caused by: java.lang.IllegalStateException: Control has no id!. Method is called before initial write occured, fetchID() was called, or after detach. at com.top_logic.layout.basic.AbstractControlBase.getID(AbstractControlBase.java:380) at com.top_logic.layout.structure.BrowserWindowControl.unregisterAndClosePopupDialog(BrowserWindowControl.java:420) at com.top_logic.layout.structure.PopupDialogControl.closePopupDialog(PopupDialogControl.java:345) at com.top_logic.layout.structure.PopupDialogControl.handleDialogClosed(PopupDialogControl.java:388) at com.top_logic.layout.structure.DialogModel$1.internalDispatch(DialogModel.java:41) ... at com.top_logic.layout.structure.DefaultPopupDialogModel.setClosed(DefaultPopupDialogModel.java:175) at com.top_logic.layout.structure.BrowserWindowControl.unregisterAndCloseAllPopupDialogs(BrowserWindowControl.java:477) at com.top_logic.layout.structure.BrowserWindowControl.openDialog(BrowserWindowControl.java:307) at com.top_logic.mig.html.layout.DialogSupport.registerOpenedDialog(DialogSupport.java:84) at com.top_logic.tool.boundsec.OpenModalDialogCommandHandler.internalOpenDialog(OpenModalDialogCommandHandler.java:335) ... at com.top_logic.tool.boundsec.commandhandlers.GotoHandler.handleCommand(GotoHandler.java:136)
Ursache
In BrowserWindowControl.popupDialogs bleibt ein bereits detachtes PopupDialogControl (ohne ID) als "offenes Popup" stehen.
unregisterAndClosePopupDialog() setzt voraus: "steht in popupDialogs" und "steht nicht in popupDialogsToOpen" impliziert "wurde geschrieben und ist attached" -- und ruft deshalb aDialog.getID() auf. Diese Invariante kann brechen:
- Popups hängen im LocalScope des BrowserWindowControl: updatePopupDialogs() wird aus internalRevalidate() innerhalb von context.validateScoped(this, ...) aufgerufen, und UpdateWriter.add() schreibt die FragmentInsertion sofort. Das Popup attacht sich damit an das BrowserWindowControl. Anders als Dialoge bekommen Popups keinen eigenen getLayerScope().
- Bei einem kompletten Repaint des Fensters (requestRepaint()) laufen internalRequestRepaint() -> BrowserWindowControl.detachInvalidated() -> dropIncrementalUpdates() (leert popupDialogsToOpen/`popupDialogsToClose`) und anschließend clearLocalScope() -> alle Popups werden detacht (id == null).
- Der Repaint rendert über den BrowserWindowRenderer neu -- und der schreibt den Popup-Anker pdlgPopupDialogs leer. Dialoge werden aus dialogs neu gerendert (writeDialogs()), Popups werden nirgends neu geschrieben.
- popupDialogs wird dabei nie geleert. Das Popup ist am Client weg, detacht, ohne ID -- steht aber weiter in der Liste, und sein Listener am PopupDialogModel ist noch registriert (deregisterAsListener() passiert nur in closePopupDialog()).
Beim nächsten Dialog-Öffnen läuft unregisterAndCloseAllPopupDialogs() über diesen Zombie-Eintrag, setClosed() feuert, handleDialogClosed() -> closePopupDialog() -> unregisterAndClosePopupDialog() -> getID() -> Exception.
Denselben Effekt hat der zweite Pfad, der im Code sogar explizit kommentiert ist (attachRevalidated(): "Drop updates for dialogs and popups that are potentially added in detached state"): Ein Popup, das im detachten Zustand geöffnet wird, landet in popupDialogs und in popupDialogsToOpen; beim nächsten Attach wird nur popupDialogsToOpen geleert -- der Eintrag in popupDialogs bleibt als nie attachter Zombie zurück.
Der Goto ist nur der Auslöser, der über die verrottete Liste stolpert. Der Schaden entsteht typischerweise in einem früheren Request (Layout-Neuaufbau, Theme-/Sprachwechsel, Invalidierung des Fenster-Controls) während ein Popup-Menü offen war.
unregisterSinglePopupDialog(String) ist von derselben Invariante betroffen: Dort wird popup.getID() über alle Einträge von popupDialogs aufgerufen.
Beispiel zur Reproduktion
- Burger-Menu öffnen.
- F5 drücken
- Goto ausführen.
Lösung
- Ursache: In attachRevalidated() und detachInvalidated() werden die offenen Popups zusätzlich zu den Inkrement-Listen verworfen (unregisterAllPopupDialogs() -- entfernt aus popupDialogs, schließt und detacht, ohne Client-Update). Das entspricht der Realität: Popups überleben ein Neuzeichnen des Fensters nicht.
- Defense in depth: unregisterAndClosePopupDialog() und unregisterSinglePopupDialog() rufen getID() nur noch für attachte Popups auf.
Test
- test.com.top_logic.layout.structure.TestBrowserWindowControl