Results 1 to 5 of 5

Thread: [RESOLVED] How do I create a many-to-many Relation?

  1. #1

    Thread Starter
    Fanatic Member
    Join Date
    Mar 2013
    Location
    San Francisco, CA
    Posts
    528

    Resolved [RESOLVED] How do I create a many-to-many Relation?

    I need to bind a DataGridView to a many-to-many Relation.

    Name:  2021-04-25_16-55-42.jpg
Views: 416
Size:  20.7 KB

    I know how to create both of the one-to-many Relations:

    Code:
    'VendorTypes and VendorMembers
    dsMain.Relations.Add("VendorTypes_VendorMembers", dsMain.Tables("VendorTypes").Columns("RecID"), dsMain.Tables("VendorMembers").Columns("VendorTypeID"))
    fkc = dsMain.Relations("VendorTypes_VendorMembers").ChildKeyConstraint
    fkc.DeleteRule = Rule.Cascade
    fkc.UpdateRule = Rule.Cascade
    fkc.AcceptRejectRule = AcceptRejectRule.None
    
    'Vendors and VendorMembers
    dsMain.Relations.Add("Vendors_VendorMembers", dsMain.Tables("Vendors").Columns("RecID"), dsMain.Tables("VendorMembers").Columns("VendorID"))
    fkc = dsMain.Relations("Vendors_VendorMembers").ChildKeyConstraint
    fkc.DeleteRule = Rule.Cascade
    fkc.UpdateRule = Rule.Cascade
    fkc.AcceptRejectRule = AcceptRejectRule.None
    I know how to bind a DataGrid view to a Relation, for example:

    Code:
    With bsFuel
        .DataSource = Nothing
        .DataMember = "Vehicle_Fuel"    '...bind to relationship (before setting the DataSource property, which fires the PositionChanged() event of the BindingSource)
        .DataSource = bsVehicles    '...fires the BindingSource's PositionChanged() event
        .Sort = "VehicleID, Odometer"
    End With
    If flgDebug Then Debug.WriteLine("bsFuel datasource has " & bsFuel.Count & " items.")
    
    dgvFuel.DataSource = bsFuel
    How do I create a many-to-many Relation and use it for binding a DataGridView?

    For example, I have a form that is bound to the VendorTypes DataTable via a BindingSource (bsVendorTypes). When the bsVendorTypes.PositionChanged method is called, I want the Vendors DataGridView (dgvVendors) to automatically show the Vendors that are related to the current VendorType.

    Name:  2021-04-25_18-03-35.jpg
Views: 431
Size:  37.5 KB

    In the above example, I am populating a temporary DataTable (dtTemp) via a temporary DataAdapter (dad) and then binding Vendors DataGridView (dgvVendors) to dtTemp DataTable.

    Code:
    strSQL = "SELECT Vendors.* " &
        "FROM Vendors INNER JOIN VendorMember ON Vendors.RecID = VendorMember.VendorID " &
        "WHERE (VendorMember.VendorTypeID = " & lngVendorID & ") " &
        "ORDER BY Vendors.VendorName, Vendors.VendorCity, Vendors.VendorState"
    
    If con.State <> ConnectionState.Open Then con.Open()
    Using dad As New OleDb.OleDbDataAdapter(strSQL, con)
        dad.Fill(dtTemp)
    End Using
    con.Close()
    
    dgvVendors.DataSource = dtTemp
    
    lblGridInfo.Text = "Vendors (" & dgvVendors.Rows.Count & CStr(IIf(dgvVendors.Rows.Count = 1, " item", " items")) & "):"
    I want to use the many-to-many Relation instead of reading the data from the underlying MS Access database via a DataAdapter.
    Last edited by Mark@SF; Apr 25th, 2021 at 06:05 PM.

  2. #2
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    111,221

    Re: How do I create a many-to-many Relation?

    You can't automatically bind a many:many relation. A 1:many works because you're using a single parent to filter multiple related children. In this case, you're using a single child to get a single parent and then filtering multiple children. Here is what I'd do.

    Start by binding the two child DataTables to BindingSources and then bind those directly to your controls. Handle the CurrentChanged event of the BindingSource on the side where the user is going to select a record. In that event handler, get the Current property value as a DataRowView and then get the corresponding DataRow. Call the GetParent method of that DataRow to get the parent DataRow in the join table. You can then use the appropriate field value from that to filter the second BindingSource, e.g.
    vb.net Code:
    1. Private Sub BindingSource1_CurrentChanged(sender As Object, e As EventArgs) Handles BindingSource1.CurrentChanged
    2.     Dim row1 = DirectCast(BindingSource1.Current, DataRowView)?.Row
    3.  
    4.     If row1 IsNot Nothing Then
    5.         Dim parentRow = row1.GetParentRow("Parent_Child1")
    6.         Dim parentId = parentRow.Field(Of Integer)("ParentId")
    7.  
    8.         BindingSource2.Filter = $"ParentId = {parentId}"
    9.     End If
    10. End Sub
    Why is my data not saved to my database? | MSDN Data Walkthroughs
    VBForums Database Development FAQ
    My CodeBank Submissions: VB | C#
    My Blog: Data Among Multiple Forms (3 parts)
    Beginner Tutorials: VB | C# | SQL

  3. #3

    Thread Starter
    Fanatic Member
    Join Date
    Mar 2013
    Location
    San Francisco, CA
    Posts
    528

    Re: How do I create a many-to-many Relation?

    jmc -

    Thanks for looking at this for me. I cannot get your code to work.

    As you suggested, I have bound the dgvVendors.DataSource to the Vendors DataTable (via the bsVendors BindingSource) and I have bound the dgvVendorTypes.DataSource to the VendorTypes DataTable (via the bsVendorTypes BindingSource).

    In your example, I have set BindingSource1 to be bsVendorTypes and BindingSource2 to be bsVendors.

    Here are the two Relations that are used by the junction table (VendorMember):

    Code:
    'VendorTypes and VendorMember
    dsMain.Relations.Add("VendorTypes_VendorMember", dsMain.Tables("VendorTypes").Columns("RecID"), dsMain.Tables("VendorMember").Columns("VendorTypeID"))
    fkc = dsMain.Relations("VendorTypes_VendorMember").ChildKeyConstraint
    fkc.DeleteRule = Rule.Cascade
    fkc.UpdateRule = Rule.Cascade
    fkc.AcceptRejectRule = AcceptRejectRule.None
    
    'Vendors and VendorMember
    dsMain.Relations.Add("Vendors_VendorMember", dsMain.Tables("Vendors").Columns("RecID"), dsMain.Tables("VendorMember").Columns("VendorID"))
    fkc = dsMain.Relations("Vendors_VendorMember").ChildKeyConstraint
    fkc.DeleteRule = Rule.Cascade
    fkc.UpdateRule = Rule.Cascade
    fkc.AcceptRejectRule = AcceptRejectRule.None
    When I call the GetParentRow method it returns Nothing (I've tried passing both of the above Relations). The returned parentRow is always Nothing. I'm not sure why you've suggested calling the GetParentRow method on the VendorTypes table because VendorTYpes is the "parent" table and VendorMembers is the "child" table (i.e., one VendorType can have many Vendors).

    Also, the Vendors DataTable does not have the "ParentID" field (which as per the "VendorTypes_VendorMember" Relation will be the "VendorTypeID" field in the VendorMember junction table) your line #8 won't work.

    Code:
    BindingSource2.Filter = $"ParentId = {parentId}"
    I would like to figure out how to get your example to work - what am I missing?

    As an alternate way to populate the dgvVendors from the Vendors table in my dataset (instead of populating it from the underlying table in the MS Access database), I've added the following to the bsVendorTypes.CurrentChanged method.

    Code:
    dtVendors = dsMain.Tables("Vendors").Clone
    
    For Each rowParent As DataRow In dsMain.Tables("VendorMember").Select("VendorTypeID = " & lngVendorID)
        strFilter = "RecID = " & CLng(rowParent("VendorID"))
        For Each rowChild As DataRow In dsMain.Tables("Vendors").Select(filterExpression:=strFilter)
            row = dtVendors.NewRow
            With row
                For Each col As DataColumn In dsMain.Tables("Vendors").Columns
                    .Item(col.ColumnName) = rowChild(col.ColumnName)
                Next col
            End With
            dtVendors.Rows.Add(row)
        Next rowChild
    Next rowParent
    
    If dtVendors.Rows.Count <> 0 Then
        dgvVendors.DataSource = dtVendors.Select(filterExpression:="", sort:=strSort).CopyToDataTable
    Else
        dgvVendors.DataSource = Nothing
    End If
    
    lblGridInfo.Text = "Vendors (" & dgvVendors.Rows.Count & CStr(IIf(dgvVendors.Rows.Count = 1, " item", " items")) & "):"
    So I've got a couple of work-arounds that are working, but I would like to figure out how to make your suggested code work.

    Thanks for your help with this question.
    Last edited by Mark@SF; Apr 26th, 2021 at 06:34 AM.

  4. #4
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    111,221

    Re: How do I create a many-to-many Relation?

    Sorry, got my relations a bit mixed up. Did all that off the top of my head and my head was apparently not all there at the time. Here's an example that I just tested and confirmed is working:
    vb.net Code:
    1. Public Class Form1
    2.  
    3.     Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    4.         Dim data As New DataSet
    5.  
    6.         Dim parent1Table = data.Tables.Add("Parent1")
    7.         Dim parent2Table = data.Tables.Add("Parent2")
    8.         Dim joinTable = data.Tables.Add("Join")
    9.  
    10.         With parent1Table.Columns
    11.             parent1Table.PrimaryKey = { .Add("Parent1Id", GetType(Integer))}
    12.             .Add("Parent1Name", GetType(String))
    13.         End With
    14.  
    15.         With parent1Table.Rows
    16.             .Add(1, "A1")
    17.             .Add(2, "A2")
    18.             .Add(3, "A3")
    19.         End With
    20.  
    21.         With parent2Table.Columns
    22.             parent2Table.PrimaryKey = { .Add("Parent2Id", GetType(Integer))}
    23.             .Add("Parent2Name", GetType(String))
    24.         End With
    25.  
    26.         With parent2Table.Rows
    27.             .Add(1, "B1")
    28.             .Add(2, "B2")
    29.             .Add(3, "B3")
    30.             .Add(4, "B4")
    31.             .Add(5, "B5")
    32.             .Add(6, "B6")
    33.         End With
    34.  
    35.         With joinTable.Columns
    36.             joinTable.PrimaryKey = { .Add("Parent1Id", GetType(Integer)),
    37.                                      .Add("Parent2Id", GetType(Integer))}
    38.         End With
    39.  
    40.         With joinTable.Rows
    41.             .Add(1, 1)
    42.             .Add(2, 4)
    43.             .Add(3, 2)
    44.             .Add(1, 5)
    45.             .Add(2, 3)
    46.             .Add(3, 6)
    47.         End With
    48.  
    49.         With data.Relations
    50.             .Add("Parent1_Join", parent1Table.Columns("Parent1Id"), joinTable.Columns("Parent1Id"))
    51.             .Add("Parent2_Join", parent2Table.Columns("Parent2Id"), joinTable.Columns("Parent2Id"))
    52.         End With
    53.  
    54.         BindingSource1.DataSource = parent1Table
    55.         BindingSource2.DataSource = parent2Table
    56.  
    57.         DataGridView1.DataSource = BindingSource1
    58.         DataGridView2.DataSource = BindingSource2
    59.     End Sub
    60.  
    61.     Private Sub BindingSource1_CurrentChanged(sender As Object, e As EventArgs) Handles BindingSource1.CurrentChanged
    62.         Dim parent1Row = DirectCast(BindingSource1.Current, DataRowView).Row
    63.         Dim joinRows = parent1Row.GetChildRows("Parent1_Join")
    64.         Dim parent2ids = joinRows.Select(Function(dr) dr.Field(Of Integer)("Parent2Id"))
    65.  
    66.         BindingSource2.Filter = $"Parent2Id IN ({String.Join(",", parent2ids)})"
    67.     End Sub
    68.  
    69. End Class
    Why is my data not saved to my database? | MSDN Data Walkthroughs
    VBForums Database Development FAQ
    My CodeBank Submissions: VB | C#
    My Blog: Data Among Multiple Forms (3 parts)
    Beginner Tutorials: VB | C# | SQL

  5. #5

    Thread Starter
    Fanatic Member
    Join Date
    Mar 2013
    Location
    San Francisco, CA
    Posts
    528

    Re: How do I create a many-to-many Relation?

    jmc -

    That's it, thanks!

    I added one other piece to your code to check that the parent2id.Count <> 0 (to prevent an error if the junction table has no rows for the parent1id):

    Code:
    If parent2ids.Count <> 0 Then
        bsVendors.Filter = $"RecID IN ({String.Join(",", parent2ids)})"
    Else
        bsVendors.Filter = "RecID Is Null"
    End If
    Thanks again for your help. I learned something new -- always a good thing!

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width