Friday, September 17, 2010

About BindingContext in C#

When using simple list binding (see Simple List Binding) the simple bound control needs to synchronize to the "current item" of its data source. In order to do this, the binding needs to get the "Current" item for the CurrencyManager associated with the binding. The binding gets a CurrencyManager for its data source through its Control’s "BindingContext" property (a Control typically gets its BindingContext from the parent Form). The "BindingContext" is a per-Form cache of CurrencyManagers (more precisely, BindingContext is a cache of BindingManagerBase instances and BindingManagerBase is the base class of CurrencyManager).

As an example, suppose a developer had a list of Customers bound to a DataGrid control (e.g. DataGrid.DataSource is set to a list of Customers). In addition, the developer had a simple control, such as a TextBox, bound to the same list (e.g. TextBox.Text is bound to the property "Name" on the Customer list using Simple List Binding). When clicking on an item in the DataGrid, the DataGrid will make the clicked item the currently selected item. The DataGrid does this by first asking its BindingContext for the BindingManagerBase (CurrencyManager) of its data source (list). The BindingContext returns the cached BindingManagerBase (and creates one if it doesn’t exit). The DataGrid will use the BindingManagerBase API to change the "Current" item (it does this by setting the CurrencyManager Position property). When a simple binding is constructed binding it will get the BindingManagerBase (CurrencyManager) associated with its data source. It will listen to change events on the BindingManagerBase and synchronize updates to its bound property (e.g. "Text" property) with updates to the BindingManagerBase "Current" item. The key to this working correctly is that both the DataGrid and the TextBox need to be using the same CurrencyManager (BindingManagerBase). If they are using different CurrencyManagers, then the simple bound property will not correctly update when the DataGrid item changes.

As previously mentioned, Controls (and developers) can get a BindingManagerBase for a data source using a Controls BindingContext. When requesting a BindingManagerBase from the BindingContext, the BindingContext will first look in its cache for the requested BindingManagerBase. If the BindingManagerBase doesn’t exist in the cache, then the BindingContext will create and return a new one (and add it to the cache). The BindingContext is typically global per form (child controls delegate to their parents BindingContext) so BindingManagerBases (CurrencyManagers) are typically shared across all Controls on a Form. The BindingContext has two forms:

/* Get a BindingManagerBase for the given data source */
bmb = this.BindingContext[dataTable];

/* Get a BindingManagerBase for the given data source and data member */
bmb = this.BindingContext[dataSet, "Numbers"];

The first form is commonly used when getting a BindingManagerBase for a list such as an ADO.NET DataTable. The second form is used to get a BindingManagerBase for a parent data source that has child lists (e.g. DataSet with child DataTables). One of the most confusing aspects of BindingContext is using the different forms to specify the same data source you result in two different BindingManagerBases instances. This is by far and away the most common reason Controls bound to the same data source don’t synchronize (Controls use BindingContext to get a BindingManagerBase).

Sample: Control Synchronization (VS 2005) (VS Projects: CurrencyAndBindingContext and DataBinding Intro)

/* Create a DataSet with 1 DataTable */
DataSet dataSet = new DataSet();
DataTable dataTable = dataSet.Tables.Add("Numbers");

dataTable.Columns.Add("ID", typeof(int));
dataTable.Columns.Add("Name", typeof(string));

dataTable.Rows.Add(0, "Zero");
dataTable.Rows.Add(1, "One");

/*******************************************************************
* Bind the first DataGridView and TextBox to the "Numbers" table in
* dataSet. The DataGridView will use BindingContext to get a
* CurrencyManager for the data source. DataGridView1 will use
* the following form of BindingContext:
*
* bmb = BindingContext[dataSet, "Numbers"];
*
* The textBox1’s Text Binding will also get a BindingManagerBase
* and will use the following BindingContext form:
*
* bmb = BindingContext[dataSet, "Number"];
*
* Therefore both dataGridView1 and textBox1 will share the same
* BindingManagerBase (CurrencyManager).
*******************************************************************/

this.dataGridView1.DataSource = dataSet;
this.dataGridView1.DataMember = "Numbers";

this.textBox1.DataBindings.Add("Text", dataSet, "Numbers.Name", true);

/*******************************************************************
* The variable "dataTable" contains the "Numbers" table. Although
* the above DataGridView and TextBox bound to this table using
* "DataSource" and "DataMember" form, they could have bound to the
* same Table (and data) by binding directly to "dataTable" as shown
* below. When doing this, DataGridView2 will use the following form
* of BindingContext:
*
* bmb = BindingContext[dataTable];
*
* The textBox12’s Text Binding will use the following BindingContext
* form:
*
* bmb = BindingContext[dataTable];
*
* Therefore both dataGridView2 and textBox2 will share the same
* BindingManagerBase (CurrencyManager) however they will not
* share the same CurrencyManager since they used a different form
* to specify their bindings.
*******************************************************************/

this.dataGridView2.DataSource = dataTable;
this.textBox2.DataBindings.Add("Text", dataTable, "Name", true);

0 comments: