I spent the last week learning FW/1 (you can see my quick review here) and while - in general - it was pretty easy - one thing was a bit confusing for me. I had a hard time wrapping my head around the logic of:
- User submits a form.
- Controller checks the form.
- If bad, we go back to the form.
- If good, we go to some other view.
That's not complex, but as I said, it was tricky for me to get it working with FW/1. In a Model-Glue application I'd post to an event that runs one controller method. That method would do it's validation and then either throw a bad result or fire off a service method and use a good result. This logic doesn't work the same in FW/1. Thanks go to Sean Corfield for explaining why and hopefully this blog entry will make it easier for others.
First off - what happens if you use a simple controller method? Imagine you had code like so:
<cffunction name="addComment" output="false">
<cfargument name="rc" type="struct" required="true">
<cfparam name="rc.comment_name" default="">
<cfparam name="rc.comment_email" default="">
<cfparam name="rc.comment_comments" default="">
<cfset rc.errors = []>
<cfif not len(trim(rc.comment_name))>
<cfset arrayAppend(rc.errors, "You must include a name.")>
</cfif>
<cfif not len(trim(rc.comment_email)) or not isValid("email", rc.comment_email)>
<cfset arrayAppend(rc.errors, "You must include a valid email.")>
</cfif>
<cfif not len(trim(rc.comment_comments))>
<cfset arrayAppend(rc.errors, "You must include your comments.")>
</cfif>
<cfif arrayLen(rc.errors)>
<cfset variables.fw.redirect("blog.entry", "id,comment_name,comment_email,comment_website,comment_comments,errors")>
<cfelse>
<cfset variables.fw.service("blog.addcomment", "data")>
<cfset variables.fw.redirect("blog.entry", "id")>
</cfif>
</cffunction>
This is very similar to how I'd have written it in Model-Glue. Get the values - check em - and either create a bad or good result. Obviously this didn't work. When I tested with errors it worked fine. When I tested with a good form, the redirect work but not the service call. There are a couple of things wrong here.
First - I had forgotten that FW/1 will run my service call automatically. I didn't need to run the call. Secondly - as is described in the Designing Controllers section of the FW/1 docs, there is an implicit set of controller events run on every request. This is more than just the controller method with the same name as the event ("addComment"). It includes both a pre and post invocation as well. What this means is that I can use the implicit calls, in this case, startAddComment and endAddComment, to handle 'wrapping' my service call. From my previous example application, here is the rewritten logic:
<cffunction name="startAddComment" output="false">
<cfargument name="rc" type="struct" required="true">
<cfparam name="rc.comment_name" default="">
<cfparam name="rc.comment_email" default="">
<cfparam name="rc.comment_comments" default="">
<cfset rc.errors = []>
<cfif not len(trim(rc.comment_name))>
<cfset arrayAppend(rc.errors, "You must include a name.")>
</cfif>
<cfif not len(trim(rc.comment_email)) or not isValid("email", rc.comment_email)>
<cfset arrayAppend(rc.errors, "You must include a valid email.")>
</cfif>
<cfif not len(trim(rc.comment_comments))>
<cfset arrayAppend(rc.errors, "You must include your comments.")>
</cfif>
<cfif arrayLen(rc.errors)>
<cfset variables.fw.redirect("blog.entry", "id,comment_name,comment_email,comment_website,comment_comments,errors")>
</cfif>
</cffunction>
<cffunction name="endAddComment" output="false">
<cfargument name="rc" type="struct" required="true">
<cfset variables.fw.redirect("blog.entry", "id")>
</cffunction>
Notice - I don't even have an addComment controller method. Rather - I run code both in the beginning and end of my event. The service call is implicit. The error handling in onAddComment, specifically the redrect, will handle exiting out of the current process on error. Otherwise, things proceed normally with the service call and endAddComment running automatically.
Makes sense once you see it I guess but it means that your controller code has to be a bit... I don't know. I won't say more complex, since you get to leave a lot out - but coming from Model-Glue, you definitely have to think... different about your controllers.
Archived Comments
Thanks a lot for that Ray, it is definitely not obvious the first time you do it. I'm a huge fan of FW/1 in theory and will be building my first app using it soon, so I am keeping up with docs and posts until I do and this was really helpful.
I use MG almost exclusively and find the "convention over configuration" philosophy of FW1 takes a little getting used to. I am beginning to appreciate FW1 and how fast you can get a skeleton app up and going though. FW1 subsystems have a lot a promise and will probably make this a very extensible application in the future.
Your code omits the declaration of "rc" as an argument to the controller functions. A user on the FW/1 mailing list just followed your code and they are not seeing all the rc data preserved on the redirect (because your cfparam sets it as a component variables scope entry instead of part of the argument).
I'll fix that up tonight and update the blog entry (and download). Thanks Sean.
<cfargument name="rc" type="struct" required="true"> is missing from your functions in your code examples.
I forgot to fix it last time @Sean commented on it. Added.
I'm late to the party here but have a question. And, I've used fusebox quite a bit when building CF apps (as opposed to MG or CB) and so am coming from that mindset - or at least how I've structured this sort of thing within my fusebox apps.
When you post a form, and run into errors in your form handler, and thus want to present that form back to the user, isn't the cleanest thing to do to "hijack" the view rendered rather than use a redirect? If you redirect, you obviously have to persist (or pass) all of the relevant data. Seems much more efficient and easier to code for if you simply re-render the same view that they saw when they posted the form.
So instead of...
<cfset variables.fw.redirect("blog.entry", "id,comment_name,comment_email,comment_website,comment_comments,errors")>
You'd use...
<cfset variables.fw.setview("blog.entry")>
...and just use the existing data (in the fc struct) to round things out without needing to make another round trip to the server and persist data that you already have available.
Is this not a valid approach?
@Dane, yes, you can do that but then the URL in the browser is the form-processing action and if the user then reloads the page you may get a blank form submission (depending on the browser) which may not be a good situation. With the redirect, if the user reloads the page, they'll get the form again, pre-populated if appropriate. It's an edge case and therefore a trade off: some people prefer setView() - and that's why it was added - and some people prefer the redirect.
Thanks Sean, appreciate the feedback.