Modular Util - Context listeners stop working

jbfiedler's Avatar

jbfiedler

16 Feb, 2011 01:16 AM

Hello,

I am using the modular util (Hooks fork) and have created a service to handle the loading of modules using ModuleManager. One of the modules I am working on is a simple logging viewer. I want logging data model to be part of the shell application, however, I want the logging view to be modular since it is only necessary for development.

I have two different LoggingEvent types DO_LOG and LOGGED. DO_LOG is mapped to a LogCommand that adds the message to the LoggingModel. When the message is added to the LoggingModel it disptaches a LOGGED event to indicate the model has changed.

In my application mediator I add a context listener for the LOGGED event that then calls a function that redispatches the event to the modules, namely the logging module, which should then display the message. The problem I am encountering is that after I load the module the application context listener for the LOGGED event no longer gets called.

I have attached a skeleton application demonstrating the issue. To test click "Load module" then click "Dispatch". You will notice nothing happens.

Oddly enough if I dispatch a DO_LOG event right after loading the module then the context listener works correctly. In the code see ModuleManagerService.as line 34. If you follow the same steps above with this line uncommented you will then see all the logging messages appear in the LoggingModuleView each time you click "Dispatch".

Thanks in advance,
Justin

  1. 1 Posted by Stray on 16 Feb, 2011 10:16 AM

    Stray's Avatar

    Hi there,

    the most common reason for what you're describing - context listener only works if you use it fast enough - is that you've forgotten to hold on to the instance of the context in that module.

    This will cause the problem you're describing:

    var context:ModuleContext = new ModuleContext(this...);

    And the fix is just to assign it to a peristent variable

    protected var context:ModuleContext;

    // then later

    context = new ModuleContext(this... );

    Can you confirm whether that's the problem? If it's definitely not that then I'll take a look at the skeleton example and try to figure out what's going wrong.

    Cheers,

    Stray

  2. 2 Posted by jbfiedler on 16 Feb, 2011 04:25 PM

    jbfiedler's Avatar

    Hi Stray,

    Thanks for your response. I double checked and I am keeping an instance of both contexts in my application (shell app & module) so this is not a GC issue.

    Playing around with it some more I found that as long as I dispatch a DO_LOG event from the shell before I dispatch one from the module the context listener for the LOGGED event in the shell works correctly. It is only when I dispatch a DO_LOG event from the module first that the shell context listener doesn't work.

    Thanks again,
    Justin

  3. 3 Posted by Stray on 16 Feb, 2011 04:43 PM

    Stray's Avatar

    Hi Justin, no problem,

    I think it's a fairly straightforward one (come back if this doesn't fix it):

    Line 26 of MainMediator - if you want to listen to module events you need to use addModuleListener not addContextListener.

    The reason it works when you uncomment line 34 of ModuleManagerService is that this line is doing a dispatch on the shell's local application eventDispatcher. But to get the event from the LoggingModule to the shell you need to dispatch it on the module eventDispatcher, and listen for it on that dispatcher too.

    Stray

  4. 4 Posted by jbfiedler on 16 Feb, 2011 05:04 PM

    jbfiedler's Avatar

    Thanks Stray,

    but the context listener that is failing is listening for an event dispatched by the LoggingModel not the module . The LoggingModel is technically part of the shell, but the module mediator also has an injected reference.

    The flow is as follows:
    1) Module dispatchToModules(DO_LOG)
    2) Shell context maps DO_LOG to LogCommand via moduleCommandMap
    3) LogCommand adds logging message to LoggingModel
    4) LoggingModel dispatch(LOGGED)
    5) Shell mediator context listener for LOGGED event fails.

    To make the LoggingModel more portable I want to disptach() the LOGGED event with the eventDisptacher, then the shell mediator's context listener can determine if this event should be broadcast to modules with dispatchToModules(). This way I can use logging model for modular and non modular applications.

  5. Support Staff 5 Posted by Stray on 16 Feb, 2011 05:37 PM

    Stray's Avatar

    Oh, I see.

    Hmm - I'm afraid I'm totally stumped.

    I'm not able to compile your example so I can't test it myself. What I'd suggest is that you:

    1) Compile from source and put some traces in the eventMap / commandMap / base module mediator and so on.

    2) Try adding a listener direct to the eventDispatcher and moduleEventDispatcher (they are just properties of the context, mediator and so on) - just for diagnostics.

    Hopefully someone who is more familiar with flex than me will pop up and solve it for you. I'm a pure-as3 person.

    Sorry not to have solved it - I'm fairly sure it's something simple in your logic flow, but it's not jumping out at me.

    Stray

  6. 6 Posted by jbfiedler on 16 Feb, 2011 06:55 PM

    jbfiedler's Avatar

    Stray, thanks again for all the help.

    Is there a problem with the code I posted that it wont compile? I tested it before posting but can change it is something is missing/broken.

    I think this could be a Robotlegs issue maybe having to do with lazy instantiation. I found that changing nothing else but injecting the LoggingModel into the MainMediator causes the context listener to work correctly even if I dispatch a DO_LOG event from the module first. In short it seem like the shell app needs to instantiate the LoggingModel first for everything to work.

    I'll keep digging through the framework but I am stumped too :(

  7. 7 Posted by Stray on 16 Feb, 2011 07:11 PM

    Stray's Avatar

    That's an interesting point - yes, you do need to instantiate the modules - I'd assumed you were doing that in your module service?

    I can't compile it because you didn't include an ant file or anything (as far as I could see) and I don't use flash builder - I use the command line compiler + project sprouts.

    Where are you actually creating the module and passing through the parent injector etc?

    Check out this example:

    https://github.com/awassenaar/robotlegs-examples-dynmodules/blob/dynmodules/DynModules.mxml

  8. 8 Posted by jbfiedler on 16 Feb, 2011 07:48 PM

    jbfiedler's Avatar

    I have attached a new version of the skeleton app with a ANT build.xml. Let me know if it works for you. I also made minor changes with notes to show where the issue is occurring in onRegister() in MainMediator.as.

    ModuleManagerService.as loads and instantiates the modules but I believe the problem is with where the model is instantiated.

    Thanks for the example, I reviewed it before creating my service. However I feel they do not face my issue as they are only using the moduleEventDispatcher, not eventDisptacher.

    Thanks

  9. 9 Posted by Stray on 16 Feb, 2011 07:56 PM

    Stray's Avatar

    Hi Justin,

    my suspicion is that it's entirely to do with how your module is being created.

    I'm afraid I'm out of time for today - it's late here in the UK - but the workflow you're showing is just a normal workflow that is well tested in the modular unit tests - many of us use that daily - passing events from the module dispatcher to the internal one.

    I couldn't find anywhere in your application where you were passing in the parent injector - if you don't do that then the module dispatcher instance won't be shared between the two modules.

    Stray

  10. 10 Posted by jbfiedler on 16 Feb, 2011 09:27 PM

    jbfiedler's Avatar

    Stray, thanks for putting up with me all day :)

    The parent injector is injected into the module automagically with the following code:

    MainContext.as:49:
    viewMap.mapType(ILoggingModule);

    LoggingModuleView.mxml:24:
    [Inject] public function set parentInjector($injector:IInjector):void {

        context = new LoggingModuleContext(this, true, $injector, ApplicationDomain.currentDomain);
    

    }

    This is identical to the method used in https://github.com/awassenaar/robotlegs-examples-dynmodules/blob/dy...

    I did find something that I think exposes the core of this issue.

    In the shell I have MainMediator.as:35:
    addContextListener(LoggingEvent.LOGGED, onLoggedHandler, LoggingEvent);

    I understand this line to mean that anytime a LOGGED event is disptach()ed in the shell context the onLoggedHandler() should be called. This made me think that if items are still being added to the LoggingModel and the dispatch(LOGGED) is being called but the shell listener is not, then LoggingModel must not be part of the shell context.

    I was able to prove this by adding a similar context listener in the LoggingModuleMediator.as:22 which is part of the module context.:
    addContextListener(LoggingEvent.LOGGED, onModuleConextLoggedHandler, LoggingEvent);

    If I invoke the LogCommand from the module first and thus instantiate the LoggingModel from the module context first, the listener in the module context responds to the LOGGED event. The shell does nothing. So, LoggingModel is part of the module context in this case.

    This seems incorrect to me. When I map a singleton in the shell app I expect this singleton instance to belong to the shell context regardless of its use in other modules. Do you agree with this assumption?

    I have attached updated code which shows this difference in context ownership of the LoadingModel.

    To test:
    1) Click "Load Module" then click "Dispatch". You will see " ERROR: LoggingModel belongs to the LoggingModuleContext!" in the trace output.
    2) Click "Dispatch", then click "Load Module", the click "Dispatch" again. You will see "SUCCESS: LoggingModel belongs to the MainContext!" in the trace output.

    Thanks again! I really appreciate your input.

  11. Support Staff 11 Posted by Stray on 16 Feb, 2011 09:35 PM

    Stray's Avatar

    Hi there - the point is that unless you actually instantiate the LoggingModule using the shell's injector that injection won't be fulfilled - there's magic, but there's only so much magic!

    Anything that needs to be injected has to be instantiated using the injector.

    The alternative, with the modular stuff, is to pass that injection in when you create the module - as in the example I showed you previously ... or using the setter function normal-style. Either of these methods counts as 'non magic' injection.

    But - as far as I can tell, you're not providing the module with the parent injector via any of those 3 methods. This means that it can't refer to the parent to get the injection for the model, and that the moduleEventDispatcher instance will be a different one as well.

    But I've asked Joel Hooks to jump in now as the flex part is confusing me slightly!

    Stray

  12. Support Staff 12 Posted by Stray on 17 Feb, 2011 09:41 AM

    Stray's Avatar

    Hi Justin - I have figured it out... (ignore my previous message, I was having some sort of fail where I had mis-read the first paragraph of your message.)

    The singleton in both the shell and the loaded module are the same instance.

    But - because of lazy instantiation, that LoggingModel won't be instantiated until you first use it. And its injections are only provided at the moment when you first need it - and will be supplied by the injector local to the place where you first use it.

    So - the injection for eventDispatcher against IEventDispatcher will be fulfilled with the local injector.

    I suspect that you may be the first person to come up against this, because typically people are building things to work modular, and you're trying to be more flexible (a good idea) so you're using the normal eventDispatcher.

    The solution is quite simple now I realise what is happening.

    In your shell context, after you map the singleton, just add a line that forces the injector to instantiate the LoggingModel in this scope:

    injector.mapSingleton(LoggingModel);
    var disposableLocalReference:loggingModel = injector.getInstance(LogginModel);
    

    This means that the logging model will be created local to the shell, and when the module picks it up from the parent injector its dependencies will already be fulfilled.

    Apologies for having taken so long to figure this out - it's the first time I've come across it, but I'll put it on the list for the robotlegs team to think about.

    Can I ask you to confirm which versions of robotlegs/swiftsuspenders are in the swc?

    Thanks

    Stray

  13. Support Staff 13 Posted by Stray on 17 Feb, 2011 10:01 AM

    Stray's Avatar

    BTW - the reason this problem arises is that you've injected the model into your mediator in the logging module:

        [Inject]
        public var loggingModel:LoggingModel;
    
        var logs:ArrayCollection = loggingModel.logs;
    

    Personally I don't share injections between contexts. Instead I just relay the events between the local and module eventDispatchers in the mediator of each module/shell.

    It is more longhand - though not much - but it's much simpler to follow.

    Still - I've asked Till to confirm the problem and hopefully the fix I've given you will work!

    Stray

  14. 14 Posted by jbfiedler on 17 Feb, 2011 05:44 PM

    jbfiedler's Avatar

    Thanks Stray,

    I am using robotlegs-framework-v1.4.0, SwiftSuspenders-v1.5.1 and the latest robotlegs-utilities-Modular from github.

    Thanks for the clean work around. I put it into a util class:

    
    /**
     * For any class that shares injections across multiple contexts this
     * function ensures that the Class instances will belong to the current
     * context
     * 
     * http://knowledge.robotlegs.org/discussions/problems/261-modular-util-context-listeners-stop-working
     */
    public static function mapToCurrentContext($injector:IInjector, $class:Class):void {
        //Create an instance in this context
        //The local reference will be freed after this function call
        var disposableLocalReference:* = $injector.getInstance($class);
    }
    

    Now in my MainContext I do:

    
    injector.mapSingleton(LoggingModel);
    RobotLegsUtil.mapToCurrentContext(injector, LoggingModel);
    

    And everything works great!

    The reason I am sharing the model between contexts is to get the current state of the LoggingModel. How would you handle this with events? I just felt it was much easier to keep it synchronous.

    Justin

  15. Support Staff 15 Posted by Stray on 17 Feb, 2011 06:49 PM

    Stray's Avatar

    Nice use of a utility! I like that approach.

    I'm glad it fixed it - having spoken to Till it seems that basically this corner case has never come up before so the behaviour wasn't specified. We're still digging into it to work out the best way to address it, as it could be that people are relying on how it currently works in their current projects.

    My logging module is its own module - it just expects to receive the logging events on the moduleEventDispatcher, so every other module - and the shell - is ignorant of its existence. The logging module only uses the local eventDispatcher, and the mediator for that module handles the relaying of events to and from the moduleEventDispatcher...

    All the mappings are done with a bootstrap Command (a command that just does the injector mappings and commandMap mappings and so on) - so to use it in a single context project I just hook-up that bootstrap Command (to STARTUP_COMPLETE) in the main context instead of pulling in the module.

    Anyway - great that we got to the bottom of it - thanks for bearing with us,

    Stray

  16. jbfiedler closed this discussion on 17 Feb, 2011 07:09 PM.

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