How to Add Outline Treeviews to a WinForms Datagrid Application
One of the unique and popular features of the FlexGrid for WinForms control is the ability to add hierarchical grouping to regular unstructured data. To achieve this, the C1FlexGrid introduces the concept of Node rows. Node rows are almost identical to regular rows, except for the following:
- Node rows are not data-bound. When the grid is bound to a data source, each regular row corresponds to an item in the data source. Node rows do not. Instead, they exist to group regular rows that contain similar data.
- Node rows can be collapsed or expanded. When a node row is collapsed, all its data and child nodes are hidden. Users can collapse and expand nodes using the mouse or the keyboard if the outline tree is visible. If the outline tree is not visible, nodes can only be expanded or collapsed using code.
For example, suppose you had a grid showing product, country, city, and sales amounts. This typical grid would normally look like this:
All the information is there, but it is hard to see the total sales for each country or customer. You could use the C1FlexGrid's outlining features to group the data by country (level 0), then by city within each country (level 1), then by product within each city (level 2).
Here is the same grid after adding the outline:
This grid shows the same information as the previous one (it is bound to the same data source), but it adds a tree where each node contains a summary of the data below it. Nodes can be collapsed to show only the summary or expanded to show the detail. Note that each node row can show summaries for more than one column (in this case, total units sold and total amount).
This blog will walk you through turning a regular data-bound grid into a richer outline grid.
Interested in what other features ComponentOne offers? Download a FREE trial today!
Creating Outlines
In this section, we will learn about the different built-in ways like Grouping and Subtotal method FlexGrid provides to easily create outlines or tree grids even with unstructured regular data. The following section will discuss how we can manually create outlines in Flexgrid.
Grouping
Grouping refers to organizing the grid data into a tree-like structure where rows with common column values are displayed as a group. Each group header is a Node row and lets you expand or collapse the groups to facilitate easier grid data analysis. In FlexGrid, grouping can be achieved through code and user interaction via the column context menu and FlexGridGroupPanel control.
If we want to create the outline grid we showed above, we will have to do two things, show aggregates for the columns Cost and Quantity and Group the data by Country and City.
For aggregates, you can set the Aggregate property of the Column to a value of AggregateEnum, and the FlexGrid will show the aggregate value in the Group header accordingly. In the case of the grid shown above, we will show the SUM of the quantity and Cost columns using the code below:
c1FlexGrid1.Cols["Quantity"].Aggregate = C1.Win.C1FlexGrid.AggregateEnum.Sum;
c1FlexGrid1.Cols["Cost"].Aggregate = C1.Win.C1FlexGrid.AggregateEnum.Sum;
Now that you have set the aggregates, you can add grouping by columns Country and City using the Code below:
c1FlexGrid1.HideGroupedColumns = true;
c1FlexGrid1.GroupDescriptions = new List<C1.Win.C1FlexGrid.GroupDescription> {
new C1.Win.C1FlexGrid.GroupDescription("Country"),
new C1.Win.C1FlexGrid.GroupDescription("City") };
Our grid will show the same outline as the grid shown above.
You can refer to the FlexGrid Documentation for more details about different methods used to do grouping.
Using Subtotal Method
You can also create trees using the C1FlexGrid's Subtotal method. The Subtotal method also performs the grouping and adds aggregates, except it does both in a single step. The code below shows how you can use the Subtotal method to accomplish the same thing we did before:
// group and total by country and city
_flex.Subtotal(C1.Win.C1FlexGrid.AggregateEnum.Sum, 0, "Country", "Cost");
_flex.Subtotal(C1.Win.C1FlexGrid.AggregateEnum.Sum, 0, "Country", "Quantity");
_flex.Subtotal(C1.Win.C1FlexGrid.AggregateEnum.Sum, 1, "City", "Cost");
_flex.Subtotal(C1.Win.C1FlexGrid.AggregateEnum.Sum, 1, "City", "Quantity");
// hide columns that are grouped on
// (they only have duplicate values which already appear on the tree nodes)
// (but do not make them invisible, that would also hide the node text)
_flex.Cols["Country"].Width = 0;
_flex.Cols["City"].Width = 0;
_flex.AllowMerging = C1.Win.C1FlexGrid.AllowMergingEnum.Nodes;
//show outline tree
_flex.Tree.Column = 0;
_flex.Tree.Show(1);
The Subtotal method is very convenient and flexible. It has several overloads that allow you to specify which columns should be grouped on and totaled on by index or by name, whether to include a caption in the node rows that it inserts, how to perform the grouping, and so on.
You can refer to the FlexGrid Documentation for more information on subtotals and customizing subtotals etc.
Creating Outline Manually
Creating Node Rows
Apart from the method explained above for creating outlines, you can also create outlines in Grid manually by creating node rows manually. Node rows can be created in three ways:
- Use the Rows.InsertNode method. This inserts a new node row at a specified index. Once the node row has been created, you can use it like any other row (set the data for each column, apply styles, etc.). This is the low-level way of inserting totals and building outlines. It gives the most control and flexibility and is demonstrated below.
- If the grid is unbound, you can turn regular rows into node rows by setting the IsNode property to true. Note that this only works when the grid is unbound. Trying to turn a regular data-bound row into a node will cause the grid to throw an exception.
The code below shows how you could implement a GroupBy method that inserts node rows grouping identical values on a given column.
void GroupBy(string columnName, int level)
{
object current = null;
for (int r = _flex.Rows.Fixed; r < _flex.Rows.Count; r++)
{
if (!_flex.Rows[r].IsNode)
{
var value = _flex[r, columnName];
if (!object.Equals(value, current))
{
// value changed: insert node
_flex.Rows.InsertNode(r, level);
//set subtotal0 style to the node row
_flex.Rows[r].Style = _flex.Styles[CellStyleEnum.Subtotal0];
// show group name in first scrollable column
_flex[r, _flex.Cols.Fixed] = value;
// update current value
current = value;
}
}
}
}
The code scans all the columns, skipping existing node rows (so it can be called to add several levels of nodes), and keeps track of the current value for the column being grouped on. When the current value changes, a node row shows the new in the first scrollable column.
Now, to create the same grid as our first example, you could use this method to create a two-level outline by calling:
private void groupByCountryCity_Click(object sender, EventArgs e)
{
GroupBy("Country", 0);
GroupBy("City", 1);
}
Quite simple, but there are some caveats.
- The method assumes that the data is sorted according to the outline structure. In this example, if the data were sorted by Product instead of by Country, the outline would have several level-0 nodes for each country, which is not what you want.
- Also, the GroupBy method may insert many rows, which would cause the grid to flicker. To avoid this, you would normally use the BeginUpdate method before making the updates and EndUpdate when done.
- You might have noticed that the node rows are created as expected, but the outline tree is not visible, so you cannot expand and collapse the nodes. We will set the Tree Column, explained later in the “Outline tree” section.
To handle these issues, the code that creates the outline should be re-written as follows:
private void groupByCountryCity_Click(object sender, EventArgs e)
{
_flex.BeginUpdate();
ResetBinding();
GroupBy("Country", 0);
GroupBy("City", 1);
_flex.EndUpdate();
}
private void ResetBinding()
{
// re-bind grid
_flex.DataSource = null;
_flex.DataSource = _ordersData;
_flex.Tree.Column = 0;
// resize columns to fit their content
_flex.AutoSizeCols();
}
Adding Subtotals
So far, we have covered the creation of node rows and outline trees. However, to make the outlines useful, the node rows should include summary information for the data they contain. If you create an outline tree using Grouping or the Subtotal method, then the subtotals are added automatically. Suppose you created the outline tree using the Rows.InsertNode method as described above, you need to use the Aggregate method to calculate the subtotals for each group of rows and insert the result directly into the node rows.
The Subtotal method listed below shows how to do this:
void AddSubtotals(int level, string colName)
{
// get column we are going to total on
int colIndex = _flex.Cols.IndexOf(colName);
// scan rows looking for nodes at the right level
for (int r = _flex.Rows.Fixed; r < _flex.Rows.Count; r++)
{
if (_flex.Rows[r].IsNode)
{
var node = _flex.Rows[r].Node;
if (node.Level == level)
{
// found a node, calculate the sum of extended price
var range = node.GetCellRange();
var sum = _flex.Aggregate(AggregateEnum.Sum,
range.r1, colIndex, range.r2, colIndex,
AggregateFlags.ExcludeNodes);
// show the sum on the grid
// (will use the column format automatically)
_flex[r, colIndex] = sum;
}
}
}
}
This method scans the grid rows looking for node rows. When a node row of the desired level is found, the method uses the Node.GetCellRange method to retrieve the node's child rows. Then it uses the Aggregate method to calculate the sum of the values on the target column over the entire range. The call to Aggregate includes the ExcludeNodes flag to avoid double-counting existing nodes. Once the subtotal has been calculated, it is assigned to the node row's cell with the usual _flex[row, col] indexer. In this example, we will add totals for the Quantity and Cost columns. In addition to sums, you could add other aggregates such as average, maximum, minimum, etc.
We can use this method to create a complete outline with node rows, outline tree, and subtotals:
private void groupByCountryCity_Click(object sender, EventArgs e)
{
_flex.BeginUpdate();
// restore original sort (by Country, City, SalesPerson)
ResetBinding();
// group by Country, City
GroupBy("Country", 0); // group by country (level 0)
GroupBy("City", 1); // group by city (level 1)
// hide columns that we grouped on
// (they only have duplicate values which already appear on the tree nodes)
// (but don't make them invisible, that would also hide the node text)
_flex.Cols["Country"].Width = 0;
_flex.Cols["City"].Width = 0;
// allow node content to spill onto next cell
_flex.AllowMerging = AllowMergingEnum.Nodes;
// add totals per Country, City
AddSubtotals(0, "Cost"); // cost per country (level 0)
AddSubtotals(0, "Quantity"); // quantity per country (level 0)
AddSubtotals(1, "Cost"); // cost per city (level 1)
AddSubtotals(1, "Quantity"); // quantity per city (level 1)
// show outline tree
_flex.Tree.Column = 0;
_flex.AutoSizeCol(_flex.Tree.Column);
_flex.Tree.Show(1);
_flex.EndUpdate();
}
The result will be something like the following:
You can refer to the sample here.
Outline Tree
The outline tree is like the one you see in a regular TreeView control. It shows an indented structure with collapse/expand icons next to each node row so the user can expand and collapse the outline to see the desired level of detail.
Now you might have noticed we have used the Tree.Column property in the above section shows an outline tree because the outline tree can be displayed in any column defined by the Tree.Column property. By default, this property is set to -1, which causes the tree not to be displayed at all.
The Tree property returns a reference to a GridTree object that exposes several methods and properties used to customize the outline tree. The main ones are listed below:
- Column: Gets or sets the index of the column that contains the outline tree. Setting this property to -1 causes the outline tree to be hidden from the users.
- Indent: Gets or sets the indent, in pixels, between adjacent node levels. Higher indent levels cause the tree to become wider.
- Style: Gets or sets the type of outline tree to display. Use this property to determine whether the tree should include a button bar at the top to allow users to collapse/expand the entire tree, whether lines and/or symbols should be displayed, and whether lines should be displayed connecting the tree to data rows as well as node rows.
- LineColor: Gets or sets the color of the tree's connecting lines.
- LineStyle: Gets or sets the style of the tree's connecting lines.
For example, by changing the code for our example outline tree to include the following lines:
// show outline tree
_flex.Tree.Column = 0;
_flex.Tree.Style = TreeStyleFlags.CompleteLeaf;
_flex.Tree.LineColor = Color.Blue;
_flex.Tree.Indent = 30;
The outline tree would change as follows:
Notice the buttons labeled "1", "2", and "*" on the top-left cell. Clicking these buttons would cause the entire tree to collapse or expand to the corresponding level. Also, notice the much wider indentation and the lines connecting the tree to regular rows ("Boston Crab Meat", "Gnocchi di nonna Alice", etc.) as well as to node rows.
Outline Maintenance
So far, we have discussed how to create outlines with trees and totals using the high-level Grouping and Subtotal method and lower-level Rows.InsertNode and Aggregate methods. At this point, it is important to remember that the outline tree is created based on the data but is not bound to it in any way and is not automatically maintained when there are changes to the grid or the data. For example, if the user modifies a value in the "ExtendedPrice" column, the subtotals will not automatically update. If the user sorts the grid, the data will be refreshed, and the subtotals will disappear.
There are two common ways to maintain the outlines:
- Prevent the user from making any changes that would invalidate the outline. This is the easiest option. You would set the grid's AllowEditing, AllowDragging, and AllowSorting properties to false and prevent any changes affecting the outline
- Update the outline when there are changes to the data or the grid. You would attach handlers to the grid's AfterDataRefresh, AfterSort, and AfterEdit events and re-generate the outline appropriately
Option 2 is usually more interesting since it provides a quick and simple tool for dynamic data analysis. This approach is illustrated by the Analyze sample provided with the C1FlexGrid. The sample creates an initial outline and allows users to reorder the columns. When the column order changes, the sample automatically re-sorts the data and re-creates the outline. The user can easily create simple reports showing sales by country, product, salesperson, etc.
Using Node Class
The Node class provides several methods and properties that can be used to create and manage outline trees. Many of these methods and properties are based on the standard TreeView object model, so they should be familiar to most developers. To obtain a Node object, you can either: Use the return value of the Rows.InsertNode method:
var node = _flex.Rows.InsertNode(index, level);
Or you can retrieve the node for an existing row using the row's Node property:
var node = _flex.Rows[index].IsNode
? _flex.Rows[index].Node
: null;
Either way, once you have a Node object, you can manipulate it using the following properties and methods:
- Level: Gets or sets the node level in the outline tree
- Data: Gets or sets the value in the cell defined by Node.Row and the Tree.Column
- Image: Gets or sets the image in the cell defined by Node.Row and the Tree.Column
- Checked: Gets or sets the check state of the cell defined by Node.Row and the Tree.Column
- Collapsed/Expanded: Gets or sets the node's collapsed/expanded state
You can also explore the outline structure using the following methods:
- GetCellRange(): Gets a CellRange object that describes the range of rows that belong to this node
- Children: Gets the number of child nodes under this node
- Nodes: Gets a node array containing this node's child nodes
- GetNode: Gets the node with a given relationship to this node (parent, first child, next sibling, and so on)
The discussion above focused on bound scenarios, where the grid is attached to a data source that provides the data. You can also create trees and outlines in unbound scenarios. Things are more straightforward in this case since you can turn any row into a node row by setting its IsNode property to true. If the grid is unbound, it owns all the data displayed, and you do things that are impossible when a data source owns the data. For example, you can move nodes around the tree using the Node.Move method as shown by the TreeNodesample provided with the C1FlexGrid. Using nodes in an unbound grid is like using nodes in a regular TreeView control.
You can check out the FlexGrid product samples here and the documentation here.
Interested in what other features ComponentOne offers? Download a FREE trial today!