Many-to-Many Relations Data Binding
If you look back at
Figure 1, you can see that authors and titles are related to one another through a many-to-many relationship. This is evident in a relational schema like a DataSet because of the need of a join table (
titleauthor) that holds foreign keys into the two related tables.
There is no inherent way to automatically synchronize the contents of the two tables (
authors and
titles) in this case, because it is up to you to decide which table to treat as a parent and which to treat as a child for display purposes. Also, the presence of the intermediate table poses an additional complication. However, BindingSources make this situation relatively simple to tackle as well.
Suppose that you want to show a table of authors, and when each author is selected in the grid, their published titles show up in a second grid in the form (
Figure 3). You could achieve this by re-querying the database on each selection, performing a
JOIN on the
authors,
titles, and
titleauthor tables for the appropriate
au_id. However, that means extra round trips to the server, which is usually unnecessary.
 | |
| Figure 3: Many-to-many data binding sample. |
What we can do is set up a BindingSource for each one of the tables involved (
authors,
titles, and
titleauthor), and use a combination of the automatic synchronization for master-details binding, the events raised by a BindingSource, and the filtering capabilities of a BindingSource to get what you want with minimal code. To support this solution, the underlying data source for the BindingSource will have to support filtering through an implementation of the IBindingList interface. DataSets provide this for you out of the box. If you are using custom object collections, you will have to do a fair amount of extra work to achieve that capability.
Here's what you'll do: The
authors grid will have a straightforward binding to the
authors table through its BindingSource. Likewise, the
titles grid will have a similar binding for the
titles table. You'll set up an additional BindingSource for the
titleauthor table as a child binding on the
authors BindingSource. Then, whenever the list changes for the
titleauthor BindingSource (which will happen whenever a new author is selected in the author grid through the master-details mechanisms), you'll set up a new filter criteria for the titles BindingSource to only show the related titles.
Listing 2 shows the complete form code that sets all this up.
The Form Load event handler in
Listing 2 first loads up the PubsDataSet as described earlier. It then binds the authors and titles grids to their respective BindingSource objects. The
authors and
titles BindingSources are bound to their respective tables in the DataSet. The
m_TitleAuthorBindingSource is bound using a master-details binding as described in the previous section so that the list exposed by that BindingSource will update whenever the selection in the authors BindingSource changes. You can ignore the publishers binding code for now; that will be discussed in the next section.
Finally, the Load event handler wires up some event handlers for events raised by the BindingSource objects. The
BindingComplete and
ListChanged events are raised by a BindingSource object when the data binding process is complete against its underlying data source, and when the contents of the list it manages have changed, respectively. By hooking up event handlers for these events against the
m_TitleAuthorBindingSource, you will be notified whenever the child list of rows in the join table has been updated due to a selection change in the
authors table. You could have also done the same against the
authors table, instead monitoring the
CurrentChanged event.
The event subscriptions are done in
Listing 2 using a new C# language feature called delegate inference. You can simply set the event subscription to the name of the handler method, and the compiler will worry about creating an instance of an appropriate delegate to add to the event's subscriber list.
The event handlers just described call a helper method called
BindTitles(). This method loops through the rows in master-details bound
m_TitleAuthorBindingSource list, and generates a filter string that is then applied to the
m_TitlesBindingSource object to restrict which of the rows in its collection it displays.
"Details-Master" Data Binding
You won't hear it called that often, but another common requirement in data binding is to provide a display mechanism for many-to-one relations between collections of data. This is basically synchronizing from a selection in a collection of child items to update another control that will display the corresponding parent item.
The easiest way to do this is to tap into the events raised by a BindingSource again. The code I skipped over in the
Load event handler of
Listing 2 sets up a data binding between the
publishers table and the textbox at the bottom of the form that displays the publisher name for the currently selected title in the titles grid.
m_PublishersBindingSource.DataSource =
m_PubsDataSet.publishers;
// Set the simple binding on the textbox
m_PublisherTextBox.DataBindings.Add("Text",
m_PublishersBindingSource, "pub_name");
The
Add method on the
DataBindings collection of a control creates a new Binding object and adds it to the collection of bindings for that control. In this case, you are tying the
pub_name column of the
publishers table to the
Text property of the TextBox control through the
m_PublishersBindingSource as the data source. Whatever item is current in the data source will automatically be displayed in the TextBox, and any edits to the text will be pushed back into the underlying data source when the focus leaves the control.
With just those two lines of code, there is no synchronization set up between selected titles and the publishers. There are a couple of ways to achieve that synchronization. One would be to not even bother with data binding as shown above, but simply set the Text property of the TextBox directly based on row selections in the titles grid. This will trigger the
CurrentChanged event on the
m_TitlesBindingSource for which you have an event handler. In that event handler, you could just navigate to the parent row using the properties exposed on a typed DataSet, and use it to set the TextBox value.
private void OnCurrentTitleChanged(object sender,
EventArgs e)
{
// Get the current row from the titles source
DataRowView rowView =
m_TitlesBindingSource.Current as
DataRowView;
// Cast to the strongly typed row type
PubsDataSet.titlesRow row = rowView.Row as
PubsDataSet.titlesRow;
// Set the value — no data binding
m_PublisherTextBox.Text =
row.publishersRow.pub_name;
}
However, if you want to stick with a data bound approach, you can again use the filtering capabilities of BindingSources to filter the publishers list down to only the matching row.
private void OnCurrentTitleChanged(object sender,
EventArgs e)
{
// Get the current row from the titles source
DataRowView rowView =
m_TitlesBindingSource.Current as
DataRowView;
// Cast to the strongly typed row type
PubsDataSet.titlesRow row = rowView.Row as
PubsDataSet.titlesRow;
// Set the filter on the publishers source
m_PublishersBindingSource.Filter =
"pub_id = '"
+ row.pub_id + "'";
}
Now the publisher name is constantly kept synchronized through data binding and filtering.