Synchronizing the column sizing of multiple NodeLevel layouts in an UltraTree

When using the UltraTree with multiple Columns the tree structure (defined by parent and child nodes) ensures the expected indentation of the first column (left side of the first column). However by default the Infragistics Tree does not provide functionality to synchronize the columns widths of any other column – causing the column separator which allows column sizing by the user – to hardly ever provide a through line through the whole tree view. As a multi column tree view is often compared with a hierarchical Grid control this is causing confusion and reduces the usability of multi column tree views.

The Consultingwerk.Windows.Util.UltraTreeColumnResizeHelper provides functionality to synchronize multiple NodeLevel ColumnSets of an UltraTree during the initialization of the tree view and when the user resizes columns at runtime. In the UltraTree each ColumnSet defines the layout (including width and caption) of nodes (rows) of the tree view.

First of all the UltraTree needs to be setup to display the Nodes in a Grid style. Without this setting the UltraTree would just display an ordinary tree view without the possibility to display cells and rows.

Initialize Tree - ViewStyle
oTree:ViewStyle = Infragistics.Win.UltraWinTree.ViewStyle:Grid.

In addition the ColumnHeader can wrap the text which can be important when resizing the columns. This setting can be turned on and off as follows.

Initialize Tree - ColumnHeader
oTree:ColumnSettings:ColumnHeaderWrapText = Infragistics.Win.DefaultableBoolean:True.

 

The UltraTreeColumnResizeHelper can be used in two ways.

1. All the NodeLevels have the same layout.

This setup is useful for fully recursive structures like property viewers where the same set of columns is supposed to be used for all nodes in all levels. This behavior can be seen in the Object Properties Dialog accessible from the Consultingwerk Error Message Dialog (available from OpenEdge 11.6 on). The tree view rendered by the Consultingwerk.Windows.Util.Controls.ProgressLangObjectPropertyControl displays properties of the Error objects and all referenced objects in a nested tree view:

To implement this layout, the UltraTreeColumnResizeHelper needs an initial NodeLevel layout for level 0 defined on the tree. This layout can be defined using the Visual Designer and the custom property sheet of the UltraTree.

 

Those settings will be used as a template for every new NodeLevel created by an instance of the UltraTreeColumnResizeHelper class. To have the same instance of the Helper available with each of the calls, I defined an instance variable on class level.

UltraTreeColumnResizeHelper - Definition
DEFINE PRIVATE VARIABLE oUltraTreeColumnResizeHelper AS UltraTreeColumnResizeHelper NO-UNDO.

Directly in the constructor of the Form class the class wide instance of the UltraTreeColumnResizeHelper is created. This needs to be done after the call to InitializeComponent as that is where the initial layout of the UltraTree will be defined.

UltraTreeColumnResizeHelper - Initialization
/* Initialize resizing for NodeColumnSets */
oUltraTreeColumnResizeHelper = NEW UltraTreeColumnResizeHelper (THIS-OBJECT:ultraTree1,
																175,
																"PropertyLabel":U).

The Parameters are:

Parameter name

Description
poTreeThe UltraTree to be used for resizing
piPreferredCellSizeThe prefered cell size to be applied
pcFirstColumnKeyThe reference to the first column of the tree 

For each node that is added the InitializeColumnResizing method needs to be called. This method checks if there is an existing NodelLevel layout for this level. If not, then the layout of level 0 is cloned and automatically registered for synchronization by the UltraTreeColumnResizeHelper instance.

UltraTreeColumnResizeHelper - Definition
/*------------------------------------------------------------------------------
	Purpose: Displays the properties of the passed in object in the property grid
	Notes:
	@param poObject The object to visualize
	@param poTreeNodes The parent nodes collection to add the object properties to
------------------------------------------------------------------------------*/
METHOD PROTECTED VOID DisplayObjectProperties (poObject AS Progress.Lang.Object,
											   poTreeNodes AS TreeNodesCollection):
	&IF DEFINED (AblReflection) NE 0 &THEN
	DEFINE VARIABLE oProperty AS Progress.Reflect.Property NO-UNDO .
	DEFINE VARIABLE oProperties AS Progress.Reflect.Property NO-UNDO EXTENT .
	DEFINE VARIABLE i AS INTEGER NO-UNDO .
	DEFINE VARIABLE cType AS CHARACTER NO-UNDO .
	DEFINE VARIABLE cValue AS CHARACTER NO-UNDO .
	DEFINE VARIABLE cName AS CHARACTER NO-UNDO .
	DEFINE VARIABLE oValue AS Progress.Lang.Object NO-UNDO .
	DEFINE VARIABLE oNode AS UltraTreeNode NO-UNDO .
	DEFINE VARIABLE iIndex AS INTEGER NO-UNDO .
	DEFINE VARIABLE oItemNode AS UltraTreeNode NO-UNDO .
	
	IF VALID-OBJECT (poTreeNodes:ParentNode) THEN
		oUltraTreeColumnResizeHelper:InitializeColumnResizing (poTreeNodes:ParentNode:Level + 1).
	
	oProperties = poObject:GetClass ():GetProperties(Progress.Reflect.Flags:Public:SetFlag (Progress.Reflect.Flags:Instance)) .
	
	DO i = 1 TO EXTENT (oProperties):

		ASSIGN oProperty = oProperties [i]
			   cValue = ?
			   cName = oProperty:Name
				   oValue = ?.

		/* Skip Next-Sibling and Prev-Sibling */
		IF oProperty:Name = "Next-Sibling":U OR oProperty:Name = "Prev-Sibling":U THEN
			NEXT .

		IF VALID-OBJECT (oProperty:DataType) AND oProperty:DataType <> Progress.Reflect.DataType:Object THEN
			ASSIGN cType = STRING (oProperty:DataType) .
		ELSE
			ASSIGN cType = oProperty:DataTypeName .

		CASE oProperty:DataType:
			WHEN Progress.Reflect.DataType:Object THEN DO ON ERROR UNDO, THROW:

				oValue = oProperty:Get (poObject) .

				IF VALID-OBJECT (oValue) THEN
					ASSIGN cName = SUBSTITUTE ("&1 : &2":U, cName, oValue:GetClass():TypeName)
						   cValue = oValue:ToString () .
				ELSE
					cValue = "<invalid object reference>":U .

				CATCH err AS Progress.Lang.Error :
					cValue = SUBSTITUTE ("Error: &1":U, err:GetMessage (1)) .
				END CATCH.
			END .
			OTHERWISE DO ON ERROR UNDO, THROW:

				cValue = STRING (oProperty:Get (poObject)) .

				CATCH err AS Progress.Lang.Error :
					cValue = SUBSTITUTE ("Error: &1":U, err:GetMessage (1)) .
				END CATCH.
			END.
		END CASE .

		oNode = poTreeNodes:Add (GUID) .
		oNode:Cells ["PropertyLabel":U]:Value = cName .
		oNode:Cells ["PropertyValue":U]:Value = cValue .

		IF oProperty:DataTypeName > "":U AND VALID-OBJECT (oValue) THEN
			THIS-OBJECT:DisplayObjectProperties (oValue, oNode:Nodes) .

		/* Iterate IEnumerable, but not TableModel as that would dump all records */
		IF VALID-OBJECT (oValue) AND TYPE-OF (oValue, IEnumerable) AND NOT TYPE-OF (oValue, TableModel) THEN DO:
			ASSIGN iIndex = 1 .

			{Consultingwerk/foreachABL.i Progress.Lang.Object oItem in oValue}

				oItemNode = oNode:Nodes:Add (GUID) .

				IF NOT VALID-OBJECT (oItem) THEN DO:

					oItemNode:Cells ["PropertyLabel":U]:Value = STRING (iIndex) .

					oItemNode:Cells ["PropertyValue":U]:Value = "<invalid object reference>":U .
				END.
				ELSE DO:
					DO ON ERROR UNDO, THROW:

						ASSIGN cValue = oItem:ToString () .

						CATCH err AS Progress.Lang.Error :
							cValue = err:GetMessage (1) .
						END CATCH.
					END.

					oUltraTreeColumnResizeHelper:InitializeColumnResizing (oNode:Level + 1).

					oItemNode:Cells ["PropertyLabel":U]:Value = SUBSTITUTE ("&1 : &2":U, iIndex, oItem:GetClass():TypeName) .

					oItemNode:Cells ["PropertyValue":U]:Value = cValue .

					/* Avoid infinite recursion caused by objects referencing themselves (circular references) */
					IF oNode:Level < 25 THEN
 						THIS-OBJECT:DisplayObjectProperties (oItem, oItemNode:Nodes) .
				END.
			END.
		END.
	END.
	&ENDIF
END METHOD .

 

2. There are multiple slightly different layouts having the same columns at the end of the line.

This scenario may be used, when displaying time-related information like stock flow per item and item category. There is different information displayed in the first columns but the last couple of columns of a NodeLevel layout will all be related to time or dates and be the same as in the other levels. The layout needs to be defined manually for each level on the current tree instance. Typically the maximum depth of the tree view is known upfront and each node (row) in the tree view can be created knowing the node level layout to be used here – based on the type of data shown. The UltraTreeColumnResizeHelper will not duplicate node level definitions, it will only ensure proper column separation for the common columns (rightmost columns).

To activate the synchronization for NodeLevels, the RegisterNodeLevelsForSynchronization method needs to be called.

UltraTreeColumnResizeHelper - NodeLevel based resizing
/*------------------------------------------------------------------------------
	Purpose: Populates the Tree with all Items and where they are stored
	Notes:
	@param poNodesCollection The NodesCollection of the Tree to be populated
------------------------------------------------------------------------------*/
METHOD PUBLIC VOID PopulateTree (poNodesCollection AS TreeNodesCollection):
	
	DEFINE VARIABLE oNode AS UltraTreeNode NO-UNDO.
	DEFINE VARIABLE oParentNode AS UltraTreeNode NO-UNDO.
	DEFINE VARIABLE pcKey AS CHARACTER NO-UNDO.
	DEFINE VARIABLE oUltraTreeColumnResizeHelper AS UltraTreeColumnResizeHelper NO-UNDO.

	FOR EACH Item NO-LOCK:

		oParentNode = NEW UltraTreeNode (Item.ItemNumber, Item.ItemLabel).

		poNodesCollection:ADD (oParentNode).

		FOR EACH Warehouse NO-LOCK:

			pcKey = SUBSTITUTE ("&1_&2":U, Item.ItemNumber, Warehouse.WarehouseNumber).

			oNode = NEW UltraTreeNode (pcKey, Warehouse.WarehouseName).

			oParentNode:Nodes:Add (oNode).

			oUltraTreeColumnResizeHelper:RegisterNodeLevelsForSyncronization (oNode:Level, 1, 1).

		END.

	END.

END METHOD.

The first parameters is the current NodeLevel to be synchronized with its parent. The second and third parameter define in which column the synchronization will start for each of the levels.