package comp { import mx.collections.Sort; import mx.collections.SortField; import mx.controls.DataGrid; import mx.controls.dataGridClasses.DataGridColumn; import mx.core.mx_internal; import mx.events.DataGridEvent; import mx.formatters.DateFormatter; import mx.utils.ObjectUtil; use namespace mx_internal; /** * PROBLEM STATEMENT: * Have you ever tried using a dataField on a DataGridColumn like "object.property"? Normally * this causes a RTE because when Flex tries to use the dataField path to get the correct label * value, it cannot handle nested object or XML properties. * * Even if we were able to mitigate this threat by using a labelFunction, the same problem * exists when we try to sort. Flex once again is not able to resolve "object.property" to do a * proper comparison. To solve this problem, you would normally use a combination of * labelFunctions and sortFunctions, or just opt for using itemRenderers. While these are both * valid solutions, NestedDataGrid attempts to solve these problems without spending extra time * and effort developing these "work-around" solutions. * * FEATURES: * Nested Properties: * NestedDataGrid solves any problems with nested properties and makes it so that you * can not only use "object.property" as a dataField property, but you can even use * "object.propertyList[2]", and it will still be handled correctly. The sorting not * only works with Numbers and Strings, but with Date objects as well. * Custom Date Formatting: * If you pass in a dateFormatter with a formatString specified, the labels of * nested date properties will maintain that format. * XML Support: * NestedDataGrid allows for nested XML dataFields as well. You can use either of * the following formats: "xml.property", "xml.xmlList[3]", or xml.@attribute in * various nested depths. * * In the Flex docs, the following is said about the dataField property which outlines my * strategy: "The name of the field or property in the data provider item associated with the * column. Each DataGridColumn control requires this property and/or the labelFunction property * to be set in order to calculate the displayable text for the item renderer. If the dataField * and labelFunction properties are set, the data is displayed using the labelFunction and * sorted using the dataField. If the property named in the dataField does not exist, the * sortCompareFunction must be set for the sort to work correctly." * * LABEL PROBLEM -- * Flex uses a itemToLabel function to determine what the label in a cell should be. For * some reason, by default in the ListCollectionView, sort.findItem is used to discover * what the label should be based on using the dataField property. However, sort is unable * to resolve nested properties. To address this, I created a default labelFunction which * crawls through the nested property chain using introspection to grab the correct value * and display it back to the user. * * SORTING PROBLEM -- * When first addressing this, I thought the limitation was in the "dataField" property of * DataGrid, but instead, the problem exists in SortField, which is what "dataField" uses to * perform a sort. In order to get around this, I use a simple Sort object without passing * it any SortField references. On the Sort object I give it a sortCompareFunction which * lets me do the sorting manually rather than letting the Flex framework try to do it for * me and crash along the way. * * @author: Nate Ross (http://natescodevault.com) */ public class NestedDataGrid extends DataGrid { /** * A custom DateFormatter which formats any Date instances so that they match the format in * dateFormatter's formatString property. This property must be set in order for it to work * properly. */ public var dateFormatter:DateFormatter; /** * A private reference to the column that is being sorted (onHeaderRelease sets this * property) */ protected var sortColumn:DataGridColumn; /** * Ascending/descending setting for the currently sorted column */ protected var desc:Boolean = false; /** * Constructor */ public function NestedDataGrid() { super(); addEventListener(DataGridEvent.HEADER_RELEASE, onHeaderRelease); } /** * Here we prevent the DataGrid class from interacting with the DataGridEvent.HEADER_RELEASE * event. Instead we handle it ourselves to set up the proper sorting behavior. * * @param event DataGridEvent.HEADER_RELEASE type */ protected function onHeaderRelease(event:DataGridEvent):void { if (!event.isDefaultPrevented()) { // Prevent the default Flex DataGrid behavior. event.preventDefault(); sortByColumn(event.columnIndex); } } /** * This function was basically taken from DataGrid.sortByColumn, except for a few * modifications to support the sorting of nested properties. * * @param index The columnIndex for the column we wish to sort. */ protected function sortByColumn(index:int):void { var c:DataGridColumn = columns[index]; desc = c.sortDescending; sortIndex = index; var nestedProperty:Boolean = isNestedProperty(c.dataField); // do the sort if we're allowed to if (c.sortable) { var s:Sort = collection.sort; if (nestedProperty) { s = getNestedSort(c); } else { s = getDefaultSort(s, c); } sortDirection = (desc) ? "DESC" : "ASC"; c.sortDescending = desc; // set the grid's sortIndex lastSortIndex = sortIndex; sortColumn = c; collection.sort = s; collection.refresh(); } } /** * Returns a default sort object (without a nested dataField). This uses the same logic as * is found in Flex's DataGrid. * * @param s the current sort object. * @param c the DataGridColumn to sort. * * @return the default sort object. */ protected function getDefaultSort(s:Sort, c:DataGridColumn):Sort { var f:SortField; if (s) { // analyze the current sort to see what we've been given var sf:Array = s.fields; if (sf) { for (var i:int = 0; i < sf.length; i++) { if (sf[i].name == c.dataField) { // we're part of the current sort f = sf[i] // flip the logic so desc is new desired order desc = !f.descending; break; } } } else { s = new Sort(); f = new SortField(c.dataField); desc = false; } } if (!f) { s = new Sort(); f = new SortField(c.dataField); // if you have a labelFunction you must supply a sortCompareFunction f.compareFunction = null; desc = false; s.fields = [f]; } f.descending = desc; s.fields = [f]; s.compareFunction = null; return s; } /** * Returns a sort object which takes advantage of the nested property logic. This function * is only executed when a column with a nested dataField is sorted. * * @param c the DataGrid column to sort. * * @return the nested-friendly sort object. */ protected function getNestedSort(c:DataGridColumn):Sort { var s:Sort = new Sort(); s.compareFunction = c.sortCompareFunction; if (lastSortIndex == sortIndex) { desc = !c.sortDescending; } else { desc = false; } return s; } /** * @override * * If any of the columns being passed in require special "nested" treatment, then set up the * locally defined labelFunction and sortCompartFunction unless they have already been * defined. * * @param value The new column values passed in from the user. */ override public function set columns(value:Array):void { for each (var c:DataGridColumn in value) { if (isNestedProperty(c.dataField)) { if (c.labelFunction == null) { c.labelFunction = nestedLabelFunction; } if (c.sortable) { c.sortCompareFunction = nestedPropertySortFunction; sortColumn = c; } } } super.columns = value; } /** * @param itemA The first object to compare * @param itemB The second object to compare * @param fields Array of SortFields being compared. Since we are not using SortFields, this * will be null. * * @return -1 if itemA < itemB. 0 if itemA = itemB. 1 if itemA > itemB. */ protected function nestedPropertySortFunction(itemA:*, itemB:*, fields:Array = null):int { var value1:* = getNestedPropertyValue(sortColumn.dataField, itemA); var value2:* = getNestedPropertyValue(sortColumn.dataField, itemB); if (value1 is XMLList) { value1 = String(value1); } if (value2 is XMLList) { value2 = String(value2); } var value:int = ObjectUtil.compare(Object(value1), Object(value2)); // If we are sorting descended, then reverse the returned values. if (sortColumn.sortDescending) { value = value * -1; } return value; } /** * The default label function for any nested properties. * * @param item An item that the label will be derived from. * @param col The corresponding column for the label. * * @return The label to be displayed in the cell of the DataGrid. */ protected function nestedLabelFunction(item:*, col:DataGridColumn):String { var value:* = getNestedPropertyValue(col.dataField, item); if (value is Date) { if (dateFormatter != null) { return dateFormatter.format(value as Date); } else { return (value as Date).toDateString(); } } else { if (value != undefined && value != null) { return String(value); } } return ""; } /** * This function is used to recursively drill down into the dataField to retrieve the nested * property value. * * It will accept "object.property" or "object.propertyList[2].property" etc. * * @param propRef The property chain found in the dataField * @param object The actual object that we need to get the property value from * * @return The property value. At this point we don't care what type it is (Number, String, * Date, etc.) */ protected function getNestedPropertyValue(propRef:String, object:*):* { // Split the propRef into an Array delimited by "." // i.e. info.properties[0][1].value is delimited to an array containing // [info, properties[0][1], value] var propPath:Array = propRef.split("."); for each (var level:String in propPath) { // Does the level have brackets? If so, we need to navigate down the bracketed property chain. if (level.indexOf("[") > -1 && level.indexOf("]") > -1) { // Grab everything that matches the property pattern. // i.e. properties[0][1] will create an Array for each subLevel --> // ["properties","0","1"] var subPath:Array = level.match(/(\w+)/g); // Traverse the subLevel. for each (var subLevel:String in subPath) { if (object != null) { object = object[subLevel]; } } } else { if (object != null) { // Traverse the level. object = object[level]; } } } return object; } /** * This function determines if a property chain has nested properties by looking to see * if it contains either a "." or a "[" * * @param propRef The property chain for the dataField i.e. object.property[1] * * return true if the property chain has nested elements, false otherwise. */ protected function isNestedProperty(propRef:String):Boolean { if (propRef && (propRef.indexOf(".") > -1 || propRef.indexOf("[") > -1)) { return true; } return false; } } }