2. Data Binding
Data binding is the
ability to associate an object that holds data with a control that
displays and updates that data.
The DataGrid control can display the entire contents of a bound data object.
You bind to a DataGrid control by setting its DataSource property.
Unlike the desktop DataGrid control, the .NET Compact Framework version is read-only.
The ListBox and ComboBox controls can display one column of a DataTable/DataView and can use one other column for an identifying key.
You can bind to a ListBox or ComboBox control by setting its DataSource, DisplayMember, and ValueMember properties.
Unlike the desktop ComboBox
control, the .NET Compact Framework version is read-only. That is, the
user can select from the list of choices but cannot enter text into the ComboBox control.
Single-item controls, such as the TextBox, Label, RadioButton, and CheckBox, can bind to a single data element of a data object.
You can bind to a single-item control by adding entries to its DataBindings collection.
The DataBindings collection specifies the column to be bound to but not the row. A “current” row of a DataTable/DataView is always used for binding.
Data binding is a two-way street. Changes to data made in the control are automatically pushed back to the bound data table.
To lead into our upcoming discussion of data binding, we add the following information to our list.
Every DataTable object has an associated DataView
object that can be used for data binding. Additional views can be
created for each table, but one view is always present once the table
has been created.
DataTable objects do not really have a “current” row. Instead, the DataTable’s CurrencyManager object, located within the form’s BindingContext property, must be used to position the DataTable to a row.
We begin by writing a
very simple application to illustrate the benefits and issues of data
binding. We use a very simple SQL Server CE database to populate the
data set. The tables in the data set are the Categories and Products tables; the relationship between them has been defined within the data set by adding a DataRelation object named FKProdCat
to the data set. Once the data set has been populated, we use data
binding to display the information in the tables to the user. The focus
here is on the binding of data between the data set and the controls,
not on the movement of data between the data set and the database.
The first form, shown in Figure 1, consists of two DataGrid controls, one for displaying the Categories rows and one for displaying the Products rows of whichever Category the user selects. In other words, we want to reflect the parent/child relationship between categories and products on the form.
The second form, shown in Figure 2, displays one product at a time in TextBox
controls and provides the user with a variety of ways to specify the
desired product. It also allows the user to update the product
information.
The two forms are not
related in a business sense; we are simply using one to illustrate
multi-item data binding and the other to illustrate single-item data
binding.
The first form is the simpler to program, as we shall see, for it binds data tables to multi-item controls.
2.1. Binding to Multi-Item Controls
The form shown in Figure 6.3 declares a private variable to hold the data set named dsetDB; creates a new data set, storing the reference in dsetDB; and loads dsetDB
with data from the SQL Server CE database. At this point, we have a
data set that contains two data tables and the relationship between
them.
To reflect that relationship on the form, we bind the upper DataGrid control to the Categories data table so that the entire table is displayed within the DataGrid control. We bind the lower DataGrid control to the default view of the Products table because we can easily update the view whenever the user selects a new category. Here is the data-binding code:
private void mitemDisplayDS_Click(object sender, EventArgs e)
{
// Display the Categories and Products tables
// in the parent and child DataGrids.
dgridParent.DataSource =
dsetDB.Tables["Categories"];
dgridChild.DataSource =
dsetDB.Tables["Products"].DefaultView;
}
Whenever the user selects a new category, the application reacts to the CurrentCellChanged event by setting the row filter for the Products view to select only products of that category, as shown in this code:
private void dgridParent_CurrentCellChanged(object sender,
EventArgs e)
{
DataTable dtabParent = (DataTable)dgridParent.DataSource;
DataView dviewChild = (DataView)dgridChild.DataSource;
dviewChild.RowFilter =
"CategoryID = " +
dtabParent.Rows[dgridParent.CurrentRowIndex]["CategoryID"];
}
Thus, the DataView class is the key piece in reflecting the parent/child relationship to the user.
2.2. Binding to Single-Item Controls
When asked to display the second form shown in Figure 6.4 the first form stores a reference to the data set in an Internal
variable so that the second form can retrieve it and then displays the
second form. The second form displays one product row at a time, using Label and TextBox
controls. This makes it inherently more complex than the first form.
Binding to single-item controls is more difficult than binding to
multi-item controls for three reasons.
The user must be given a mechanism to specify which product should be displayed.
The TextBox controls must display the specified row.
The TextBox controls can be used to update data as well as display it.
Data binding to single-valued controls, such as TextBox
controls, requires some additional design decisions. Making good
decisions requires an understanding of data-binding internals. So, let’s
look at the following decisions that must be made and the impacts of
each.
How does the user designate which row is to be displayed?
By matching binding. Bind the data object to a multi-item control, such as a ComboBox or DataGrid, as well as to the single-item controls, such as the Label and TextBox controls. The user selects the desired row from the multi-item control.
By indexing.
Provide a scroll bar or some other position control. The user indicates
the relative position of the desired row within the data object. The
application positions to that row.
By search key. Provide a text box. The user enters a value. The application searches the data object for the row containing the entered value.
How should that row be assigned to the control?
By current row. The application designates the desired row as the current row of the data object.
By data view. The application binds the single-item controls to the data table’s DefaultView object and then sets the view’s Filter property to limit the view to just the desired row.
When should the data table be updated with the values from the single-item controls?
When the user moves to a new field?
No. It is not necessary to update the data object if the user is
continuing to modify other fields of the same row; wait until the user
moves to a new row.
When the user positions to a new row? No. Data binding will automatically update the old row values whenever the user moves to a new row.
When
the user indicates that he or she is finished with the edit (e.g., by
using a Cancel or Update button or by closing the form)? Yes. You need to use the data table’s CurrencyManager object to complete or cancel the update of the current row if the user exits the operation without moving to a new row.
The following subsections discuss these three decisions in more detail.
2.3. Designating the Row to Be Displayed
Figure 3
shows the single-item form providing all three methods mentioned
previously for designating which row to display. We use this form to
cover the issues being addressed here. It’s not the most beautiful form
we ever designed, but it does illustrate the functionality we want to
cover.
Matching Binding
Of the three
designation methods, the easiest to program is matching binding. Once
you bind the single-item controls and the multi-item controls to the
same data object, you no longer have to decide how to assign the row to
the single-item controls because the user’s selection is automatically reflected in the single-item controls. In our example, the binding is done in the form’s Load event handler, as shown here:
// Bind the ComboBox with the Product names.
comboProductIDs.DisplayMember = strPKDesc;
comboProductIDs.ValueMember = strPKName;
comboProductIDs.DataSource = dtabProducts;
comboProductIDs.SelectedIndex = 0;
// Bind the DataTable's columns to the text boxes.
textProductID.DataBindings.Add
("Text", dtabProducts, strPKName);
textProductName.DataBindings.Add
("Text", dtabProducts, strPKDesc);
textCategoryName.DataBindings.Add
("Text", dtabProducts, strFKDesc);
When the user taps on
an entry in the combo box, that row is displayed in the text boxes. If
the user enters new values in the text boxes, those values are updated
to the data set as soon as the user selects a new row; and the
validation events for the text box fire before the row is updated. What
more could we want?
When writing the code to do data binding, set the DisplayMember property prior to setting the DataSource property. Setting the DataSource property causes things to happen, some of them visible to the user. Therefore, you want everything in place before setting the DataSource property. To illustrate this, you can take the sample code shown on the previous page and reposition the two calls so that the DataSource property is set prior to the DisplayMember property. When you run the code you may notice a slight flicker in the combo box.
Indexing
Indexing is easy to
implement but not as easy as matching binding. And although indexing is
easy, it is not obvious. When the user positions the scroll bar to a
value of n, the application knows that it needs to make row n
the current row of the data table. Unfortunately, data tables do not
have a current row property because the concept of a current row is
meaningful only within data binding. To maintain a current row context
when not data-bound would be wasted overhead.
Because data binding involves controls and because controls reside within forms, the Form
class assumes the responsibility for managing the bindings and for
maintaining the concept of the current row. When the first binding in
the previous code excerpt that involved the dtabProducts data table executed, the form created an instance of the BindingContext class for managing dtabProducts bindings. As the subsequent binding code executed, that object was updated with information about the additional bindings.
When the user tapped on the nth entry in the combo box, the form reacted to the combo box’s SelectedIndexChanged event by noting that the combo box was bound to dtabProducts and that the text boxes were also bound to dtabProducts. So, the form notified the text boxes to obtain their Text property values from row n of the data table, and it updated the binding context object for dtabProducts to have a Position property value of n.
To control the current row
of a bound data table and thus control which row is displayed in the
text boxes, you need to tap into the form’s binding management
capability. Specifically, you need to obtain the CurrencyManager object for the data table (or data view) and set its Position property’s value to n. The CurrencyManager object for a data object is contained in the default collection within a form’s BindingContext object. In our application, this means handling the scroll bar’s ValueChanged event as shown in the following line of code (assuming the scroll bar is named hsbRows and its minimum and maximum values are 0 and the number of rows in the data object minus 1, respectively):
this.BindingContext[dtabProducts].Position = hsbRows.Value;
This causes the text boxes to update their Text property values with the values from the row of the dtabProducts table indicated by hsbRows.Value.
It seems like we are
saying that indexing implies assigning controls according to the current
row. Instead, you might wonder what’s wrong with binding the text boxes
to dtabProducts.DefaultView, rather than to the data table itself, and then reacting to the scroll bar’s ValueChanged event by setting the view’s Filter property with the primary key of the nth row, as shown here:
dtabProducts.DefaultView.RowFilter =
"ProductID = " +
dtabProducts.Rows[hsbRows.Value]["ProductID"];
Now the text boxes are
bound to a view that contains only one row, which is the row they
display, and the concept of current row becomes meaningless.
You can do it this way,
and it works fine until you try to update the fields by entering new
values into the text boxes. When you enter data into the text boxes and
then reposition the scroll bar thumb, two things go wrong. First,
tapping in the scroll bar does not cause the text box to lose focus, and
thus the validation events do not fire. Second, you do not move from
one row to another row within the bound view; instead, you replace the
contents of the bound view with new contents, and thus the values in the
underlying data table are not updated.
So, although
indexing does not necessarily imply assigning controls by current row,
we recommend doing it that way whenever you choose to use indexing. And
as you just saw, you can implement that choice in one line of code.
Search Key
The third method to use
when designating the desired row has the user enter a search value,
which fits nicely with the indexing option because both are value-based
rather than position-based techniques. The single-item controls can be
bound to the data table’s default view, as shown in the following code:
// Bind the DataTable's columns to the text boxes.
textProductID.DataBindings.Add
("Text", dtabProducts, strPKName);
textProductName.DataBindings.Add
("Text", dtabProducts, strPKDesc);
textCategoryName.DataBindings.Add
("Text", dtabProducts, strFKDesc);
Then
the controls can be positioned to the requested row by extracting the
user-entered key value and specifying it when setting the default view’s
Filter property:
dtabProducts.DefaultView.RowFilter =
"ProductID = " + textGet.Text;
Because ProductID is the primary key, this limits the view to, at most, one row.
Using data views to
assign the row to the text boxes in this situation does not lead to the
data set updating problems that option had when used in conjunction with
a scroll bar. When the user begins to enter a new key value after
entering new field values in the old row, the focus does shift, the
validation events are called, and the underlying data table is updated.
Conversely, assigning
rows according to the current row, which is position-based rather than
value-based, does not fit well with having users designate rows by
search keys. No single property or method of the DataTable, DataView, or DataRow
class translates a primary key value into a row number. If you need
this type of translation, you need to write your own routine to provide
it.
2.4. Assigning the Controls to a Row
Because, as we have
just seen, this subject is so tightly tied to the issue of designating
the row to be displayed, we have already covered it. We summarize with
these points.
If the user is
designating the desired row by selecting from a multi-item control
bound to the same data object as the single-item controls, do nothing.
If
the user is designating the desired row by entering an index value,
bind your single-item controls to the data table, obtain the binding
context for the data table from the form, and set the Position property to the index value.
If
the user is designating the desired row by entering a search value,
bind your single-item controls to the data table’s default view, and use
the Filter property to accept only rows that contain the search value.
2.5. Updating the Bound Data Table
We
said earlier that data binding is a two-way street: Changes made in the
control propagate back to the data set. However, bound controls do not
update the underlying row until they are repositioned to a new row.
Normally, you want this behavior because the row is probably out of sync
with itself as its individual fields are being entered and updated, and
it is best to wait for the automatic update that occurs when the user
moves to a new row.
It is always possible
for the user to complete the editing of a row and not move to a new
row. If the user positions to a row, makes a change to the contents
through a bound control, and then closes the form, the change the user
made is not persisted—it is lost. This is because the application never
repositioned to a new row after the change was made, and therefore the
data table was not modified.
This is why forms tend to have specific Update and Cancel buttons on them and why we handle not only the Click events of those buttons but also the form’s Closing and Deactivate
events. To programmatically complete or cancel an update (i.e., to
force or prevent the transfer of data from the single-item controls to
the data table), use the CurrencyManager object’s EndCurrentEdit or CancelCurrentEdit method, respectively. For instance, the following code reacts to the form’s Closing event by completing the edit of the current row, thus causing the data in the controls to propagate to the row:
using System.ComponentModel;
:
:
private void FormUpdate_Closing(object sender,
CancelEventArgs e)
{
// Force the current modification to complete.
this.BindingContext[dtabCategories].EndCurrentEdit ();
}
The CurrencyManager object has both an EndCurrentEdit method to force the update to occur and a CancelCurrentEdit method to prevent the update from occurring, as well as the Position property that is used to specify the current row.
So,
the key points to consider when using data binding to update data set
data is how to let the user position the controls to a specific row and
how to prevent lost or unintended updates to that row.
This concludes our
discussion of moving data between the data set and the presentation
layer.