Click here to Skip to main content
15,921,941 members
Articles / Desktop Programming / WPF

Filling the WPF TreeView with Multilevel Hierarchy Data

Rate me:
Please Sign up or sign in to vote.
4.33/5 (5 votes)
24 Aug 2019CPOL3 min read 17.1K   1.1K   4   1
Presentation of the code which iteratively fills the WPF TreeView with multilevel hierarchy data

Sample Image - maximum width is 600 pixels

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 the GIER_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 populate TreeView. Let's assume that "GIER_Hierarchy_List" generic list is database table and therefore its contents will be loaded to datatable object for further processing in memory.
  • In the next step, all data from this generic list will be loaded into "InputDT" datatable object using foreach 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 and NodeViewModel. TreeViewModel contains "Items" field to hold the collection of NodeViewModel objects. The NodeViewModel 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 the TreeViewModel.

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, using foreach loop, the NodeViewModel objects are created for all items within specific level. These nodes are created with empty Children 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 hold NodeViewModel objects to the next iteration in which their children collection will be added.
  • At the end, TreeViewModel object is bound with datatreeView component.

C# Code

C#
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

XML
 <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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Poland Poland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
mattica29-Nov-19 18:31
mattica29-Nov-19 18:31 
The was wonderfully succinct and had to the point. Exactly what I needed! (not sure, why others gave it such a low vote).

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.