Adding Todo Lists (Handlers and the Service Layer) - ColdBox Series Part 8d
Now we are really getting into this ColdBox stuff. We've got a view set up, we've got some handlers, we've auto-wired our handlers, and we've added ColdSpring into our application. Now we can start to do stuff.
So where did we leave off last time? Thinking.... thinking... thinking... oh yeah.
Accepting user input
Before we went off on a tangent of setting up ColdSpring and Autowiring Handlers, we had two simple handler methods inside of the list.cfc handler. We had addList() and doAddList().addList():
<cffunction name="addList" access="public" returntype="void" output="false">
<cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
<cfset event.setValue('xehDoit', 'list.doAddList') />
<cfset event.setValue('btn', 'Add')>
<cfset event.setView('listForm') />
</cffunction>
doAddList():
<cffunction name="doAddList" access="public" returntype="void" output="false">
<cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
</cffunction>
addList() just added some values to the event object and then set the view to be displayed. doAddList() accepted the event arguments, which includes the form fields that were submitted from the form inside of the view listForm.cfm and is now ready to do something with that data.
NOTE: Don't forget that in the last post, we set up this handler to be autowired with the todoService, so right now it is available to us in the variables scope.
But FIRST, let's make some changes
So just like with any project, change happens. In this case, it was due to my design. Originally, I thought we would auto-increment our primary key in the lists and items tables, but it occurs to me that I don't like that, and I don't want to have to deal with getting the key back after it is set. So we are going to modify our DB design, and we'll need to make some model changes too. Nothing major.First, since we have not yet done anything with our database, we can just drop the tables and recreate them. Here's the script.
DROP TABLE IF EXISTS `todo`.`lists`;
CREATE TABLE `todo`.`lists` (
`listid` varchar(36) NOT NULL,
`name` varchar(50) NOT NULL,
`priority` int(11) default NULL,
PRIMARY KEY (`listid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
DROP TABLE IF EXISTS `todo`.`items`;
CREATE TABLE `todo`.`items` (
`itemid` varchar(36) NOT NULL,
`title` varchar(45) NOT NULL,
`dueDate` datetime NOT NULL,
`listid_fk` varchar(36) NOT NULL,
PRIMARY KEY (`itemid`,`listid_fk`),
KEY `fk_items_lists` (`listid_fk`),
CONSTRAINT `FK_list_items` FOREIGN KEY (`listid_fk`) REFERENCES `lists` (`listid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Next, we need to make some changes to the model objects to accommodate this change. In the todoGateway's createList() method we need to add the listID to the INSERT statement.
<cfquery name="qCreate" datasource="#variables.dsn#">
INSERT INTO lists (
listid,
name,
priority
) VALUES (
<cfqueryparam value="#arguments.list.getListId()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.list.getname()#" CFSQLType="cf_sql_varchar" />,
<cfqueryparam value="#arguments.list.getpriority()#" CFSQLType="cf_sql_integer" null="#not len(arguments.list.getpriority())#" />
)
</cfquery>
We'll need to do the same for createitem():
<cfquery name="qCreate" datasource="#variables.dsn#">
INSERT INTO items (
itemID,
title,
dueDate,
listid_fk
) VALUES (
<cfqueryparam value="#arguments.item.getItemId()#" CFSQLType="cf_sql_varchar" />,
<cfqueryparam value="#arguments.item.gettitle()#" CFSQLType="cf_sql_varchar" />,
<cfqueryparam value="#arguments.item.getdueDate()#" CFSQLType="cf_sql_timestamp" />,
<cfqueryparam value="#arguments.item.getlistid_fk()#" CFSQLType="cf_sql_varchar" />
)
</cfquery>
Ok, one more change. In the WHERE statements for updateList(), updateItem() and createItem() inside of todoGateway, we need to change the CFSQLType attributes to cf_sql_varchar. Change it in both where clause statements in updateItem() and in the last value statement for createItem(). A total of 4 changes are needed.
Working with the service layer
We have this fancy new service layer with one method in it, and that is just for dependency injection, what else are we going to use it for? Well, what I use the service layer for is to communicate between the controller (ColdBox) and the model(Beans and Gateway). Some might argue that the service layer is part of the model. I'm not sure if I care.We want our doListAdd() method to so something now, don't we? But what? Well we want it to take the user input, validate it, and persist it to the database. We're going to do all but the validation today. Let's start with this.
<cffunction name="doAddList" access="public" returntype="void" output="false">
<cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
<cfset var rc = event.getCollection()>
<cfset var listBean = variables.todoService.newList() />
<cfset getPlugin('beanFactory').populateBean(listBean) />
<cfset variables.todoService.saveList(listBean) />
<cfset setNextEvent('list.allLists') />
</cffunction>
There is actually quite a lot going on in these 5 new lines of code. Let's examine each.
1. Here we are putting the request collection into a "virtual scope" (a struct) called rc. This rc scope contains all of the FORM and URL scoped variables and the name of the event that was called. This way we can get the data out of the event without needing the very verbose getValue() method. We will not actually be using this struct today.
2. Here we are calling the todoService (the one that was autowired in) method newList(). I supposed we'll need to create that method now ;). This method will create a new bean, persist it to the database as an empty record with only and id and return the bean with the id in it.
3. This line is pretty cool. ColdBox has a beanFactory plugin that does several things. One of those things is to populate a bean from the event object data. It will look at the object's properties and call setter methods for each of the properties on the bean that is passed in. So if there is a firstname property, it will call setFirstname() on the bean and pass in the value from the event object. So as long as our formfields were named the same as the bean properties, we can use this.
4. The listBean is then passed to the todoService.saveList() method to be persisted to the database.
5. The last line tells ColdBox which event to fire next. We are telling it to fire the list.allLists event so that we can view all of the lists that we have made so far. We'll need to create that too.
Our first Service Method
In our doAddList() handler we are making a call to a todoService method called newList(). Let's make that method.<cffunction name="newList" access="public" returntype="model.todos.List" output="false">
<cfset var bean = createObject('component','model.todos.List').init(createUUID()) />
<cfset variables.todoGateway.createList(bean) />
<cfreturn bean />
</cffunction>
Here we are using the createObject() method to create an instance of the List object. To do this we are calling the init() method and passing in a new UUID using the createUUID() method. NOTE: In the future this would be a good place for an Object Factory that would be injected using ColdSpring, but we are trying to keep this simple.
The method is then going to take that newly created bean and pass it into the todoGateway's createList() method. NOTE: remember, the todoGateway was injected with ColdSpring. The createList method takes the bean as-is and persists it to the database. At this point we have not yet called populateBean(), so, other than the UUID ListID, this bean is empty.
Finally, it returns the bean to the handler.
Now our handler populates the bean:
<cfset getPlugin('beanFactory').populateBean(listBean) />
This will take the form field values and populate the bean with them.
The bean is then passed to the todoService.saveList() method. So we'll need to create that.
<cffunction name="saveList" access="public" returntype="void" output="false">
<cfargument name="bean" type="model.todos.List" required="true" />
<cfset variables.todoGateway.updateList(arguments.bean) />
</cffunction>
This is simple enough. It takes in the bean as an argument, then passes it to the todoGateway's updateList() method, which will actually save it to the database.
Finishing Up
Finally, the handler uses setNextEvent() to fire the next ColdBox event. Which is list.allLists. So let's create that event handler method.<cffunction name="allLists" access="public" returntype="void" output="false">
<cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
<cfset var qAllLists = variables.todoService.getAllLists() />
<cfset event.setValue('qAllLists', qAllLists) />
<cfset event.setView('allLists') />
</cffunction>
Like all event handler methods, we are first taking in the event object as an argument. Then we are creating a query object by calling getAllLists() from the todoService. Next we are injecting that query into the event object using the setValue() method. I am giving it the same name, "qAllLists". Lastly, the method sets a new view to display, allLists, which is actually allLists.cfm inside of the /views directory.
We have a few things here to create. todoService does not have a getAllLists() method, and there is not a allLists.cfm file.
todoService.getAllLists():
<cffunction name="getAllLists" access="public" returntype="query" output="false">
<cfreturn variables.todoGateway.getAllLists() />
</cffunction>
allLists.cfm:
<cfset qAllLists = event.getValue('qAllLists')>
<cfdump var="#qAllLists#">
getAllLists(), like many of our service methods, makes a call to the gateway object. The method todoGateway.getAllLists() is for requesting all of the lists. Now, there was an oversight in my design, because we never added this method to the todoGateway, so let's.
<cffunction name="getAllLists" access="public" output="false" returntype="query">
<cfset var qLists = "" />
<cftry>
<cfquery name="qLists" datasource="#variables.dsn#">
SELECT listid, name, priority
FROM lists
</cfquery>
<cfcatch type="database">
<cfset qLists = QueryNew("id") />
</cfcatch>
</cftry>
<cfreturn qLists />
</cffunction>
Then our allLists.cfm view is getting the query object our of the event object and dumping the values. This will show that the lists we create are actually being persisted.
Conclusion
Well, that was fun. We got a lot done today. We handled our user input, created a bean, populated it with the user input and persisted it to the database. We also set up some methods and a view for displaying all of the lists. On top of all that we made some changes to the model layer to make things a little better. I am attaching the entire application to the post.Next time we will look at adding data validation, I think. Who knows, maybe we will talk about astro-photography instead.





Great reading and learning a lot. Head still hurts, though ;o)
Doug
Doug
You wouldn't. Why would I want a todo list without a name? In my application all Lists must have a name. The fact that I enforce this in the database is really just a personally choice. There is no reason that you couldn't make the field nullable in the DB if you wanted too. Then you could make a list without a name.
I will point out though, that it might make your UI a little hard to work with if you or your users are allowed to make unnamed lists, especially if the id is a UUID. Good luck telling those apart in table :)
I've been following your series, trying to learn how to use ColdBox, with the only change, really, being that I'm using Oracle as a database. No biggie.
The problem I have is that when I try to add a new list, I get an error (ORA-01400: cannot insert NULL into ("COLDBOXTEST"."TODO_LISTS"."LIST_NAME")).
Looking at the code, the only thing I can figure is that we call TodoService.newList() from list.doAddList() and we don't send any arguments. We create a UUID in newList(), but I don't see how it ever gets access to the list name. I'm using ColdSpring.
Could you at least tell me how the createList() call in newList() knows the name of the list so I can backtrack and figure out what I screwed up?
And before you ask, I know the table name is different, and I changed the form elements to reflect that. :)
So the easiest way to fix it is to remove the NOT NULL constraint from the listname field in the database. Otherwise your next option would be to modify the newList() method to only create the bean and return it, then use the saveList() methods to determine if the bean needs to be INSERTed or UPDATEd.
Thanks for pointing out the mistake. I am not sure how I did not notice it. I am guessing that I did not have the NOT NULL constraint on the column.
I have been going through this tutorial, and It has been great. Thanks!!! I just wanted to point out that under the "But FIRST, let's make some changes" section the code is scrambled. Just wanted to give you a heads up.
Sorry to bother you again, but I am getting some strange error when I try and run the event handler doAddList.
Element LIST is undefined in a Java object of type class [Ljava.lang.String;.
I went back through my code several times, and I can't seem to figure it out. Any help would be greatly appreciated.
Thanks for all of your comments. I'm glad to see that you are enjoying the posts.
I think that may be a hard one to deduce without more info. I'm sending you a chat request in gmail, or if you can use something like http://pastebin.com to link me to your code, I can see if I can reproduce it.
Thanks again for all of your help. I have been pulling my hair out all day. I guess I should have taken a walk or something to clear my head. I just needed another pair of eyes to look at it. Strange error though, for one misspelled word?
Firstly, thanks for putting together a great series! Is there a reason why you create a new list bean in newList()? If there is any problem downstream, you could end up with empty rows of lists in your DB. You could put the UUID in as a placeholder so you would have the id handy before and after the save operation. This would allow you to keep the update as well - just skip the extra DB hit and potential for bugs.
Just like with so much code that we write, looking back on this several months later, I see MANY things that I would do differently. Just as has been repeated so many time in this OO learning experience, there are many ways to skin the proverbial cat.
Definitely, I think that if I was rewriting this that I would not persist the list at that point, I would wait. Part of the problem was that I had never created an app that used a model built using the PU36 code generator, so I was, sometimes, confused about how to use the model in certain places. Perhaps, one of these days, i should revisit and rewrite this. Maybe.
I get the same error message except mine is Element TODOSERVICE ...
As I recall, it was a typo. It was something really simple like todoGateway was spelled todoGatway or something.
It is because you have to setup the autowire=true and add a <cfproperty> tag in the pseudo-constructor area of the component. The pseudo-constructor area is in between the <cfrcomponent> tag and the first function.
var listBean = variables.todoService.newList()
in do add list is it a proper place to call..?? I can not find out todoService object created before this listbean is creating.
Could you please help me..
The todoService object should be created through autowiring. Which we took care of in Part 8c when we turned on the autowire interceptor, added autowire = true to the event handler, and added a cfproperty for the todoService. If that stuff is not working, then that may be your issue. At the top of your event handler you should be able to dump the todoService and abort and it should show you that the service exists.
<cfdump var="#variables.todoService#">
<cfabort>
I'm a frameworks newbie, and this series is increasing my understanding of dependencies and conventions. But I think I hit a roadblock.
I'm getting the error message: "The method settodoGateway was not found in component /Applications/ColdFusion8/wwwroot/todoslist/model/todos/todoService.cfc." I'm not sure why the app is looking for a setTodoGateway method. Because in the coldspring.xml.cfm the dsn property is included in the todoGateway bean, should it be looking for a setter?
Thanks. I am glad you are enjoying the series.
In your coldspring.xml you have this definition:
<bean id="todoService" class="model.todos.todoService">
<property name="todoGateway">
<ref bean="todoGateway" />
</property>
</bean>
Right?
This is where we are telling ColdSpring that it needs to inject the todoGateway into the todoService. It does this through a setter method. So our todoService needs a setTodoGateway() method in it. Like this.
<cffunction name="setTodoGateway" access="public" returntype="void" output="false">
<cfargument name="todoGateway" required="true" type="model.todos.todoGateway" />
<cfset variables.todoGateway = arguments.todoGateway />
</cffunction>
We covered it in part8b be of the series, but you may have missed it.
http://www.12robots.com/index.cfm/2008/11/3/Adding...
Let me know if this doesn't help. Thanks for the comment.
How would this be done in code? I've noticed the post-error empty rows and would like to avoid that. I'm not used to working with UUIDs.
Many thanks. I made it to the end of the tutorial. So, thanks for taking the time Jason. Much appreciated.
This is my second time around on this series (love it!), I was able to resolved my error but now I got an error that I just can't figure out. I was hoping you can give me some pointer. I'm getting this error when I try to submit the form. It seem appears that the "@REWRITE@" is not rewriting. Thank you in advance!
HTTP Error 404.0 - Not Found
The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.
Module IIS Web Core
Notification MapRequestHandler
Handler StaticFile
Error Code 0x80070002
Requested URL http://127.0.0.1:80/dev/mycoldbox@REWRITE@/list/al...
Physical Path M:\wwwroot\mycoldbox@REWRITE@\list\allLists
Logon Method Anonymous
Logon User Anonymous
I'm new to CB and not sure if this is the correct way but it worked for me.