Using a Model class to hold a property of "application state?"

Stray's Avatar

Stray

03 Nov, 2010 10:18 PM

I prefer to use the state approach a bit more thoroughly for this - so, a different view/mediator is present depending on what the state is.

I think of it as a one-mediator-per-state kind of approach. In my case I actually switch the view in and out - they're just different subclasses of a core view that has most of the functionality, but you could switch the mediator mapped to that view instead.

Basically, the 'if' should be shifted as high up the pecking order as possible. This also leads to far fewer 'ifs' over all.

I think the approach you're describing below will work but boy is it a PIA to maintain and follow.

The view/mediator map makes it very, very easy to implement application state variations through the view.

Consider:

CoreView, CoreViewMediator

ActiveView, ActiveViewMediator (extending CoreView, CoreViewMediator)

PassiveView, PassiveViewMediator (extending CoreView, CoreViewMediator)

you get the idea - you just switch the Active/Passive views over.

The half-way approach is to use the state-change event to switch/add the handler.

Food for thought - but I'd say don't take the route you're describing below if you can avoid it :)

  1. 2 Posted by rickcr on 04 Nov, 2010 02:30 AM

    rickcr's Avatar

    Ok thanks Stray. I'll have to think more about that.

    For those reading this in the discussion forums, I had ended up deleting the original post since after I posted it, I realized it didn't really solve as much as I hoped it would.

    To sum up for sake of context, I was proposing that when certain states of the application be stored in an Application model class. The reason being is I could then avoid passing special ids in an event, which were used just to help toggle whether or not certain handlers should ignore the event fired. Instead the if check in the handler could look at the appState prop and compare it to the view's id and toggle based on the match.

    Let me provide a real life concrete example (that is trimmed down a bit), and I'm curious Stray how the activeView, passiveView approach would work or help out.

    1) A view stack is used.
    2) Two of the views (view A, viewB) of the stack both use a reusable view (reusableView.)
    3) view A and view B have certain events that can fire (eg click an item in a tree), that should end up firing a service call that returns results and the results returned need to reflect in the "reusableView," BUT obviously you want to show the results in the proper resuableView (either the one on viewA or the one on viewB.)

    Currently, I'm accomplishing this by:
    1) ReusableView has a field called 'viewType'
    2) when ResuableView is declared on a view it passes it the view type. eg:

     <ResuableView viewType="viewA" ../>

    3) The custom event for kicking off the service call also has to take a "viewType" and consequently you have to make sure this viewType is passed back when your service calls returns (which is annoying right there since you have to set the type on an AysncToken or Promise and then be sure to pull it off in the result handler to send back.)
    4) ResuableView's mediator event handler has to then do a check to make sure its updating the correct component:
    if ( event.viewType = view.viewType) //ok update this resuableView

    I wanted to clean all this up especially 3, so I'm curious the approaches you guys take? One way I thought I could avoid the ugly params being sent through the events, is by simply setting a state property on a Model class when you clicked on a menuItem. The my resuableViewMediator just needs to check:
    if ( AppModel.currentState == view.viewState) { //ok update this view

    Stray, how would you suggest setting up a one-mediator-per-state kind of approach here? I supposed I'd have to programmatically unregister and register the mediators when menu items were clicked for the view stack? Even if I could do that, I wouldn't want to, since I'd want to keep the old state of the views in the viewstack intact.

  2. 3 Posted by Stray on 04 Nov, 2010 09:17 AM

    Stray's Avatar

    Ah, Ok, so you're not really talking 'state' - just wondering where the user action was so you can update the correct view?

    Yes - in this case I wouldn't switch stuff in and out.

    There's a simple solution which only applies if you've got a one-at-a-time situation - though the applicationState modelling you described has the same problem. So -this won't work if you can have both views waiting for their data at the same time. But - if it's one-at-a-time:

    onRegister():
    {
    view.dataRequestedSignal.add(dataRequestedHandler);
    }

    dataRequestedHandler(payload:SomeType):void
    {
    eventMap.mapListener(eventDispatcher, SomeServiceEvent.DATA_ARRIVED, dataArrivedHandler);
    requestEvent:SomeDataRequestEvent = new SomeDataRequestEvent(SomeDataRequestEvent.DATA_REQUESTED, payload);
    dispatch(requestEvent);
    }

    dataArrivedHandler(event:SomeServiceEvent):void
    {
    eventMap.unmapListener(eventDispatcher, SomeServiceEvent.DATA_ARRIVED, dataArrivedHandler);
    view.update(event.someData);
    }

    Of course the minute viewA and viewB have both requested data at the same time this breaks down.

    Is your service synchronous or asynchronous?

    I'm using a pattern of paired signals for this situation where the data is already arrived and the updates to views are synchronous - eg a 'next' on multiple news story views where the news is a buffered stream of items where there are always several items remaining in the queue to display.

    The mediator for the specific view fires a signal that is mapped to the command that triggers the service.

    The payload of this signal is another signal which is used as the reply channel. This signal is created in the mediator that fires the request, so only it receives the reply.

    I usually use this on synchronous operations, so you might need to pass the reply signal into the service to store it somewhere until it's required - or map it on to a model.

    You could use a token from the service call and map it against a dictionary model that holds the reply signal to keep it all fully decoupled.

    Something in your request command like:

    [Inject]
    public var responseSignal:Signal

    [Inject]
    public var requestData:uint;

    [Inject]
    public var someService:ISomeService

    [Inject]
    public var responseMap:IResponseMap

    execute():void
    {
    var responseToken:Object = someService.loadData(requestData);
    responseMap.mapResponseToToken(responseToken, responseSignal);
    }

    Something in your service like

    private var responseTokenByLoader:Dictionary;

    public function loadData(itemNumber:uint):Object
    {
    var responseToken:Object = createLoaderAndLoadData(itemNumber);

    return responseToken;
    }

    protected function createLoaderAndLoadData(itemNumber:uint):Object
    {
    var urlLoader:URLLoader = new URLLoader();
    var responseToken:Object = new Object();
    responseTokenByLoader[urlLoader] = responseToken;

    // do loading...

    return responseToken;
     
    }

    protected function loadCompleteHandler(e:Event):void
    {
    var responseToken:Object = responseTokenByLoader(e.target);

    var payloadData:DataVO = processLoadedData(e.target.data);

    var evt:SomeServiceEvent = new SomeServiceEvent(SomeServiceEvent.DATA_ARRIVED, responseToken, payloadData);
    }

    Then - the SomeServiceEvent.DATA_ARRIVED is mapped to a command like:

    [Inject]
    public var responseMap:IResponseMap

    [Inject]
    public var someServiceEvent:SomeServiceEvent;

    execute():void
    {
    var responseSignal:Signal = responseMap.getResponseToToken(someServiceEvent.responseToken);
    responseSignal.dispatch(someServiceEvent.payloadData);
    }

    So - what do you think?

    You could strongly type those responseTokens but as they're being used as dictionary keys it's not really necessary. I probably would because I like to use classes to show intent.

    As far as I can tell, everything is pretty decoupled. If you preferred, you could create the responseToken outside the service and pass it to the loadData() instead of getting it back.

    It's robust to multiple calls, and it doesn't have a single 'if'...

    Could be over-engineered for your purposes though!

  3. 4 Posted by rickcr on 04 Nov, 2010 02:50 PM

    rickcr's Avatar

    Thanks for the detailed post. I'm going to have to study this more to follow it exactly. I haven't used Signals yet within my app.

    You mention above that you are using the above for a synchronous operation. Does the above work well for async operations as well? My main concern is async operations (service call returns data) where I only want a particular view responding to a system event that was fired from a service call result handler (but there is obviously more than one view/mediator existing listening for the same event.)

    I just found it a bit cleaner to have the event handler in the mediator check against a "what state is the app in" verses checking some kind of unique token passed along in the Event. Maybe Signals has a cool way to deal with all this that I should look into.

  4. 5 Posted by Stray on 04 Nov, 2010 03:39 PM

    Stray's Avatar

    That approach - the complex part of it with the token - is for async - for synchronous you don't need the token and so on because you just get the data from the model right there in the command and then fire it back in the return signal.

    You still didn't answer the key question: can more than one request for data be happening at a time?

    Consider what happens if viewA asks for data, then viewB asks for data while the viewA async process is still running... are those calls processed parallel or serial?

    In my experience the if() stuff when handling events is definitely a 'bad solution' smell. If your data calls are serial then the timely registering of listeners is a much better solution. If your data calls are parallel then your if() solution is going to come unstuck anyway.

    So - the solution I outlined would only be needed (or something similar) if you have multiple simultaneous requests running.

    You could shorten the chain by using anonymous functions as your listeners in your service, and passing the responseSignal into the service when you call the load:

    public function loadData(itemID:uint, responseSignal:Signal):void
    {

    var completeHandler:Function = function(e:Event):void
    {
    var payload:DataVO = processData(e.target.data);
    responseSignal.dispatch(payload);
    }

    var urlLoader:URLLoader = new URLLoader();
    urlLoader.addEventListener(Event.COMPLETE, completeHandler);

    }

    It's all a matter of how much you want to edit your service to fit the view's peculiarities. If you don't want to introduce signals to your service then the method I suggested before lets you add that layer on top of the conventional event approach.

    Signals are a mechanism for using strongly typed handlers. In some ways it's more reminiscent of how we coded in AS1 / AS2, but with much better type checking etc.

    Anyway - think about the parallel / serial question and then go from there.

  5. 6 Posted by rickcr on 04 Nov, 2010 04:36 PM

    rickcr's Avatar

    You still didn't answer the key question: can more than one request for data be happening at a time?

    Consider what happens if viewA asks for data, then viewB asks for data while the viewA async process is still running... are those calls processed parallel or serial?

    Those requests for data will not happen at the same time since once one request is made, I make the app modal with a spinner showing, since I definitely don't want the user doing anything else until the server call returns. So from a practical standpoint I'm not concerned about the calls ever happening in parallel.

  6. 7 Posted by Stray on 04 Nov, 2010 04:49 PM

    Stray's Avatar

    Great - in that case no need for the application state or if() stuff at all.

    Just add your listener when the mediator dispatches the request - eg:

    onRegister():void
    {
    eventMap.mapListener(view, SomeEvent.SUBMIT_CLICKED, submitClickedHandler);
    }

    private function submitClickedHandler(e:SomeEvent):void
    {
    eventMap.mapListener(eventDispatcher, DataServiceEvent.DATA_ARRIVED, dataArrivedHandler);
    var requestEvent:DataRequestEvent = new DataRequestEvent(DataRequestEvent.DATA_LOAD_REQUESTED);
    dispatch(requestEvent);
    }

    private function dataArrivedHandler(e:DataServiceEvent):void
    {
    eventMap.unmapListener(eventDispatcher, DatatServiceEvent.DATA_ARRIVED, dataArrivedHandler);
    // do stuff with your data...
    }

  7. 8 Posted by rickcr on 04 Nov, 2010 05:32 PM

    rickcr's Avatar

    Doh! That is one kick ass idea! I should have thought of that. Map and unmap the listener!

    You need to add that tip to a wiki or FAQ (or I will if I find the correct place.) It seems so obvious now but it didn't dawn on me while I was coding.

    THANKS

  8. 9 Posted by Stray on 04 Nov, 2010 05:46 PM

    Stray's Avatar

    Awesome! Sorry that we went around the houses a bit - I'd assumed it must be a concurrent loading problem ... nice to have a neat simple solution. Always treat those ifs in event handlers as a sign that something should be different!

    You're right - it's a good one to add to the FAQ - please go ahead and add it (you can fork the wiki on github).

    Stray

  9. 10 Posted by rickcr on 04 Nov, 2010 06:13 PM

    rickcr's Avatar

    No problem going around a bit. I'm sure the concurrent issue will come at some point also so I want to take a look more closely when I have some time at the code above (and Signals.)

    I will add this to the wiki at some point, because I actually see the question come up a lot on the forums about 'how to address making sure the correct view is notified in a mediator event handler (when a system even fires and you might have a few instances of the view/mediator.) ' The register/unregister of the listener is a great approach because I think 'in most cases' people are typically dealing with what they want as a synchronous event - it just so happens to be async because of the remote service call (I still wish they'd just provide a sync service call option out of the box:)

  10. 11 Posted by rickcr on 04 Nov, 2010 06:29 PM

    rickcr's Avatar

    crap. I got too excited too soon.

    I looked closer at my code and I can't bind the listener to a view event since the mediator is notified by a system event (it's from a click event on a tree on a different component(s)

  11. Support Staff 12 Posted by creynders on 04 Nov, 2010 06:46 PM

    creynders's Avatar

    I looked closer at my code and I can't bind the listener to a view event since the mediator is notified by a system event

    That shouldn't make any difference, whether you bind to a system event or a view event. Maybe I don't understand what the problem exactly is. Can you provide an example?

  12. 13 Posted by Stray on 04 Nov, 2010 06:52 PM

    Stray's Avatar

    Hi Camille ... you need to read the whole thread (or at least the last few posts) to get the picture.

    Rick - I'll have a think.

  13. 14 Posted by Stray on 04 Nov, 2010 08:45 PM

    Stray's Avatar

    Are the trigger and the recipient related in any way at all?

    Are the two different triggers dispatching identical events to kick the service off, or are they different?

    How does the service / command / whatever know which data to load?

    How do *you* know which view should display the results in the end?

  14. 15 Posted by rickcr on 04 Nov, 2010 11:40 PM

    rickcr's Avatar

    Are the trigger and the recipient related in any way at all?

    Not sure what you mean by related.

    Are the two different triggers dispatching identical events to kick the service off, or are they different?

    They currently dispatch the same event, but I certainly could make them different events if that helps.

    How does the service / command / whatever know which data to load?

    The data to load is the same based on an id passed in the event. For simplicity imagine it was a list of 'car models' after you select the car make. The service is the same - it returns models based on the makeID. The view that displays the models is used on different views so you want to make sure the correct view is updated with models.

    How do you know which view should display the results in the end?

    Well, this goes back to the beginning. I've tackled it two different ways.

    In solution 1: The view knows which one should display the results because the initial event trigger passes along in the event a "viewType" that gets passed along. So then in the result handler it checks the event.viewType with the viewType of the view its mediating.

    The new solution: an appState is set on a model and that is what is checked vs the view (no need to pass along an id through the event)...(viewState in view is just a string passed into the view when it was created.)

    if ( AppModel.currentState == view.viewState) { //ok update this view

    From my initial post:

    Currently, I'm accomplishing this by:

    1) ReusableView has a field called 'viewType'

    2) when ResuableView is declared on a view it passes it the view type. eg:

    3) The custom event for kicking off the service call also has to take a "viewType" and consequently you have to make sure this viewType is passed back when your service calls returns (which is annoying right there since you have to set the type on an AysncToken or Promise and then be sure to pull it off in the result handler to send back.)

    4) ResuableView's mediator event handler has to then do a check to make sure its updating the correct view:

    if ( event.viewType = view.viewType) //ok update this resuableView

  15. Stray closed this discussion on 13 Feb, 2011 03:05 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