Processing Uploaded Files Asynchronously with Status Updates Using AjaxCFC

Posted on Nov 16, 2006

I was presented with a problem for a recent project. A user would be able to upload a file at which point it would be processed with the results of the processing to be presented to the end user on the screen. This sounds simple, the problem was that the processing could be lengthy, and if it occurred in conjunction with the upload, the user would be waiting for the response page to load and left wondering what happened to their file (and might be tempted to hit back or refresh). So, what I wanted to do was find a way to let the user know we got the file and then to update them on the status of the processing so that they know something is occurring. I was able to accomplish this quite simply using Rob Gonda's AjaxCFC. Keep in mind, this code was a proof of concept and doesn't reflect best practices in all aspects (and I am also very happy to hear if someone has another solution).Obviously, first you need to download AjaxCFC. In this case, I just dumped it in the root, but if this were a more permanent solution, having everything in the root is a bad practice. My form page is a basic upload form that is self-submitting:

<form action="#CGI.SCRIPT_NAME#" method="post" enctype="multipart/form-data">    <input type="file" name="theFile" /><br />    <input type="submit" name="submitted" value="Upload" /> </form>

When the form is submitting, the file is uploaded. I save the file name in a variable which I will use later (you will also see the reason I param it to [empty string]):

<cfparam name="uploadedFileName" default="" /> <cfif structKeyExists(form,"fieldNames")>    <cffile action="upload" filefield="theFile" destination="#expandPath('./')#" nameconflict="overwrite" />    <cfset uploadedFileName = cffile.ServerFile /> </cfif>

Ok, so far this is all pretty basic. Now lets look at my Ajax component methods:

<cfcomponent extends="ajax">        <cffunction name="process" output="no" access="private" returntype="string">       <cfargument name="uploadedFileName" required="Yes" type="string" />              <cfset var tableHTML = "<table>" />       <cfset var csvContents = "" />       <cfset var theRow = "" />       <cfset var theColumn = "" />              <!--- yeah this is bad practice...so sue me --->       <cfset session.recordcounter = 0 />              <cffile action="read" file="#expandPath(arguments.uploadedFileName)#" variable="csvContents" />              <cfloop list="#csvContents#" delimiters="#chr(10)##chr(13)#" index="theRow">          <cfset tableHTML = tableHTML & "<tr>" />             <cfloop list="#theRow#" index="theColumn">                <cfset tableHTML = tableHTML & "<td>" & theColumn & "</td>" />             </cfloop>          <cfset tableHTML = tableHTML & "</tr>" />          <cfset session.recordcounter = session.recordcounter + 1 />       </cfloop>              <cfset tableHTML = tableHTML & "</table>" />       <cfset session.recordcounter = 0 />       <cfreturn tableHTML />    </cffunction> <P>
   <cffunction name="getRecordCounter" output="no" access="private" returntype="numeric">       <cfreturn session.recordcounter />    </cffunction> </cfcomponent>

The process() function actually takes the file name to process as an argument (which for our test it just assumes is a csv) and converts this csv into an html table. Finally it sends the html back as a string. My test upload file included about 10 columns and 1500 rows of data.

You may also have noticed that it saves a session variable with the a counter status of the processing. This value is returned by the getRecordCounter method.

Now let's look at the JavaScript required:

<script type='text/javascript'>    _ajaxConfig = {   '_cfscriptLocation':'asynchProcess.cfc',                      '_jsscriptFolder':'js',                      'debug':true}; </script> <script type='text/javascript' src='js/ajax.js'></script> <P>
<script type="text/javascript">    function doProcess() {       DWREngine._execute(_ajaxConfig._cfscriptLocation, null, 'process', '#uploadedFileName#', doProcessResult);    }              // call back function function doProcessResult (r) {       clearInterval(uploadStatus);       // appends response to div    $('processOutput').innerHTML = r;    } <cfif structKeyExists(form,"fieldNames")>    function checkProcessingStatus(){       DWREngine._execute(_ajaxConfig._cfscriptLocation, null, 'getRecordCounter', doStatusResult);    }    // call back function function doStatusResult(r) {       if (r > 0 ) {          $('processOutput').innerHTML = "Processing, please be patient...\<br\>" + r + " records processed.";       }    }              //perform checkProcessingStatus every 2000 milliseconds (2 second) uploadStatus = setInterval(checkProcessingStatus, 2000); </cfif> </script>

The first part is just a standard AjaxCFC conguration. The doProcess() function sends off the file name of the uploaded file to the doProcessResult() of my Ajax component above. The callback function for this, doProcessResult(), simply dumps the contents into a div tags with an ID of processOutput.

The next portion of script is contained within the if statement because it should not be set unless the page is actually processing a file (remember this is a self-submitting form). The checkProcessingStatus() and doStatusResult() functions simply check and display the number of records processed from the getRecordCounter() function of my Ajax component. Lastly, I create an interval of 2 seconds on which checkProcessingStatus() is called. The interval is named so that I can kill this interval when the file processing comes back in doProcessResult().

To display the results and kick off the whole processing I have a div below my form like so:

<div id="processOutput">    <cfif structKeyExists(form,"fieldNames")>    <script language="javascript">       doProcess();    </script>    <p>Processing, please be patient...</p>    </cfif> </div>

Ok, so this is probably not the best place for my doProcess() call, but like I said, this was a proof of concept.

So, when this code is all run in conjunction, you will see the number of records processed appear to increment in chunks depending on how long the processing takes. Once processing is done, the user is presented with a table containing all of the contents of their CSV file. While this specific use case may not be common, the ideas presented here would be useful in any number of cases where a long process needs to be kicked off, but you want to present some feedback to your user. I am sure you could take this code and refine it (rob?) and I would obviously do so before anything like this goes into production, but in my basic tests it seemed to be a very simple and workable solution.

Comments

dave do you happen to have an example up of this in action?

thanks

Posted By dave / Posted on 11/16/2006 at 7:30 PM


Brian Rinaldi Sorry, I don't. The one I did isn't in a public place. I also thought about putting one, but I don't want to be processing files large to require this technique on my personal shared hosting account.

Posted By Brian Rinaldi / Posted on 11/16/2006 at 8:13 PM


Write your comment



(it will not be displayed)





About

My name is Brian Rinaldi and I am the Web Community Manager for Flash Platform at Adobe. I am a regular blogger, speaker and author. I also founded RIA Unleashed conference in Boston. The views expressed on this site are my own & not those of my employer.