Friday, October 06, 2006

Iterate through all nodes of a Windows Forms TreeView without recursion

MSDN has an article on the same topic which uses recursion as base for iterating through nodes of a treeview. However a quick glance at tree traversal algorithms shows that an iterative approach is entirely possible by implementing a class with IEnumerator interface for the TreeNode object. So here it is -

''' <summary>
''' TreeNodesEnumerator - Can be used to iterate through treeview
''' nodes collection. The nodes are visited in depth first manner
''' i.e. all children of a node are visited before visiting siblings.
'''
</summary>
Public Class TreeNodesEnumerator
    
Implements IEnumerable(Of TreeNode)
    
Implements IEnumerator(Of TreeNode)

    
''' <summary>
    ''' Holds the first item of the TreeNodeCollection passed
    ''' to the constructor. Set to Nothing if collection is empty.
    ''' </summary>
    Private RootNode As TreeNode
    
''' <summary>
    ''' Holds the current node.
    ''' </summary>
    Private CurrentNode As TreeNode
    
''' <summary>
    ''' Tree traversal requires some kind of stack for holding the
    ''' state. In recursive algorithms, system stack is used. Here
    ''' we have to create our own stack.
    ''' </summary>
    ''' <remarks></remarks>
    Private NodeStack As Stack(Of TreeNode)

    
''' <summary>
    ''' Initializes class to start iteration over the Nodes collection
    ''' of supplied <see cref="TreeView"/> object.
    ''' </summary>
    ''' <exception cref="ArgumentNullException">
    ''' Thrown when <paramref name="tree_view"/><c>Is Nothing</c>.
    ''' </exception>
    ''' <param name="tree_view">TreeView object to iterate.</param>
    Public Sub New(ByVal tree_view As TreeView)
        
If tree_view Is Nothing Then
            Throw New ArgumentNullException("tree")
        
End If
        Me.InitClass(tree_view.Nodes)
    
End Sub
    ''' <summary>
    ''' Initializes class to start iteration over the supplied
    ''' <see cref="TreeNodeCollection"/> object.
    ''' </summary>
    ''' <exception cref="ArgumentNullException">
    ''' Thrown when <paramref name="nodes_collection"/><c>Is Nothing</c>.
    ''' </exception>
    ''' <param name="nodes_collection">
    ''' <see cref="TreeNodeCollection"/> object to iterate.
    ''' </param>
    Public Sub New(ByVal nodes_collection As TreeNodeCollection)
        
If nodes_collection Is Nothing Then
            Throw New ArgumentNullException("nodes")
        
End If
        Me.InitClass(nodes_collection)
    
End Sub
    ''' <summary>
    ''' Initializes all the variables and calls <see cref="Reset"/>.
    ''' </summary>
    ''' <param name="nodes_collection">
    ''' <see cref="TreeNodeCollection"/> object to iterate.
    ''' </param>
    Private Sub InitClass(ByVal nodes_collection As TreeNodeCollection)
        
Me.NodeStack = New Stack(Of TreeNode)
        
' If the collection is empty, assign Nothing to the RootNode
        If nodes_collection.Count > 0 Then
            Me.RootNode = nodes_collection(0)
        
Else
            Me.RootNode = Nothing
        End If
        Me.Reset()
    
End Sub
    ''' <summary>
    ''' Implements <see cref="Generic.IEnumerator.Current"/> interface.
    ''' </summary>
    ''' <exception cref="ObjectDisposedException">
    ''' Thrown if this method is called after calling <see cref="Dispose"/>().
    ''' </exception>
    ''' <value>
    ''' Current node in the <see cref="TreeNodeCollection"/>
    ''' </value>
    ''' <returns>
    ''' Current node in the <see cref="TreeNodeCollection"/>
    ''' </returns>
    Public ReadOnly Property Current() As TreeNode _
        
Implements Generic.IEnumerator(Of TreeNode).Current
        
Get
            If Me.NodeStack Is Nothing Then
                Throw New ObjectDisposedException("TreeNodesEnumerator")
            
End If
            Return Me.CurrentNode
        
End Get
    End Property
    ''' <summary>
    ''' Implements <see cref="Collections.IEnumerator.Current"/> interface.
    ''' </summary>
    ''' <exception cref="ObjectDisposedException">
    ''' Thrown if this method is called after calling <see cref="Dispose"/>().
    ''' </exception>
    ''' <value>
    ''' Current node in the <see cref="TreeNodeCollection"/>
    ''' </value>
    ''' <returns>
    ''' Current node in the <see cref="TreeNodeCollection"/>
    ''' </returns>
    Public ReadOnly Property CurrentObject() As Object _
        
Implements IEnumerator.Current
        
Get
            If Me.NodeStack Is Nothing Then
                Throw New ObjectDisposedException("TreeNodesEnumerator")
            
End If
            Return Me.CurrentNode
        
End Get
    End Property
    ''' <summary>
    ''' Implements <see cref="Collections.IEnumerator.MoveNext"/>() interface.
    ''' </summary>
    ''' <exception cref="ObjectDisposedException">
    ''' Thrown if this method is called after calling <see cref="Dispose"/>().
    ''' </exception>
    ''' <returns>
    ''' <c>True</c> if there are nodes remaining in the <see cref="TreeNodeCollection"/>
    ''' </returns>
    Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
        
If Me.NodeStack Is Nothing Then
            Throw New ObjectDisposedException("TreeNodesEnumerator")
        
End If
        ' When all nodes are visited, the stack becomes empty.
        If Me.NodeStack.Count = 0 Then Return False
        ' Get next node from stack
        Me.CurrentNode = Me.NodeStack.Pop()
        
' First push the sibling (if any) of the node on the stack.
        If Me.CurrentNode.NextNode IsNot Nothing Then
            Me.NodeStack.Push(Me.CurrentNode.NextNode)
        
End If
        ' Then push the first child (if any) of the node on the stack.
        If Me.CurrentNode.FirstNode IsNot Nothing Then
            Me.NodeStack.Push(Me.CurrentNode.FirstNode)
        
End If
        ' As stack is a FIFO, we shall get the child in next call to
        ' MoveNext. NOTE: If the breadth first iteration needs to be done
        ' (i.e. all siblings of a node are visited before children) then
        ' sequence of above two code lines should be reversed.
        Return True
    End Function
    ''' <summary>
    ''' Resets the iterator to start at beginning.
    ''' </summary>
    Public Sub Reset() Implements IEnumerator.Reset
        
If Me.NodeStack Is Nothing Then
            Throw New ObjectDisposedException("TreeNodesEnumerator")
        
End If
        ' Clear current status
        Me.NodeStack.Clear()
        
' Push the first node on the stack
        If Me.RootNode IsNot Nothing Then
            Me.NodeStack.Push(Me.RootNode)
        
End If
        ' Set CurrentNode to Nothing indicating
        ' iterator has not started yet.
        Me.CurrentNode = Nothing
    End Sub
    ''' <summary>
    ''' Implements <see cref="IDisposable.Dispose"/>() interface.
    ''' Only the internal stack is released. This function is not
    ''' really necessary but needs to be implemented as part of the
    ''' <see cref="Generic.IEnumerator"/> interface.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Dispose() Implements IDisposable.Dispose
        
' Check if the object is already disposed.
        If Me.NodeStack IsNot Nothing Then
            ' Clear the stack & set it to nothing
            Me.NodeStack.Clear()
            
Me.NodeStack = Nothing
            ' Set all other variables to nothing
            Me.RootNode = Nothing
            Me.CurrentNode = Nothing
        End If
        GC.SuppressFinalize(Me)
    
End Sub
    ''' <summary>
    ''' Implements <see cref="Generic.IEnumerable.GetEnumerator"/>() interface.
    ''' </summary>
    ''' <returns>Current object is returned.</returns>
    Public Function GetEnumerator() As Generic.IEnumerator(Of TreeNode) _
        
Implements Generic.IEnumerable(Of TreeNode).GetEnumerator
        
Return Me
    End Function
    ''' <summary>
    ''' Implements <see cref="Collections.IEnumerable.GetEnumerator"/>() interface.
    ''' </summary>
    ''' <returns>Current object is returned.</returns>
    Public Function GetEnumeratorObject() As IEnumerator _
        
Implements IEnumerable.GetEnumerator
        
Return Me
    End Function
End
Class

This class reduces the process to a simple For loop like this -

For Each n As TreeNode In New TreeNodesEnumerator(Me.TreeView1)
    
'Do Stuff
Next n

Some features of this class are -

  • The IEnumerable interface is implemented so that class can easily be used in a For loop.
  • Direction of traversal is 'Depth First' (i. e. All children of current node before its siblings) which can be changed to 'Breadth First' by simply swapping two lines in MoveNext() function.
  • Bad documentation. Well I am still learning to properly document the code :)

Implementing a search iterator

We can use this class as base for iterators of more exotic kind i. e. one which searches for certain nodes in a TreeView. Following class implements a RegEx searcher -

Imports System.Text.RegularExpressions
''' <summary>
''' NodeSearcher - Can be used to search text of all nodes
''' in a
<see cref="TreeNodeCollection"/> using regular expression.
'''
</summary>
Public Class NodeSearcher
    
Implements IEnumerable(Of TreeNode)
    
Implements IEnumerator(Of TreeNode)

    
''' <summary>
    ''' Our private guide to traverse the entire tree.
    ''' </summary>
    Private NodeEnumerator As TreeNodesEnumerator
    
''' <summary>
    ''' Holds the regular expression used for searching the tree
    ''' </summary>
    Private SearchPattern As Regex
    
''' <summary>
    ''' Initializes class to search the supplied <see cref="TreeNodeCollection"/>
    ''' object. Using the supplied <see cref="Regex"/> pattern.
    ''' </summary>
    ''' <exception cref="ArgumentNullException">
    ''' Thrown when <paramref name="tree"/><c>Is Nothing</c>.
    ''' </exception>
    ''' <param name="nodes"><see cref="TreeNodeCollection"/> object to search.</param>
    ''' <param name="search_pattern"><see cref="String"/> as
    ''' <see cref="Regex"/> pattern for searching.</param>
    Public Sub New(ByVal nodes As TreeNodeCollection, ByVal search_pattern As String)
        
Me.InitClass(nodes, New Regex(search_pattern))
    
End Sub
    ''' <summary>
    ''' Initializes class to search the supplied <see cref="TreeNodeCollection"/>
    ''' object. Using the supplied <see cref="Regex"/> pattern.
    ''' </summary>
    ''' <exception cref="ArgumentNullException">
    ''' Thrown when <paramref name="nodes"/><c>Is Nothing</c> or
    ''' <paramref name="search_pattern"/><c>Is Nothing</c>.
    ''' </exception>
    ''' <param name="nodes"><see cref="TreeNodeCollection"/> object to search.</param>
    ''' <param name="search_pattern"><see cref="Regex"/> pattern for searching.</param>
    Public Sub New(ByVal nodes As TreeNodeCollection, ByVal search_pattern As Regex)
        
Me.InitClass(nodes, search_pattern)
    
End Sub
    ''' <summary>
    ''' Initializes class to search the <see cref="TreeView.Nodes"/>
    ''' collection of supplied <see cref="TreeView"/> object. Using the
    ''' supplied <see cref="String"/> as <see cref="Regex"/> pattern.
    ''' </summary>
    ''' <exception cref="ArgumentNullException">
    ''' Thrown when <paramref name="tree"/><c>Is Nothing</c>.
    ''' </exception>
    ''' <param name="tree"><see cref="TreeView"/> object to search.</param>
    ''' <param name="search_pattern"><see cref="String"/> as
    ''' <see cref="Regex"/> pattern for searching.</param>
    Public Sub New(ByVal tree As TreeView, ByVal search_pattern As String)
        
If tree Is Nothing Then Throw New ArgumentNullException("tree")
        
Me.InitClass(tree.Nodes, New Regex(search_pattern))
    
End Sub
    ''' <summary>
    ''' Initializes class to search the <see cref="TreeView.Nodes"/>
    ''' collection of supplied <see cref="TreeView"/> object. Using the
    ''' supplied <see cref="Regex"/> pattern.
    ''' </summary>
    ''' <exception cref="ArgumentNullException">
    ''' Thrown when <paramref name="tree"/><c>Is Nothing</c> or
    ''' <paramref name="search_pattern"/><c>Is Nothing</c>.
    ''' </exception>
    ''' <param name="tree"><see cref="TreeView"/> object to iterate.</param>
    ''' <param name="search_pattern"><see cref="Regex"/> pattern for searching.</param>
    Public Sub New(ByVal tree As TreeView, ByVal search_pattern As Regex)
        
If tree Is Nothing Then Throw New ArgumentNullException("tree")
        
Me.InitClass(tree.Nodes, search_pattern)
    
End Sub
    ''' <summary>
    ''' Initializes all the variables.
    ''' </summary>
    ''' <exception cref="ArgumentNullException">
    ''' Thrown when <paramref name="nodes"/><c>Is Nothing</c> or
    ''' <paramref name="search_pattern"/><c>Is Nothing</c>.
    ''' </exception>
    ''' <param name="nodes"><see cref="TreeNodeCollection"/> object
    ''' to search.</param>
    ''' <param name="search_pattern"><see cref="Regex"/> for
    ''' searching</param>
    Private Sub InitClass(ByVal nodes As TreeNodeCollection, ByVal search_pattern As Regex)
        
If nodes Is Nothing Then Throw New ArgumentNullException("nodes")
        
If search_pattern Is Nothing Then Throw New ArgumentNullException("search_pattern")
        
Me.NodeEnumerator = New TreeNodesEnumerator(nodes)
        
Me.SearchPattern = search_pattern
    
End Sub
    ''' <summary>
    ''' Implements <see cref="Generic.IEnumerator.Current"/> interface.
    ''' </summary>
    ''' <exception cref="ObjectDisposedException">
    ''' Thrown if this method is called after calling <see cref="Dispose"/>.
    ''' </exception>
    ''' <value>
    ''' Current node matching the search criteria in <see cref="TreeNodeCollection"/>.
    ''' <c>Nothing</c> if search has not started or no matching nodes are found.
    ''' </value>
    ''' <returns>
    ''' Current searched node in the <see cref="TreeNodeCollection"/>.
    ''' </returns>
    Public ReadOnly Property Current() As TreeNode Implements _
        Generic.IEnumerator(
Of TreeNode).Current
        
Get
            If Me.NodeEnumerator Is Nothing Then
                Throw New ObjectDisposedException("TreeNodesEnumerator")
            
End If
            Return Me.NodeEnumerator.Current
        
End Get
    End Property
    ''' <summary>
    ''' Implements <see cref="Collections.IEnumerator.Current"/> interface.
    ''' </summary>
    ''' <exception cref="ObjectDisposedException">
    ''' Thrown if this method is called after calling <see cref="Dispose"/>.
    ''' </exception>
    ''' <value>
    ''' Current node matching the search criteria in <see cref="TreeNodeCollection"/>.
    ''' <c>Nothing</c> if search has not started or no matching nodes are found.
    ''' </value>
    ''' <returns>
    ''' Current searched node in the <see cref="TreeNodeCollection"/>.
    ''' </returns>
    Public ReadOnly Property CurrentObject() As Object Implements IEnumerator.Current
        
Get
            If Me.NodeEnumerator Is Nothing Then
                Throw New ObjectDisposedException("TreeNodesEnumerator")
            
End If
            Return Me.NodeEnumerator.CurrentObject
        
End Get
    End Property
    ''' <summary>
    ''' Implements <see cref="Generic.IEnumerator.MoveNext"/> interface.
    ''' </summary>
    ''' <exception cref="ObjectDisposedException">
    ''' Thrown if this method is called after calling <see cref="Dispose"/>.
    ''' </exception>
    ''' <returns>
    ''' <c>True</c> if there are nodes to be searched.
    ''' <c>False</c> if all nodes have been searched.
    ''' </returns>
    Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
        
If Me.NodeEnumerator Is Nothing Then
            Throw New ObjectDisposedException("TreeNodesEnumerator")
        
End If
        ' Go through the treenodecollection by using the iterator
        Do While Me.NodeEnumerator.MoveNext()
            
' If current node matches search pattern then halt search
            ' and indicate that a match is found.
            If Me.SearchPattern.IsMatch(Me.NodeEnumerator.Current.Text) Then
                Return True
            End If
        Loop
        ' Entire collection has been searched so return false
        Return False
    End Function
    ''' <summary>
    ''' Implements <see cref="Collections.IEnumerator.Reset"/> interface.
    ''' </summary>
    Public Sub Reset() Implements IEnumerator.Reset
        
If Me.NodeEnumerator Is Nothing Then
            Throw New ObjectDisposedException("TreeNodesEnumerator")
        
End If
        ' Call reset of underlying iterator.
        Me.NodeEnumerator.Reset()
    
End Sub
    ''' <summary>
    ''' Implements <see cref="Generic.IEnumerator.Dispose"/> interface.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Dispose() Implements IDisposable.Dispose
        
' If iterator is not already disposed then
        If Me.NodeEnumerator IsNot Nothing Then
            ' Dispose the iterator.
            Me.NodeEnumerator.Dispose()
            
' Clear up variables.
            Me.NodeEnumerator = Nothing
            Me.SearchPattern = Nothing
            GC.SuppressFinalize(Me)
        
End If
    End Sub
    ''' <summary>
    ''' Implements <see cref="Generic.IEnumerable.GetEnumerator"/> interface.
    ''' </summary>
    ''' <returns>Current object is returned.</returns>
    Public Function GetEnumerator() As Generic.IEnumerator(Of TreeNode) _
        
Implements Generic.IEnumerable(Of TreeNode).GetEnumerator
        
Return Me
    End Function
    ''' <summary>
    ''' Implements <see cref="Collections.IEnumerable.GetEnumerator"/> interface.
    ''' </summary>
    ''' <returns>Current object is returned.</returns>
    Public Function GetEnumeratorObject() As IEnumerator _
        
Implements IEnumerable.GetEnumerator
        
Return Me
    End Function
End
Class

Well this is a very simple class to search through nodes collection using a For loop like this -


Using searcher As New NodeSearcher(Me.TreeView1, Me.TextBox1.Text)
    
For Each n As TreeNode In searcher
        
'Do Stuff
    Next n
End Using

This class can be extended to have MovePrevious() function by adding a List(Of TreeNode) for storing old results and having an index variable to keep track of position (index < 0 is before start, index >= size of list is search for next node in treeview). Then it can be used as search tracker in applications where user can search through treeview.

No comments: