Explicity show many-to-many tables in Entity Framework - vb.net

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.

Related

vb.net create objects in for each loop from partial info in Ienumerable

Summary:I want to create objects in a for each loop.
I have a class - Dashboard, which has some properties - length, height, etc.
This info is contained within an XML document, but my class' properties is only a small subset of the information in the XML.
I have created a Collection of XElement, and I can iterate over these, but how do I create my object on each iteration of the loop?
Dim Col_DashBoards As IEnumerable(Of XElement) = From XDashboard In XDocument.Load(filename).Descendants("dashboard")
For Each XDashboard In Col_DashBoards
'PseudoCode
Dim Xdashboard.Name as New DashboardClassObject
Xdashboard.Name.Height = XDashboard.Element("Height").value
...
Next
If I have understood your question correctly, you wish to create a new object based on a subset of data from within an XML document?
The below is a function that will generate a new DashboardClassObject for every matching node and populate that object. A list of type DashboardclassObject is returned.
Public Function GenerateDashBoardFromXML(filename as string) As List(Of DashboardClassObject)
Dim dashboardList As List(Of DashboardClassObject) = New List(Of DashboardClassObject)()
Dim Col_DashBoards As IEnumerable(Of XElement) = From XDashboard In XDocument.Load(filename)?.Descendants("dashboard")
For Each XDashboard In Col_DashBoards
Dim dashboard As DashboardClassObject = New DashboardClassObject () With {
.Name = XDashboard.Element("Height")?.value
}
dashboardList.Add(dashboard)
Next
return dashboardList
End Function
It should be noted that Null checking is used here. The below is only populated if the matching element is found. Null coalescing operator is also an option here.
.Name = XDashboard.Element("Height")?.value

VB.Net Use XmlNodeList in a parallel ForEach

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)

Create Tree Structure from Table

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.

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

Make VB code that replaces LINQ expressions

I found this code on another answered question C# - How to xml deserialize object itself?
I really like this code and want to use it in my application but I target the .net 2.0 Compact framework, so I cant use the LINQ expressions. Is there anyone who can tell me how I can convert this into "Normal VB" code?
Original code from from user wheelibin.
I went for this approach:
Public Class SerialisableClass
Public Sub SaveToXML(ByVal outputFilename As String)
Dim xmls = New System.Xml.Serialization.XmlSerializer(Me.GetType)
Using sw = New IO.StreamWriter(outputFilename)
xmls.Serialize(sw, Me)
End Using
End Sub
Private tempState As Object = Me
Public Sub ReadFromXML(ByVal inputFilename As String)
Dim xmls = New System.Xml.Serialization.XmlSerializer(Me.GetType)
Using sr As New IO.StreamReader(inputFilename)
tempState = xmls.Deserialize(sr)
End Using
For Each pi In tempState.GetType.GetProperties()
Dim name = pi.Name
' THIS IS THE PART THAT i CANT FIGURE OUT (HOW TO DO THIS WITHOUT LINQ)
Dim realProp = (From p In Me.GetType.GetProperties
Where p.Name = name And p.MemberType = Reflection.MemberTypes.Property).Take(1)(0)
' -------------------------------------------------------------
realProp.SetValue(Me, pi.GetValue(tempState, Nothing), Nothing)
Next
End Sub
End Class
You can replace that LINQ part with "normal" For Each loop, for example :
Dim realProp As PropertyInfo
For Each p As PropertyInfo In Me.GetType.GetProperties()
If p.Name = Name And p.MemberType = Reflection.MemberTypes.Property Then
'set `realProp` with the first `p` that fulfil above `If` criteria'
'this is equivalent to what your LINQ (...).Take(1)(0) does'
realProp = p
Exit For
End If
Next