Create Tree Structure from Table - vb.net

Alright, it's been one of those weeks. I just can't figure out the simplest logic patterns and it's doing my head in.
I've got an object, lets call it TreeNode:
Object:
Public Class TreeNode
Public Key As Object
Public Children As List(Of TreeNode)
Public Sub AddToNode(parentNode As TreeNode)
parentNode.Children.Add(Me)
End Sub
End Class
Simple enough - Create an instance of the class with a Key, and if it has children, I can add each child using the AddToNode method.
Onto the data I have.
I have a CTE that is being executed that returns a datatable with the levels clearly defined:
CTE:
WITH CTE AS
(
SELECT Asset, Name, ParentAsset, [level] = 0 FROM database.schema.Asset WHERE Asset = WhateverAsset and (ParentAsset IS NULL)
UNION ALL
SELECT t.Asset, t.Name, t.ParentAsset, [level] = CTE.[level] + 1 FROM CTE
INNER JOIN database.schema.Asset AS t
ON t.ParentAsset = CTE.Asset
)
SELECT Asset, Name, ParentAsset, [level], CONVERT(BIT, 'FALSE') as Added FROM CTE ORDER BY [level]
This returns a datatable that looks like so:
Data:
2236|PARENT ASSET|NULL|0|0
2233|CHILD ASSET ONE|2236|1|0 2235|CHILD ASSET TWO|2236|1|0 2234|CHILD OF CHILD ASSET ONE|2235|1|0
Now I have this as the following code to generate a list of Nodes and all their children as lists of nodes, etc. etc.
Recursive Function
Private Sub GetChildren(dt As DataTable)
For each row in dt.Rows
If row.Item("Added") = False Then
dim node as new Node
node.Key = row.Item("Asset")
parentAsset = F.GetNumber(row.Item("ParentAsset"))
dim foundNode = FlattenAndFind(node)
....
If IsNothing(foundNode) Then
'Add to overall list
Else
node.AddToNode(foundNode)
End If
dim nextDt as New Datatable
dim rows = dt.AsEnumerable.Where(function(suppRow) F.GetNumber(suppRow.Item("ParentAsset")) = parentAsset)
if rows.Any Then
dtt = rows.CopyToDataTable()
End If
GetChildreen(nextDt)
Next
End Sub
FlattenAndFilter (thanks to someone on SO for this function):
Private Function FlattenAndFilter(source As Node) as IEnumerable(Of Node)
Dim l As New List(Of Node)
If source.Key = parentAsset Then l.Add(source)
If l.Count = 1 Then
Return l.Concat(source.Children.SelectMany(Function(child) FlattenAndFilter(child)))
Else
Return Nothing
End If
End Function
What I get back is a list of each of the objects, not in a tree structure. With some modifications, I get down one level, but the child of a child is added as a parent node.
My question is - can anyone see the flawed logic?
I'm sure it's an easy fix, but I just can't grasp it for whatever reason.
Any help is appreciated, please don't hesitate to comment and ask for further clarification.

Related

Explicity show many-to-many tables in Entity Framework

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.

Unable to cast object error when attempting to return key/value pairs by querying a datacontext

I am attempting to get data from a datacontext. Normally, I've had no problems doing it, but I'm having trouble trying to return a list of key/value pairs.
Basically I'm attempting to grab all unique names from a table as the key column and the number of times they appear in the table as the value column.
My data would look like this:
apple 5
banana 1
dragonfruit 3
.
.
.
The full error message is:
Unable to cast object of type 'System.Data.Linq.DataQuery1[VB$AnonymousType_32[System.String,System.Int32]]' to type 'System.Collections.Generic.List`1
The code I'm using is this:
Dim indicators As List(Of Object)
Public Sub GetIndicatorData()
Using context = new A_DataContext
indicators = (From p In chartdata Group p By __groupByKey1__ = p.INDK8R Into g = Group
Select New With {.name = __groupByKey1__, .count = g.Count()}).AsEnumerable()
indDataSource = indicators
End Sub
but I've also tried to:
Return indicators as a list
Return indicators as an enumerable.
Use a class to encapsulate your anonymous type
Public Class NameAndCount
Public ReadOnly Property Name As String
Public ReadOnly Property Count as Integer
Public Sub New(name As String, count As Integer)
Me.Name = name
Me.Count = count
End Sub
End Class
' ...
Private indicators As IEnumerable(Of NameAndCount)
Public Sub GetIndicatorData()
Using context = new A_DataContext
indicators = From p In chartdata Group p By __groupByKey1__ = p.INDK8R Into g = Group
Select New NameAndCount(__groupByKey1__, g.Count())
indDataSource = indicators.ToList()
End Using
End Sub
Figured it out. I changed the declaration of indicators to simply be an object then removed the .enumerable (or .toList) from the query result.
Thank you for the consideration and time.

LINQ fill nested list of Object from query results

I am joining two tables Race and Odds (joined on RaceID) and would like to create an object for each Race with its nested odds. Via LINQ I am able to group the query results into objects but can't populate the nested Odds list for each Race. Any ideas?
Ideally, I should have
obj[0] with RaceID = 1, Odds{1,2,3}
obj[1] with RaceID = 2, Odds{4,5,6,7}
etc
Public Class Race
Private RaceID As Integer
Private Name As String
'other properties here
Private Odds As New List(Of Odds)
End Class
Public Class Odds
Private OddsID As Integer
Private Odds As System.Nullable(Of Decimal)
Private RaceID As Integer
'other properties here
End Class
My function
Dim objRace As New List(Of Race)
Using context As IDataContext = DataContext.Instance()
Dim repository = context.GetRepository(Of Race)()
objRace = repository.Find("SELECT Odds.OddsID, Odds.Odds, Odds.Name, Odds.Type, Odds.DateUTC, Odds.WebsiteName, Odds.RaceID AS RaceID, Race.RaceID AS RaceID,
Race.Name AS RaceName, Race.RaceDate, Race.Location, Race.URL, Race.PortalID AS PortalID, Race.RaceTime
FROM Odds RIGHT OUTER JOIN
Race ON Odds.RaceID = Race.RaceID
WHERE (Race.PortalID = #0)", PortalId)
End Using
LINQ to get objects
Dim result = objRace.GroupBy(Function(records) records.RaceID).[Select](Function(r) New Race() With {
.RaceID = r.Key
.Odds = ' <------ **somehow populate this nested list**
}).ToList()
my query results:
The r should be a implementation of IGrouping. This contains a Key that you are using for your RaceID and it is a IEnumerable itself.
So you can store the Odds by calling r.ToList() or r.ToArray. How ever you want to store them.
I just noticed that you already create the list instance for your Odds in the constructor. You may want to change this, so you can put the collection in directly using the With initialisation.

Flatten Treeview Nodes collection to List without recursion

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

How to return distinct result in linq to collection

Below query result duplicates of class code.
cboFilterValues.DataSource = (From i In allDetails Select New LookUpItem With {.ItemText = i.ClassCode, .ItemValue = i.ClassCode} Distinct).ToList()
Can any one suggest me how i could achieve distinct result for above query. I need result set as IList(Of LookupItems)
Thank You
Your Distinct is not working because (presumably - you didn't provide the code) you have not overridden the Equals and GetHashCode methods in your LookUpItem class, so instances are being compared using reference equality. If you implement those methods, the Distinct should work:
Public Overrides Function Equals(o As Object) As Boolean
If o Is Nothing OrElse Not Me.GetType().Equals(o.GetType()) Then Return False
Dim other = DirectCast(o, LookUpItem)
Return Me.ItemText = other.ItemText ' or some other fields
End Function
Public Overrides Function GetHashCode() As Integer
Return Me.ItemText.GetHashCode() ' or some other fields
End Function
Alternatively, you could modify your query a little, since you are only using the ClassCode property from allDetails, and put the distinct there (assuming that ClassCode is a String, or something else that uses value equality):
cboFilterValues.DataSource = (
From i In (From d In allDetails Select d.ClassCode Distinct)
Select New LookUpItem With {.ItemText = i, .ItemValue = i}
).ToList()
cboFilterValues.DataSource = (From x In (From i In allDetails Select i Distinct) Select New LookUpItem With {.ItemText = x.ClassCode, .ItemValue = x.ClassCode}).Tolist
I assuming what you have above doesn't work because of some problem with the select new, this should get round it if that's the problem.
Tim