Filling the WPF TreeView with Multilevel Hierarchy Data






4.33/5 (5 votes)
Presentation of the code which iteratively fills the WPF TreeView with multilevel hierarchy data
Introduction
The code fills WPF TreeView
with a multilevel hierarchical data. It steps through all the levels of hierarchy, from top to bottom and adds to each item a collection of its children. The source of data is a self-referencing collection of items with the following fields: Id of Item, Name of Item, Id of parent. The code supports the parent/child hierarchy with one root. Each parent can have numerous children, but a child can have only one parent.
Background
The presented code was written to fill TreeView
UI with the hierarchical data from database. To simplify the example, the database and relevant entity framework's classes were replaced by generic list. The main goal was to create the TreeView
UI using basic, fully controllable statements.
Using the Code
Description of Declared Classes and Objects
GIER_Hierarchy
class and theGIER_Hierarchy_List
generic list were created only to define some examples of hierarchical data, in this case, fictional organizational company structure. It will be our source of data to populateTreeView
. Let's assume that "GIER_Hierarchy_List
" generic list is database table and therefore its contents will be loaded todatatable
object for further processing in memory.- In the next step, all data from this generic list will be loaded into "
InputDT
"datatable
object usingforeach
statement. - "
InputDT
"datatable
object reflects the same structure as the "GIER_Hierarchy_List
" list and is used to process the data in memory. During single iteration, "InputDT
"datatable
is joined with "sub
"datatable
using LINQ statement to select rows with children for specific level of hierarchy in the subsequent iterations. "sub
" datatable is an auxiliary object with similar structure as "InputDT
"datatable
, but contains additional "Level
" column. It allows to select only a specific level of hierarchy in the relevant iteration. - There are two special classes to define nodes of the tree:
TreeViewModel
andNodeViewModel
.TreeViewModel
contains "Items
" field to hold the collection ofNodeViewModel
objects. TheNodeViewModel
object contains the following 4 fields: The element ID (numeric type), element name (string types), expand property (bool type, to define whether node should be expanded or collapsed at the start) and the fourth field is the collection of children of the item. - "
NodesFactory
" is a recursive function that accepts three parameters: "InputDT
"datatable
, "sub
"datatable
, level number. This function contains the entire logic to step through all the levels of hierarchy iteratively to find collections of children and create items of theTreeViewModel
.
Overall Description of How the Code Works
- First, the "
InputDT
"datatable
object is queried using LINQ statement to find the root item. The results are loaded to "sub
"datatable
object and at the same time, the level of hierarchy number 0 is assigned. Further processing takes place inside the "NodesFactory
" function. Joining with "sub
"datatable
and filtering the relevant hierarchy level number makes it possible to find children of the nodes created in the previous iteration. Using recursive function, LINQ statement, joining with "sub
"datatable
and assigning relevant level of hierarchy and then filtering by relevant level ,"InputDT
"datatable
is iteratively queried to find all items for specific level of hierarchy. In each individual iteration, one hierarchy level is processed. During single iteration, usingforeach
loop, theNodeViewModel
objects are created for all items within specific level. These nodes are created with emptyChildren
field first. This field will be filled in the next iteration. In the current iteration, created nodes are added to the children collection field of the nodes created in previous iteration. "List
<nodeviewmodel>nodesList
" list is created to holdNodeViewModel
objects to the next iteration in which theirchildren
collection will be added. - At the end,
TreeViewModel
object is bound withdatatreeView
component.
C# Code
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Windows;
namespace TreeView_DataHierarchy
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public void NodesFactory(DataTable InptDT, DataTable subDT, long level)
{
level = level + 1;
var querryLevel =
from itemsInputDT in InptDT.AsEnumerable()
join itemSub in subDT.AsEnumerable() on
itemsInputDT.Field<long>("GparentId") equals itemSub.Field<long>("Gid")
where itemSub.Field<long>("level") == level - 1
select new { Gid = itemsInputDT.Field<long>("Gid"),
Gname = itemsInputDT.Field<string>("Gname"),
GparentId = itemsInputDT.Field<long>("GparentId"), level = level };
foreach (var x in querryLevel)
{
DataRow rowSub = subDT.NewRow();
rowSub["Gid"] = x.Gid;
rowSub["Gname"] = x.Gname;
rowSub["GparentId"] = x.GparentId;
rowSub["level"] = x.level;
subDT.Rows.Add(rowSub);
nodesList.Add(new NodeViewModel { Id = x.Gid, Name = x.Gname,
Expand = true, Children = new ObservableCollection<NodeViewModel>() });
nodesList.Find(gNode => gNode.Id == x.GparentId).Children.Add
(nodesList.Last());
}
if (querryLevel.Count() > 0)
{ NodesFactory(InptDT, subDT, level); }
}
public event PropertyChangedEventHandler PropertyChanged;
List<NodeViewModel> nodesList = new List<NodeViewModel>();
TreeViewModel MyTreeModel = new TreeViewModel();
public MainWindow()
{
//generic list comprising hierarchical data- source to fill TreeViewModel
List<GIER_Hierarchy> GIER_Hierarchy_List = new List<GIER_Hierarchy>();
GIER_Hierarchy_List.Add(new GIER_Hierarchy(1, "root", 0));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(2, "Production B", 7));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(12, "Document Control", 17));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(17, "Engineering", 7));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(16, "Executive", 1));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(14, "Facilities and Maintenance", 7));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(10, "Finance", 16));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(9, "Human Resources", 16));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(11, "Information Services", 4));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(4, "Marketing", 16));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(7, "Production", 16));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(8, "Production Control", 7));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(5, "Purchasing", 18));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(13, "Quality Assurance", 7));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(6, "Research and Development", 16));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(3, "Sales", 16));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(15, "Shipping and Receiving", 18));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(19, "Tool Design", 2));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(18, "Logistic", 16));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(20, "Logistic A", 18));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(21, "Logistic A1", 20));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(22, "Logistic A1", 21));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(23, "Logistic A1", 22));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(24, "Logistic A1", 23));
GIER_Hierarchy_List.Add(new GIER_Hierarchy(25, "Logistic A1", 24));
if (GIER_Hierarchy_List.Count > 0)
{
//datatable to load data from GIER_Hierarchy_List
DataTable InputDT = new DataTable();
InputDT.Columns.Add("Gid", typeof(long));
InputDT.Columns.Add("Gname", typeof(string));
InputDT.Columns.Add("GparentId", typeof(long));
//loading the data from to datable
foreach (var x in GIER_Hierarchy_List)
{
DataRow rowInput = InputDT.NewRow();
rowInput["Gid"] = x.Gid;
rowInput["Gname"] = x.Gname;
rowInput["GparentId"] = x.GparentId;
InputDT.Rows.Add(rowInput);
}
//querying the datable to find the row with root as first level of hierarchy
var queryRoot =
from itemsInputDT in InputDT.AsEnumerable()
where itemsInputDT.Field<long>("GparentId") == 0
select new { Gid = itemsInputDT.Field<long>("Gid"),
Gname = itemsInputDT.Field<string>("Gname"),
GparentId = itemsInputDT.Field<long>("GparentId"), level = 1 };
//creating auxiliary datatable for iteratively querying
//in the subsequent levels of hierarchy
DataTable sub = new DataTable();
sub.Columns.Add("Gid", typeof(long));
sub.Columns.Add("Gname", typeof(string));
sub.Columns.Add("GparentId", typeof(long));
sub.Columns.Add("level", typeof(long));
//looping through the result of the query to load the
//result of the query to auxiliary datable and adding nodes to list of nodes
foreach (var x in queryRoot)
{
DataRow rowSub = sub.NewRow();
rowSub["Gid"] = x.Gid;
rowSub["Gname"] = x.Gname;
rowSub["GparentId"] = x.GparentId;
rowSub["level"] = 0;
sub.Rows.Add(rowSub);
//creating NodeViewModel and adding to the list of NodeViewModel object list
nodesList.Add(new NodeViewModel
{
Id = x.Gid,
Name = x.Gname,
Expand = true,
Children =
new ObservableCollection<NodeViewModel>()
});
}
//adding Collection of NodeViewModel object with the root to TreeModel
MyTreeModel.Items =
new ObservableCollection<NodeViewModel> { nodesList.Last() };
long level = 0;
NodesFactory(InputDT, sub,level);
}
else { MessageBox.Show("List of Directory is empty"); }
InitializeComponent();
// binding TreeView component with TreeModel
GIER_catalogHierarchy.ItemsSource = MyTreeModel.Items;
}
}
public class GIER_Hierarchy
{
public long Gid { get; set; }
public string Gname { get; set; }
public long GparentId { get; set; }
public GIER_Hierarchy(long xGid, string xGname, long xGparentId)
{
this.Gid = xGid;
this.Gname = xGname;
this.GparentId = xGparentId;
}
}
public class TreeViewModel
{
public ObservableCollection<NodeViewModel> Items { get; set; }
}
public class NodeViewModel : INotifyPropertyChanged
{
public long Id { get; set; }
public string _Name;
public bool _Expand;
public string Name
{
get { return _Name; }
set
{
if (_Name != value)
{
_Name = value;
NotifyPropertyChanged("Name");
}
}
}
public bool Expand
{
get { return _Expand; }
set
{
if (_Expand != value)
{
_Expand = value;
NotifyPropertyChanged("Expand");
}
}
}
public ObservableCollection<NodeViewModel> Children { get; set; }
#region INotifyPropertyChanged Members
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}
WPF Code
<xmp>
<Window x:Class="TreeView_DataHierarchy.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TreeView_DataHierarchy"
xmlns:src="clr-namespace:TreeView_DataHierarchy"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TreeView x:Name="GIER_catalogHierarchy" BorderThickness="1"
DataContext="{Binding TreeModel}" SelectedValuePath="Id"
MinWidth="300" MinHeight="440" Width="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
Margin="1,1,1,1" Height="Auto">
<TreeView.Resources>
<HierarchicalDataTemplate
DataType= "{x:Type local:NodeViewModel}" ItemsSource="{Binding Children}" >
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="Line" Width="20" />
<ColumnDefinition x:Name="Rectangle" />
<ColumnDefinition x:Name="Empty1" Width="10" />
<ColumnDefinition x:Name="Department" />
</Grid.ColumnDefinitions>
<!--drawing Connecting Lines -->
<!-- Horizontal Lines -->
<Border Grid.Column="0" x:Name="HorLn" Margin="9,0,0,2"
HorizontalAlignment="Stretch" Height="1"
BorderThickness="0,0,0,1" VerticalAlignment="Bottom">
<Border.BorderBrush>
<LinearGradientBrush StartPoint="0,0"
EndPoint="2,0" SpreadMethod="Repeat" MappingMode="Absolute">
<GradientStop Color="Transparent" Offset="0" />
<GradientStop Color="Transparent" Offset="0.499" />
<GradientStop Color="#999" Offset="0.5" />
</LinearGradientBrush>
</Border.BorderBrush>
</Border>
<!-- Vertical Lines -->
<Border Grid.Column="0" x:Name="VerLn" Margin="0,0,1,2"
Grid.RowSpan="2" VerticalAlignment="Stretch"
Width="1" BorderThickness="0,0,1,0" >
<Border.BorderBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,2"
SpreadMethod="Repeat" MappingMode="Absolute">
<GradientStop Color="Transparent" Offset="0" />
<GradientStop Color="Transparent" Offset="0.499" />
<GradientStop Color="#999" Offset="0.5" />
</LinearGradientBrush>
</Border.BorderBrush>
</Border>
<!--drawing catalog shapes-->
<Rectangle Grid.Column="1" Width="14" Height="10"
Stroke="Gold" SnapsToDevicePixels="true" VerticalAlignment="Center">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,2" StartPoint="0.5,0">
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="Gold" Offset="0.5"/>
<GradientStop Color="Honeydew" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Column="1" Width="7" Height="4"
Stroke="Gold" SnapsToDevicePixels="true" VerticalAlignment="Top"
HorizontalAlignment="Left" Fill="White" />
<!--textblock to display the names of catalogs-->
<TextBlock x:Name="textBlockHeader" Grid.Column="3"
Grid.Row="1" Text="{Binding Name}" FontSize="9" />
</Grid>
</HierarchicalDataTemplate>
<!--triger to expand nodes-->
<Style TargetType="{x:Type TreeViewItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Expand}" Value="True">
<Setter Property="IsExpanded" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
</xmp>
History
- 24th August, 2019: Initial version