0
votes

I have a Backbone Marionette application whose layout's regions are not working properly. My app is structured using Require modules and some of these modules' regions are failing to close themselves when the module has been returned to a second time. The first time through the regions are closing as expected but upon return the layout object no longer contains the region objects it did during the first visit: I am using the browser debugger to ascertain this difference.

Here is my Module code:-

define(["marionette", "shell/shellapp", "interaction/videoreveal/model", "interaction/videoreveal/controller", "utils/loadingview", "utils/imagepreloader"], function(Marionette, shellApp, Model, Controller, LoadingView, imagePreloader){
    var currentModuleModel = shellApp.model.get("currentInteractionModel");                      // get module name from menu model
    var Module = shellApp.module(currentModuleModel.get("moduleName"));           // set application module name from menu model

    Module.init = function() {                                                    // init function called by shell
        //trace("Module.init()");
        Module.model = new Model(shellApp.interactionModuleData);             // pass in loaded data
        Module.controller = new Controller({model: Module.model, mainRegion:shellApp.mainRegion});             // pass in loaded data and region for layout
        Module.initMain();
    };

    Module.initMain = function() {
        trace("Module.initMain()");
        shellApp.mainRegion.show(new LoadingView());

        // do some preloading
        var imageURLs = this.model.get('imagesToLoad');
        imagePreloader.preload(imageURLs, this.show, this);


    };

    Module.show = function() {
        Module.controller.show();
    };

    Module.addInitializer(function(){
        //trace("Module.addInitializer()");

    });
    Module.addFinalizer(function(){
        //trace("Module.addFinalizer()");
    });

    return Module;
});

Here is the Controller class which is handling the Layout and Views:-

define(["marionette", "shell/vent", "shell/shellapp", "interaction/videoreveal/layout", "interaction/videoreveal/views/mainview", "ui/feedbackview", "ui/videoview"], function(Marionette, vent, shellApp, Layout, MainView, FeedbackView, VideoView){

    return Marionette.Controller.extend({
        initialize: function(options){
            trace("controller.initialize()");
            // store a region that will be used to show the stuff rendered by this component
            this.mainRegion = options.mainRegion;
            this.model = options.model;
            this.model.on("model:updated", this.onModelUpdate, this);
            this.layout = new Layout();
            this.layout.render();
            this.mainView = new MainView({model:this.model, controller:this});
            this.feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"});
            this.videoView = new VideoView({videoContainerID:"vrVideoPlayer"});
            vent.on("feedbackview:buttonclicked", this.onFeedbackClick, this);
            vent.on("videoview:buttonclicked", this.onVideoClick, this);
        },
        // call the "show" method to get this thing on screen
        show: function(){
            // get the layout and show it
            this.mainRegion.show(this.layout);
            this.model.initInteraction();
        },
        initFeedback: function (index) {
            this.model.set("currentItem", this.model.itemCollection.models[index]);
            this.model.set("itemIndex", index);
            this.model.initFeedback();
        },
        initVideo: function (index) {
            this.model.set("currentItem", this.model.itemCollection.models[index]);
            this.model.set("itemIndex", index);
            this.model.initVideo();
        },
        finalizer: function() {
            this.layout.close();
        },
        // events

        onFeedbackClick: function(e) {
            this.layout.overlayRegion.close();
        },
        onVideoClick: function(e) {
            this.layout.overlayRegion.close();
        },
        onFinishClick: function() {
            this.model.endInteraction();
        },
        onFeedbackClosed: function() {
            this.layout.overlayRegion.off("close", this.onFeedbackClosed, this);
            if (this.model.get("currentItem").get("correct") === true) {
                this.model.initThumb();
            }
        },
        onModelUpdate: function() {
            trace("controller onModelUpdate()");
            switch (this.model.get("mode")) {
                case "initInteraction":
                    this.layout.mainRegion.show(this.mainView);
                    break;
                case "initFeedback":
                    this.layout.overlayRegion.on("close", this.onFeedbackClosed, this);
                    this.feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"})
                    this.feedbackView.setContent(this.model.get("currentItem").get("feedback"));
                    this.layout.overlayRegion.show(this.feedbackView    );
                    break;
                case "initVideo":

                    this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
                    break;


                case "interactionComplete":
                    vent.trigger('interactionmodule:completed', this);
                    vent.trigger('interactionmodule:ended', this);
                    break;
            }
        }
    });
});

Here is the FeedbackView class:-

define(['marionette', 'tweenmax', 'text!templates/ui/feedbackWithScrim.html', 'shell/vent'], function (Marionette, TweenMax, text, vent) {
    return Marionette.ItemView.extend({
        template: text,
        initialize: function (options) {
            this.model = options.model;
            this.content = options.content;                 // content to add to box
            this.feedbackBoxID = options.feedbackBoxID;     // id to add to feedback box
            this.hideScrim = options.hideScrim;             // option to fully hide scrim

        },
        ui: {
            feedbackBox: '.feedbackBox',
            scrimBackground: '.scrimBackground'
        },
        events : {
            'click button': 'onButtonClick'                 // any button events within scope will be caught and then relayed out using the vent

        },
        setContent: function(content) {
            this.content = content;

        },

        // events
        onRender: function () {
            this.ui.feedbackBox.attr("id", this.feedbackBoxID);
            this.ui.feedbackBox.html(this.content);
            if (this.hideScrim) this.ui.scrimBackground.css("display", "none");
            this.$el.css('visibility', 'hidden');
            var tween;
            tween = new TweenMax.to(this.$el,0.5,{autoAlpha:1});


        },

        onButtonClick: function(e) {
            trace("onButtonClick(): "+ e.target.id);
            vent.trigger("feedbackview:buttonclicked", e.target.id)         // listen to this to catch any button events you want
        },


        onShow : function(evt) {
            this.delegateEvents();      // when rerendering an existing view the events get lost in this instance. This fixes it.
        }


    });
});

Any idea why the region is not being retained in the layout when the module is restarted or what I can do to correct this?

Much thanks,

Sam

3
Since the problem occurs when the module is stopped and restarted it would be useful to see the module's initializer and finalizer code. If I had to venture a guess, I'd bet that the layout is closed in the finalizer and never re-created on restart.Chris Camaratta
Hi Chris, thanks for getting back to me. I have added the Module code into the above post. Does this shed any more light on what might be going on?SamBrick
I'd like to see the layout's usage in context. There's nothing wrong with how you're creating and closing your views (although @ekeren makes a valid point about storing a ref to the feedback view), so seeing them in a broader context would help shed some light on what's going on.Chris Camaratta
Thanks Chris, I have added the Controller class in full above....SamBrick
I'm not seeing any code that does any sort of cleanup at all in the module or the controller. What I do see is that the module/controller's layout is shown via the application's region in the controller's show call. So what's going on is that when one of your other modules' layout is shown, this module's layout is closed, making it unusable as @ekeren states below. I'll go ahead and add an answer with specific suggestions.Chris Camaratta

3 Answers

2
votes

Okay.... I got there in the end after much debugging. I wouldn't have got there at all if it wasn't for the generous help of the others on this thread so THANKYOU!

Chris Camaratta's solutions definitely pushed me in the right direction. I was getting a Zombie layout view in my Controller class. I decided to switch a lot of my on listeners to listenTo listeners to make their decoupling and unbinding simpler and hopefully more effective. The key change though was to fire the Controller class's close method. I should have had this happening all along but honestly it's my first time getting into this mess and it had always worked before without needing to do this. in any case, lesson hopefully learned. Marionette does a great job of closing, unbinding and handling all of that stuff for you BUT it doesn't do everything, the rest is your responsibility. Here is the key modification to the Module class:-

    Module.addFinalizer(function(){
        trace("Module.addFinalizer()");
        Module.controller.close();
    });

And here is my updated Controller class:-

    define(["marionette", "shell/vent", "shell/shellapp", "interaction/videoreveal/layout", "interaction/videoreveal/views/mainview", "ui/feedbackview", "ui/videoview"], function(Marionette, vent, shellApp, Layout, MainView, FeedbackView, VideoView){

    return Marionette.Controller.extend({
        initialize: function(options){
            trace("controller.initialize()");
            // store a region that will be used to show the stuff rendered by this component
            this.mainRegion = options.mainRegion;
            this.model = options.model;
            this.listenTo(this.model, "model:updated", this.onModelUpdate);
            this.listenTo(vent, "feedbackview:buttonclicked", this.onFeedbackClick);
            this.listenTo(vent, "videoview:buttonclicked", this.onVideoClick);
        },
        // call the "show" method to get this thing on screen
        show: function(){
            // get the layout and show it
            // defensive measure - ensure we have a layout before axing it
            if (this.layout) {
                this.layout.close();
            }
            this.layout = new Layout();
            this.mainRegion.show(this.layout);
            this.model.initInteraction();
        },
        initFeedback: function (index) {
            this.model.set("currentItem", this.model.itemCollection.models[index]);
            this.model.set("itemIndex", index);
            this.model.initFeedback();
        },
        initVideo: function (index) {
            this.model.set("currentItem", this.model.itemCollection.models[index]);
            this.model.set("itemIndex", index);
            this.model.initVideo();
        },
        onClose: function() {
            trace("controller onClose()");
            if (this.layout) {
                this.layout.close();
            }
        },
        // events
        onFeedbackClick: function(e) {
            this.layout.overlayRegion.close();
        },
        onVideoClick: function(e) {
            this.layout.overlayRegion.close();
        },
        onFinishClick: function() {
            this.model.endInteraction();
        },
        onFeedbackClosed: function() {
            if (this.model.get("currentItem").get("correct") === true) {
                this.model.initThumb();
            }
        },
        onModelUpdate: function() {
            trace("controller onModelUpdate()");
            switch (this.model.get("mode")) {
                case "initInteraction":
                    this.layout.mainRegion.show(new MainView({model:this.model, controller:this}));
                    break;
                case "initFeedback":
                    var feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox", controller:this});
                    feedbackView.setContent(this.model.get("currentItem").get("feedback"));
                    this.layout.overlayRegion.show(feedbackView);
                    this.listenTo(this.layout.overlayRegion, "close", this.onFeedbackClosed);
                    break;
                case "initVideo":
                    this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
                    break;
                case "interactionComplete":
                    vent.trigger('interactionmodule:completed', this);
                    vent.trigger('interactionmodule:ended', this);
                    break;
            }
        }
    });
});
0
votes

If I understand your question correctly your views do not work well after they are closed and re-opened.

It looks like you are using your layout/views after they are closed, and keeping them for future use with these references:

this.feedbackView = new FeedbackView();

Marionette does not like this, once you close a view, it should not be used again. Check out these issues:

I would advise you not to store these views and just recreate them when you show them

layout.overlayRegion.show(new FeedbackView());
0
votes

@ekeren's answer is essentially right; I'm just expanding on it. Here's some specific recommendations that I believe will resolve your issue.

Since you're utilizing regions you probably don't need to create your views ahead of time:

initialize: function(options) {
    this.mainRegion = options.mainRegion;
    this.model = options.model;
    this.model.on("model:updated", this.onModelUpdate, this);
    vent.on("feedbackview:buttonclicked", this.onFeedbackClick, this);
    vent.on("videoview:buttonclicked", this.onVideoClick, this);
},

Instead, just create them dynamically as needed:

onModelUpdate: function() {
    switch (this.model.get("mode")) {
        case "initInteraction":
            this.layout.mainRegion.show(new MainView({model:this.model, controller:this}));
            break;

        case "initFeedback":
            var feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"})
            feedbackView.setContent(this.model.get("currentItem").get("feedback"));
            this.layout.overlayRegion.show(feedbackView);
            this.layout.overlayRegion.on("close", this.onFeedbackClosed, this);
            break;

        case "initVideo":
            this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
            break;

        case "interactionComplete":
            vent.trigger('interactionmodule:completed', this);
            vent.trigger('interactionmodule:ended', this);
            break;
    }
}

The layout is a bit of a special case since it can be closed in several places, but the principle applies:

show: function(){
    // defensive measure - ensure we have a layout before axing it
    if (this.layout) {
        this.layout.close();
    }
    this.layout = new Layout();
    this.mainRegion.show(this.layout);
    this.model.initInteraction();
},

Conditionally cleanup the layout:

finalizer: function() {
    if (this.layout) {
        this.layout.close();
    }
},