A Simple Iterator - No Question!
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:
<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:
<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:
<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:
<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!


