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:

  1. 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
  2. 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).
  3. 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.
  4. 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.
  5. 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:

  1. 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
  2. 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 :)
  3. 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).
  4. 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>

  1. 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.
  2. I set var scoped variables for an error array(aErrors), a page bean(oPage), and the page service(pageService)
  3. 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.
  4. If the form passes error checking then we move on to the form processing.
  5. 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.
  6. Using the beanFactory plugin in ColdBox I populate the bean from the values posted in the form.
  7. 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.
  8. If all goes well (usually I would use a try/catch here) then I set a messagebox value saying the request was completed.
  9. 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.

Comments
Matt Quackenbush's Gravatar Jason, thanks for the post. Nice work. :-) You asked 'does this seem right?', and so I figured I'd answer. In my opinion, there's nothing inherently "wrong" with what you're doing here. You could (of course) do a few things differently, but that doesn't necessarily make them right or wrong.

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! :-)
# Posted By Matt Quackenbush | 7/7/08 2:34 AM
Jason Dean's Gravatar @Matt - Thanks for taking the time. I appreciate it. That all sounds like good advice. I will try those things out along with some additional advice I received form Mark Mandel and Sean Corfield last night in #coldfusion on DalNET. I think I will make the changes and repost the article in a week or two. This has been a great learning experience.
# Posted By Jason Dean | 7/7/08 7:28 AM
Bob Silverberg's Gravatar Jason, thanks for the great post. It's interesting to see that others are struggling with the same issues as me.

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.
# Posted By Bob Silverberg | 7/7/08 11:45 AM
Jason Dean's Gravatar @Bob, thanks for the post! Actually, your advice is almost exactly the same as the advice I received from Mark Mandel and Sean Corfield in #ColdFusion. It's nice to see another that agrees. Thanks for taking the time to post your thoughts. I hope others can benefit from these discussions.
# Posted By Jason Dean | 7/7/08 11:54 AM
Peter Bell's Gravatar Just a +1 to Bob/Sean/Marks advice to just pass the bean into the form so you can do stuff like:

&lt;input type="text" name="FirstName" value="#User.getFirstName()#"&gt;

@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.
# Posted By Peter Bell | 7/7/08 12:11 PM
Bob Silverberg's Gravatar @Peter, I like that idea, but my Business Objects _are_ Transfer Objects, so I don't have that option. I suppose I could add a layer of abstraction around the Transfer Object, perhaps by using an abstract get() and set(), but that seems like a lot of overhead to me (doubling every method call). Is that how you do this with Transfer Objects?
# Posted By Bob Silverberg | 7/7/08 12:24 PM
Peter Bell's Gravatar I don't use Transfer :-)

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!
# Posted By Peter Bell | 7/7/08 12:33 PM
Bob Silverberg's Gravatar @Peter, I kinda figured that you didn't use it. You're pretty active on the mailing list for a non-user ;-)
# Posted By Bob Silverberg | 7/7/08 12:57 PM
Jason Dean's Gravatar @peter, thanks for the input. It really sounds like that is the way to go.
# Posted By Jason Dean | 7/7/08 4:15 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner