Data Binding Exposed
The secret behind data binding lies in two objects that you don't ordinarily see: BindingContext and CurrencyManager. Every Windows Form provides a BindingContext object. In turn, every BindingContext provides a collection of zero or more CurrencyManager objects. Both objects are contained in the System.Windows.Forms namespace.The CurrencyManager object shoulders the responsibility for tracking the user's position in the bound data and synchronizing all the controls that are bound to it. To this end, the CurrencyManager provides a small set of properties, including Count, and the ever-important Position, which indicates an integer row index. It performs its work automatically.The BindingContext object, on the other hand, creates CurrencyManager objects as required. Depending on how you configure your form, you could have several different CurrencyManager objects, allowing you to bind to different data sources (or different positions in the same data source). Figure 9-14 diagrams this relationship.

Figure 9-14: Data binding under the hood
There are really only three reasons that you might want to access the data binding objects:
To programmatically control record navigation.
To programmatically react to record navigation.
To create a new BindingContext that allows you to store a different position to the same data.
Navigation with Data Binding
To navigate programmatically you need to access the form's BindingContext object, and modify its Position property. Unfortunately, to find the correct BindingContext object, you need to submit the data source object. That means you either need to store the data source in a form-level variable, or create a form-level variable to track the binding object. The following example demonstrates the second technique with the DataSet product example.First, create the variable for storing the BindingContext object:
private BindingManagerBase storeBinding;
Next, in the Form.Load event handler create the bindings and store a reference to the binding object. The only new line is highlighted in bold.
private void MultipleControlBinding_Load(object sender, System.EventArgs e)
{
DataSet dsStore = new DataSet();
dsStore.ReadXmlSchema(Application.StartupPath + "\\store.xsd");
dsStore.ReadXml(Application.StartupPath + "\\store.xml");
cboModelName.DataSource = dsStore.Tables["Products"];
cboModelName.DisplayMember = "ModelName";
lblModelNumber.DataBindings.Add("Text", dsStore.Tables["Products"],
"ModelNumber");
lblUnitCost.DataBindings.Add("Text", dsStore.Tables["Products"], "UnitCost");
lblDescription.DataBindings.Add("Text", dsStore.Tables["Products"],
"Description");
storeBinding = this.BindingContext[dsStore.Tables["Products"]];
}
Now you can control the position through the StoreBinding object. Here's an example with Previous and Next buttons that allows the user to browse through the data (see Figure 9-15):
private void cmdPrev_Click(object sender, System.EventArgs e)
{
storeBinding.Position--;
}
private void cmdNext_Click(object sender, System.EventArgs e)
{
storeBinding.Position++;
}

Figure 9-15: Data binding with custom navigation controls
Reacting to Record Navigation
As it stands, the navigation controls harmlessly fail to work if you try to browse past the bounds of the data source (for example, click the Previous button on the first record). However, a more intuitive approach would be to disable the controls at this position. You can accomplish this by reacting to the Binding.PositionChanged event.First, you connect the event handler (after binding the data source):
storeBinding = this.BindingContext[dsStore.Tables["Products"]];
storeBinding.PositionChanged += new EventHandler(Binding_PositionChanged);
The PositionChanged event doesn't provide you with any useful information (such as the originating page). But it does allow you to respond and update your controls accordingly. In the example below, the previous and next buttons are disabled when they don't apply.
private void Binding_PositionChanged(object sender, System.EventArgs e)
{
if (storeBinding.Position == storeBinding.Count - 1)
{
cmdNext.Enabled = false;
}
else
{
cmdNext.Enabled = true;
}
if (storeBinding.Position == 0)
{
cmdPrev.Enabled = false;
}
else
{
cmdPrev.Enabled = true;
}
}
If you want to be able to track the previous record, you need to add a formlevel variable and track it in the PositionChanged event handler. This technique has a few interesting uses, including validation (which you examine later in this chapter).
private int currentPage;
private void Binding_PositionChanged(object sender, EventArgs e)
{
// At this point, currentPage holds the previous page number.
// Now we update currentPage:
currentPage = storeBinding.Position;
}
Tip
You could use the PositionChanged event handler to update the data source (the original database record or the XML file), if it has changed. By increasing the frequency of updates, you lower performance, but reduce the chance of concurrency errors.
Creating Master-Detail Forms
Another interesting use of the PostionChanged event is to create master-detail forms. The concept is simple: you bind two controls to two different tables. When the selection in one table changes, you update the second by modifying the set of displayed rows with the RowFilter property.This example uses two list controls, one that displays categories and one that displays the products in a given category. The lists are filled in the normal manner:
private BindingManagerBase categoryBinding;
private DataSet dsStore = new DataSet();
private void MasterDetail_Load(object sender, System.EventArgs e)
{
dsStore.ReadXmlSchema(Application.StartupPath + "\\store.xsd");
dsStore.ReadXml(Application.StartupPath + "\\store.xml");
lstCategory.DataSource = dsStore.Tables["Categories"];
lstCategory.DisplayMember = "CategoryName";
lstProduct.DataSource = dsStore.Tables["Products"];
lstProduct.DisplayMember = "ModelName";
categoryBinding = this.BindingContext[dsStore.Tables["Categories"]];
categoryBinding.PositionChanged += new EventHandler(Binding_PositionChanged);
// Invoke method once to update child table at startup.
Binding_PositionChanged(null,null);
}
Now, when the PositionChanged event is detected for the category binding, the current view of products is automatically modified:
private void Binding_PositionChanged(object sender, System.EventArgs e)
{
string filter;
DataRow selectedRow;
// Find the current category row.
selectedRow = dsStore.Tables["Categories"].Rows[categoryBinding.Position];
// Create a filter expression using its CategoryID.
filter = "CategoryID="' + selectedRow["CategoryID"].ToString() + "';
// Modify the view onto the product table.
dsStore.Tables["Products"].DefaultView.RowFilter = filter;
}
The result is a perfectly synchronized master-detail list, as shown in Figure 9-16.

Figure 9-16: Data binding with a master-detail list
Creating a New Binding Context
In the previous example, both controls were synchronized separately and had separate binding contexts because they were bound to two different tables (and hence two different DataViewManager objects). In some cases, however, you might want the ability to bind to two different positions in the same table (or any other data source). To accomplish this, you need to manually create an extra binding context.The last task is easy. All you need to do is place the controls that you want in different binding contexts into different container controls (like a group box). Before you bind the data to the controls in the group boxes, manually create a new BindingContext object for one of them. Voila-you have two sets of controls that are synchronized separately.The code that follows carries out this operation for two list controls in different group boxes.
// Make sure all the controls in this group box have a different binding.
grpCategory.BindingContext = new BindingContext();
DataSet dsStore = new DataSet();
dsStore.ReadXmlSchema(Application.StartupPath + "\\store.xsd");
dsStore.ReadXml(Application.StartupPath + "\\store.xml");
// Configure the first group.
lstCategory.DataSource = dsStore.Tables["Categories"];
lstCategory.DisplayMember = "CategoryName";
// Configure the second group.
lstProduct.DataSource = dsStore.Tables["Categories"];
lstProduct.DisplayMember = "CategoryName";
Figure 9-17 shows the separately synchronized panels.

Figure 9-17: Separately synchronized view of the same data