WPF TreeGrid using a DataGrid





5.00/5 (11 votes)
Presenting the concept of a TreeGrid control that utilizes the DataGrid
Introduction
This article presents a way to create a TreeGrid
, TreeListView
, or whatever you want to call it, using the built in WPF DataGrid
. There are many advantages to using the built in DataGrid
; however, there are some shortcomings as well, namely performance using the method presented herein.
Disclaimer
This article is intended to be a proof of concept and not a fully deployable / tested solution. In fact, there are a lot of functions that you will find just stubbed out and not implemented. I deemed them not necessary for the overall presentation of the concept. Additionally, there most likely are still bugs in my implementation.
Background
With all these years gone by and still not a single built in TreeGrid
control for WPF from Microsoft. Seriously, what the heck? There are numerous attempts that can be found on Codeproject and online in general to create a TreeGrid
control that can display hierarchical data in a grid like fashion. After doing the research, all of the controls I have encountered in the wild have many more shortcomings than what I think is presented here.
The major shortcomings of the controls that are found online are listed below:
- The control reinvents the wheel. This means that the control is built from the ground up and as a result, misses major functionality. To be a complete control in my mind, it would have to provide all of the same feature set as a
DataGrid
: Selection, multiselection, columns, column resizing, column hiding, the crazy amount of customization through templating, etc.. Sadly, people generally build these controls for their specific use case and don't implement the entire feature set of theDataGrid
due to time. - The control uses a composite of existing controls. For example, trying to convert a
TreeView
or aListView
into aTreeGrid
using clever control templates. Since the TreeView
andListView
are notDataGrids
... they end up missing a massive number of customization features that theDataGrid
offers. - UI virtualization. All of the above methods do not work very well with UI virtualization. If you add a giant number of items, the view control items actually get created in the background. I have actually tried adding items to a
DataGrid
before without virtualization and allowed it to create aDataGrid
row with all of the child controls for every data item. I was able to create 100,000 items before my 32 bit process ran out of memory, and it took 'minutes' to create all of those items. TheDataGrid
rows did not have a complicated control hierarchy either.
I looked at many professional third party controls such as the XCeed WPF DataGrid
. This DataGrid
seems to have a hierarchical feature, I think called master views or something; however, the master views feature requires you to pay for the upgraded version. The only control that I encountered that was from a professional company and free was the syncfusion TreeGrid
controls. I tested these out, and while they worked, I decided to not use them due to a few issues. The first was that, I was seriously irked by the fact that I had to install their "studio" crap (which was a large install) just to get access to the DLL I was looking for. Secondly, once I got to using the controls in code, it just seemed like they were hacked together garbage. Like I said, the controls worked, they just smelled awful in the code. Any other third party controls that I found were not free.
So with the above said, wouldn't it be nice if we could reuse the DataGrid
in a semi efficient way to create a TreeGrid
? It is a very sophisticated control that has built in support for UI virtualization, selection, columns, and templating.
There is another tactic for attempting to create a TreeGrid
using a DataGrid
that I encountered online. The DataGrid
has a concept of grouping where you can nest child DataGrids
in a group to create a tree like effect. I assure you that this is not what this article will present. This method has a host of issues, the main one being it just doesn't scale. What if you have a deep hierarchy of tree nodes? Each tree node would have to have its own DataGrid
.
Disadvantages
I am going to start with the main disadvantages of the approach in this article, so you know up front what you are getting. Using the DataGrid
in the way I will present is not how the designers of the DataGrid
intended for it to be used. As a result, there are several performance issues due to the internal WPF implementation.
- Loading lots of "root" tree elements up front with a lot of child elements that are initially in the "expanded" state takes awhile.
- After some basic performance testing, about 10,000 items can be loaded on initialization without seeing time delays. Don't hold me to that number.
- About 100,000 items overall can be in the open or expanded state in the tree allowing for nodes to be expanded or collapsed without seeing too much of a time delay. Don't hold me to that number.
Advantages
So with the above disadvantages, I would wager that this implementation is still going to get you better results than a hacked TreeView
or ListView
that doesn't support UI virtualization. So... if you have a dataset
that is 'sorta small' sorta not, I think this solution will work. My use case fits this description exactly. Without going and yelling at the WPF team to fix some internal implementation issues, this is what we have to work with.
- The solution gives you the power of the
DataGrid
control 'minus' theDataGrid
grouping feature. I'm sure you could find a way to use the grouping feature, I don't know.. but displaying hierarchical data in aTreeGrid
kinda negates the need to use grouping feature in theDataGrid
. - UI virtualization due to using the
DataGrid
. - Lazy loading of tree items is supported.
AlternationCount
works. If you look at the screenshot at the top or run the above demo, you will notice that the alternation count works. Ever tried to set an alternation count on aTreeView
control? Some items in theTreeView
will not get the correct index based on where they are in the tree hierarchy.Treeview
items do not have a concept of overall placement within the parentTreeView
, so the best theTreeView
can do is reset the index here or there.
Implementation
Without further ado, let's discuss how we can trick the DataGrid
into displaying a tree hierarchy. So let's look at the problem here. A tree structure or model generally has roots at the bottom and each root can have a recursive set of children. The DataGrid
control has no concept of hierarchy and it just wants a list of items to display. So therefore, is it possible to convert a tree hierarchy into a list of items; a FLAT list of items?
The answer to the previous question is yes. This is called flattening the model. In my implementation, the flattened tree hierarchy is known as the "FlatModel
".
Here is how it works. My implementation starts at what's called the TreeGridModel
. The TreeGridModel
allows root tree elements to be added to it. Each tree element can have a recursive set of children. Whenever roots or children are added to parent tree elements, the TreeGridModel
is notified using internal methods. The TreeGridModel
then constructs what is known as the TreeGridFlatModel
based on the events.
When a TreeGridElement
is expanded or collapsed, the TreeGridModel
is again notified and updates the TreeGridFlatModel
accordingly.
The major components of the model are summarized below:
TreeGridElement
: Exposes several dependency properties that can be bound to from theDataGrid
. The most important property is theIsExpanded
property. When this is changed, theTreeGridModel
is notified so it can update theTreeGridFlatModel
. TheTreeGridElement
also manages some internal properties such as its parent in the hierarchy and level in the hierarchy. TheLevel
property of the element can be used to implement the spacing in the column.TreeGridModel
: The model allows rootTreeGridElements
to be added to it. All roots are 'shown' or added to theTreeGridFlatModel
but may not be in an expanded state showing child elements. The model updates theTreeGridFlatModel
when it handles various events coming from childTreeGridElements
or the model itself.TreeGridFlatModel
: This is just a wrapper around an observable collection to implement some internal methods. Notably, this can be bound to from theDataGrid
; however, it cannot be modified by the user, because its an internal collection that theTreeGridModel
uses to produce the flattened tree hierarchy.
So instead of explaining how a flattened model is produced from the hierarchical tree model, it's probably better that you go and explore and step through the code yourself. Keep in mind that the above demo only has add, expand, and collapse functionality implemented for proof of concept. Additionally, I want to note that this is 'my' implementation of how to produce a flattened model from a hierarchy, there could be a billion other more efficient ones.
Performance
I want to keep this section brief. In essence, my implementation of producing a flat model from the hierarchical model relies heavily on collection range functions. What is the one thing that is missing from observable collections? - Range functions. When items are expanded or collapsed, a range of child items needs to be added or removed from the FlatModel
, and due to the WPF implementation, this operation cannot be batched. This makes it very non performant. Go yell at the WPF team for assuming that nobody would ever want range functions in an observable collection.
Note
You can inherently add range functions to an observable collection or even implement your own, the issue comes into play when a WPF ItemsControl
binds to a collection that implements INotifyCollectionChanged
. If your event passes more than one item added or removed from the collection at a time, you get a nice exception: "Range operations are not supported."
Usage
To use the TreeGridModel
, you basically have to create it and then the public property FlatModel
can be bound to using a DataGrid's ItemsSource
property. Once this is done, you can modify the hierarchy by adding, expanding, or collapsing nodes.
The TreeGridElement
serves as the base class for concrete TreeGrid
items to be implemented by the user. This means that to create your own items, you derive from TreeGridElement
and start adding fields that can be bound to using a DataGrid
. To implement lazy loading, you can use the IsExpanding
event to load children into a node. There is also a property called "HasChildren
" that can be bound to either show or hide the little toggle button for each node.
In the demo, I create a DataGrid
with the first column being a template column. I then create the layout of how I want to display nodes in the cell. Each cell in the first column has a grid with 3 columns. The first column is used to add whitespace so that the TreeItem
child toggle button is properly positioned based on the item's Level
property. The second column is used to show the TreeItem
child toggle button. Here, I just use a CheckBox
, since it is an easy toggle button; however, you could create an arrow. The third column is used to display the item's name by binding to the Name
property.
History
- 11/2/17: Article inception
- 11/3/17: Fixed some formatting and grammar mistakes. Fixed download link.