Wanting to use DRY priciples, wondering on best practice for using resusable components...

rickcr's Avatar

rickcr

26 Apr, 2010 03:19 PM

I'm still a Flex newb, and I apologize up-front if I phrase this poorly, and this isn't probably a question specific to RL, but possibly RobotLegs helps address my concerns.

To try to sum up the concern: How do you best handle the situation when you have multiple views of the same type in existence but when you fire off system events you only want the specific view's mediator instance to handle the system event (even though the mediator type is the same across the common views?)

To try it break it down into a really dumbed down example:

  • You have a "Sports" view page that contains two generic subviews each with a generic view - one subview of "baseball" players the other subview of "hockey" players (subview is type "PlayersComponent" with a corresponding "PlayersMediator.)
  • When the user clicks 'add a new player' in a sub view, it fires off the getPlayers chain being passed in a "sportsTypeID" and it calls the service which returns the players from the db.
  • When the service returns it fires a system event that needs to be picked up the appropriate subView's mediator (it's more than just a simple update the bound model of players.)

The issue is that since it's a systemEvent (let's call it "PlayersRetrievedEvent") it will get picked up by both the mediator of the hockey players view and the mediator of the baseball players view (since it's the same type of Mediator.)

The easy way "I think" to make sure only the appropriate mediator instance handles the event is to make sure look for a particular id - in this example proposed I could pass back the "sportsType" ID in the event and in the mediator:

 onPlayersRetrieved(event:PlayersRetrievedEvent) {
    if (event.sportsTypeID != this.sportsTypeID ) return;
    ...//ok to proceed we our handling in the correct mediator
 }

Is that approach ok? It seems sort of hackish but maybe it's common to handle it as such (I'm still pretty green to RIA programming.) I know this situation has to be quite common, I just haven't found the best way to handle it.

  1. Support Staff 1 Posted by Joel Hooks on 26 Apr, 2010 05:59 PM

    Joel Hooks's Avatar

    It isn't hackish, that is proper.

  2. 2 Posted by rob on 28 Apr, 2010 03:41 PM

    rob's Avatar

    This is a situation which I feel like pureMVC attempted to address with its Notification's type property. If Im not mistaken your Mediator would register to listen for a particular Notification and then it could check that Notification's type to see if it should actually respond. Essentially sending along an
    ID in RobotLegs is the same thing I would guess.

  3. 3 Posted by Nikos on 20 Aug, 2010 11:38 AM

    Nikos 's Avatar

    How are your finding this approach, is it working for you?

  4. 4 Posted by rickcr on 05 Oct, 2010 03:36 PM

    rickcr's Avatar

    Sorry, finally getting around to getting caught up with RL list stuff...

    Nikos, not sure if you were asking me or Rob how it's working out, but in my code doing the check for an ID in the event (as shown in my original post) is working out just fine.

  5. 5 Posted by squeedee on 05 Oct, 2010 06:05 PM

    squeedee's Avatar

    I wish we could vote up like this was StackOverflow. This is a very common and easy 'doubt' to have at the start.

    ++

  6. 6 Posted by rickcr on 28 Oct, 2010 06:09 PM

    rickcr's Avatar

    Actually I'm still not happy with this approach to much and I should have changed the subject since it's not just tied to the components but the services. I tried to sum it up below. In this example notice how viewType is passed though all the layers. ...

    For sake of discussion, you have a lot of DIFFERENT views that might have a dropdown of company names. You select a company name and you then need to get a list of employees based on the company selected.

    How do you properly notify the correct view's mediator (or model injected into that mediator) of the new list of employees?

    I'd love it if the service class that retrieves the employees could be generic in the sense that it doesn't need to concern itself with 'who' called it and 'who' it needs to notify when its done, but I'm a bit stumped how to best set this up?

    Right now, what I have seems very ugly - I end up setting some field on an event like "viewType" that's initiated all the way in the view and ends up having to be passed along everywhere so that when the service is complete it can dispatch a 'complete' event that has this 'viewType' in it. Then all the mediators listening check to see if the viewType matches what they are concerned with.

    Pseudo code:

    //viewA mediator

    dispatch(new RetrieveEmployees(RetrieveEmployeesEvent.RETRIEVE_EMPLOYEES, companyID, viewType));

    //RetrieveEmployeesCommand

    service.retrieveEmployees(companyID, viewType);

    //Service class

    [Inject] public var employeesModel:EmployeesModel;
    
    function retrieveEmployees(companyID, viewType):void
        var responder:Responder = new Responder(handleServiceResult, handleServiceFault);
        var token:AsyncToken = service.send(parameters);
        token.addResponder(responder);
        token.viewType = viewType
    
    
    function handleReturnedResults:
            employeesModel.employees = event.result
            dispatch(new EmployeesReturnedEvent(EmployeesReturnedEvent.EMPLOYEES_RETURNED, viewType);

    //viewA mediator listener

    onEmployeesReturned(event:EmployeesReturnedEvent):void {
       if (event.viewType == view.viewType) {
           view.employees = employeesModel.employees;
        }
    }

    Notice how that ugly "viewType" is passed all around in so many places. Is this a normal approach. It just seems so ugly. I don't think the service class should care "who called" it. Is there a better approach to take?

    Thanks for any tips

  7. Support Staff 7 Posted by Shaun Smith on 28 Oct, 2010 07:33 PM

    Shaun Smith's Avatar

    If something feels ugly/messy/painful, then it probably is. De-coupling is only good when it helps, and can be taken too far.

    dispatch(new RetrieveEmployees(RetrieveEmployeesEvent.RETRIEVE_EMPLOYEES, companyID, viewType));

    Don't use events to "drive" actions. An Event should be used to inform the system that something has happened, not to cause something to happen.

    RetrieveEmployeesCommand

    I also hate commands that do nothing more than call a single method.

    What you've got there is a messy contract - essentially an obfuscated method call. What is it you want to do? "Load a list of companies into a view". The contract could be much simpler (pseudo code):

    // Mediator
    onRegister():
      view.companies = companyService.getCompanies().result;
      view.companySelected.add(onCompanySelected); // Signal, but could be event driven
    
    onCompanySelected(companyId:String)
      view.employees = employeeService.getEmployees(companyId).result;
    

    Of course, I'm assuming that you use binding and that you have a mechanism (like an Operation or Promise) that allows you to bind to the result before it has returned. If not, things would be a little more verbose (more handlers) but not by much.

    This would be a fairly tightly coupled solution, but that may not be a problem. Does any other part of the system care that you've loaded some employees? If not, then why dispatch a system level event?

    You might choose to return an AsyncToken directly from the service call. That might be OK, so long as you don't need to parse/manipulate the results first. I always have at least 1 layer that transforms data before handing over to a view. I return Promises from my service calls, so I can do things like this:

    view.data = service.getUser(userId)
      .addResultHandler(onUserResult)
      .addErrorHandler(onUserError)
      .result;
    

    If you want to play around with that approach you can try an extension I'm working on called "Oil":

    http://github.com/darscan/robotlegs-extensions-Oil

    EDIT

    Sorry, was half-asleep. You can't bind directly to the result itself in the fashion outlined above. You have to bind through the promise. For example:

    <fx:Declarations>
        <async:Promise id="user"/>
    </fx:Declarations>
    <s:DataGroup dataProvider="{promise.result}" />

    and then

    view.user = service.getUser(userId);
  8. 8 Posted by rickcr on 28 Oct, 2010 08:49 PM

    rickcr's Avatar

    Very interesting. A few comments:

    1) Wow. I thought the general pattern was that your Commands should call services? which is why I started doing that (even though I often thought so super verbose since I ended up with extra Events all just so I could proxy through a command to a Service.) Not forcing all my service calls to go though a Command (and using an Event) would DRASTICALLY clean things up.

    2) The main issue is that my Services are decoupled and that I haven't seen a lot of examples describing using Promises or Operations so not sure exactly how to set them up (but I will check out that extension.) When you mention "If not [using Promises,Operations], things would be a little more verbose (more handlers) but not by much" - I'm curious how'd you do it?

    I'm calling a system event from my service class since I wasn't sure how to get around the async issue (and the fact that I was using the pattern that all my Service calls go through a Command.) The problem is since my service is async, I can't just do this (as it stands now) from my mediator:

    view.companies = companyService.getCompanies().result

    If I use the AsyncToken approach would it work something like this (I'll be testing this shortly but want to get this email out quickly in case you or someone gets to it today since I want to bust a move and get this prototype finished:)

    //In Mediator

    view.companies = companyService.getCompanies().result

    //CompanyService

    function getCompanies():Object {
            var service:HTTPService = new HTTPService();
            //...
            var responder:Responder = new Responder(handleResult, handleServiceFault);
            var asyncToken:AsyncToken = service.send(parameters);
            asyncToken.addResponder(responder);
            asyncToken.myResult = null;
            return asyncToken.myResult;
    }
    
    function handlResult(event:ResultEvent):void {
        var token:AsyncToken = event.token;
        token.myResult = event.result;
    }

    Wow if something like the above worked that would reduce sooo much clutter. (not sure I could use AysncToken just as I described though, I'll have to find out.) (It makes me think I'd be doing something wrong though since l'd sure be reducing a lot of my Commands by just calling services directly from my mediator.)

  9. 9 Posted by rickcr on 28 Oct, 2010 09:16 PM

    rickcr's Avatar

    Ok I think I answered the first question:

    "When you mention "If not [using Promises,Operations], things would be a little more verbose (more handlers) but not by much" - I'm curious how'd you do it?"

    I ended up passing a result handler from my mediator into the service. Not sure if this is the proper approach so would appreciate knowing if it is acceptable. It seems to work and does clean things up a LOT, I just hadn't seen many of the examples calling services directly from Mediators.

    //mediator onRegister

    var xmlList:XMLList = XMLList(service.getStuff(handleServiceResult));

    //mediator handler

    public function handleServiceResult(event:ResultEvent):void {
        var xml:XML = event.result as XML;
        view.reportsXMLCollection.source = new XMLList(xml);
    }

    //service class method

    public function findReportsForAssociate(handler:Function):void {
        var parameters:Object = new Object();
        parameters._eventName = "findStuff";
        var service:HTTPService = new HTTPService();
        service.url = "theURL";
        service.method = "POST";
        service.resultFormat = "e4x";
        var responder:Responder = new Responder(handler, handleServiceFault);
        var token:AsyncToken = service.send(parameters);
        token.addResponder(responder);
    }
  10. Support Staff 10 Posted by Shaun Smith on 28 Oct, 2010 09:27 PM

    Shaun Smith's Avatar

    You definitely shouldn't be parsing results in your mediator!

  11. Support Staff 11 Posted by Shaun Smith on 28 Oct, 2010 09:27 PM

    Shaun Smith's Avatar

    Sorry, was half-asleep earlier. You can't bind directly to the result itself in the fashion outlined above. You have to bind through the promise. For example:

    <fx:Declarations>
        <async:Promise id="user"/>
    </fx:Declarations>
    <s:DataGroup dataProvider="{user.result}" />

    and then

    view.user = service.getUser(userId);
  12. 12 Posted by rickcr on 28 Oct, 2010 09:47 PM

    rickcr's Avatar

    Before I go down the async Promise route can you recommend an approach related to ""If not [using Promises,Operations], things would be a little more verbose (more handlers) but not by much"

    You definitely shouldn't be parsing results in your mediator!

    Should I not be defining a handler in my Mediator and then passing to my service? or do mean just pass off that parsing to a helper class or something?

    thanks for all your help so far.

  13. Support Staff 13 Posted by Shaun Smith on 28 Oct, 2010 10:41 PM

    Shaun Smith's Avatar

    No probs. Just beware, I'm quite sleepy.. :)

    Should I not be defining a handler in my Mediator and then passing to my service? or do mean just pass off that parsing to a helper class or something?

    The handler approach is ok, but be sure to parse the results into a suitable format before passing back to the handler. In other words, don't send the raw results straight to the handler, parse them in the service (or preferably, a helper passed to the service) first.

    The problem with your handler approach is that you have to pass the handler through the service method call. Not cool - it muddies the service's api. Also you can only have one callback. And what about error callbacks? And progress etc.

    It might be better to return the AsyncToken itself, but to add a responder to it in the service that processes the results first.

    If not [using Promises,Operations], things would be a little more verbose (more handlers) but not by much

    What I meant was that you'd end up with something like this:

    // Mediator
    onRegister():
      companyService.getCompanies()
        .addResponder(
          new Responder(resultHandler, faultHandler)
        );
    

    My Promise impl is very similar conceptually to an AsyncToken, but, in my opinion, reads better (and has other benefits):

    // Token approach:
    companyService.getCompanies()
      .addResponder(
        new Responder(resultHandler, faultHandler)
      );
    
    // Promise approach
    companyService.getCompanies()
      .addResultHandler(resultHandler)
      .addErrorHandler(faultHandler)
      .addProgressHandler(progressHandler);
    

    Have a quick look at the Promise source:

    http://github.com/darscan/robotlegs-extensions-Oil/blob/master/src/...

    Adding a handler simply pushes it onto a stack. Handlers are passed a reference to the Promise when called.

    I've just updated the docs: http://github.com/darscan/robotlegs-extensions-Oil

  14. 14 Posted by rickcr on 28 Oct, 2010 11:52 PM

    rickcr's Avatar

    Thanks for all your help especially while tired:). It's helping me learn a lot. I downloaded the Oil extensions and looked some at the docs on the homepage. Looks very cool although I'm not exactly sure how'd I'd implement it with the HttpService call which returns an AsyncToken right now from the send operation:

    var token:AsyncToken = service.send(parameters);

    Until I get a firm handle on your Promise implementation with an HttpService call, I took your suggestion about removing the handler argument being passed to the service call and instead am having the service call return the AsyncToken directly.

    Since you all mentioned not processing in the Mediator, I took this approach for now and want to run it by you.

    //mediator onRegister

    service.getCompanies()
        .addResponder(
            new Responder(handleServiceResult, null)
        );

    //mediator handler

    public function handleServiceResult(event:ResultEvent):void {
        view.companies.source = new XMLList(XML(event.token.xml));
    }

    //In Service class

    public function getCompanies():AsyncToken {
        //... HttpService service set up.. urls, parms, etc
        var responder:Responder = new Responder(
            handleServiceResult, handleServiceFault
        );
        var token:AsyncToken = service.send(parameters);
        token.addResponder(responder);
        return token;
    }

    //Service class also has handlers

    public function handleServiceFault(event:ResultEvent):void {
        //handle 
    }
    
    public function handleServiceResult(event:ResultEvent):void {
        //Does the processing? Is this ok, shove processed result back into token?
        var xml:XML = event.result as XML;
        AsyncToken(event.token).xml  = xml;
    }

    I take it the Responders for an Async token are fired in the order they are added? (Didn't see it mentioned in the docs, but that's how it seems to be working.) I wanted to keep some default handlers like for faults in the service class itself and then users of the service just add their own handler for what they need.

    I wanted to follow what you suggested and not do any processing in the mediator's handler, so I do that first in the service's handler, and then shove it back into the token. (Is that ok?)

  15. Support Staff 15 Posted by Shaun Smith on 29 Oct, 2010 01:27 AM

    Shaun Smith's Avatar

    I take it the Responders for an Async token are fired in the order they are added? (Didn't see it mentioned in the docs, but that's how it seems to be working.)

    Well, that's the thing - it's not guaranteed.

    I'm not exactly sure how I'd implement it with the HttpService

    I've updated the docs. Hopefully it's a little clearer now. If not, let me know.

  16. 16 Posted by rickcr on 29 Oct, 2010 02:00 AM

    rickcr's Avatar

    Not guaranteed. Sheesh that sucks. How is the Async approach mentioned above acceptable then? It looks like I should just use result handler and deal with the processing right there in it (or handing it off to a helper.)

    Re: the docs on the promise. Nice to see you updated them, but man, I feel dumb because I'm completely lost. Where is the reference to "promises" even existing in the handleComplete function? I take it you are keeping a dictionary of them somewhere in the service class that is holding getUser() ?

    protected function handleComplete(event:Event):void
      {
        var loader:URLLoader = event.target as URLLoader;
        var promise:Promise = promises[loader];
        delete promises[loader];
       promise.handleResult(loader.data);

    }

    To be honest, what would really help me is to see how you'd implement the Promise using an HttpService call, which I believe would be a common case. Maybe others understand what's going on with the code as is, so not wanting you to do extra work or anything. I'm lost at this stage.

    Right now I'm frustrated. Returning stuff from a service class shouldn't be this difficult. What the majority of people doing that aren't using your Promise solution? Retuning AsyncToken doesn't seem great either (especially since it's become difficult as heck for me to mock out returning dummy data from my services... I'm totally lost there trying to mock things out. I don't want to mock a full HttpService implementation.)

  17. 17 Posted by rickcr on 29 Oct, 2010 02:02 AM

    rickcr's Avatar

    Ok I'm looking at RestClientBase. This might clear things up http://github.com/darscan/robotlegs-extensions-Oil/blob/master/src/...

  18. 18 Posted by rickcr on 29 Oct, 2010 02:18 AM

    rickcr's Avatar

    sorry I should have scrolled down more in the docs to look at the "Rest" section. It's become much clearer now. I'm trying it out now.

  19. 19 Posted by rickcr on 29 Oct, 2010 03:06 AM

    rickcr's Avatar

    wow. much easier than I thought with your Promise implementation! Here's what I now have:

    //CompanyService

    public function getCompanies():Promise {
        var client:IRestClient = new RestClientBase("urlBase/");
        var promise:Promise = client.get("Company.action?_eventName=find");
        return promise;
    }

    //Mediator

    onRegister:
        companyService.getCompanies().addResultHandler(handleSuccess);
    
    //Handler
    public function handleSuccess(promise:Promise):void {
        var result:Object = promise.result;
    }

    I tested with adding a result handler in the service to the client also and that fired as well... so it it safe to say that your implementation does preserve the order the handlers were added? (If so, awesome.)

    Is there any reason I shouldn't add the dynamic keyword to Promise so that I could have it behave like AsyncToken in regard to allowing me to add arbitrary fields to the Promise so they're available in my handlers? (It seems to work ok but maybe you have a reason for not having it.)

    Thanks so much for your patience with all this! If you have a wiki I'll be happy to give back with some examples how I end up using the Service/Promise implementation.

    (Also do I need to do anything with cleanup or anything? Your "Making Promises' handleComplete in the docs still has me confused.

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