ColdFusion 8 Binding Part 1
The idea behind this tutorial is to produce something a little more real world. While this tutorial shouldn't be put on a production machine I will be expanding on this tutorial to produce something that you may find useful.
We will be producing using the new ajax features of Coldfusion 8 a file explorer that will eventually allow a user to browse the files on the server and upload, download, and delete files.
In Part 1 of this multipart tutorial we will build the Component that will feed our tree and grid with data. In part 2 we will bind our grid and tree to our component. And then in later tutorials we will expand our file explorer application to allow for uploading, downloading and deleting of files on the server. We will also explore ways to restrict our little application to a specific directory and finally allow our users to create and delete directories.
The Component
The first thing we need to do is to create our component. This component will contain a function to return the directory structure, and a function to return the files contained in a directory. I have also added a helper function to check to see if a directory has sub-directories this will allow us to correctly set whether or not a directory needs to be an expandable node or not.<cffunction name="getDirectories" access="remote" returnformat="json" returntype="any">
<cfargument name="itemPath" type="string" required="yes"/>
<cfargument name="itemValue" type="string" required="yes"/>
<cfargument name="addSubDirs" type="boolean" required="no" default="true"/>
<!--- we set the base directory here though we should really do this in the application.cfm or a
ini file. --->
<cfset var baseDir ="/"/>
<!--- the tree expects to receive a json array so we initialize our array here --->
<cfset var result =arrayNew(1)/>
<cfset var sbaseDir=""/>
<!--- get the absolute path of the base --->
<cfset baseDir = expandpath(basedir)/>
<!--- this piece of code will only run on the first call --->
<cfif arguments.itemValue eq ''>
<cfset element = StructNew()>
<cfset element.value = "#request.approot#">
<cfset element.display = "Root">
<cfset arrayappend(result,element)>
<cfreturn result/>
</cfif>
<!--- this block of code will run on each subsequent call and return the directories for
the passed in path --->
<cfset sbaseDir ="#basedir##itemvalue#"/>
<cfdirectory action="list" name="allDirs" directory="#sbaseDir#"/>
<cfquery name="dirs" dbtype="query">
select name
from alldirs
where type='Dir'
</cfquery>
<cfoutput query="dirs">
<cfset element = StructNew()>
<cfset element.value = "#itemvalue#\#name#">
<cfset element.display = "#name#">
<!--- the hasSubDirs is a helper function so that we format the tree nodes correctly as either
being an end node or one having children --->
<cfif not hasSubDirs('#sbaseDir#\#name#')>
<cfset element.LeafNode = true/>
</cfif>
<cfset arrayappend(result,element)>
</cfoutput>
<cfreturn result/>
</cffunction>
<cffunction name="getFiles" access="remote" returnformat="json">
<cfargument name="page" type="numeric" required="yes"/>
<cfargument name="pageSize" type="numeric" required="yes"/>
<cfargument name="gridSortColumn" type="string" required="yes"/>
<cfargument name="gridSortDirection" type="string" required="yes"/>
<cfargument name="path" type="string" required="false" default="/"
hint="again we should have the default value in a application variable here"/>
<cfset var webpath="" & path>
<cfdirectory action="list" directory="#expandpath('#path#')#" name="files"/>
<cfquery name="getFiles" dbtype="query">
select name,size,'#webpath#' as directory ,datelastmodified
from files
where type='File'
<cfif gridsortcolumn neq ''>
order by #gridsortcolumn# #gridsortdirection#
</cfif>
</cfquery>
<cfset mq = queryNew("name,size,directory,datelastmodified","varchar,integer,varchar,varchar")>
<cfset i = 0>
<cfoutput query="getfiles">
<cfset i = i + 1/>
<cfset temp = queryAddRow(mq,1)/>
<cfset temp= querySetCell(mq,"name","#name#",i)/>
<cfset temp= querySetCell(mq,"size","#size#",i)/>
<cfset temp= querySetCell(mq,"directory","#directory#",i)/>
<cfset temp= querySetCell(mq,"datelastmodified","#dateformat(dateLastModified,'medium')# #timeformat(datelastmodified,'medium')#",i)/>
</cfoutput>
<cfreturn queryConvertForGrid(mq,arguments.page,arguments.pageSize)/>
</cffunction>
<cffunction name="hasSubDirs" access="private" returntype="boolean">
<cfargument name="path" type="string" required="yes"/>
<cfdirectory action="list" name="hasDirs" directory="#path#"/>
<cfquery name="dirs" dbtype="query">
select name
from hasDirs
where type='Dir'
</cfquery>
<cfif dirs.recordcount eq 0>
<cfreturn false/>
<cfelse>
<cfreturn true/>
</cfif>
</cffunction>
</cfcomponent>
Testing the component - getDirectories
Now that we have our component created we should test to see if it is return expected results. Since we will be calling the component directly from our Tree and Grid we should be able to type in a URL to our component and do the same thing. So lets do that. On my server I have a virtual directory called mycf and created my component in the directory com/gg so the url to my components directory would be "http://localhost/mycf/com/gg/". To call my component directly I just do the following.
What is new here is that we are calling our CFC directly, this can only be done when we have the access attribute set to remote. We then specify
what method we want to call in our component using the method attribute. Also new in Coldfusion 8 is the returnformat argument. This argument allows us
to specify how we want our data formated when it is returned as WDDX, JSON or Plain. It is important to note that if you are using other AJAX frameworks
Coldfusion returns the JSON data escaped or prefixed so it may be a good idea to set the returnformat to plain. Be aware though that if you specify plain as
your return format you will need to build the json structure yourself and won't be able to rely on CF to convert a complex structure for you.
Then last but not least you need to pass along your functions arguments. What you should get back from the component is a directory listing looking something like:
[{"DISPLAY":"Root","VALUE":"\/mycf"}]
Modifying the itemvalue parameter slightly by passing in value of the root node, /mycf in this case, should result in a larger return set where you can see
which of the nodes are leaves (meaning they don't have subdirectories) and which ones are branches.
Testing the Component - getFiles
Now that we know that our component returns the root of our tree we should also make sure that our getFiles function returns files (makes sense doesn't it). So we will have a similar URL as above except that we will be calling a different method and passing different arguments.
We must pass the arguments page,pageSize,gridSortcolumn and gridSortDirection as they are required for the queryConvertForGrid function to correctly parse our query and return the results specifically set up for the grid. When everything works as it should we should end up with something that looks like:
Whats Next
We now have a component with a function that returns a data structure containing our directories and another function returning a data strucutre with the files contained in a directory. Both functions return our data to us in specifically formated JSON data structures. We hand built the structure for the tree (we could build a function to do that for us), and we used a new Coldfusion 8 function queryConvertForGrid() to, as the function indicates, format a query into JSON specifically for the EXT grid.
In part 2, as I mentioned above, we will be setting up our tree and grid to bind to the functions in the component and then see how we bind the grid to the change event of the tree so that the grid is updated when a node of the tree is clicked.