Encapsulation with Data Controls
With this exhaustive look at data binding, you now know how to tailor data for your user interface without writing manual code to loop through records and commit changes. However, all this functionality comes at a price. Namely, if you use data binding in the way I've described, you'll soon end up with a tangle of database-specific details (such as formatting and field names) directly in your form code. What's worse, this code is fragile and loosely typed-meaning that if a field name changes in the database, your user interface code needs immediate modifications to survive.This state of affairs is far from ideal. Luckily, there are a few ways to minimize the problem. One way is not to use data binding at all. Instead, create a database table with three columns: FormName, ControlName, and DataField. You can then fill this table with content that maps individual controls to specific data fields. A simple helper function in a database adapter class can then manage all the information transfer:
public class DBHelper
{
public static void FillForm(Form formToBind,
DataTable mappings)
{
DataRow[] rowMatch();
foreach (Control ctrl in formToBind.Controls)
{
// See if this menu item has a corresponding row.
rowMatch = mappings.Select("ControlName = "' + ctrl.Text + "');
// If it does, configure the binding accordingly.
if (rowMatch.GetLength(0) > 0)
{
// We use offset 0 because there can only be one match.
string fieldToUse = rowMatch[0]["DataField"];
// We assume the text property is the one to be filled.
// Alternatively, we could add a database field with
// this information.
ctrl.Text = dt.Rows[fieldToUse];
}
}
}
}
This technique works well because it establishes an extra layer of indirection between the database and the controls. It's easy to modify this table if field names or user interface elements change. Best of all, the routine to fill the user interface is quite generic. Of course, you need to manually call this method every time the user moves to a new row to ensure that control synchronization occurs as naturally as it does with data binding.
Note
Variations on this theme are examined in Chapter 14, which shows you how to use a database to roll your own context-sensitive Help.Another way to help separate your database from your user interface code is by keeping database-specific content like field names and constants (used in the Parse and Format methods) in a separate resource class. The next example, which shows how you can use proper validation with data binding, demonstrates a perfect example of this technique with validation.
Validating Bound Data
Earlier in this chapter, you learned that one problem with ADO.NET data binding is validation. You can write specific error-handling code for each control, which is often a good approach, but one that creates extra code and ends up importing database details into your form code. Another approach is to handle the DataTable events like ColumnChanging, ColumnChanged, RowChanging, and RowChanged. The potential problem here is that the user may browse to another record, not realizing that invalid data has been rejected.Taking control of data binding navigation allows you to provide a more elegant solution. First, you create two form-level variables: one that tracks the current page, and the other that tracks the validity of the current record.
private int currentPage;
private bool errFlag;
You also need to hook up the events for column changes and position changes.
storeBinding.PositionChanged += new EventHandler(Binding_PositionChanged);
dsStore.Tables["Products"].ColumnChanged += new
DataColumnChangeEventHandler(TableChanging);
Next, you make the record navigation conditional on the current record being valid. If the ErrFlag member variable is set to true, the user is automatically sent back to the original page.
private void Binding_PositionChanged(object sender, System.EventArgs e)
{
if (errFlag)
{
// Reset the page.
storeBinding.Position = currentPage;
}
else
{
// Allow the page to change and update the currentPage variable.
currentPage = storeBinding.Position;
}
}
Next, you add the validation code, which occurs in response to a table change. This event is fired when the user tabs to a new field after making a modification, or tries to browse to a new record after making a modification. It always occurs before the PositionChanged event.
private void TableChanging(object sender,
System.Data.DataColumnChangeEventArgs e)
{
string errors = DBStore.ValidateProduct(e.Row);
if (errors == ")
{
errFlag = false;
}
else
{
errFlag = true;
}
lblErrorSummary.Text = errors;
}
You'll notice that so far this form doesn't contain any database-specific code. Instead, the validation is performed by passing the current row to a special static method provided by a database class. This method returns an error string, or an empty string if the validation succeeded.
public class DBStore
{
public static string ValidateProduct(DataRow row)
{
string errors = ";
if (((decimal)row["UnitCost"]) <= 0)
{
errors += "* UnitCost value too low\n";
}
if (row["ModelNumber"].ToString() == ")
{
errors += "* You must specify a ModelNumber\n";
}
if (row["ModelName"].ToString() == ")
{
errors += "* You must specify a ModelName\n";
}
return errors;
}
}
The error message is displayed in the window. Everything works nicely together. Database validation code is in a database component, but record navigation is halted immediately if an error is found.Figure 9-21 shows the final application detecting an error.

Figure 9-21: Custom row validation with data binding