Filterable TreeView






2.72/5 (8 votes)
Filterable TreeView WinForm UserControl
Introduction
I will show my filterable treeview
windows control developed with C# .NET.
TreeView
shows its elements as treenode
s. 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:
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.
/// <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:
/// <summary>
/// FilteredList keeps selected nodes from filtering operation by text
///
/// </summary>
private Dictionary<int, T> FilteredList;
AddNode
function is here:
/// <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:
/// <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:
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
:
//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:
/// <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.
History
- 11.11.2018: Initial version