Saturday, May 17, 2008    
Home My Books Blog ColdFusion About Me Back    

Calendar
<< Jun 2007 >>
S M T W T F S
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
             

Search

Categories
 • Adobe (61) [RSS]
 • AdobeMAX06 (45) [RSS]
 • AdobeMAX07 (59) [RSS]
 • AdobeMAX08 (6) [RSS]
 • AIR (96) [RSS]
 • Appearances (105) [RSS]
 • Books (66) [RSS]
 • CFEclipse (14) [RSS]
 • ColdFusion (1081) [RSS]
 • Flash (91) [RSS]
 • Flex (319) [RSS]
 • Jobs (81) [RSS]
 • JRun (12) [RSS]
 • Labs (27) [RSS]
 • LiveCycle (12) [RSS]
 • MAX (141) [RSS]
 • Regular Expressions (12) [RSS]
 • SQL (36) [RSS]
 • Stuff (492) [RSS]
 • Tips (CF Studio) (80) [RSS]
 • Tips (CF) (795) [RSS]
 • Tips (Dreamweaver) (91) [RSS]
 • Tips (Flex Builder) (2) [RSS]
 • Using CF (131) [RSS]
 • Wireless (96) [RSS]

Other BLOGs
 • Ray Camden
 • Tim Buntel
 • Sean Corfield
 • John Dowdell
 • Steven Erat
 • Brandon Purcell
 • Charlie Arehart
 • Full As A Goog

RSS Feeds
 • Feed
 • Subscribe

Join my mailing list and find out about new books and other topics of interest.

Thoughts, ideas, tips, musings, and pontifications (not necessarily in that order) by Ben Forta ...
NOTE: This is my personal blog, and the opinions and statements voiced here are my own.

Viewing By Entry / Main
June 5, 2007

ColdFusion Ajax Tutorial 5: File System Browsing With The Tree Control

<CFTREE> has been part of ColdFusion since CF2 - originally a Java applet, and then a Flash control, and in ColdFusion 8 an HTML tree control with powerful Ajax support. To demonstrate how to use the new HTML <CFTREE>, here is an example that will let you browse the file system tree on your server. What makes this an Ajax control is that the entire tree is not loaded on startup. Rather, it is loaded incrementally, minimal data is loaded on startup, and when a node is expanded an asynchronous call is made back to the server to obtain the children for that node, and so on. This improves performance (by not requiring that entire trees be loaded if they are not needed), and also simplifies actual tree data (formatting and parsing nested tree data is not pretty).

The client-side code for this example is really simple, as seen here:

<cfform>
<cftree name="dirtree" format="html">
   <cftreeitem bind="cfc:dirtree.getDirEntries({cftreeitempath}, {cftreeitemvalue})">
</cftree>
</cfform>

This <CFTREE> uses a "bind" to point to a CFC method to obtain tree contents. When a binding is used, as it is here, only a single <CFTREEITEM> may be specified, and it is responsible for loading all data as needed. On initial tree load the CFC method is invoked (passing an empty string for the current path and value), and then as branches are expanded the CFC method is invoked again (passing the current path and value back to the server).

It is worth noting that, by default, results are cached on the client. So expanding and collapsing a branch repeatedly does not trigger multiple CFC method calls. Caching can be disabled if needed.

The CFC method that the <CFTREE> is bound to is shown here:

<!--- Get directory entries for Ajax CFTREE --->
<cffunction name="getDirEntries" access="remote" returnType="array">
   <cfargument name="path" type="string" required="false" default="">
   <cfargument name="value" type="string" required="false" default="">

   <!--- Init variables --->
   <cfset var dir="">
   <cfset var entry="">
   <cfset var result=arrayNew(1)>
   <cfset var filepath="">

   <!--- Check if top level --->
   <cfif ARGUMENTS.value IS "">
      <!--- Yes, top level, get drives --->
<cfset dir=getDrives()>
      <!--- Loop through dir list --->
      <cfloop query="dir">
         <!--- Add each drive/root --->
         <cfset entry=StructNew()>
         <cfset entry.value=dir.name>
         <cfset entry.display=dir.name>
         <cfset entry.img="fixed">
         <cfset ArrayAppend(result, entry)>
      </cfloop>
   <cfelse>
      <!--- Not top level, get dir list --->
      <cfdirectory action="list"
               directory="#ARGUMENTS.value#"
               name="dir"
               sort="name">

      <!--- Loop through dir list --->
      <cfloop query="dir">
         <!--- Create entry --->
         <cfset entry=StructNew()>
         <!--- Use full path as value --->
         <cfset entry.value=ARGUMENTS.value & THIS.separator & dir.name>
         <!--- Use just name for display --->
         <cfset entry.display=dir.name>
         <!--- Is this a file or a dir? --->
         <cfif dir.type IS "file">
            <!--- A file, so no children --->
            <cfset entry.leafnode=TRUE>
            <!--- Use document icon --->
            <cfset entry.img="document">
         <cfelse>
            <!--- A dir, use folder icon --->
            <cfset entry.img="folder">
            <cfset entry.imgopen="folder">
         </cfif>
         <!--- Add it to the array --->
         <cfset ArrayAppend(result, entry)>
      </cfloop>
   </cfif>

   <!--- And return the results --->
   <cfreturn result>
</cffunction>

<CFTREE> expects the bound CFC method to return an array of entries represented as structures. These structures should, at a minimum, contain two members - "display" is the string to be displayed in the tree, and "value" is the value to be returned (back to the bound CFC when branches are expanded, as well as when the form containing the <CFTREE> is actually submitted). The structures may also contain additional members (equivalent to the attributes supported by <CFTREEITEM>), for example, "img" and "imgopen". One important optional member is "leafnode" which tells the tree whether or not the current entry is a leaf (has no children) or a branch (could have children). As tree data is loaded as needed, <CFTREE> can't know if a branch has children until the user tries to expand it. In this example we want folders to be expandable, but not files, and so <leafnode> is set to "true" for file entries.

If the method is being invoked for the top level of the tree (in which case ARGUMENTS.value will be an empty string) then a function is called to get all system drives (or roots). The code then loops through the results, creating a structure for each, and appending these to a results array. If the method is being invoked for a child branch (in which case ARGUMENTS.value will contain the path to the branch that was expanded), <CFDIRECTORY> is used to obtain the contents of that folder, and then the results are looped over, creating a structure for each entry, and again appending these to a results array.

This example returns all file and folders. If you wanted to return a subset of data, perhaps to allow the user to find files of a specific type, you'd just need to modify the <CFDIRECTORY>, possibly replacing it with two <CFDIRECTORY> calls, one for folders and one for the files (using a filter to select just the files you want).

This method refers to a function named getDrives() (to obtain drives or roots) and a variable named THIS.separator (used to refer to the OS specific path separator character), both of which need to be defined. The following is the complete dirtree.cfc, containing the previously seen getDirEntries() method, as well as all supporting code:

<cfcomponent output="false">


<!--- Constructor code --->
<cfset THIS.separator=getPathSeparator()>


<!--- Get system path separator character --->
<cffunction name="getPathSeparator" access="private" returntype="string">
   <!--- Init variables --->
   <cfset var jifObj="">
   <!--- Need java.io.File class --->
   <cfobject type="java" class="java.io.File" name="jifObj">
   <!--- Return seperator --->
   <cfreturn jifObj.separator>
</cffunction>


<!--- Get system drives (or roots) --->
<cffunction name="getDrives" access="private" returntype="query">
   <!--- Init variables --->
   <cfset var jifObj="">
   <cfset var drives="">
   <cfset var i=0>
   <cfset var result=QueryNew("name")>

   <!--- Need java.io.File class --->
   <cfobject type="java" class="java.io.File" name="jifObj">
   <!--- Get drives/roots --->
   <cfset drives=jifObj.listRoots()>

   <!--- Loop through results and create query --->
   <cfloop from="1" to="#ArrayLen(drives)#" index="i">
      <cfset QueryAddRow(result)>
      <cfset QuerySetCell(result, "name", drives[i].toString())>
   </cfloop>

   <!--- Return results --->
   <cfreturn result>

</cffunction>


<!--- Get directory entries for Ajax CFTREE --->
<cffunction name="getDirEntries" access="remote" returnType="array">
   <cfargument name="path" type="string" required="false" default="">
   <cfargument name="value" type="string" required="false" default="">

   <!--- Init variables --->
   <cfset var dir="">
   <cfset var entry="">
   <cfset var result=arrayNew(1)>
   <cfset var filepath="">

   <!--- Check if top level --->
   <cfif ARGUMENTS.value IS "">
      <!--- Yes, top level, get drives --->
      <cfset dir=getDrives()>
      <!--- Loop through dir list --->
      <cfloop query="dir">
         <!--- Add each drive/root --->
         <cfset entry=StructNew()>
         <cfset entry.value=dir.name>
         <cfset entry.display=dir.name>
         <cfset entry.img="fixed">
         <cfset ArrayAppend(result, entry)>
      </cfloop>
   <cfelse>
      <!--- Not top level, get dir list --->
      <cfdirectory action="list"
               directory="#ARGUMENTS.value#"
               name="dir"
               sort="name">

      <!--- Loop through dir list --->
      <cfloop query="dir">
         <!--- Create entry --->
         <cfset entry=StructNew()>
         <!--- Use full path as value --->
         <cfset entry.value=ARGUMENTS.value & THIS.separator & dir.name>
         <!--- Use just name for display --->
         <cfset entry.display=dir.name>
         <!--- Is this a file or a dir? --->
         <cfif dir.type IS "file">
            <!--- A file, so no children --->
            <cfset entry.leafnode=TRUE>
            <!--- Use document icon --->
            <cfset entry.img="document">
         <cfelse>
            <!--- A dir, use folder icon --->
            <cfset entry.img="folder">
            <cfset entry.imgopen="folder">
         </cfif>
         <!--- Add it to the array --->
         <cfset ArrayAppend(result, entry)>
      </cfloop>
   </cfif>

   <!--- And return the results --->
   <cfreturn result>
</cffunction>


</cfcomponent>

As you can see, once you get your arms around how data is formatted and passed back and forth, the new Ajax enabled HTML based <CFTREE> is both powerful and really easy to use.

TrackBacks
There are no trackbacks for this entry.

No trackback URL. Trackbacks are only allowed via interactive form.

Comments
Thank you Ben.
# Posted By Gary Funk | 6/5/07 10:20 AM
I get a blank screen in FF and a JS error in IE 7 when I try the sample code, http://h127449.cf8beta.com/cftree.cfm
# Posted By Jose | 6/5/07 10:32 AM
Jose, I tested it in FF2 and IE7, and both worked. Can you get any Ajax controls to work? I wonder if the CFIDE mapping is missing or something like that.

--- Ben
# Posted By Ben Forta | 6/5/07 10:37 AM
Actually, it doesn't look like I can get the simple (no CFC) Auto Suggest example to work either.

http://h127449.cf8beta.com/autoSuggest2.cfm

Should I tell the hosting company to create a virtual directory for CFIDE?
# Posted By Jose | 6/5/07 10:48 AM
Ben, in the function getDirEntries, you have 'path' as the first argument. I don't see where path is ever used. What would this be used for?
# Posted By Gary Funk | 6/5/07 10:52 AM
Jose,
You either have to have them create a virtual directory for the /CFIDE folder or just upload that folder to your home root directory
# Posted By Dan | 6/5/07 10:56 AM
Gary, value returns whatever value you provided previously for the entry, path returns the dynamically constructed path down the tree. As I build the value to be the full path I am just using that for subsequent searches, that's simpler in this case as <CFDIRECTORY> needs the full path to get listings. But if this were a sequential database lookup for example, then I'd probably want the value to be the primary key, in which case the path would be useful to know the path down the tree to the current branch.

--- Ben
# Posted By Ben Forta | 6/5/07 11:14 AM
Yes, that's clear. I missed that one. Will this worked with mapped networked drives? So far, I'm not having any luck in that area.
# Posted By Gary Funk | 6/5/07 11:38 AM
Gary, I am not sure about that one. My guess is that if CF itself sees the mapped drive then it should work (and this will likely require that CF run as a user with rights to those remove shares).

--- Ben
# Posted By Ben Forta | 6/5/07 11:54 AM
I ran across a blog entry by Nathan Mische yesterday, which shows how to hook into the YUI library to add a context menu to CFTREE. This seems like a perfect companion article to this entry, so I thought I'd post the link:

http://www.mischefamily.com/nathan/index.cfm/2007/...

-Dan
# Posted By Dan G. Switzer, II | 6/5/07 12:30 PM
Guys,

Thanks for your help. I got it to work by adding /CFIDE. However, it takes an eternity to return and then if I click on a subfolder Firefox eventually crashes.
# Posted By Jose | 6/5/07 12:34 PM
Jose, I've not seen that happen. What version of FF? All I can think of is that the folder list may be so huge on that shared box that the control is having a hard time with it, but that's a guess.

--- Ben
# Posted By Ben Forta | 6/5/07 12:43 PM
I'm running FF 2.0.0.4, and you're probably right regarding the lengthy folder list although I can't tell for sure..
# Posted By Jose | 6/5/07 12:47 PM
@Ben:

I'm also seeing the code lock up on my laptop in FF2.0.0.4. I'll look in to and and report what I find. I think this potentially could be an issue with the CFMX internal web server. I seem to recall I've had issues using it w/AJAX in the past. (Yes, I'm using the internal web server for the RC testing.)

It seems to work ok in IE7, but it definitely pegs the CPU in FF and eventually requires me to hit press "Stop the Script".

-Dan
# Posted By Dan G. Switzer, II | 6/5/07 12:51 PM
Actually my FF crashed too when looking at Jose's example, im using the latests version of it as well.
# Posted By Raul Riera | 6/5/07 1:16 PM
It *looks* like this happens when the folder list is massive (thousands of entries), but without seeing that file system I can't know for sure. C:\ expands for me fine (using your URL, Jose). Jose, can you do a <cfdirectory action="list"> and see what it returns for the branch you were trying to expand?

--- Ben
# Posted By Ben Forta | 6/5/07 1:24 PM
My Application.cfc seems to be causing some greif to this poor thing. I get the following error:

error:widget: Bind failed for tree dirtree, bind value is not a 1D array of key value pairs
info:http: CFC invocation response:
info:http: HTTP GET /test/dirtree.cfc?method=getDirEntries&returnFormat=json&argumentCollection=%7B%22path%22%3A%22%22%2C%22value%22%3A%22%22%7D&_cf_nodebug=true&_cf_nocache=true
info:http: Invoking CFC: /test/dirtree.cfc , function: getDirEntries , arguments: {"path":"","value":""}
info:widget: Created tree, id: dirtree
info:widget: Created tree, id: dirtree
info:LogReader: LogReader initialized
info:global: Logger initialized
# Posted By Gary Funk | 6/5/07 1:37 PM
Gary, try calling the CFC method with a <CFINVOKE>, and see if it is throwing an error or what is being returned.

--- Ben
# Posted By Ben Forta | 6/5/07 1:40 PM
Ben, when I comment out the following from Application.cfc it works as you wrote it. This is from Ray's Application.cfc

<cffunction name="onRequest" returnType="void">
<cfargument name="thePage" type="string" required="true" />
<cfinclude template = "#arguments.thePage#" />
</cffunction>
# Posted By Gary Funk | 6/5/07 2:01 PM
@Ben: My C:\ drive only has 69 entries in it and that causes the crash.
# Posted By Dan G. Switzer, II | 6/5/07 2:01 PM
Gary, yep, onRequest and ACCESS="remote" don't play nicely together.

--- Ben
# Posted By Ben Forta | 6/5/07 2:04 PM
@Ben:

I played around with this a bit more and the issue seems to be with either the CFTREE.js or the CFAJAX.js. I can invoke the CFC URL directly with FF and the page loads just fine, so something is happening when the code is being deserialized into the tree.

It's causing memory in FF to absolutely go through the roof. I'm going to guess the problem is in the CFTREE and has something to do w/the DOM creation...
# Posted By Dan G. Switzer, II | 6/5/07 3:33 PM
As usual, Ray and SEan have the answer, and it's a simple fix.

http://ray.camdenfamily.com/index.cfm?mode=entry&a...

You can use web services, flash remoting or event gateway requests CFC's with an Application.cfc containing an onRequest method. This can be done by creating another application.cfc within a subfolder where your CFC's are, containing the code...

<cfcomponent name="Application" extends="test.Application">
<cfset StructDelete(variables,"onRequest")/>
<cfset StructDelete(this,"onRequest")/>
</cfcomponent>

This is why I am a CF coder. At any given moment, each of us is a beginner or an expert and we help each other.
# Posted By Gary Funk | 6/5/07 10:35 PM
Thanks Ben for all the new examples for CF8.

There is definitely a problem with FF (2.0.0.4). Looks like a memory leak. I had to kill FF when memory jumped over 400Mb.

In IE it works better but it takes forever to load the information when expanding a directory.
I have seen this behavior in other instances where controls are bind and the information retrieved is rather large, the browser just hangs.

I really hope that the development team will fix this otherwise I see all of this functionality as having a negative effect on CF in general because you can be it will be used in enterprise environments where is not uncommon to deal with thousands of records and if it's that slow the reaction is that CF is not good as an enterprise solution.
# Posted By Marius M | 6/10/07 2:10 PM
It looks like there is a lot of extra code for looping through drive letter contents. What would the dirtree.cfc look like if we just wanted to loop through a certain directory?
# Posted By Kurt | 8/27/07 2:30 PM
To get a specific Directory I added the following code to replace the CFDIRECTORY tag in the CFC.

<CFIF #arguments.VALUE# EQ "">
<cfdirectory action="list" directory="{directoryname}" name="dir" sort="type">
<CFELSE>
<cfdirectory action="list" directory="{directoryname}\#arguments.value#" name="dir" sort="type">
</CFIF>

Also I added a line to put in a href for the files. I put it after the IF statement checking whether it is a file. See below.

<CFIF #arguments.value# EQ "">
<cfset entry.href="/documentation/files/#session.support.release#/#name#">
<cfelse>
<cfset entry.href="/Documentation/files/#session.support.release#/#arguments.value#/#name#">
</CFIF>
# Posted By Chris | 11/15/07 11:47 AM
Hi, Ben:
I have tried this feature. It's cool. Just wonder if I could add href and browse the file from this cftree. Unfortunately I can't make it work via entry.href and entry.target, or using dynamically cftreeitem or cftreeitemvalue, etc.

Thanks,
Steven
# Posted By Steven | 3/19/08 7:58 PM
To those who are seeing browsers lock up when many nodes are returned: I had this problem until I enabled whitespace management in CF Administrator. <cfprocessingdirective suppresswhitespace="Yes"> did not help however.
# Posted By Jason | 4/8/08 4:47 PM
Just found you don't have to enable the server wide whitespace management setting if you make sure the cffunction tag in the cfc has the attribute output="No"..
# Posted By Jason | 4/8/08 6:00 PM

  © Copyright 1997-2008 Ben Forta, All Rights Reserved