By Peter Bell

A Simple Iterator - No Question!

Many thanks to all who helped with the iterator. FYI, the iterator I chose including design decisions and code is below . . .

Background
I have an iterating business object that wraps a recordset with SMART generic getters and setters. They allow for information hiding (if something isn't on gettable or settable list you can't access/mutate it even if it exists in the bean) and encapsulated changes to the access/mutate code (get(whatever) will check for and call getwhatever() if you have custom code and if you don't it'll use the generic accessor).

Getter/setter code below:

<cffunction name="get" output="false" returntype="any" hint="Generic getter that confirms the property is gettable, then runs a custom getter if it exists or runs the generic getter if there isn't a custom getter for the property. I return the value requested or an empty string if the value didn't exist.">
   <cfargument name="PropertyName" type="string" required="true">
   <cfscript>
      var ReturnValue = "";
      If (variables.GettablePropertyList EQ "*" OR ListFindNoCase(variables.GettablePropertyList, arguments.PropertyName))
      {
      If (StructKeyExists(variables, "get#arguments.PropertyName#"))
      {
         // There is custom getter, so run it          ReturnValue = evaluate("variables.get#arguments.PropertyName#()");
      }
      Else
      {
         // Manually get the variable          ReturnValue = access(arguments.PropertyName);
      };
      }
      Else
      {
         ReturnValue = "";
      };
   </cfscript>
   <cfreturn ReturnValue>
</cffunction>

<cffunction name="access" output="false" returntype="any" access="private" hint="Private 'hard' getter called by the generic get method if there is no custom getter for a given property.">
   <cfargument name="PropertyName" type="string" required="true">
   <cfscript>
      var ReturnValue = "";
      If (StructKeyExists(variables.Data[variables.IteratorRecord], arguments.PropertyName))
      {
         ReturnValue = variables.Data[variables.IteratorRecord][arguments.PropertyName];
      };   
   </cfscript>
   <cfreturn ReturnValue>
</cffunction>

<cffunction name="set" output="false" returntype="boolean" hint="Generic setter that confirms the property is settable, then runs a custom setter if it exists or runs the generic setter if there isn't a custom setter for the property. I return a boolean set to 1 if the save was successful and a 0 if it wasn't.">
   <cfargument name="PropertyName" type="string" required="true">
   <cfargument name="PropertyValue" type="any" required="true">
   <cfscript>
      var ReturnValue = true;
      If (variables.SettablePropertyList EQ "*" OR ListFindNoCase(variables.SettablePropertyList, arguments.PropertyName))
      {
      If (StructKeyExists(variables, "set#arguments.PropertyName#"))
      {
         // There is custom setter, so run it          ReturnValue = evaluate("variables.set#arguments.PropertyValue#(arguments.PropertyValue)");
      }
      Else
      {
         // Manually set the variable          mutate(argumentcollection=arguments);
      };
      }
      Else
      {
         ReturnValue = false;
      };
   </cfscript>
   <cfreturn ReturnValue>
</cffunction>

<cffunction name="mutate" output="false" returntype="void" access="private" hint="Private 'hard' setter called by the generic set method if there is no custom setter for a given property.">
   <cfargument name="PropertyName" type="string" required="true">
   <cfargument name="PropertyValue" type="any" required="true">
   <cfscript>
      variables.Data[variables.IteratorRecord][arguments.PropertyName] = arguments.PropertyValue;
   </cfscript>
</cffunction>

The Recordset
You can load lots of different things into an IBO, but the most common is a query which uses the following code:

<cffunction name="loadQuery" returntype="void" access="public" output="false" hint="I take a query and use it to load the object with its data.">
   <cfargument name="Recordset" type="query" required="yes" displayname="Recordset" hint="I am the query that needs to be loaded.">
   <cfscript>
      // Based on code by Peter J. Farrell (pjf@maestropublishing.com) via cflib.org        var theQuery = arguments.Recordset;
      var theStructure = StructNew();
      var cols = ListToArray(theQuery.columnlist);
      var row = 1;
      var thisRow = "";
      var col = 1;
      If (theQuery.recordcount LT 1)
      {   
         theStructure[row] = variables.setDefaultValues(theQuery.columnlist);
         THIS.Recordset = theStructure[row];
      }
      Else
      {
         for(row = 1; row LTE theQuery.recordcount; row = row + 1)
         {
            thisRow = StructNew();
            for(col = 1; col LTE arraylen(cols); col = col + 1)
            {
               thisRow[cols[col]] = theQuery[cols[col]][row];
            }
            theStructure[row] = Duplicate(thisRow);
            THIS.Recordset = arguments.Recordset;
         }
      };
      variables.Data = theStructure;
      variables.NumberofRecords = theQuery.recordcount;
      variables.IteratorRecord = 1;
      variables.ImportedPropertyNameList = theQuery.columnlist;
      THIS.Query = theQuery;
   </cfscript>
</cffunction>

The Iterator
The iterator allows you to go to the "zeroth" record using reset() and then just to call a next() method in your cfloop for outputting. The code looks like this:

<cffunction name="reset" returntype="void" access="public" output="false" hint="I set the business object to point to before the first record so it is ready for a next() method to call the first record as part of the iterator functionality.">
   <cfscript>
      variables.IteratorRecord = 0;   
      variables.FinishedIterating = false;
   </cfscript>
</cffunction>

<cffunction name="next" returntype="boolean" access="public" output="false" hint="I increment the position of the business object iterator, returning success or failure as a boolean. If failure (already on final record), I leave the count the same.">
   <cfscript>
      var ReturnValue = "";
      If (variables.IteratorRecord GTE variables.NumberofRecords)
      {   
         ReturnValue = False;
         variables.IteratorRecord = variables.NumberofRecords;
      }
      Else
      {
         ReturnValue = True;
         variables.IteratorRecord = variables.IteratorRecord + 1;
      };
   </cfscript>
   <cfreturn ReturnValue>
</cffunction>

Usage
You use the code in the following manner. This snippet creates part of a table for displaying n-fields dynamically from a collection of m-objects:

<cfset Object.reset() />
<cfloop condition="#Object.next()#">
<tr>
   <cfloop list="#PropertyNameList#" index="PropertyName">
      <td>#Object.displayValue(PropertyName)#</td>
   </cfloop>
   <cfloop list="#AdminObjectActionList#" index="ActionName">
      <td><a href="index.cfm?filepath=#Page.get("FilePath")#&object=#Input.get("Object")#&action=#lcase(ActionName)#">#lcase(ActionName)#</a></td>
   </cfloop>
</tr>
</cfloop>

I can live with the reset() setting to zero for the next() method to work. I get that using a single method for both command (next()) and query (hasNext()) rolling them up into one isn't generally a best practice, but I feel it works fine for an iterator.

I'll release the IBO as part of the LightBase code (hey - it's coming) as I think it is a little small to be a project (even though the more I use it the more time it saves me!).

THANKS AGAIN EVERYONE FOR ALL THE HELP - MUCH APPRECIATED!

Comments
Is this the same code as the zip that is available for download here: http://www.pbell.com/index.cfm/2006/11/21/My-First...
# Posted By Aaron Roberson | 3/20/07 4:11 PM
Actually, the zipped code is newer, so both do similar things and the code "could" be identical, but I'd go with the new file if you want to see the latest implementation.
# Posted By Peter Bell | 3/20/07 4:29 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.