Library tutorials & articles

Winforms Data Binding Lessons Learned

Displaying Many-to-Many Relationships

Displaying a master-details relation in a form is really not a big deal. You set up one control, which is bound to the master table, and one DataGrid, which is bound to the relation between the master table and the child table like so:

adpSoldiers.Fill(ds, "Soldiers")
ds.Relations.Add("SoldierNotes", ds.Tables("Soldiers").Columns("ID"),
    ds.Tables("Notes").Columns("SoldierID"))

In this code, we have one table, Soldiers and for each soldier we can have one or more notes, which is a simple one-to-many relationship. All we have to do in order to display a master details form here is have two DataGrids, one bound to ds.Tables("Soldiers") and one bound to ds with a DataMember property of "Soldiers.SoldierNotes".

The problem arises when we have a many-to-many relationship. Let's say we have another table named Courses.” Each soldier can be enrolled into one or more courses, and each course can be taken by one or more soldiers. For that purpose, we have a third table, Registrations,” which contains only SoldierID and CourseID as columns with a PK comprised of both of these columns. Suppose we want to show, for the selected soldier in grid 1, all the courses, including course name, in which he is enrolled. Simple data binding to the relation between soldiers and registrations will not do.

adpSoldiers.Fill(ds, "Soldiers")
adpSoldiers.Fill(ds, "Courses")
adpSoldiers.Fill(ds, "Registrations")
ds.Relations.Add("SolCourses", ds.Tables("Soldiers").Columns("ID"),
    ds.Tables("Registrations ").Columns("SoldierID"))

We get unreadable data in the Details grid only showing SoldierID and CourseID columns. So what's the solution? Unfortunately, as much as I looked, there is not really an elegant solution, but here's the best way I've found to overcome this. We can add calculated columns into our Registrations data table, which, for each data row in the table, displays the value of the parent Courses row's Name value, using a predefined relation.

To accomplish this, we first add this relation into our relations collection, which enables us to retrieve the parent Courses row for each Registrations row according to the CourseID value:

ds.Relations.Add("CourseReg", ds.Tables("Courses").Columns("ID"),
    ds.Tables("Registrations ").Columns("CourseID"))

Next, we add the calculated columns to our Registrations data table:

ds.Tables("Registrations").Columns.Add("Course", GetType(String), "Parent(CourseReg).Name")

What I pass in to the Columns constructor is the name of the new columns, the type of displayed data in those columns, and the expression that the columns should hold. To retrieve the parent row using a specified relation, I write Parent(RelationName).ColumnName. I could also get a child aggregate function using this data column, by specifying Count(Child(RelationsName).ID). If the DataTable only has one relation defined, I don't even have to specify the name of the relation to use, like so: Sum(Child.Price). To learn more about calculated columns, here's a nice article about the subject.

Now that we have our calculated column in place, we can use the same relation in the details grid, only this time we see the name of the course as well as the other columns. The calculated columns in the DataGrid are read-only, as they should be. A new problem is encountered, though. The user still cannot add new items to the DataGrid, without knowing in advance the ID of the needed course and the soldier. Once those values are entered inside the new row in the grid, the Name column displays the name of the new course registration, but not before then. For that you either have to have a combo box that is placed over the CourseID Active cell, or other, more user-friendly external means of inserting a row to the data table.

Comments

  1. 22 May 2007 at 05:25
    I didn't like any of the solutions to this problem until I came up with this one...

    The solution is to bind the entire Child table to a bindingSource and that to a dataGridView.  In this case the child table would be the courses that the soldier registered for.

    Then, whenever a soldier is selected, store the selected row in a global variable, such as lastSelectedSoldierRow. Then, when that changes, you do a select on the table that connects the two. The registers table in this case. For example registerDataTable.Select("soldier_ID = " + lastSelectedSoldierRow.ID.ToString());

    Take all those rows and add the course Ids to a globally declared stringBuilder. Whenever a different soldier is selected, add -1 to the stringBuilder as an initializer. Then, for each additional course ID added to the stringBuilder, add a comma to the stringBuilder before it. You'll end up with something like "1,2,3,4,5,6". Next, set the child table's bindingSource.Filter to a that string like so: Filter = "ID in (1,2,3,4,5)".

    That will filter all the courses, down to the ones which belong to the soldier. Whenever a new course is added, append the ID to the stringBuilder and reset the filter.

    Here's an example scrap of source code from my own project. It's messy, but may be helpful to somebody if the above wasn't enough.

    StringBuilder settingsFilterSB = new StringBuilder();
            private void dataGridViewComputers_SelectionChanged(object sender, EventArgs e)
            {
                if (this.dataGridViewComputers.SelectedRows.Count == 1 || this.dataGridViewComputers.SelectedCells.Count > 0)
                {
                    object dataBoundItem;
                    if (this.dataGridViewComputers.SelectedRows.Count == 1)
                        dataBoundItem = this.dataGridViewComputers.SelectedRows[0].DataBoundItem;
                    else
                        dataBoundItem = this.dataGridViewComputers.Rows[this.dataGridViewComputers.SelectedCells[0].RowIndex].DataBoundItem;
                    lastSelectedPcRow = (CallistoDataSet.ComputerOrGroupRow)((DataRowView)dataBoundItem).Row;

                    settingsFilterSB = new StringBuilder();
                    settingsFilterSB.Append("-1");
                    int selectedPcId = lastSelectedPcRow.ID;
                    foreach(CallistoDataSet.ComputerOrGroup2SettingsRow row in this.callistoDataSet.ComputerOrGroup2Settings.Rows)
                    {
                        if(row.ComputerOrGroup_ID == selectedPcId)
                            settingsFilterSB.Append("," + row.ComputerSettings_ID.ToString());
                    }
                   
                    this.computerSettingsBindingSource.Filter = "ID in (" + settingsFilterSB.ToString() + ")";


































  2. 04 Oct 2006 at 14:57

    Use a BindingSource control then use its Find method to get the index of the row.

  3. 31 Mar 2006 at 02:50

    I'm getting this error message: "Object reference not set to an instance of an object." when I try to run the code below. I've spent a couple days trying to figure out what it is, but I'm kind of new to vb.net and programming. I have two tables that look something like below (not all fields are shown). They should be related via the userID fields as a "many tasks to each user" relation. Any help on how to do this or why there is an error when I run the code would be greatly appreciated.

     

    Table: Users                        Table:Tasks                  

    userID                                 userID

    userName                           taskID

    password                           taskName

     

    Dim

    connection As New OleDbConnection("Provider=Microsoft.JET.OLEDB.4.0; data source=" & Application.StartupPath & "\blueteam2.mdb")

    Dim adpUsers As New OleDbDataAdapter("SELECT * FROM Users,Tasks,Projects", connection)

    Dim ds As New DataSet

    Try

    adpUsers.Fill(ds, "Users")

    ds.Relations.Add("UserTasks", ds.Tables("Users").Columns("userID"), ds.Tables("Tasks").Columns("userID"))

    dgUsers.SetDataBinding(ds, "Users.userID")

    dgTasks.SetDataBinding(ds, "Tasks.userID")

    Catch ex As Exception

    MessageBox.Show(ex.Message, "Admin Main Form Load")

    End Try

     

  4. 24 Sep 2005 at 18:54
    Well, not really. I'm exagerating a litte bit

    Anyway, I just want to thank you for this nice and realy usefull article.
    I not only got the relationships shown in a DataGrid, but also I could
    modify them through the DataSet.
  5. 27 Jul 2005 at 14:46

    try  
    Private view As  DataView = m_ds.Tables("Stuff").DefaultView


    But anyway I think that the trick is that the field ID is PK

  6. 13 Jul 2005 at 22:11

    I tried your code but the dv index is out of sync with the bm index. Please explain. You can email me at glenn_r@shaw.ca.



    Here my challenge. I have reference to a datarow in a datatable. I want the set the bindingcontext position to that datarow. How can I do this? When I sort the dataview it goes out of sync with the datatable index returned when I use the dv find method.


    Thanks,
    Glenn

  7. 04 Mar 2004 at 07:41
    I think the program is cool and understanding
  8. 01 Jan 1999 at 00:00

    This thread is for discussions of Winforms Data Binding Lessons Learned.

Leave a comment

Sign in or Join us (it's free).

Roy Osherove Roy Osherove has spent the past 6+ years developing data driven applications for various companies in Israel. He's acquired several MCP titles, written a number of articles on various .NET topics, ...
AddThis

Related podcasts

  • xpert to Expert: Inside Concurrent Basic (CB)

    "Concurrent Basic extends Visual Basic with stylish asynchronous concurrency constructs derived from the join calculus. Our design advances earlier MSRC work on Polyphonic C#, Comega and the Joins Library. Unlike its C# based predecessors, CB adopts a simple event-like syntax familiar to VB progr...

Want to stay in touch with what's going on? Follow us on twitter!