Adding new child-Node to specific parent-Node - vb.net

I am trying to add a new child node into specific parent node.
The thing is that I can't find a property I could use to specify which parent Node I want to use.
Only I can use is:
TreeView1.SelectedNode.Nodes.Add(newNode)
But I don't want to use SelectedNode.
What I need should look like this:
TreeView1.ParentNode(Me.ds_Tables.Table.Rows(a).Item(0)).Nodes.Add(newNode)
edit:
So, I wrote ParentNode just to make it clear that that's a node where I will add a new node to.
Relationship between nodes and datatable is that I am using table column result to give a name to Node.
The thing is that my table looks like (id, code, name, parentId) parentId is the id column from that table. So, when parentId is filled (not Null) that means that that result is a part of another result from that table. (I hope that's clear for you, if it's not I'll try to explain in different way).
So, basically I have to find all results who has parentId filled and find which result own it and put that name into that "parent Node".

Option 1
It seems you read all the data at once at some point and then construct the tree. If that is the case, you can construct the TreeNodes fully before adding them to the TreeView:
Dim items As New List(Of Item)()
Dim map As New Dictionary(Of Integer, TreeNode)()
' first, create all TreeNode objects
For Each item As var In items
Dim node As New TreeNode()
' set node values
map.Add(item.Id, node)
Next
' second, construct the relations
For Each item As var In items
Dim node = map(item.Id)
If item.ParendID.HasValue Then
map(item.ParentID).Nodes.Add(node)
Else ' no parent = root node
TreeView.Nodes.Add(node)
End If
Next
Option 2
If your tree is dynamic, you can still keep a global dictionary indicating which Id links to which TreeNode:
Private map As New Dictionary(Of Integer, TreeNode)();
Option 3
Use the Tag property and write an Extension Method for TreeNodeCollection:
<System.Runtime.CompilerServices.Extension> _
Public Shared Function Find(nodes As TreeNodeCollection, item As Object) As TreeNode
For Each node As var In nodes
If node.Tag IsNot Nothing AndAlso node.Tag.Equals(item) Then
Return node
End If
Next
Return Nothing ' or throw an exception
End Function
and then use
TreeView.Nodes.Find(parentID).AddNodes(...)

Related

Nodes with variables

I'm not a experience vb.net programmer so I'm struggling with some code related to treeview node implementation:
My goal is to implement a hierarchical and editable structure related to a "Bill of Materials" (e.g. a house has a foundation, walls, roof,.. and walls has stones, plaster,....)
The number of levels of this hierarchical structure is (per definition) unknown.
I'm able to populate a treeview(1) with SQL data to multiple but a fixed number of levels.
The code I've implemented:
TreeView1.Nodes.Add(var01, var01).Nodes.Add(var02, var02) (example 2 levels)
where var01 and var02 the names within the structure are (e.g. House-Wall). The Node structure is build using a "for - next loop"
Officious by adding ".Nodes.Add(varXX, varXX)" I'm able to extend the level of the structure.
My goal however is to implement "the adding of .Nodes.Add(varXX, varXX)" through a loop make the number of hierarchical levels flexible.
I tried to convert Treeview1.Nodes... to a string and build a (overall) string through a loop. Then I tried to convert this string to the treeview control. This principle doesn't unfortunately work.
Any advice would be appreciated.
Try something like this:
Public Function GetBOMTree() As TreeNode
Dim BOMTable As DataTable
' Assuming your hierarchy is small enough to do a full table load into BOMTable, put your SQL statements here
Dim BOMDictionary = BOMTable.Rows.Cast(Of DataRow) _ ' Assumed to be a data table
.Where(Function(F) F.Item("ParentKey").GetType IsNot GetType(DBNull)) _
.GroupBy(Function(F) CInt(F.Item("ParentKey"))) _ ' Assuming integer keys
.ToDictionary(Function(F) F.Key)
Dim Root = BOMTable.Rows.Cast(Of DataRow) _
.Where(Function(F) F.Item("ParentKey").GetType Is GetType(DBNull)) _
.FirstOrDefault
If Root IsNot Nothing Then
Dim GetTree As Func(Of DataRow, TreeNode) = ' The "as Func" is necessary to allow recursion
Function(D As DataRow) As TreeNode
Dim Result As New TreeNode
Dim Key = CInt(D.Item("PrimaryKey"))
Result.Tag = Key
Result.Text = CStr(D.Item("Description"))
If BOMDictionary.ContainsKey(Key) Then
Dim Children = BOMDictionary.Item(Key)
For Each Child In Children.OrderBy(function(F) cstr(f.item("Description")))
Result.Nodes.Add(GetTree(Child))
Next
End If
Return Result
End Function
Return GetTree(Root)
Else
Return Nothing
End If
End Function
The way this works is using a little recursion, and some LINQ to objects to get an initial dictionary. I've made the assumption that your hierarchy is in you table and not an external table, but if it's external, you can modify what you see here to make it work. I've assumed integer keys. Code is trivial to modify if you are using GUID's, just cast appropriately.

Remove reference from List Of Objects in vb.net

I want to copy the content of one List(Of Object) to another and modify a single value. Is there a way to remove the reference?
In the code sample I get the output 'test2 test2' when I expect 'test1 test2'.
Module Module1
Sub Main()
Dim ListOfSample As New List(Of sample)
Dim var1 As New sample
var1.Name = "test"
ListOfSample.Add(var1)
ListOfSample.Add(var1)
Dim NewListOfSample As New List(Of sample)
NewListOfSample.AddRange(ListOfSample)
NewListOfSample(1).Name = "test2"
Console.Write(NewListOfSample(0).Name & " " & NewListOfSample(1).Name)
End Sub
End Module
Public Class sample
Public Name As String
End Class
Since your list is a list of Objects, when you perform add range, you are not adding "copies", instead you are adding the pointers (references) to the same objects that are in your original list.
You will need to clone all of your objects in the first list, and then add those clones to your second list. When it comes to cloning there are several different ways in .NET. Here's a post on getting deep copies of objects that does a good job explaining your options: Deep Copy of an Object
You can either create a clone method on your "sample" object to return a newly initialized copy of itself, or you can use some of the serialization methods mentioned in the post I linked to.
In the line NewListOfSample.AddRange(ListOfSample) you're adding references to your new list. So whatever you change in your new list will update the reference in your original list (they're both pointing to the same objects). You need to add new instances of Sample to the second list for it to contain independent items.

VB.NET ComboBox Verify if Value Exists

I am currently working on an application that was coded in VB. I am making modifications and adding features to it.
The issue I have is that I want to run a verification check for the ComboBox based on if the value exists before I attempt to select it.
The combo box is populated from a sql query with a dictionary data source
Dim TimerComboSource As New Dictionary(Of String, String)()
TimerComboSource.Add(varSQLReader("ID").ToString, varSQLReader("Name").ToString)
'Binds the values to the comboboxes
cmbTimer.DataSource = New BindingSource(TimerComboSource, Nothing)
cmbTimer.DisplayMember = "Value"
cmbTimer.ValueMember = "Key"
I select a value from a different ComboBox which is populated with a different SQL Query/SQL Table.
When I select the second ComboBox value, the table it comes from contains the ID of the first ComboBox. I want to verify if the Value Exists in the first ComboBox before I select it.
The following does not work:
If cmbTechnician.Items.Contains(varSQLReader("Tech_ID").ToString) Then
cmbTechnician.SelectedValue = varSQLReader("Tech_ID").ToString
End If
Is there a specific way in VB to make this work without it being overly complicated? The other work around would to make a more complicated SQL query but I rather not do that if there's a simpler way.
Since you are using a BindingSource on a Dictionary, you should use the DataSource to determine of things exist. If you tried to add to cmbTimer.Items or delete from it directly, you'd get an error telling you to use the DataSource. So do the same for checking if something exists (dont use a dictionary with local scope):
' form or class level collection
Private cboSource As New Dictionary(Of String, String)
cboSource.Add("red", "red")
cboSource.Add("blue", "blue")
cboSource.Add("green", "green")
cbo.DataSource = New BindingSource(cboSource, Nothing)
cbo.DisplayMember = "Value"
cbo.ValueMember = "Key"
If cbo.Items.Contains("red") Then
Console.Beep() ' wont hit
End If
If cboSource.ContainsValue("red") Then
Console.Beep() ' hits!
End If
The suggestion in comments suggests casting the DataSource of the BindingSource back to dictionary:
Dim tempBS As BindingSource = CType(cbo.DataSource, BindingSource)
Dim newCol As Dictionary(Of String, String) = CType(tempBS.DataSource, Dictionary(Of String, String))
If newCol.ContainsValue("red") Then
Console.Beep() ' hits!
End If
It is easier and more direct to retain a reference to the dictionary, but recreating it will work.
Here are another way =>
If cmbTechnician.FindString(varSQLReader("Tech_ID").ToString) <= -1 Then
'Do Not Exist
End If
This will find the display member and will return an index of the row if there exist value, otherwise will return -1.
More Info are here=>ComboBox.FindString

How to get specific index of a Dictionary?

I'm working on a code that returns a query result like MySqlCommand, all working well but what I'm trying to do is insert the result inside a ComboBox. The way for achieve this is the following:
Form load event execute the GetAvailableCategories function
The function executed download all the values and insert it into a dictionary
Now the dictionary returned need an iteration for each Items to insert in the ComboBox
Practice example:
1,3. Event that fire the function
Private Sub Service_Load(sender As Object, e As EventArgs) Handles MyBase.Load
For Each categoria In Categories.GetAvailableCategories()
service_category.Items.Add(categoria)
Next
End Sub
GetAvailableCategories function
Dim dic As New Dictionary(Of Integer, String)
For Each row In table.Rows
dic.Add(row(0), row(1))
Next
Return dic
How you can see in the 1,3 points I call the function that return the result. What I want to do is insert the row(0) as value of the item and row(1) as Item name. But Actually I get this result in the ComboBox:
[1, Hair cut]
and also I can't access to a specific position of the current item in the iteration. Maybe the dictionary isn't a good choice for this operation?
Sorry if the question could be stupid, but it's a long time since I don't program in vb.net and now I need to brush up a bit.
UPDATE
I've understood that I can assign the value access to the .key of my dictionary, so the result that I want achieve is correct if I do:
cateogoria.key (return the id of record taken from the db)
categoria.value (is the item name that'll display in the ComboBox)
now the problem's that: How to assign the value of the current item without create any other new class? For example:
service_category.Items.Add(categoria.key, categoria.value)
But I can't do this, any idea?
A List as a DataSource sounds like what you are really after. Relying on relative indices in different arrays is sort of flaky. There is not a lot about what these are, but a class would keep the related info together:
Public Class Service
Public Property Name As String
Public Property Category As String
Public Property Id As Int32
End Class
This will keep the different bits of information together. Use them to store the info read from the db and use a List to store all of them:\
Private Services As New List(of Service)
...
For Each row In table.Rows
Dim s As New Service
s.Name = row(0).ToString() '???
s.Category =...
s.Id = ...
Services.Add(s) ' add this item to list
Next
Finally, bind the List to the CBO:
myCbo.DataSource = Services
myCbo.DisplayMember = "Name" ' what to show in cbo
myCbo.ValueMember = "Id" ' what to use for SelectedValue
I dont really know what you want to show or what the db fields read are, so I am guessing. But the larger point is that a Class will keep the different bits of info together better than an array. The List can be the DataSource so that you dont even have to populate the CBO directly. The List can also be Sorted, searched, Filtered and so forth with linq.
When the user picks something, myCbo.SelectedItem should be that item (though it will need to be cast), or you can use SelectedIndex to find it in the list:
thisOne = Services(myCbo.SelectedIndex)
It is also usually a good idea to override ToString in the item/service class. This will determine what shows when a DisplayMember mapping is not available. Without this, WindowsApp2.Service might show for your items:
Public Overrides ToString() As String
Return String.Format("{0} ({1})", Name, Price)
End Sub
This would show something like
Haircut ($12.30)

Listbox Control Item - Multiple Names (and/or variables)?

I have a lot of data that's coming from a database and being adding to a listbox. The data that's coming from the database is a unique ID, and a name.
Is there any way I can make the item contain both the ID and name? I know I can append it, that's not what I'm looking to do. I need a way to be able to either get the ID, or get the name, while displaying them both.
I have gotten as far as creating a class:
Class Item
Property ID as Integer
Property Name as String
Sub New(ID as Integer, Name as String)
Me.ID = ID
Me.Name = Name
End Sub
Overrides Function ToString() as String
Return Name
End Function
End Class
That looks like it should do the trick, but I'm having trouble getting the ID, instead of the name. To put it simply, I wish I could do this: listbox1.selecteditem(id) to get the id, or listbox1.selecteditem(name) to get the name.
Any ideas on how to implement this?
You can bind to a List(Of Item) like this:
Dim ItemList = New List(Of Item)
' Fill the list with appropiate values.'
listbox1.DataSource = ItemList
listbox1.DisplayMember = "Name"
listbox1.ValueMember = "ID"
Then listbox1.SelectedValue holds the ID and you can access the name like this:
DirectCast(listbox1.SelectedItem, Item).Name
If you want to show both the ID and the Name, then I suggest you add a property to be the displayed value in the Itemclass:
Public ReadOnly Property DisplayedValue() as String
Get
Return Me.Name & " (" & Me.ID.ToString & ")"
End Get
End Property
Then when binding the list make
listbox1.DisplayMember = "DisplayedValue"
Update:
Based on your comments below I'd say my solution still works. However with this methodology the items must be added to the list and then the list bound to the object. The items can not be added individually and directly to the list box (as you would be separating data from presentation I don't see that as a problem).
To show a message box with the selected item then you just need to do:
MessageBox.Show(DirectCast(listbox1.SelectedItem, Item).ID.ToString))
I think you'll have to write a helper method to do this. If you're using VB 3.5 or newer (part of VS2008 and newer) you can write an extension method so that you can at least get nice syntax. You could write one such that it looked like:
listbox1.SelectByID(123)
listbox1.SelectByName("hello")
In the methods you'd have some search algorithm that went through the items and found the right one.