MVC Form Handling with ColdBox, ColdSpring, and Transfer. Does this seem right?
So I have been working on my first ColdBox application the last couple of week, and one of the things I have been struggling with is Form Handling and Validation. I have not been struggling because these concepts are difficult, but with not knowing how they should be implemented in ColdBox or any other MVC framework.
The heart of my question is, when I want to display a form with values pre-populated (like when a user clicks on an 'edit' button for page in a CMS), do I pass the whole bean to the view for insertion into the input tags using getter and setter methods, or do I use the getter and setter methods in the controller to insert the values into the event/request context object and then using the events getValue and setValue() method in the view?
I have been doing the latter. Here are some examples.
First, the user browses to where they can add/edit a page, by calling the pages.edit event:
http://12robots.com/index.cfm?event=pages.edit&pageid=D2462194-B6AA-163D-B3CA80CBB638FE69
So the 'event' and 'pageid' URL parameters get added to the request context and the request context is passed to the event handler, which looks like this:
<cffunction name="edit" access="public" returntype="void" output="false">
<cfargument name="Event" type="coldbox.system.beans.requestContext">
<cfset var oPage = "" />
<cfset var pageService = getPlugin("ioc").getBean("pageService") />
<cfif Event.valueExists('pageid')>
<!--- Get Transfer Object --->
<cfset oPage = pageService.getPage(event.getValue('pageid')) />
<cfelse>
<cfset oPage = pageService.getPage() />
<cfset oPage.setpageStatus("Active") />
<cfset oPage.setShowOnNav(0)/>
</cfif>
<!--- Param rc values --->
<cfset Event.paramValue('pageid', oPage.getPageId()) />
<cfset Event.paramValue('pageName', oPage.getPageName()) />
<cfset Event.paramValue('pageTitle', oPage.getPageTitle()) />
<cfset Event.paramValue('pageNavName', oPage.getPageNavName()) />
<cfset Event.paramValue('pageStatus', oPage.getPageStatus()) />
<cfset Event.paramValue('showOnNav', oPage.getShowOnNav()) />
<cfset event.setView('pageEdit')>
</cffunction>
Here is what is going on:
- First, var scoped variables are set for the page bean (oPage) and the page service (pageService). The page service receives the pageService object from ColdSpring through the getPlugin() controller method in ColdBox
- Next, I check to see if the pageid was passed in the event/rc object. If it was, I use it to ask the pageService object for a bean populated from the database (the page service uses Transfer for this).
- If a page id was not passed in, then I know that this is a request for a new page, so I call the same method from the page service, but with no param. (This could be done with one function call, but...). I then set default values into the bean for later use in the form.
- After I have a bean prepared, either with values from the database, or with no values except for the defaults I inserted, I then use the Event.paramValue() method in ColdBox to add those values to the Event object. I am using paramValue() instead of setValue() because I do not want the bean to override the values that may have been passed in with the form and added to the event object.
- Finally, I set the view to display the pageEdit.cfm page that has the edit form on it.
Now, in the pageEdit.cfm page, I using this code:
<cfset WriteOutput(getPlugin("messageBox").renderit())>
<cfform name="addPageForm" method="post" preservedata="true">
<cfinput type="text" name="pageid" id="pageid" value="#event.getValue('pageid', '')#" />
<cfinput type="hidden" name="event" value="pages.editPost" />
<cf_label for="pageName" required="true" label="Page Name">
<cfinput type="text" name="pageName" id="pageName" size="35" maxlength="50" value="#event.getValue('pageName', '')#" />
</cf_label>
<cf_label for="pageTitle" required="false" label="Title">
<cfinput type="text" name="pageTitle" id="pageTitle" size="35" maxlength="50" value="#event.getValue('pageTitle', '')#" />
</cf_label>
<cf_label for="pageNavName" required="false" label="Nav Bar Name">
<cfinput type="text" name="pageNavName" id="pageNavName" size="35" maxlength="50" value="#event.getValue('pageNavName', '')#" />
</cf_label>
<cf_label for="pageStatus" required="true" label="Status">
<cfinput type="radio" name="pageStatus" value="Active" checked="#IIf(event.getValue('pageStatus', 'Active') EQ 'Active', '''true''','''false''')#"> Active<br />
<cfinput type="radio" name="pageStatus" value="Inactive" checked="#IIf(event.getValue('pageStatus', 'Active') EQ 'Inactive', '''true''','''false''')#"> Inactive
</cf_label>
<cf_label for="showOnNav" required="true" label="Show on Nav?">
<cfinput type="radio" name="showOnNav" value="1" checked="#IIf(event.getValue('showOnNav', 1) EQ 1, '''true''','''false''')#"> Yes<br />
<cfinput type="radio" name="showOnNav" value="0" checked="#IIf(event.getValue('showOnNav', 1) EQ 0, '''true''','''false''')#"> No
</cf_label>
<cf_label for="btnSubmit">
<cfinput type="submit" name="btnSubmit" value="Add/Edit" />
</cf_label>
</cfform>
Ignore all of the <cf_label> tags. They are a simple custom tag I use for laying out forms.
Here is what is going on:
- The first line will simply display an error message from the request context if I set one. You will see where I set this in the post event coming up
- The form is posting to itself. (Yes Luis, I know I should be using exit handlers. I only just learned what they were 2 days ago and I have not implemented them yet :)
- I am using simple hidden fields to pass in the name of the event (pages.editPost) that I want to handle the form and to pass in the pageid (The pageid will be a UUID if it was an edit request or it will be '00000000-0000-0000-0000000000000000' if it is a request for a new page).
- I then populate each of the form fields, radio buttons, etc using the event.getValue() method from the request context. I am using Iif() in several place to determine which radio button should have checked="true"
So this form gets posted to me pages.editPost event handler. Here is what that looks like.
<cffunction name="editPost" access="public" returntype="void" output="false">
<cfargument name="Event" type="coldbox.system.beans.requestContext">
<!--- Set Local vars --->
<cfset var aErrors = ArrayNew(1) />
<cfset var oPage = "" />
<cfset var pageService = getPlugin("ioc").getBean('pageService') />
<!--- Error checking --->
<cfif NOT Event.valueExists('pageName') or NOT Len(event.getValue('pageName'))>
<cfset ArrayAppend(aErrors, "You must enter a Page Name") />
</cfif>
<cfif ArrayLen(aErrors)>
<cfset getPlugin("messagebox").setMessage("error","Error: ", aErrors)>
<cfelse>
<cfset oPage = pageService.getPage(Event.getValue('pageid')) />
<cfset getPlugin("beanFactory").populateBean(oPage) />
<cfset pageService.save(oPage) />
<cfset getPlugin("messagebox").setMessage("info","The record has been Added/Updated") />
</cfif>
<cfset setNextEvent('pages.edit','',"false",'pageid')>
</cffunction>
- First, I am receiving the event argument(request context) as I would for any event handler. This object will contain all of my form field values that were posted.
- I set var scoped variables for an error array(aErrors), a page bean(oPage), and the page service(pageService)
- I do my error checking. For simplicity I have made pageName the only required value. I check to make sure it exists and contains a value. If it does not pass error checking, an error is set using the messagebox plugin and the next event is set.
- If the form passes error checking then we move on to the form processing.
- The pageService is used to request the page bean, the pageid is passed in. The pageService will determine if a new bean should be created or if it should get the values from the DB.
- Using the beanFactory plugin in ColdBox I populate the bean from the values posted in the form.
- Then I pass the page bean to the page service to be saved. It will use Transfer to determine if it should INSERT a new record in the DB or UPDATE an existing record.
- If all goes well (usually I would use a try/catch here) then I set a messagebox value saying the request was completed.
- Finally, we set the next event to go back to the edit form so that the user can make additional edits and so that the message (or errors) can be displayed. This setNextEvent() method is telling the controller that the next event is 'pages.edit, that there is no query string, that it should not append the session token, and that it should persists the pageid value so that the pages.edit handler can load the correct bean.
Wow! That was a lot of typing. I hope someone takes the time to read it and correct me if I am doing something foolish, wrong, or at least not inline with best practices. I am really enjoying working with ColdBox, ColdSpring and Transfer. I have no doubt this would be just as thrilling in any other set of frameworks.
If you have made it this far, thanks for taking the time. I would love to hear feedback.






1. Don't post the form back to itself (yeah, I realize you already mentioned that)
2. Push your validation into your business objects (you'll want Transfer Decorators for this). The more complex the validation becomes, the happier you'll be about that.
3. You might want to add a getter/setter on your controller (handler) for your PageService object, and then add autowire="true" to the <cfcomponent> tag. This tells ColdBox to autowire the dependencies. As long as you have the bean defined in your ColdSpring config file, and have the autowire interceptor defined in your ColdBox.xml, you'll be rockin and rollin.
The whole OO thing is rather vast, and the learning curve is steep, but worth it. It's one of those things that every time you think you have everything figured out, you realize there's another degree of awesomeness to it. It's a fun journey. Don't be deterred! :-)
I agree with everything that Matt said, and had a couple of things to add.
Further to Matt's suggestion #2, I'd encourage you to look at moving some of the logic that is currently in your controller into your service. I definitely agree that it makes sense to move the validations into the business objects, and I'd also look at moving some of the logic from page.edit into pageservice.getpage().
This next item isn't really a suggestion of something that's "better", just another way of doing it: I too started by approaching this problem the way that you've described. I was using my business object to populate a struct (in my case) and then that struct was used to populate my form fields. This allowed me to redisplay the values submitted by the user, without having to introduce any conditional code in my view.
I changed my approach awhile ago, however, and now I just use the business object itself in the view, so each form field is populated via getters. I originally felt that there was value in introducing a layer of abstraction between my view and my business object, but I was convinced otherwise. The only problem I encountered with this approach was what to do about invalid values submitted by the user, as those cannot be "forced" into the business object. For example, a user enters "ABC" into a price field that is numeric. I came up with a workaround to that which I blogged about a few months ago.
So in my mind there's nothing wrong with your approach and it definitely addresses the problem in a creative way.
<input type="text" name="FirstName" value="#User.getFirstName()#">
@Bob, personally I don't type the properties of my business objects so if you want to User.setDateofBirth("weird string"), you can do so. It's only when you call User.isValid() (or in more complax apps, User.IsValid#State#() - e.g. User.IsValidEmailSubscriber()) that it runs the validations to see whether the values are well formed. I find that approach allows for a simpler architecture as my controllers can just load the appropriate properties from any input scope (form, URL, etc.) into the appropriate bean and I can encapsulate the validation fairly easily.
It's a greay system but I have my own little data mapper that solves a much simpler subset of the ORM problem but that meets all of my practical needs. I'll be doign an article on it in FAQU shortly!