Tuesday, October 17, 2006

Using Microsoft's OracleClient - Tips & Tricks

The OracleClient bundled with .Net 2.0 is a very good staring point for developing applications which don't require moderately advanced features of oracle (such as proxy login). However there are some corners that I have discovered which are really useful.

Using Oracle Instant Client

The this I liked best about OracleClient is it's ability to work with Oracle Instant Client. Now as many of you know Oracle instant client is just a collection of dll's with very lenient licensing terms. All you need to do is put these files in either application directory or any other directory in system path and viola no more bulky Oracle Client installations.

Getting rid of tnsnames.ora

For instant client this is very simple; just set DataSource in connection string to -

server.domain.com:1521/ORACLE_SERVICE_NAME

For Oracle 9, you have to use a longer form to get rid of tnsnames.ora

(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=server.domain.com)(PORT=1521)))
(CONNECT_DATA=(SERVICE_NAME=ORACLE_SERVICE_NAME)))

 Rolling out a TableAdapter

.Net framework does not have a base class for TableAdapter and the adapters generated by DataSet designer for Oracle are terrible. What's more, the stored procedures and functions are not supported and code generated for INSERT, DELETE and UPDATE by analyzing SELECT never seems to be satisfactory (to me at least). So lets roll up our sieves and code a TableAdapter base class.

Imports System.Data.OracleClient

Public MustInherit Class TableAdapter(Of Table As DataTable)
    
Implements IDisposable

    
Protected WithEvents Adapter As OracleDataAdapter

    
Public Property Connection() As OracleConnection
        
Get
            Return Me.Adapter.SelectCommand.Connection
        
End Get
        Set(ByVal value As OracleConnection)
            
With Me.Adapter
                
If .SelectCommand.Connection IsNot value Then
                    .SelectCommand.Connection = value
                    
If .InsertCommand IsNot Nothing Then .InsertCommand.Connection = value
                    
If .UpdateCommand IsNot Nothing Then .UpdateCommand.Connection = value
                    
If .DeleteCommand IsNot Nothing Then .DeleteCommand.Connection = value
                
End If
            End With
        End Set
    End Property

    Public Sub New()
        
MyBase.New()
        
Me.Adapter = New OracleDataAdapter()
        
Me.InitAdapter()
    
End Sub

    Protected MustOverride Sub InitAdapter()

    
Public Overloads Function Fill(ByVal dt As Table) As Integer
        If dt Is Nothing Then Throw New ArgumentNullException("dt")
        
If Me.Adapter Is Nothing Then Throw New ObjectDisposedException(TypeName(Me))
        dt.Clear()
        
Return Me.Adapter.Fill(dt)
    
End Function
    Public Overloads Function Update(ByVal dt As Table) As Integer
        If dt Is Nothing Then Throw New ArgumentNullException("dt")
        
If Me.Adapter Is Nothing Then Throw New ObjectDisposedException(TypeName(Me))
        
Me.Adapter.Update(dt)
    
End Function
    Protected Overridable Sub OnFillError(ByVal sender As Object, ByVal e As FillErrorEventArgs)

    
End Sub
    Private Sub Adapter_FillError(ByVal sender As Object, ByVal e As FillErrorEventArgs) Handles Adapter.FillError
        
Me.OnFillError(sender, e)
    
End Sub
    Protected Overridable Sub OnRowUpdated(ByVal sender As Object, ByVal e As OracleRowUpdatedEventArgs)

    
End Sub
    Private Sub Adapter_RowUpdated(ByVal sender As Object, ByVal e As OracleRowUpdatedEventArgs) Handles Adapter.RowUpdated
        
Me.OnRowUpdated(sender, e)
    
End Sub
    Protected Overridable Sub OnRowUpdating(ByVal sender As Object, ByVal e As OracleRowUpdatingEventArgs)

    
End Sub
    Private Sub Adapter_RowUpdating(ByVal sender As Object, ByVal e As OracleRowUpdatingEventArgs) Handles Adapter.RowUpdating
        
Me.OnRowUpdating(sender, e)
    
End Sub
    Public Sub Dispose() Implements IDisposable.Dispose
        
If Me.Adapter IsNot Nothing Then
            Me.Adapter.Dispose()
            
Me.Adapter = Nothing
        End If
        GC.SuppressFinalize(Me)
    
End Sub
End
Class

I am sorry for not commenting the code but it does almost nothing and is very simple to understand. So here is the summery of code -

  • The subclass for this class must override the InitAdapter() function (how? we shall see shortly :).
  • The events supplied by OracleDataAdapter are captured and are provided as overridable functions. I added it as it helps to diagnose problem with DataAdaptor during runtime.
  • This class does NOT implements IComponent so cannot be placed on form.
  • This class does not opens up a connection but requires user to do so.
  • This class always clears the dataset before filling it.
Creating a DataSet for table

I have found that creating a dataset by hand in Dataset designer is better than letting Visual Studio generate it automatically from SELECT query. It hardly takes any time and I can experiment with names and their types. I am always irritated when VS assigns Decimal data type to primary ID columns WTF Int32/64 is more like what I had in mind </rant>. Anyway here is a class for the ubiquitous Employee table - 


Imports System.Data.OracleClient

Public Class EmployeeTableAdapter
    
Inherits TableAdapter(Of EmployeeDataSet.EmployeeDataTable)

    
Protected Overrides Sub InitAdapter()
        
' Create table mappings to associate columns in Oracle database to dataset
        Dim tableMapping As Common.DataTableMapping = New Common.DataTableMapping("Table", "Employee")
        tableMapping.ColumnMappings.Add(
"EMPLOYEEID", "Id")
        tableMapping.ColumnMappings.Add(
"EMPLOYEENAME", "Name")
        tableMapping.ColumnMappings.Add(
"DEPTNAME", "Department")
        
Me.Adapter.TableMappings.Add(tableMapping)
        
' Selecting data from the table
        Me.Adapter.SelectCommand = New OracleCommand( _
            
"SELECT EmployeeId, EmployeeName, DeptName" & vbCrLf & _
            
"FROM   TblEmployee" _
        )
        
' Creating a new employee
        Me.Adapter.InsertCommand = New OracleCommand( _
            
"INSERT INTO TblEmployee(EmployeeId, EmployeeName, DeptName)" & vbCrLf & _
            
"VALUES (SeqEmployeeId.NEXTVAL, :EmpName, :EmpDept)" & vbCrLf & _
            
"RETURNING EmployeeId INTO :EmpId" _
        )
        
With Me.Adapter.InsertCommand.Parameters
            .Add(
":EmpName", OracleType.VarChar, 0, "Name")
            .Add(
":EmpDept", OracleType.VarChar, 0, "Department")
            
' Parameters having OracleType.Number cannot be used with ParameterDirection.Output
            ' due to some but in OracleClient. However setting direction to InputOutput
            ' achieves the same thing.
            .Add(":EmpId", OracleType.Number, 0, "Id").Direction = ParameterDirection.InputOutput
        
End With
        ' Updating an employee by without concurrency checking
        Me.Adapter.UpdateCommand = New OracleCommand( _
            
"UPDATE TblEmployee SET " & vbCrLf & _
            
"       EmployeeName = :EmpName," & vbCrLf & _
            
"       DeptName     = :EmpDept," & vbCrLf & _
            
"WHERE  EmployeeId   = :EmpId" _
        )
        
' Required as we are not returning anything from database
        Me.Adapter.UpdateCommand.UpdatedRowSource = UpdateRowSource.None
        
With Me.Adapter.UpdateCommand.Parameters
            .Add(
":EmpName", OracleType.VarChar, 0, "Name")
            .Add(
":EmpDept", OracleType.VarChar, 0, "Department")
            .Add(
":EmpId", OracleType.Number, 0, "Id").SourceVersion = DataRowVersion.Original
        
End With
        ' Deleting an employee using stored procedure
        Me.Adapter.DeleteCommand = New OracleCommand("ProcDeleteEmployee")
        
Me.Adapter.DeleteCommand.CommandType = CommandType.StoredProcedure
        
Me.Adapter.DeleteCommand.Parameters.Add("EmployeeId", OracleType.Number, 0, "Id").SourceVersion = DataRowVersion.Original
    
End Sub
End
Class

Points to ponder -

  • The table in database contains columns with names EmployeeId, EmployeeName & DeptName (hmm bad design ;) which are mapped to corresponding columns in our dataset.
  • The Id column of dataset has AutoIncrement = True, AutoIncrementSeed = -1 and AutoIncrementStep = -1 which ensures that the rows inserted into local dataset to have unique id's. This is done in VS Dataset designer so it is not apparent in code.
  • When rows are inserted in database, their actual Id is updated by using the RETURNING clause (AFAIK it is exclusive to Oracle).
  • Writing queries and assigning parameters is very repetitive job so once you do it there is not much difference for other tables.
  • Delete command uses a stored procedure which accepts EmployeeId. Using it is similar to using a SQL query. but you have to remember exact parameter names.
  • Using this table adapter is similar to the table adapter generated by visual studio. Only additional this is you have to supply a connection to it.
  • When in doubt, create a table adapter in VS designer and observe its code.

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.