Hopefully this is not a repeat question. I looked and found similar questions but not sufficient in this case.
I have many tree view controls, and can traverse the Nodes recursively for various reasons.
However, I often need to traverse the Nodes as if they are in a List.
I would like to make a function that creates a Generic.List(of TreeNode) from the Nodes collection, without recursion if at all possible.
(Without recursion purely for the exercise of doing it without recursion - I understand it may not be possible)
This function would save alot of time with repeated use across a massive solution, where the coders could use a simple For Each paradigm to traverse the Nodes.
I have seen a technique to 'flatten' the Nodes collection using C#, which uses LINQ and recursion, but I am not sure that the syntax can be converted to VB.NET cleanly. So if there are any clever VB functions out there can that I can mold to this task - would be very helpful.
There are many similar questions and very informative answers on SO, like this one:
Enumerating Collections that are not inherently IEnumerable?
...which highlights stack overflow errors in very deep trees using some algorithms. I hope that a method that does not use recursion will not suffer from Stack overflow errors - however, I am prepared that it might be long and clumsy and slow.
I am also prepared for the answer that 'It is not possible to do this without recursion'. However, I would like to confirm or deny this claim using the power of SO (this forum)
It's possible, and not very hard at all....
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
TreeView1.ExpandAll()
For Each TN As TreeNode In TreeView1.NodesToListWithoutRecursionBecauseWhyNot(TraverseType.BreadthFirst)
Debug.Print(TN.Text)
Next
End Sub
End Class
Public Module Extensions
Public Enum TraverseType
BreadthFirst
DepthFirst
End Enum
<Runtime.CompilerServices.Extension()> _
Public Function NodesToListWithoutRecursionBecauseWhyNot(ByVal TV As TreeView, Optional ByVal Traverse As TraverseType = TraverseType.DepthFirst) As List(Of TreeNode)
Dim nodes As New List(Of TreeNode)
Select Case Traverse
Case TraverseType.BreadthFirst
Dim Q As New Queue(Of TreeNode)
For Each TN As TreeNode In TV.Nodes
Q.Enqueue(TN)
Next
While Q.Count > 0
Dim TN As TreeNode = Q.Dequeue
nodes.Add(TN)
For Each subTN As TreeNode In TN.Nodes
Q.Enqueue(subTN)
Next
End While
Case TraverseType.DepthFirst
Dim L As New List(Of TreeNode)
For Each TN As TreeNode In TV.Nodes
L.Add(TN)
Next
While L.Count > 0
Dim TN As TreeNode = L.Item(0)
L.RemoveAt(0)
nodes.Add(TN)
For i As Integer = TN.Nodes.Count - 1 To 0 Step -1
L.Insert(0, TN.Nodes(i))
Next
End While
End Select
Return nodes
End Function
End Module
Just add the nodes to the list but at the same time keep the position of the last node you processed. A node is considered process when its immediate children are added to the list.
Public Function GetAllNodes(ByVal topNode As TreeNode)
Dim allNodes As New List(Of TreeNode)
Dim lastProcessPosition As Integer = 0
allNodes.Add(topNode)
Do While lastProcessPosition < allNodes.Count
allNodes.AddRange(allNodes(lastProcessPosition).Nodes)
lastProcessPosition += 1
Loop
Return allNodes
End Function
If you don't have a top node then just substitute the parameter for a list of nodes to start with.
Public Function GetAllNodes(ByVal topNodes As TreeNodeCollection)
Dim allNodes As New List(Of TreeNode)
Dim lastProcessPosition As Integer = 0
allNodes.AddRange(topNodes)
Do While lastProcessPosition < allNodes.Count
allNodes.AddRange(allNodes(lastProcessPosition).Nodes)
lastProcessPosition += 1
Loop
Return allNodes
End Function
I'm not sure if a check for Nothing must be done on the Nodes property before using it.
Note: I was able to remove this for loop and replace it with AddRange
'For Each node As TreeNode In allNodes(lastProcessPosition).Nodes
' allNodes.Add(node)
'Next
Related
I am attempting to learn Entity Framework to try to move on from Linq to SQL, and my attempt to convert some code over failed on a many-to-many recursive table (a tree structure). I need to do a full table read and prepare the tree in memory because recursing through the database with a lot of queries is too slow.
I have a database with a Projects table, and another table called ProjectsTree. With Linq to SQL, I can get access to the ProjectsTree table, but not with Entity Framework. It puts it in an association in a way that I can't seem to query this table directly.
Here's the code before I attempted to convert Linq to SQL over to Entity Framework, and it worked. Maybe I should stick with Linq to SQL and not learn something new, and if there is no way to do this, I may go backward, or let the two co-exist.
Private Class cProjectTree2
Public Project As PDMVault.Project
Public ChildTree As List(Of cProjectTree2)
End Class
''' <summary>
''' Gets the complete PDM project tree that can be added to a tree node.
''' Each node has the description in the node text field and the primary key in the tag.
''' </summary>
''' <returns></returns>
Public Function GetPDMProjectTree() As TreeNode
' To generate the tree, first read the projects table with a complete table scan, then the ProjectTree with a complete table scan.
' Create a dictionary of objects of type cRecursiveProjectTree, but only the project is set on the first pass, with a reference to it based on its key.
' After the dictionary is built, then link up children to parent using the dictinary.
' Finally, use the created tree to create the node structure for the tree view.
Dim Diag_Stopwatch As New Stopwatch
Dim IDtoTree As New Generic.Dictionary(Of Long, cProjectTree2)
Dim C = New PDMVault.DataContext1
' C.Log = Console.Out
Dim Projects = C.Projects.ToList ' Database list of trees.
''''''''''''''''''''''This is the line that fails. ProjectTrees only shows up as an association, and I can't seem to get access to it for a full table scan
Dim Tree = C.ProjectTrees.ToList ' Database hierarcy of the projects in the previous statement
'''''''''''''''''''''''''''''''''''''''''''''
' Create the dictionary with only the "Project" item set
For Each P In Projects
Dim ProjectTreeEntry As New cProjectTree2
ProjectTreeEntry.ChildTree = New List(Of cProjectTree2)
ProjectTreeEntry.Project = P
IDtoTree.Add(P.ProjectID, ProjectTreeEntry)
Next
' Now that the dictionary has been created, the children can be linked to the parent.
For Each T In Tree
Dim Parent As cProjectTree2 = Nothing
Dim Child As cProjectTree2 = Nothing
IDtoTree.TryGetValue(T.ProjectID, Parent)
IDtoTree.TryGetValue(T.ChildProject, Child)
Parent.ChildTree.Add(Child)
Next
' The tree has been built, create the tree view from the tree (using a little recursion)
Dim GetChildTrees As Func(Of cProjectTree2, TreeNode) =
Function(P As cProjectTree2) As TreeNode
Dim Result As New TreeNode
For Each Child In P.ChildTree.OrderBy(Function(ProjectNode) ProjectNode.Project.Name)
Dim N = GetChildTrees(Child)
If N IsNot Nothing Then
Result.Nodes.Add(N)
End If
Next
Result.Tag = P.Project.ProjectID
Result.Text = P.Project.Name
Return Result
End Function
Dim RootProject As cProjectTree2 = Nothing
If IDtoTree.TryGetValue(1, RootProject) = True Then
Dim N2 As TreeNode = GetChildTrees(RootProject)
Return N2
Else
Return Nothing
End If
End Function
I came very close to going backwards and sticking with LINQ to SQL, but this is a new project and I wanted to learn EF before I had significant code investment. With a little experimenting with Entities Framework, the following handles the recursive tree nicely without me having to get access to the ProjectTrees table.
Public Function GetPDMProjectTree() As TreeNode
Dim Diag_Stopwatch As New Stopwatch
Diag_Stopwatch.Start()
Dim C = New PDMVault.DataContext1
C.Configuration.LazyLoadingEnabled = False ' Necessary for the materialization to work in the next line
Dim MaterializeDatabase = C.Projects.ToList
C.Database.Log = Sub(Log) Debug.Print(Log) ' Verify that only two table scans occurs and that it's not hitting repeatedly
Dim RootProject = (From P In C.Projects Where P.ProjectID = 1).SingleOrDefault
If RootProject Is Nothing Then Return Nothing
Dim GetTree As Func(Of PDMVault.Project, TreeNode) =
Function(P As PDMVault.Project) As TreeNode
Dim Result As New TreeNode
For Each Child In P.Projects1.OrderBy(Function(ProjectNode) ProjectNode.Name)
Result.Nodes.Add(GetTree(Child))
Next
Result.Tag = P.ProjectID
Result.Text = P.Name
Return Result
End Function
If RootProject Is Nothing Then Return Nothing
Debug.Print($"Tree building time={Diag_Stopwatch.ElapsedMilliseconds / 1000:#0.00}")
Return GetTree(RootProject)
End Function
For the SO archives, here is a former method that did it with two trips to the database (one for the initial dictionary, and one for the root) and used an external dictionary before I learned about turning off lazy loading, probably not as optimal as my final solution.
Public Function GetPDMProjectTree2() As TreeNode
Dim Diag_Stopwatch As New Stopwatch
Dim C = New PDMVault.DataContext1
C.Database.Log = Sub(Log) Debug.Print(Log) ' Verify that only one table scan occurs and that it isn't an N+1 problem.
' Force a complete table scan before starting the recursion below, which will come from cached content
Dim ProjectTreeFromDatabase = (From P In C.Projects
Select Project = P,
Children = P.Projects1).ToDictionary(Function(F) F.Project.ProjectID)
Dim GetTree As Func(Of PDMVault.Project, TreeNode) =
Function(P As PDMVault.Project) As TreeNode
Dim Result As New TreeNode
For Each Child In ProjectTreeFromDatabase(P.ProjectID).Children.OrderBy(Function(ProjectNode) ProjectNode.Name)
Dim N = GetTree(Child)
If N IsNot Nothing Then
Result.Nodes.Add(N)
End If
Next
Result.Tag = P.ProjectID
Result.Text = P.Name
Return Result
End Function
Dim RootProject = (From P In C.Projects Where P.ProjectID = 1).SingleOrDefault
If RootProject Is Nothing Then Return Nothing
Return GetTree(RootProject)
End Function
Both solutions prevent repeated trips to the database.
I have a piece of code that iterates over the nodes in an XmlNodeList and creates a different object for each one depending on the node name and adds it to a list for printing.
For Each node as XmlNode In nodeList
Select Case node.Name.ToUpper()
Case "SHAPE"
_items.Add(New ShapeTemplate(node, Me))
Case "TEXTBLOCK"
_items.Add(New TextblockTemplate(node, Me))
End Select
Next
This code works fine, but because of all the work that has to be done by the ShapeTemplate and TextblockTemplate constructors, it is VERY slow. Since the order objects are added to _items doesn't matter, I thought a good way to speed it up would be to use a parallel.ForEach loop. The problem is XmlNodeList can't be used with parallel.ForEach because it is a non-generic collection. I've been looking into ways to convert XmlNodeList to List(Of XmlNode) with no luck. The answer I keep seeing come up is
Dim nodes as New List(Of xmlNode)(nodeList.Cast(Of xmlNode)())
But when I try it, I get an error telling me that 'Cast' is not a member of XmlNodeList.
I've also tried using TryCast like this
Dim nodes as List(Of XmlNode) = TryCast(CObj(nodeList), List(Of XmlNode))
but it results in nodes being Nothing because the object can't be cast.
Does anyone know how I can use XmlNodeList in a parallel.ForEach loop?
EDIT: I'm trying to avoid using a loop for the conversion if I can
You could use Parallel LINQ instead of Parallel.ForEach, which seems like a more natural fit for this sort of transformation. This looks just like a normal LINQ query, but with .AsParallel() added.
Imports System.Linq
' _items will not be in same order as nodes in nodeList.
' Add .AsOrdered() after .AsParallel() to maintain order, if needed.
_items = (
From node In nodeList.Cast(Of XmlNode)().AsParallel()
Select item = CreateTemplate(node)
Where item IsNot Nothing
).ToList()
Function CreateTemplate(node As XmlNode) As ITemplate ' interface or base class for your types
Dim item As ITemplate
Select Case node.Name.ToUpper()
Case "SHAPE"
item = New ShapeTemplate(node, Me)
Case "TEXTBLOCK"
item = New TextblockTemplate(node, Me)
Case Else
item = Nothing
End Select
Return item
End Function
As seen here, the XmlNodeList can be converted to a generic List(Of XmlNode) by passing it to the constructor with OfType.
' I added so your code could compile.
' I assumed an interface shared by ShapeTemplate and TextblockTemplate
Dim nodeList As XmlNodeList
Dim _items As New List(Of ITemplate)
Dim _nodes As New List(Of XmlNode)(nodeList.OfType(Of XmlNode))
Now the parallel loop. Note, if you are adding to a non-threadsafe collection such as List, you will need to synchronize adding the objects to the list. So i separated the time consuming portion (Template constructor) from the fast operation (adding to the list). This should improve your performance.
Dim oLock As New Object
Parallel.ForEach(
_nodes,
Sub(node)
Dim item As ITemplate
Select Case node.Name.ToUpper()
Case "SHAPE"
item = New ShapeTemplate(node, Me)
Case "TEXTBLOCK"
item = New TextblockTemplate(node, Me)
Case Else
item = Nothing ' or, do something else?
End Select
SyncLock oLock
_items.Add(item)
End SyncLock
End Sub)
The text file contains lines with the year followed by population like:
2016, 322690000
2015, 320220000
etc.
I separated the lines substrings to get all the years in a list box, and all the population amounts in a separate listbox, using the following code:
Dim strYearPop As String
Dim intYear As Integer
Dim intPop As Integer
strYearPop = popFile.ReadLine()
intYear = CInt(strYearPop.Substring(0, 4))
intPop = CInt(strYearPop.Substring(5))
lstYear.Items.Add(intYear)
lstPop.Items.Add(intPop)
Now I want to add the population amounts together, using the .Items to act as an array.
Dim intPop1 As Integer
intPop1 = lstPop.Items(0) + lstPop.Items(1)
But I get an error on lstPop.Items(1) and any item other than lstPop.Items(0), due to out of range. I understand the concept of out of range, but I thought that I create an index of several items (about 117 lines in the file, so the items indices should go up to 116) when I populated the list box.
How do i populate the list box in a way that creates an index of list box items (similar to an array)?
[I will treat this as an XY problem - please consider reading that after reading this answer.]
What you are missing is the separation of the data from the presentation of the data.
It is not a good idea to use controls to store data: they are meant to show the underlying data.
You could use two arrays for the data, one for the year and one for the population count, or you could use a Class which has properties of the year and the count. The latter is more sensible, as it ties the year and count together in one entity. You can then have a List of that Class to make a collection of the data, like this:
Option Infer On
Option Strict On
Imports System.IO
Public Class Form1
Public Class PopulationDatum
Property Year As Integer
Property Count As Integer
End Class
Function GetData(srcFile As String) As List(Of PopulationDatum)
Dim data As New List(Of PopulationDatum)
Using sr As New StreamReader(srcFile)
While Not sr.EndOfStream
Dim thisLine = sr.ReadLine
Dim parts = thisLine.Split(","c)
If parts.Count = 2 Then
data.Add(New PopulationDatum With {.Year = CInt(parts(0).Trim()), .Count = CInt(parts(1).Trim)})
End If
End While
End Using
Return data
End Function
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim srcFile = "C:\temp\PopulationData.txt"
Dim popData = GetData(srcFile)
Dim popTotal = 0
For Each p In popData
lstYear.Items.Add(p.Year)
lstPop.Items.Add(p.Count)
popTotal = popTotal + p.Count
Next
' popTotal now has the value of the sum of the populations
End Sub
End Class
If using a List(Of T) is too much, then just use the idea of separating the data from the user interface. It makes processing the data much simpler.
I would like to iterate through a list inside of a Parallel.ForEach loop, but the list will be a shared resource that I have to take turns accessing, which I think would defeat the purpose of the parallelism. Access to the list is read-only, so I was wondering if there was a way to make copies of the list, and allow each thread to pick one if it is not in use by one of the other threads.
Dim collCopy1 As List(Of InnerType) = innerCollection.ToList()
Dim collCopy2 As List(Of InnerType) = innerCollection.ToList()
Dim collCopy3 As List(Of InnerType) = innerCollection.ToList()
Dim collCopy4 As List(Of InnerType) = innerCollection.ToList()
Dim Lock As New Object
Dim ParallelOpts As New ParallelOptions()
ParallelOpts.MaxDegreeOfParallelism = 4
Task.Factory.StartNew(Sub()
Parallel.ForEach(outerCollection,
ParallelOpts,
Sub(outerItem As OuterType)
'Pick from collCopy1, collCopy2, collCopy3, collCopy4 here, assign to innerList, and SyncLock it
For Each innerItem As InnerType In innerList
'Do some stuff
Next
End Sub)
End Sub)
I have added a tree view to my form. I want to capture the values of checkboxes, which one is checked or not.
Also I am trying to get the count of nodes. There are four nodes in the tree,
Dim nodes As TreeNodeCollection = TreeView1.Nodes
MsgBox(nodes.Count)
gives 1.
Thanks
... This probably isn't the best way to do this, but it works...
The function would look something as follows:
Function GetAllCheckedNodes(ByVal tv As TreeView, Optional ByRef tn As TreeNode = Nothing) As List(Of TreeNode)
Dim RetVal As New List(Of TreeNode)
If tn Is Nothing Then
For Each nd In tv.Nodes
RetVal.AddRange(GetAllCheckedNodes(tv, nd))
Next
Else
If tn.Checked Then RetVal.Add(tn)
For Each nd In tn.Nodes
RetVal.AddRange(GetAllCheckedNodes(tv, nd))
Next
End If
Return RetVal
End Function
And your code to use it would look something like:
Dim MyList As List(Of TreeNode) = GetAllCheckedNodes(tvAccounts)
or
Dim MyList As List(Of TreeNode) = GetAllCheckedNodes(tvAccounts, nd)
Where nd is a specific node in the treeview where you want to get all the children nodes that are checked.
Hope this helps and makes sense.