Monthly Archives: August 2010

Hierarchical Data Grids

I recently had cause to create a grid that showed rows with parent, child, and grandchild rows. Each generation’s text is indented to show the relationships, and rows with children have a plus or minus to show and hide them. While the AdvancedDataGrid can create virtual parent rows for unique values, it is not built to show actual parent and child rows.

Sample Application with source: http://flex.santacruzsoftware.com/HierarchicalDataGrids/

One way to manage parent-child relationships is to store all the raw data in one list and use a different list to display the data. Each row must indicate if they are parents and, if so,  are their children are visible. In my case, those flags were already present in the grid’s data row object (ReportRow), alternatively, one could use external lists and messages to hold this information (see The Ultimate Checkbox List Pattern). The row objects have a collection of child rows as a property, and a value that indicates what generation (level) they are.

public function onExpand(event : Event) : void
{
	this.refreshExpandedItems();
}

public function refreshExpandedItems() : void
{
	var prevSelection : Object = reportGrid.selectedItem;
	var prevScrollPosition : uint = reportGrid.verticalScrollPosition;

	this.reportData.refreshDisplayList();

	reportGrid.selectedItem = prevSelection;
	reportGrid.verticalScrollPosition = prevScrollPosition;
}

The grid’s ItemRenderer shows an expansion icon (e.g. “+” and “-“), handles indenting the text to show each row’s level, and throws a bubbling event whenever the user clicks the expansion icon. The dataChange method (or the overriden data mutator) handles indentation as well as which rows should have expansion icons.

private function onDataChange(event : Event) : void
{
	if (this.data != null)
	{
		var theRow : ReportRow = this.data as ReportRow;

		nameLabel.text = theRow.name;
		nameLabel.x = 20 + (theRow.level * INDENT_PER_LEVEL);

		if (theRow.children.length > 0)
		{
			expansionLabel.visible = true;

			if (theRow.isExpanded)
				expansionLabel.text = "-"
			else
				expansionLabel.text = "+"
		}
		else
			expansionLabel.visible = false;
	}
}

private function onExpansionClick(event : Event) : void
{
	var theRow : ReportRow = this.data as ReportRow;

	theRow.isExpanded = !theRow.isExpanded;

	var expandEvent : ExpandEvent =
		new ExpandEvent(ExpandEvent.EXPAND_PARENT, true);
	expandEvent.row = theRow;

	this.dispatchEvent(expandEvent);
}

When this event (or some other application code) marks a row as newly expanded or an expanded row as collapsed, the data object refreshes the entire display list by emptying it and iterating through the entire raw-data list and copying each parent and, if expanded, its children to the display list. Depending on one’s needs, one can recursively check the children for nested grandchildren, etc.

I created a separate class to hold and manage the data as a grid-specific model. The container holding the grid creates an instance of the data object, and that object not only manages the raw and display lists and also parses the result from a server into grid-specific row objects. It also has a bindable array collection of row objects to display; this is what the grid uses as its displayProvider.

While Flex’s update event model will likely avoid thrashing the display, one can use displayList.disableAutoUpdate() or binding with an explicit event and dispatching an explicit event to ensure that the DataGrid refreshes only once after the display list changes

[Bindable(event="displayListChange")]
public var displayList : ArrayCollection = null;

public function refreshDisplayList() : void
{
	displayList.removeAll();

	for(var counter : uint = 0; counter < rawDataList.length; counter++)
		addRow(rawDataList.getItemAt(counter) as RowData);

					//	signal that all changes are complete
	this.displatchEvent(new Event("displayListChange"));

					//	nested function
	function addRow(aRow : RowData, generationNumber : uint = 0) : void
	{
				//	put any client-side filtering tests here

		aRow.displayIndentLevel = generationNumber;
		displayList.addItem(aRow);

		if (aRow.isExpanded)
		{
			var theChildRow : RowData;

			for(var childCounter : uint = 0; childCounter < aRow.children.length; childCounter++)
			{
				theChildRow = aRow.children.getItemAt(childCounter) as RowData;

				addRow(theChildRow, generationNumber + 1);	//	recursive
			}
		}
	}
}

Sorting and Filtering

The raw data list’s sort function needs to sort without breaking the relationship between each row, its parent, and its children. One can either sort the highest level and leave the descendants unsorted; one can sort each of the generations relatives to its siblings. To filter display rows, simply omit rows from the display list rather than using a filter function on either the raw data list or the display list.

Performance

displayList.addItem(aRow) inserts a reference to the row object in the rawDataList ; it does not create a copy of the actual data. Moving a few hundred references around does not take a lot of time. Compared to redrawing the grid itself, managing the references is instantaneous.

Paging Hierarchical Data

Many Flex applications using grids show only part of the total data set (no one actually wants to see 10,000 rows). In this vein, one might want to load the children of a row if and only if the user expands it. The rawDataList will retain any children once downloaded, so repeated expansion and collapse will not overload the server, but getting the child rows the first time requires a specific protocol.

In my case, the server allowed me to get the report data passing flag indicating ParentsOnly (which gets only the top-level rows) or criteria to specify one particular top-level entity (which I used without the ParentsOnly flag to get the descendants). As I don’t get the children until needed, the item renderers need an isParent property in each item (to replace the test theRow.children.length > 0) so knows they whether to show the expansion icon at all.

The EXPAND_PARENT event listener checks the event’s ReportRow isExpanded and children == null If both are true, it has to retrieve the children from the server, insert them into the ReportRow.children property, and then call refreshExpandedItems(). This requires at least one asynchronous step, so the COMPLETE listener (as well as the fault handler) needs to clear any “Please Wait” messages. If the row is collapsing or already has its children, the listener can simply call refreshExpandedItems().