Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Filterable TreeView

2.72/5 (8 votes)
11 Nov 2018CPOL4 min read 14.6K  
Filterable TreeView WinForm UserControl

filtering treeview control

Introduction

I will show my filterable treeview windows control developed with C# .NET.

TreeView shows its elements as treenodes. So we cannot filter it like that gridlist. It should show all parents of filtered node which contain keyword and it should be shown in hierarchy. If child node contains the filter keyword, parent nodes must be displayed even they don't contain the filter keyword. If we don't want to keep them in hierarchy, there are no difficulties to display as list. The main purpose is that filter operation musn't disrupt the appearance of hierarchy.

Background

If we want to only search the keyword, there is no need for my work because search operation can be carried out by recursive search or some other way and we can list the searched data in a basic list such as listbox after search operation.

Technically speaking, TreeView .NET Control uses TreeNodeCollection to show and enumerate.

If we inherit the TreeView control to override Node.Add() function, we must inherit the TreeNodeCollection class. But TreeNodeCollection has a constructor which cannot be inherited. So we couldn't. The enthusiasts can re-create the TreeNodeCollection from .NET Framework source code.

I followed a short and easy way to catch AddNode event. I created my AddNode function to catch.

Using the Code

My Control is created as generic type. I created TreeNodeEx class to construct hierarchy and to keep non-duplicate node by using unique NodeId.TreeNodeEx class derived from TreeNode class. It has two extra properties which are NodeId and ParentNodeId.

When I want to enumerate the nodes, I use these properties. I also create the hierarchy by these properties.

If you want to extend or adjust the TreeNodeEx class, you can do so easily. For example, you can add the Image property and it can be shown in the treeview. So I left it generic typed. If you implement it by concrete class, you can use it at design time. Because it is generic typed, it is not shown in Visual Studio toolbox. But I added a sample concrete typed TreeView which is shown in Visual Studio toolbox.

NodeId and ParentNodeId are key values for my way. If you don't want to play with their values, you don't have to do this. Then, you must set the hierarchy for yourself like that Form3 example.

I created three FilterableTreeView examples. There are differences in the ways of adding node.

Form1 Example

There must already exist NodeId and ParentNodeId which provides hierarchy.

Form2 Example

There must already exist NodeId and ParentNodeId which provides hierarchy.

There is one difference here. When adding the childNode to parentNode, this uses ParentNodeId instead of ParentNode(TreeNodeEx).

In the above two examples, auto NodeId generating feature was not used.

Form3 Example

There must be hierarchical typed collection such as TreeNodeCollection. But it doesn't have to have NodeId and ParentNodeId.

For example; my hierarchical typed collection here:

C#
public class CategoryClass
 {
     public string Name { get; set; }
     public List<CategoryClass> SubCategories { get; set; }
 }

But there is the most important thing AddNode function must be used when adding the node for the above three ways.

FilterableTreeView SourceCode

Now I show you FilterableTreeView source code.

For Design, it is easy to understand that it consists of one textbox and one TreeView control.

At first, we add all nodes to the TreeDictionary collection when adding the nodes by using AddNode function. When filtering tree nodes (TreeNodeEx), we will use this collection for faster searching operation. By using this collection, we can avoid using recursive search. As you know, recursive call depth cannot be known. So it can be costly for the time.

C#
/// <summary>
/// All treeview node will be kept in this dictionary type collection
///
/// </summary>
public readonly Dictionary<int, T> TreeDictionary;

Here is FilteredList Dictionary typed collection:

C#
/// <summary>
/// FilteredList keeps selected nodes from filtering operation by text 
///
/// </summary>
private Dictionary<int, T> FilteredList;

AddNode function is here:

C#
/// <summary>
/// AddNode function provides that adds the child node to the parent node or root of treeview
/// If you want, you may set your child node id from db by primary key or autoincrement field
/// Every node must have unique nodeid and have parentnodeid  
/// </summary>
/// <param name="parentNodeId">parentNodeId is NodeId of parent node of the child node.
///If the node is root node, parent node should be null.</param>
/// <param name="child">child is currently being added node</param>
/// <returns>return NodeId of the currently being added child node</returns>
public int AddNode(int parentNodeId, T child)
{
  lock (LOCK_OBJECT)//Provides multithread safe
  { 
    if (child.NodeId < 0)
   {
     child.NodeId = TreeDictionary.Count+1;
    }
    TreeDictionary.Add(child.NodeId, child);
    
    if (parentNodeId == 0)//Root node
    {
      child.ParentNodeId = 0;
      treeView.Nodes.Add(child);
    }
    else//Child node
   {
     T tmp = GetTNode(parentNodeId);
     child.ParentNodeId = tmp.NodeId;
     tmp.Nodes.Add(child);
   }
   return TreeDictionary.Count+1;   
}  

Overloaded AddNode function is also here:

C#
/// <summary>
/// AddNode function provides that adds the child node to the parent node or root of treeview
/// If you want, you may set your child node id from db by primary key or autoincrement field
/// Every node must have unique nodeid and have parentnodeid  
/// </summary>
/// <param name="parent">parent is parent node of the child node.
/// If the node is root node, parent node should be null.</param>
/// <param name="child">child is currently being added node</param>
/// <returns>return NodeId of the currently being added child node</returns>
public int AddNode(T parentNode, T child)
{
  lock (LOCK_OBJECT)//Provides multithread safe
 { 
    if (child.NodeId < 0)
   {
     child.NodeId = TreeDictionary.Count+1;
  }  
    TreeDictionary.Add(child.NodeId, child);
    if (parentNode == null || parentNode.NodeId == 0)//Root node
   {
      child.ParentNodeId = 0;
     treeView.Nodes.Add(child);
    } 
   else//Child node
   {
      child.ParentNodeId = parentNode.NodeId;
     parentNode.Nodes.Add(child);         
    }
   return TreeDictionary.Count+1;
  }
}

FilterTextBox changed event fire:

C#
private void FilterTextBox_TextChanged(object sender, EventArgs e)
 {
    //Check the filtertextbox text is not null and length bigger than 3
    //Also if the filtertextbox has no char, then it should show all nodes without filtering
    if (FilterTextBox.Text.Trim() == String.Empty || FilterTextBox.Text.Length < 3)
    {
         if (FilterTextBox.Text.Trim() == String.Empty)
         {
             LoadData(TreeDictionary);
         }
         return;
    }

     //Create new filteredlist for Every filter text changed
     //So old filteredlist will be empty
     FilteredList = new Dictionary<int, T >();

     //easy to check text of all nodes that contain filter text by looping
     //Also, we need to add deep copy of the node to the filteredlist
     //Because we shouldn't try to add a node that is already added to the treeview
     foreach (T item in TreeDictionary.Values)
     {
         if (item.Text.Contains(FilterTextBox.Text) == true)
         {

             FilteredList.Add(item.NodeId, CloneExist(item));
         }
     }

     //Now we got all nodes which contains filter text
     //But we need to add all parent nodes which do not exist in the filteredlist
     //Example let's assume path is Electronic\Computer\Notebook and filter text is 'book'
     //Then, treeview should show fullpath of Notebook like above
     //If this node is already root node, there is nothing to do
     for (int i = 0; i < FilteredList.Values.Count; i++)
     {
         T tmp = FilteredList.Values.ToList()[i];
         while (true)
         {
             if (tmp.ParentNodeId == 0) break;
             else
             {
                 T parent = GetTNode(tmp.ParentNodeId);
                 if (FilteredList.ContainsKey(parent.NodeId) == false)
                 {
                     FilteredList.Add(parent.NodeId, CloneExist(parent));
                 }
                 tmp = parent;
             }
         }
     }
     //After populating all nodes, it is time to show nodes on the treeview
     LoadData(FilteredList);
 }

After Filtering Operation shows it in the treeview:

C#
//Data to TreeView
     private void LoadData(Dictionary< int, T> pairs)
     {
         treeView.Nodes.Clear();
         treeView.BeginUpdate();//For speeding up
         foreach (T item in pairs.Values)
         {
             if (item.ParentNodeId == 0)
             {
                 treeView.Nodes.Add(item);
             }
             else
             {
                T tmp = pairs.Values.Where(c => c.NodeId == item.ParentNodeId).FirstOrDefault();
                 if (tmp.Nodes.Contains(item) == false)
                 {
                     tmp.Nodes.Add(item);
                 }
             }
         }
         treeView.EndUpdate();
         treeView.ExpandAll();
     }

Finally, here is the local function to clone TreeNodeEx object:

C#
  /// <summary>
 /// If you added another property you should assign them new Instance Of T at here
 /// </summary>
  /// <param name="ex">Currently shown node on the treeview</param>
 /// <returns>new created TreeNodeEx object</returns>

public T CloneExist(T ex)
{
       T treeNode = (T)Activator.CreateInstance(typeof(T));
      treeNode.Text = ex.Text;
      treeNode.NodeId = ex.NodeId;
      treeNode.ParentNodeId = ex.ParentNodeId;
      return treeNode;
}

Points of Interest

TreeView control has some differences. It also is a dynamic control. It may have non-deterministic data. Its data display feature and search feature are slightly different from other controls.

I can say that if you want to add further features to the TreeView control, you should think of re-creating your custom TreeView because there are some limitations for inheriting the .NET TreeView control.

Also github source code

History

  • 11.11.2018: Initial version

License

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