Spark Window: opening and closing

dkarten's Avatar

dkarten

05 Jun, 2015 04:15 PM

I copied the procedure given here, http://knowledge.robotlegs.org/discussions/robotlegs-2/3073-multipl..., for opening another window in the application. I prefer using new window components instead of the PopUpManager and PopUp windows. However, the close handler seems to be causing problems, the removeChild call makes the stage a null reference and in the actual window.close() call, I get the following error

exception, information=TypeError: Error #1009: Cannot access a property or method of a null object reference. at spark.components::Window/close()

I notice that the window.close() function has it's own call to systemManager.removeChild(), and the systemManager references WindowedSystemManager.

public function close():void
    {
        if (_nativeWindow && !nativeWindow.closed)
        {
            var e:Event = new Event("closing", false, true);
            stage.nativeWindow.dispatchEvent(e);
            if (!(e.isDefaultPrevented()))
            {
                removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
                stage.nativeWindow.close();
                _nativeWindow = null;
                systemManager.removeChild(this);
            }
        }
    }

So do we need the removeChild call in the closing handler? Removing the this.owner.removeChild(this) causes the error to disappear, as does calling window.nativeWindow.close() instead of window.close()

  1. Support Staff 1 Posted by Ondina D.F. on 08 Jun, 2015 08:46 AM

    Ondina D.F.'s Avatar

    The code shown in your message is not from the linked discussion.
    I have no idea why you get the error without seeing more code.
    The example I used in the cited discussion worked fine.
    Could you attach or paste in here all the relevant code you're using for opening and closing a spark window, so I can test it on my end?

    So do we need the removeChild call in the closing handler?

    Yes, if you care about garbage collection.

    The WindowedSystemManager keeps a reference to the Window after it has been closed, thus the Window is not eligible for gc, and neither is its Mediator, in case it had one.
    If that's what you want (to keep the window alive) you don't need to remove all references to the window and its children.

  2. 2 Posted by dkarten on 08 Jun, 2015 01:46 PM

    dkarten's Avatar

    I should add, the call to the close() function is triggered by a "Cancel" button component, not a native window X button. Still, I should be able to programmatically close a window, no? e.g.

    <s:Window xmlns:s="...">
    
        <s:Button id="someButton" click="this.close()" />
        <!--this references parent window component-->
    </s:Window>
    

    The code shown is the Spark Window's close method. I posted it to show that it contains a call to systemManager.removeChild, and, in this case, systemManager is WindowedSystemManager, so the close() method does the same thing as the closing handler. I think the error is caused by the child being attempted to be removed twice. My code is exactly what was given in the RL forum link I posted.

    The window has a handler for the closing event, which is just

    public function closeHandler(event:Event):void {
        this.owner.removeChild(this);
    }
    

    The mediator has the same overridden destroy method given, but I have not encountered any errors in this. I open the window from an AppMediator, that mediates the main windowed application component.

    private function openWindow():void {
            var myWindow:MyWindow = new MyWindow();
            viewManager.addContainer(myWindow);
            myWindow.open();
    }
    
  3. Support Staff 3 Posted by Ondina D.F. on 08 Jun, 2015 03:32 PM

    Ondina D.F.'s Avatar

    Still, I should be able to programmatically close a window, no?

    Sure. Do you mean like shown bellow?

    AppMediator could simply do this:

    viewManager.removeContainer(myWindow);
    myWindow.close();

    and in myWindow, inside the handler of Event.CLOSING you just remove myWindow's children and different event listeners, and do this: this.owner.removeChild(this), if you want.

    The code shown is the Spark Window's close method. I posted it to show that it contains a call to systemManager.removeChild, and, in this case, systemManager is WindowedSystemManager

    Ah, ok.

    Well, if you use a profiler like the FlashBuilder's Profiler you can see whether myWindow has been removed from WindowedSystemManager or not.

    Maybe I'm wrong, but I remember that if you don't explicitly tell WindowedSystemManager to remove the window, the window would be kept in memory. I don't have the time to test it again right now. Let me know about your findings tough. I'm interested in all things pertaining AIR;)

    I think the error is caused by the child being attempted to be removed twice. My code is exactly what was given in the RL forum link I posted.

    Hmm, twice? Who is removing it and from where? Why has it worked on my end without errors?
    How are you removing the window programmatically? Can you show the code?

  4. 4 Posted by dkarten on 09 Jun, 2015 04:42 PM

    dkarten's Avatar

    When I do this

    AppMediator could simply do this:

    viewManager.removeContainer(myWindow); myWindow.close();

    and in myWindow, inside the handler of Event.CLOSING you just remove myWindow's children and different event listeners, and do this: this.owner.removeChild(this), if you want.

    I still get the error. Please tell me if I am misunderstanding something, but my logic as to the window's scope in memory is as follows.

    The view where you can open the window has a button to do so. This view's mediator listens for the button click, and in the event listener, creates an instance of the mock popup window.

    //in view mediator
    private function windowOpenButtonClick(event:Event):void {
        var myWindow:MyWindow = new MyWindow();
        viewManager.addContainer(myWindow);
        myWindow.open();
    }
    

    At this point, the viewManager is the only other thing that has a reference to the instance of myWindow. The window is associated with a MyWindowMediator, and here we override the destroy method to be as you suggest

    override public function destroy():void {
        viewManager.removeContainer(view);
        super.destroy();
    }
    

    This removes viewManager's reference to the instance of myWindow. I have triple and quadruple checked that in the native ActionScript codebase, the myWindow.close() function performs the call to WindowedSystemManager.removeChild(myWindow). Revisiting this close() code,

    public function close():void
        {
            if (_nativeWindow && !nativeWindow.closed)
            {
                var e:Event = new Event("closing", false, true);
                stage.nativeWindow.dispatchEvent(e);
                if (!(e.isDefaultPrevented()))
                {
                    removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
                    // ===ERROR HAPPENS ON LINE BELOW, stage IS NULL===
                    stage.nativeWindow.close(); 
                    _nativeWindow = null;
                    systemManager.removeChild(this);
                }
            }
        }
    

    It seems to me that if the window's reference to the stage is valid before dispatching the close event, and null after, then the closing event handler is what is causing the stage to be null. Finally, checking the numChildren property of myWindow's systemManager variable, it is 2, which makes sense with the top level window and the currently open window. I am not sure why you cannot reproduce the error. The exact layout of my application is as follows:

    Main.mxml -- s:WindowedApplication
     |--MainView -- s:VGroup
        |--Tabs -- mx:TabNavigator
           |--ViewA -- s:NavigatorContent
              |--s:VGroup
                 |--OpenWindowButton -- s:Button
           |--ViewB -- s:NavigatorContent
           |-- etc...
    

    Main has an AppMediator to mediate it.
    Tabs has a TabMediator.
    ViewA has a ViewAMediator. This listens for OpenWindowButton click and opens myWindow.
    MyWindow has a MyWindow Mediator, described above.

    Is there something especially wrong about this layout that would cause me to experience the error but not you??

  5. Support Staff 5 Posted by Ondina D.F. on 09 Jun, 2015 04:59 PM

    Ondina D.F.'s Avatar

    How are you removing the window programmatically? Can you show the code?

    Sorry for the stupid question. The code where you called close() was right under my nose, but I've got somehow distracted yesterday and didn't notice the obvious.

    In the example from the other discussion I only tested the closing of the window through its own close symbol.

    so the close() method does the same thing as the closing handler. I think the error is caused by the child being attempted to be removed twice.

    Yes, it seems to be the case.

    Now, after testing it again, I can confirm that closing the window programmatically is causing the error, that you mentioned, to be thrown, because the stage is indeed null by the time this.owner.removeChild(this); is called.
    In case you want to keep both ways of closing the window (close-symbol click and a direct call to close()), this solves the issue:

    if(stage)
    {
        this.owner.removeChild(this);
    }
    

    Of course this is a dirty workaround, but I couldn't find a better or cleaner way of removing the window from the WindowedSystemManager, when the window gets closed by clicking on its close symbol.
    I also don't have enough time to do more experiments and to find a proper way of closing a spark window so that it gets gc-ed. If you find a good solution to this, please let me know about it.

    If you only close the window programmatically, you don't need the offending code from above.

    If you close the window from the same class that has opened it and added it to the viewManager, the window will get closed properly and also gc-ed.

    viewManager.removeContainer(someWindow);
    someWindow.close();
    someWindow = null;
    
  6. Support Staff 6 Posted by Ondina D.F. on 10 Jun, 2015 02:53 PM

    Ondina D.F.'s Avatar

    After further investigation, my findings:

    A non mediated Spark Window can be closed and garbage collected without problems.

    Closing a mediated window programmatically, as shown bellow, works without problems as well.

    However, a mediated Spark Window which gets closed after clicking its close button (x) stays in memory because its mediator is keeping a reference to it.

    This has something to do with the way the ViewManagerExtension's classes handle the adding and removing of top-level root containers and the strange behaviour of the stage when the window's close button closes it.

    If I add mediatorMap.unmediate(someWindow); to the onCustomWindowClosing, everything works fine, but that's not what we want to do normally. Using owner.removeChild is also not pretty and actually what I said in the previous post about checking if stage is not null doesn't work. Very frustrating.

    I'll continue my investigation and I'll open an issue on github, and maybe Shaun will find a nice solution to this.
    I don't quite understand why there is a difference between closing the window programmatically and through its close button, e.g. between spark window's window_closeHandler(), window_closingHandler() and close(), and to be honest I have no desire at all to study adobe's code.

    Code for opening and closing a window programmatically from a View:

    protected var someWindow:SomeWindow;
    
    protected function openWindowButton_clickHandler(event:MouseEvent):void
    {
        someWindow = new SomeWindow();
        someWindow.addEventListener(Event.CLOSING, onCustomWindowClosing);
        
        viewManager.addContainer(someWindow);
        
        var someSimpleView:SomeSimpleView = new SomeSimpleView();
        someWindow.addElement(someSimpleView);
        
        someWindow.open();
    }
    
    protected function closeWindowButton_clickHandler(event:MouseEvent):void
    {
        if (!someWindow)
            return;
    
        someWindow.close();
    }
    
    protected function onCustomWindowClosing(event:Event):void
    {
        someWindow.removeAllElements();
        someWindow.removeEventListener(Event.CLOSING, onCustomWindowClosing);
    
        viewManager.removeContainer(someWindow);
    
        someWindow = null;
    }
    
  7. Support Staff 7 Posted by Ondina D.F. on 10 Jun, 2015 03:03 PM

    Ondina D.F.'s Avatar

    I just noticed your message from yesterday inside the spam folder of this forum. I have no idea why it landed there. Maybe because you're not a registered user?
    I'm going to restore it right now. Sorry for the inconvinience.

  8. Support Staff 8 Posted by Ondina D.F. on 10 Jun, 2015 03:26 PM

    Ondina D.F.'s Avatar

    Is there something especially wrong about this layout that would cause me to experience the error but not you??

    Not at all.

    I was wrong about not getting the error. As I said, the code in the other discussion has been tested only for closing the window through the close button (x) of the window.
    And, the other thing I said about if(stage), in my post from yesterday, was also wrong. I was sure it worked at one time, but today it doesn't. Either the behaviour of a spark Window and its WindowedSystemManager is buggy, or I was too tired and distracted again (having a busy and chaotic time at the moment)

    The not so pretty workaround,
    this.owner.removeChild(this), works only when the close button (x) is triggering the closing.

  9. 9 Posted by dkarten on 10 Jun, 2015 04:05 PM

    dkarten's Avatar

    That is interesting. I did some investigating in the Adobe code, and found a hidden event class, WindowExistenceEvent.

    //From the Window code:
    private function window_closeHandler(event:Event):void
    {
        dispatchEvent(new Event("close"));
            
        // Event for Automation so we know when windows 
        // are created or destroyed.
        if (FlexGlobals.topLevelApplication)
        {
            FlexGlobals.topLevelApplication.dispatchEvent(
                new WindowExistenceEvent(WindowExistenceEvent.WINDOW_CLOSE, 
                                         false, false, this));
        }
    }
    

    I don't want to pretend to know the inner workings of Robotlegs, but perhaps listening for that could help?

    I started naively trying things, throwing whatever at the wall and seeing what would stick. Here is what I found:

    add an event listener to the window's nativeWindow

    public function closeHandler(event:Event):void {
        event.preventDefault();
        event.currentTarget.removeEventListener(Event.CLOSING, closeHandler);
        this.close();
    }
    

    Then, if I click the cancel button which calls window.close(), or the "x" button at the top left, I see the ViewManager log

    130450 DEBUG Context-0-9 [object ViewManagerBasedExistenceWatcher] Adding context existence event listener to container MyWindow165
    132529 DEBUG Context-0-9 [object ViewManagerBasedExistenceWatcher] Removing context existence event listener from container MyWindow165
    

    I'm not sure if I understand what I've done here, but it appears clicking the 'x' icon triggers a different path of events than the window.close() method, but ultimately both processes pass through a NativeWindow CLOSING event. I'm sure this is messy, since it triggers more than one call to the close() method, but maybe you can figure out a cleaner way to accomplish what I have? Then you could write a base PopupWindow class with these event handlers, and any window you want to create could extend them! Just my $0.2

  10. Support Staff 10 Posted by Ondina D.F. on 10 Jun, 2015 04:35 PM

    Ondina D.F.'s Avatar

    I did some investigating in the Adobe code, and found a hidden event class, WindowExistenceEvent. I don't want to pretend to know the inner workings of Robotlegs, but perhaps listening for that could help?

    That sounds interesting, indeed! I'll mention this in the ticket that I'm going to open on github, and also the link to our discussion, but I'd like to investigate a bit more before opening an issue. I'll look into this WindowExistenceEvent.

    I started naively trying things, throwing whatever at the wall and seeing what would stick.

    Hehe, that sounds familiar. I've tried the craziest things, too.

    I think I already tried the event.preventDefault(); thing, but it didn't solve the problem. Probably it was the wrong combination of things. I'll try again.

    Problem is, it is not only important to close a window without errors of any kind, but to have it removed from whatever keeps a reference to it, so it and its mediator get gc-ed.
    You can't get a reference to the WindowedSystemManager from the class opening the window as you can with the PopUpManager, as far as I know. How exactly WindowedSystemManager keeps a reference to the window, I have no idea.

    Are you using a profiler to see what's kept in memory?

    ViewManagerBasedExistenceWatcher belongs to the modularity extension.

    Then you could write a base PopupWindow class with these event handlers, and any window you want to create could extend them!

    Interesting thought.

    I'll have to continue tomorrow... Too bad, we live in different time zones.

    Thanks for looking into this issue, much appreciated!!

    P.S.
    From Adobe's documentation for Window:

    open()
    Creates the underlying NativeWindow and opens it. After being closed, the Window object is still a valid reference, but accessing most properties and methods will not work. Closed windows cannot be reopened.

    I'm asking myself what this valid reference means....who exactly is keeping it alive

  11. 11 Posted by dkarten on 10 Jun, 2015 06:36 PM

    dkarten's Avatar

    Doesn't the ViewManagerBasedExistenceWatcher logging the second line imply that viewManager.removeChild() was called, which would imply that the mediator's destroy method is called. Is it not eligible for GC at that point?

  12. Support Staff 12 Posted by Ondina D.F. on 11 Jun, 2015 09:44 AM

    Ondina D.F.'s Avatar

    I've found a solution. Hopefully, it's the right one!
    As per your suggestion I tried to use event.preventDefault(); again and it worked.
    I'll attach the mini-project as a zip after adding some comments to the code.
    Please tell me what you think about this solution.

    There is a View, WindowOpenerView, having 2 buttons, open and close window.

    In their handlers the view sends a custom event to its mediator.

    protected function onOpenMediatedWindow(event:MouseEvent):void
    {
        dispatchEvent(new SparkWindowEvent(SparkWindowEvent.OPEN_SPARK_WINDOW));
    }
    
    protected function onCloseMediatedWindow(event:MouseEvent):void
    {
        dispatchEvent(new SparkWindowEvent(SparkWindowEvent.CLOSE_SPARK_WINDOW));
    }
    

    WindowOpenerMediator adds the Window, SomeWindow, to the viewManager and opens it. It also redispatches SparkWindowEvent.CLOSE_SPARK_WINDOW. SomeWindowMediator is listening to the event and takes care of closing the window.

    public class WindowOpenerMediator extends Mediator
    {
        [Inject]
        public var view:WindowOpenerView;
    
        [Inject]
        public var viewManager:IViewManager;
                    
        override public function initialize():void
        { 
            addViewListener(SparkWindowEvent.OPEN_SPARK_WINDOW, onOpenSparkWindow, SparkWindowEvent);
            addViewListener(SparkWindowEvent.CLOSE_SPARK_WINDOW, dispatch, SparkWindowEvent);
        }
    
        private function onOpenSparkWindow(event:SparkWindowEvent):void
        {
            var someWindow:SomeWindow;
            someWindow = new SomeWindow();                  
                
            viewManager.addContainer(someWindow);           
                
            var someSimpleView:SomeSimpleView = new SomeSimpleView();
            someSimpleView.y = 50;
            someWindow.addElement(someSimpleView);
                        
            someWindow.open();
        }
    }
    

    SomeWindowMediator

    public class SomeWindowMediator extends Mediator
    {
        [Inject]
        public var view:SomeWindow;
    
        [Inject]
        public var viewManager:IViewManager;
    
        override public function initialize():void
        {       
            addViewListener(Event.CLOSING, onWindowClosing, Event);
            
            addContextListener(SparkWindowEvent.CLOSE_SPARK_WINDOW, onCloseSparkWindow, SparkWindowEvent);
            addViewListener(SparkWindowEvent.CLOSE_SPARK_WINDOW, onCloseSparkWindow, SparkWindowEvent);
        }
            
        private function onWindowClosing(event:Event):void
        {
            event.currentTarget.removeEventListener(Event.CLOSING, onWindowClosing);
            event.preventDefault();
            destroyView();
        }
            
        private function onCloseSparkWindow(event:SparkWindowEvent):void
        {
            destroyView();
        }
            
        private function destroyView():void
        {
            view.cleanUp();
            view.close();
            viewManager.removeContainer(view);
            //view = null;  
        }
    }
    

    SomeWindow

    public function cleanUp():void
    {
        //remove all event listeners and subcomponents
        removeAllElements();
    }
    protected function closeWindowButton_clickHandler(event:MouseEvent):void
    {
        dispatchEvent(new SparkWindowEvent(SparkWindowEvent.CLOSE_SPARK_WINDOW));
    }
    
  13. Support Staff 13 Posted by Ondina D.F. on 11 Jun, 2015 11:02 AM

    Ondina D.F.'s Avatar

    Doesn't the ViewManagerBasedExistenceWatcher logging the second line imply that viewManager.removeChild() was called, which would imply that the mediator's destroy method is called. Is it not eligible for GC at that point?

    ViewManagerBasedExistenceWatcher is used by the ModularityExtension in order

    • to detect the contextView of a potential module (= Flex Module or any component with its own Context). The contextView is needed for the creation of module's Context.

    • and to inform the main Context that it can add module's Context as a child.

    But, we are not talking about Modules now, so please see the readme for more details:
    https://github.com/robotlegs/robotlegs-framework/blob/master/src/ro...

    You can create your custom MVCSBundle, where you don't include the ModularityExtension, if you don't need it. Including or excluding the ModularityExtension has no effect on removing window's references that are keeping it alive.
    You're seeing the log info, because you're probably using the default MVCSBundle.

    I guess ViewManagerBasedExistenceWatcher shouldn't get involved in detecting the opening and closing of windows or popups. Maybe another extension that would somehow listen to system managers' events of any kind would be a better fit...not sure though

    After we, you and me, decide that the solution presented in my previous post is acceptable, I'll try to see if it also works for a Window which is creating its own Context.

  14. Support Staff 14 Posted by Ondina D.F. on 11 Jun, 2015 11:17 AM

    Ondina D.F.'s Avatar

    I've attached the zip file ondina-robotlegs-bender-windows

  15. 15 Posted by dkarten on 11 Jun, 2015 02:24 PM

    dkarten's Avatar

    Awesome! I will definitely look at this in depth over the weekend and am sure I can find a way to integrate these techniques into my project. Glad we could figure this out!

  16. Support Staff 16 Posted by Ondina D.F. on 12 Jun, 2015 12:08 PM

    Ondina D.F.'s Avatar

    The TitleBar's closeButton click handler:

    closeButton.addEventListener(MouseEvent.CLICK, closeButton_clickHandler);
    
    private function closeButton_clickHandler(event:Event):void
    {
        window.close();
    }
    

    If you add event listeners for Window's added to stage and removed from stage events, you can see :

    • that the added to stage is dispatched twice, but it seems that it doesn't affect the working of the Window in any way, at least as far as I can see

    • the removed from stage is only dispatched when the window is closed by programmatically calling its close() method. However, when the TitleBar's closeButton is clicked, despite window.close(); from its click handler, none dispatches a REMOVED_FROM_STAGE event, for whatever reason (bug?). Thus Robotlegs has no idea that the Window has been removed from stage, and it can't do its clean up job.

    I did this:

    SomeWindow, from the previous example, is adding an event listener for the Event.CLOSE :

    close="onWindowClose(event)"

    //Spark Button's clickHandler
    protected function closeWindowButton_clickHandler(event:MouseEvent):void
    {
        close();            
    }
    
    // Window's Event.CLOSE handler
    protected function onWindowClose (event:Event):void
    {
        dispatchEvent(new Event(Event.REMOVED_FROM_STAGE));
                    
        cleanUp();      
    }
    
    public function cleanUp():void
    {
        //remove all event listeners and subcomponents
    }
    

    SomeWindowMediator

    override public function initialize():void
    {       
        addContextListener(SparkWindowEvent.CLOSE_SPARK_WINDOW, onCloseSparkWindow, SparkWindowEvent);
    }
    
    private function onCloseSparkWindow(event:SparkWindowEvent):void
    {
        view.close();
    }
    
    override public function destroy():void
    {
        viewManager.removeContainer(view);
        super.destroy();
    }
    

    So, in the handler of Event.CLOSE the Window dispatches Event.REMOVED_FROM_STAGE. Robotlegs is picking it up and is removing SomeWindowMediator as a consequence.
    Inside SomeWindowMediator's destroy(), the View gets removed from the viewManager.
    The View and its Mediator are now eligible for gc, as I can see in the profiler.

    Awesome! I will definitely look at this in depth over the weekend

    Please try out the code from above too and let me know if it works on your end as well. Thank you.

  17. 17 Posted by dkarten on 29 Jun, 2015 07:38 PM

    dkarten's Avatar

    Question about implementing this using class inheritance.

    I will have a number of popup windows I want to behave this way, but they will obviously have more specific mediator functionality. Right now I'm getting an injection error.

    "Injector is missing a mapping to handle injection into property "view" of object SomeExtendedWindowMediator withy type SomeExtendedWindowMediator. Target dependency SomeWindow|"

    Where SomeExtendedWindow refers to an implementation of the SomeWindow you shared in your project and SomeExtendedWindowMediator extends SomeWindowMediator.

    How to fix this?

  18. 18 Posted by dkarten on 29 Jun, 2015 07:47 PM

    dkarten's Avatar
  19. Support Staff 19 Posted by Ondina D.F. on 30 Jun, 2015 09:45 AM

    Ondina D.F.'s Avatar
  20. Ondina D.F. closed this discussion on 29 Jul, 2015 09:32 AM.

Comments are currently closed for this discussion. You can start a new one.

Keyboard shortcuts

Generic

? Show this help
ESC Blurs the current field

Comment Form

r Focus the comment reply box
^ + ↩ Submit the comment

You can use Command ⌘ instead of Control ^ on Mac