I originally wrote this as a Model-Glue tip, but I believe it applies to frameworks in general. This is something that's bitten me in the rear a few times now, and it's finally sunk in and I thought I should blog it. (Why is it that some lessons needs to be repeated multiple times before we learn?)
Ok, so the basic issue is this. Imagine you have a Model-Glue event called getShips. This fires off a message to get a list of ships that is passed to your view. Each ship has a detail link that goes to a URL like so:
index.cfm?event=ship.display&id=#id#
This event broadcasts a message, getShip, that runs controller code. The controller code grabs the id and then loads the data. Your code may look something like this (and again, if you use some other framework the code would be different).
<cfset id = arguments.event.getValue("id")>
<cfset ship = shipService.getShip(id)>
<cfset arguments.event.setValue("ship", ship)>
Ok, nice and simple, right? Now imagine a second set of events - getPlanets and getPlanet. These work almost the same way. The first event gets a list of planets while the second works as a detail view. You can imagine that URL looking like so:
index.cfm?event=planet.display&id=#id#
So this works all well and good, until the client comes along and asks for a report on ships and their docking history with planets. You now have the need to get both a ship and a planet. You already have existing messages/listeners for getShip and getPlanet, but you have a problem. Both controller functions look for ID as the value that determines which value to load.
Maybe I'm crazy, but this seems very avoidable and yet it's still bitten me a few times now on the last few Model-Glue apps I've worked on. As a workaround, I've used message arguments in Model-Glue. So instead of just doing:
<message name="getShip" />
I'll do:
<message name="getShip">
<argument name="id" value="ship" />
</message>
In my controller, instead of just looking in the Event object for "id", I'll see if the message had an argument passed to it. If so, I'll look for that key as opposed to id. So now I can pass ship=X in the URL (or Form) and it will work just as fine as id=X.
It's a workaround, but the real solution (unless my readers can suggest something better) is to start using better names for my parameters. Instead of ID, I'll uses shipid. Instead of ID, I'll use planetID.
Has anyone else run into this?
Archived Comments
Forgive my ignorance when it comes to Frameworks... but, when you're inside the shipping report controller, wouldn't the other services be called explicitly:
getShip()... getPlanet(), etc?
At this point, can't you pass in the named argument however you want?
If I'm way off base (having not used frameworks much), no need to bother explaining.
I was way too vague in my post. I blame the IPAs.
So don't worry about the service per se, but more the controller.
As with most frameworks (from what I know), MG provides an API to get to URL/Form data in an encapsulated Event argument. So instead of me doing
ship = shipService.getShip(url.id)
I'll do
ship = shipService.getShip(arguments.event.getValue("id"))
In other words: "From the Event, get the ID value, and pass it to the shipService."
The issue is the use of ID. If the controller method, getShip, looks for ID, and the controller method, getPlanet, looks for ID, I couldn't run both of those in the same request.
Does that make sense? I could include more code if it will help.
Sometimes you have to display on the same template more query'es from the same table, i.e. list of articles for different categories: in this case i use to pass an angument specifying the category, and an argument specifying the name list:
<message getQuery>
<argument name="list" value="list1"/>
<argument name="category" value="c1"/>
</message>
<message getQuery>
<argument name="list" value="list2"/>
<argument name="category" value="c2"/>
</message>
in my controller:
<cfset arguments.event.setValue(arguments.getArgument("list1"), myQuery1>
<cfset arguments.event.setValue(arguments.getArgument("list2), myQuery2>
and in my template
<cfset list1 = event.getvalue("list1")
<cfset list1 = event.getvalue("list2")
regards
excuse me
and in my template
<cfset list1 = event.getvalue("list1")
<cfset list2 = event.getvalue("list2")
I've always tried to name my variables like:
planetID
ShipID
That where there is never any confusion about what I'm requesting. And all my handlers know to look for [objectName]ID.
@Tim: Yep, from now on, that's my plan.
+1 Tim's suggestion, works well for me in M-G to always have the key cleanly separated.
So um... you guys did see that that is exactly what I said at the end of my post, right? Just trying to make sure it was obvious. :) It's kind of the whole point of this blog entry.
Another nice thing about using fooID and barID is that you can do automatic validation and authentication control at the start of a request (e.g., for each xxxID, make sure the id is a valid xxx and that you have permission to view that xxx object).
The Broadchoice Workshop / Community Platform does this (for siteId, sectionId and pageId when you are editing).
@Ray,
I'm sorry, I'm a bit dense this morning, but I am not seeing your issue. From the code you just posted, it seems that there would be no issue:
ship = shipService.getShip(arguments.event.getValue("id"))
As long as you know what the URL / FORM values were that you posted, you can't you explicitly get those back from the event object:
ship = shipService.getShip(arguments.event.getValue("ship_id"))
Forgive the format of the reply - on myiphone. The code you showed there grabs an id value from the event scope. This ends up being form.id or url.id. The issue is if two controller methods want to load data and they both want to use ID then who wins? I hope that makes sense as I don't think I can explain it any simpler. :)
@Ray,
But, I thought you won't have two controllers accepting the request? I thought you had one controller accepting the request - the reporting controller - which would then turn around and access the individually ship / planet services (at which point, you could translate the event data to the service method signatures).
It doesn't matter how many controllers you have. That isn't relevant. I think you are overthinking this. :) Let me try again.
Imagine you write a controller method that is responsible to listen for a getShip broadcast. This controller method handles asking the Service for the ship, and passing it back to the view.
Right? So question - how does the controller know _where_ find the value in the event. Is it url.ray? url.foo? No. It's url.id. MG nicely collapses that into the Event structure.
Ok, so if the controller was written to look for ID, what happens when I write getPlanet to work the same way? Normally, no big deal. But if I fire both getShip and getPlanet, and both assume ID is _their_ ID, then we have a problem.
As I said, you may be overthinking this. It's really a small thing, but I've tripped myself up on it now a few times.
@Ray,
I think I see where the disconnect might be coming from; are you having multiple controllers fire for a single page request? I think what I assumed was that you would have ONE controller fire to handle the page request (ex. reporting), which would then, in turn, request Ship and Planet data directly from the appropriate services. It sounds like rather than having one controller talk to several services, you are firing off additional events to retrieve the Ship and Planet data?
At that point, however, wouldn't you be launching new events with a completely new event object? Couldn't you just put the translated ID in the new event object?
Something like:
e = new Event()
e.id = currentEvent.getValue( "ship_id" )
e.execute()
... or however subsequent events get fired in a framework.
I know I am speaking from a point of ignorance, but my gut instinct is just telling me that if you have to start prefixing your variables (ie. ship_id rather than id), then something else is going wrong somewhere.
Ah ok, so no, there is 2 controller methods being run. In MG, when I define an event, I broadcast N messages. For the ship detail display, I'd just broadcast getShip. For planet display, I'd broadcast getPlanet.
Now for the "History of ship docking at planet" event, I could broadcast: getShipAndPlanet. That one controller method could hit the service layer to get details on both. But, why should I? I've already coded support to get details for ships and planets, so imho, it makes sense to simply fire off 2 messages. Also, if my controller is wrapping the call with security, then I'd be reusing the secured call as opposed to having to redo it.
Once I write a getX controller method, I don't want to ever re-build that code. I want to use it in every situation where I need X.
@Ray,
Ok, i see where you're coming from. So, is there any validity behind my idea behind creating a new event object to execute. It seems that the getShip "event" would be completely new event and should have a completely new event object.
It's not a new event, it's a broadcast. Every request is basically one event. The event in my case is GetShipPlanetDockingHistory. The event has various needs (ship detail, planet detail, docking history) and would broadcast messages that the controller layer listens for and handles.
@Ray,
So, are the controllers than handle the page requests the same that handle the "broadcasts"?
I wouldn't say the controller handlers the page request. The controller listens for messages. In Model-Glue, your main XML file (you can have multiple for large, complex apps) defines a relationship between events, messages, and views.
So for shipDetail, I say:
a) I need to broadcast a message to get Ship Details
b) I need to run a view
a) will fire off the message, "getShipDetails". I've defined which controller method will listen for it. The controller method handles the grunt work: "Where is my ID? Call my service. Set the result in the event."
At this point I'd recommend the new MG3 docs. :)
@Ben,
The issue is not about number of controllers per se, although it is about number of service objects. Model-Glue (for example) translates all form and URL variables into the request.event object, but the situation would hold, no matter how you accessed those variables. To simplify, let's stick with the URL scope.
Say there's a request for info on a ship:
index.cfm?event=ship.view&id=22
The controller may then grab data like this:
shipInfo = shipObject.getByID(url.id);
Say there's a request for info on a planet:
index.cfm?event=planet.view&id=47
The controller may then grab data like this:
planetInfo = planetController.getByID(url.id);
Now, following Ray's explanation, there's a request for routes a ship takes between planets. The controller (in Model-Glue) will need to call both objects, but now it can't distinguish between IDs when it needs to call both objects:
shipInfo = shipObject.getByID(url.id);
planetInfo = planetController.getByID(url.id);
Obviously this won't work :-) ...
index.cfm?event=route.view&id=22&id=47
Going back to Ray's original post, the tightest solution is to start using shipID and planetID, simply so that the service calls can always be the same for any event:
index.cfm?event=route.view&shipID=22&planetID=47
shipInfo = shipObject.getByID(url.shipID);
planetInfo = planetController.getByID(url.planetID);
Model-Glue essentially creates an Event object which represents all the information being passed around in the Request, so there is a one-to-one relationship of Request-to-Event, but a controller 'handler' may call many methods across any number of service objects (which M-G calls 'controllers', which is where it gets a bit confusing).
Hmmm. I know I don't know much about the framework, but it seems like a funky flaw not to be able to broadcast data along with the message. Something like how you might in jQuery:
MGApp.trigger({
type: getShip
id: event.getValue( "ship_id" )
)};
I think that would rock nicely and handle your problem. Of course, its a moot point since it doesn't work that way :)
You can actually. Thats what I said above Ben. I said that one of my fixes was to broadcast the _location_ of the PK. Ie:
Broadcast Msg: Get Ship
Broadcast Argument: Hey, look for the ship id in the value "ShipId"
So yes, you can do that. But why bother? If I had just used shipid from the beginning, I wouldn't have to worry about it.
Again, I really think you are overthinking this. :) This is a small little issue - a brain fart on my part - and as I've seen it a few times now I thought I'd share it. It really isn't that complex. :)
How about this - imagine you wrote a custom tag called getShip. It _always_ did caller.name = X. Now imagine a custom tag called getPlanet. It always did caller.name = X. If you ran both of those tags you would have a problem, right? Now the best solution is to pass to the tag the name of the return variable, but I could also simply do caller.shipname=x and caller.planetname=x.
@Ray,
From what you wrote, however, it looks like the id -> shipID definition is a value that is hard-coded int the MG config. Unless I am missing something, I don't think this really solves the problem - it simply shifts it until you have future naming conflicts (although admittedly, shipID, will cut down on that).
With something like jQuery (just trying to create a common language for us since I'm not MG-savvy), you can pass custom event data at run time, which I think is different than what you are talking about right?
I know you're probably exacerbated right now, but something in my gut is telling me that this is not just a small problem. If you look at the ColdFusion language (to touch on your custom tag example), the trend has been to ADD ways for people to define return variables. Even tags that used to create standard structs (ex. CFFILE, CFHTTP) now give us the ability to define "result" variables. I have to assume this was done because hard-coding value associations was creating problems.
... but, that's just my opinion - we've left the technical realm. Perhaps we can call this matter closed :)
It isn't hard coded in the config - it is hard coded in the controller. Whether you use MG or not, you have to get a value from the URL or Form scope, right? You have to look somewhere. My code was looking for ID. That's not too crazy. I think most of us normally use ID for a URL param.
I can make it dynamic with event arguments, and as I said, I did use that as a solution to this problem where the code to look for ID was already in use.
Starting from scratch, next time, I'll just use shipid though.
@Ray,
Yeah, you need to get the data from somewhere :) Perhaps we are saying the same thing?? I think hard-coding in the Controller would be just fine as it would be the controller's job to translate the request data into usable data internally.
Right, so the point of this is - I hard coded it for ID. Which worked fine - in isolation. It didn't work fine when it was run with other controller methods that _also_ wanted to use ID.
@Ray,
Oooh, you hard coded the ID in the callee controller (ship). I thought you meant that you hard-coded the ID in the callER controller (report).
Well, the caller isn't a controller - it's more the XML config file. I've said, for event so and so, call these messages.
I would _really_ recommend taking a look at the MG Quick Start. It will only take you about an hour.
@Ray,
I've been meaning too. I tried like two weeks ago, but then got de-motivated when I had to install ColdSpring... we'll suck it up and try again :)