4. Binding Controls to Data Sources

The previous chapter gave you a quick introduction to data binding in Windows Forms and a quick preview of using the new BindingSource component to provide looser coupling between data sources and data-bound controls. This chapter further explores using binding sources and binding objects, and the numerous examples of binding data to Windows Forms controls will help you learn to conquer even the most challenging data-binding scenario. This chapter covers using the BindingSource component to bind data to controls programmatically. Chapter 5 then shows you how to use the Data Sources window and the Windows Forms designer to automate much of that coding process for common scenarios.

Getting to Know the BindingSource Component

The BindingSource component solves a number of tricky problems that surfaced with the approach of directly binding data sources to controls in .NET 1.X. It provides a layer of indirection between a data source and bound controls that makes a number of things easier. Additionally, it surfaces important control points and access to the underlying data source in a way that saves you from having to delve into the data-binding mechanisms of a form the way you had to in the past. A binding source also gives you a single API to program against from the form’s perspective, and lets a lot of your form code remain decoupled from the specific data source type. This prevents you from having to adapt your programmatic coding patterns to each different type of data that your application works with, whether they are data sets, data readers, custom business objects, arrays, or other types of data. The BindingSource component also exposes a rich set of events that you can tap into to respond to changes in the underlying data coming from other controls or code in your application. I’ll be stepping through the use of all of these features throughout this chapter.

Simple Data Binding with Binding Sources

The simplest possible use of a binding source component is as an intermediary between the data you are using for display and the controls that display the data. Consider the simple form class shown in Listing 4.1.

LISTING 4.1: Binding a Grid Through a BindingSource Component


partial class CustomersForm : Form
{

   BindingSource m_CustomersBindingSource = new BindingSource( );
   DataGridView m_Grid = new DataGridView( );
   public CustomersForm( )
   {

      InitializeComponent( );
      m_Grid.Dock = DockStyle.Fill;
      this.Controls.Add(m_Grid);
      m_Grid.DataSource = m_CustomersBindingSource;
   }

   // Form.Load event handler
   private void OnFormLoad(object sender, EventArgs e)
   {

      CustomersTableAdapter adapter = new CustomersTableAdapter( );
      CustomersDataSet.CustomersDataTable customers = adapter.GetData( );
      m_CustomersBindingSource.DataSource = customers;
   }

}



This form contains a single grid named m_Grid, which has its Dock property set to Fill so that it fills the client area of the form. It also has a member binding source component, to which the grid is data bound. This can all be set up in the constructor, and is typically done by dragging and dropping both the grid and the binding source onto the form and setting the appropriate properties in the designer. However, for now, let’s focus on how to do things without the magic of the designer.

In the event handler for the form load, the code retrieves the Customers data through a table adapter (as discussed in Chapter 2), and then sets the DataSource property on the binding source to the Customers table. Because the grid was already set up with its DataSource property referencing the binding source in the constructor, this is all that is required to load and present the Customers data in the grid.

Image NOTE   Binding to a DataTable Really Binds to a DataView

When you bind to a data table within a data set, whether or not it’s strongly typed, you are really binding to the default data view exposed by that table. Every table exposes a default data view (an instance of the DataView class), and that is what is really used for data binding.

You can also explicitly construct a data view for a data table and bind to that, which gives you the flexibility to filter or sort that view to alter the presentation of the data. Alternatively, you can filter and sort data through a BindingSource, as long as the underlying data collection implements the IBindingList interface.

To change which data is displayed on the grid, you just set the DataSource property on the binding source member and the grid automatically updates. This may not seem like a big win if you only have one control on the form as in this example, but you’ll appreciate this if you have numerous controls on a form all bound to the same data source, and you need to programmatically switch the data source. Picture a data input form where you are editing all the columns for a customer or employee record. In the days before binding sources, if you needed to change the data source that each control was bound to, that meant programmatically resetting the data source binding on each individual control. Now you just need to change it in one place—on the binding source—and all of the controls bound to the binding source automatically switch to using the new data source.

A common situation when you might do this is while working with custom business objects. In that case, you might not retrieve an entire collection of objects, such as customers, into memory at once. You might get them one at a time as needed for editing or viewing the customer details. For example, if you were binding a form with input controls for a customer object’s individual properties so the user could edit that customer, you might query individually for customer objects based on their name or some identifier, and then you could update the data source for the binding source to be the retrieved customer object. For the user to edit a different customer, you would retrieve that object and set it as the data source for the binding source, and all the controls would update automatically to this new data source’s contents.

Binding sources can also be shared by more than one form. For example, to provide a simple form to let users edit certain columns of a customer record when they double-click on the row in the grid in Listing 4.1, you could design a simple form that looks like Figure 4.1.

FIGURE 4.1: Customer Editing Form

Customer Editing Form

You could have this form take the customer ID as a constructor parameter, retrieve the data into the form, edit it, and then save the data back to the database. You would then also need to refresh the main form with the grid after this dialog completes to reflect the change to the underlying data source. Depending on whether the grid itself was editable, this approach could introduce concurrency problems even for a single user in your application, and it is certainly not the most efficient approach in terms of round-trips to the database.

What you really want is for the two different forms to be working against the same data source in memory. This way you can pass the binding source that the editing form should bind to as a parameter to the editing form’s constructor from the customer listing form that launches the editing form, and then the editing form can bind against the provided binding source, as shown in the following code.

public partial class CustomerEditForm : Form
{

   public CustomerEditForm(BindingSource bindingSource)
   {

      InitializeComponent( );
      m_CompanyNameTextBox.DataBindings.Add("Text", bindingSource,
          "CompanyName");
      m_ContactNameTextBox.DataBindings.Add("Text", bindingSource,
          "ContactName");
    }


    private void OnSave(object sender, EventArgs e)
    {

       Close( );
    }

}


This form uses simple data binding between the individual text boxes and the appropriate members in the data source through the binding source that was passed to the form. By doing this, as soon as the changes are made in the form, they are automatically reflected in the underlying data, so the grid from the form that launches this form will be synchronized with that data as well. All the Save button needs to do then is close the form and the data will be saved in the client-side data source. If you wanted to persist those changes to the database at that point, you could add code to also call through your data access layer to push the changes down into the database.

Chaining Binding Sources for Master-Details Data Binding

Simple data-binding scenarios are obviously very common, but situations where you need to display multiple related sets of data in a single form are too. When you do so, you’ll want to keep the sets of data synchronized in terms of what the current record is in each data source. For example, consider the data schema shown in Figure 4.2. This schema has several tables defined with parent-child relations that cascade through several generations or layers of data.

FIGURE 4.2: Hierarchical Data Schema

Hierarchical Data Schema

As discussed in Chapter 3, it’s easy to achieve master-details data binding using a binding source. You need to chain together two binding sources, with one binding source bound to the parent data source, and the child binding source bound to the parent binding source. You then set the data member of the child binding source to the property name on the parent objects that exposes the related child collection. In the case of a data set, this property is actually the name of the data relation that links the parent and child tables. Finally, you bind the respective controls to the parent and child binding sources. When completed, the child binding source automatically manages the filtering of presented data in the child control(s) based on the current item in the parent data source. This works whether the parent and child data sources are tables or object collections.

Figure 4.3 shows an application that has two levels of parent-child data binding, which means that the three binding sources are chained together. The grandparent data at the top of the form is just a grid that is bound to a binding source whose data source is the GrandParentSet table of the typed data set generated from the schema shown in Figure 4.2. The grandparent, first sibling, second sibling, and grandchild data tables have been populated with some sample data to show the automatic filtering in action.

FIGURE 4.3: Hierarchical Data Viewing Form

Hierarchical Data Viewing Form

The first and second sibling data tables have foreign keys into the grandparent data, and the grandchild data has foreign keys into the first sibling data. When a row is selected in the GrandParentSet table, the sibling tables update to only show those rows related to the currently selected grandparent row. Likewise, when a row is selected in the first sibling table, the grandchild table updates to only show the related grandchild rows.

This is all accomplished by chaining the binding sources. Listing 4.2 shows the code used to set up the data binding.

LISTING 4.2: Master-Details Binding Source Chaining


partial class MasterDetailsChainingForm : Form
{

   MasterDetailsChainingDataSet m_Data =
      new MasterDetailsChainingDataSet( );
   BindingSource m_GrandParentBindingSource = new BindingSource( );
   BindingSource m_FirstSiblingBindingSource = new BindingSource( );
   BindingSource m_SecondSiblingBindingSource = new BindingSource( );
   BindingSource m_GrandChildBindingSource = new BindingSource( );

   public MasterDetailsChainingForm( )
   {

      InitializeComponent( );
      InitData( );
      m_GrandParentBindingSource.DataSource = m_Data;
      m_GrandParentBindingSource.DataMember = "GrandParentSet";
      m_GrandParentGrid.DataSource = m_GrandParentBindingSource;

      m_FirstSiblingBindingSource.DataSource =
         m_GrandParentBindingSource;
      m_FirstSiblingBindingSource.DataMember =
         "FK_GrandParentSet_FirstSiblingSet";
      m_FirstSiblingGrid.DataSource = m_FirstSiblingBindingSource;

      m_SecondSiblingBindingSource.DataSource =
          m_GrandParentBindingSource;
      m_SecondSiblingBindingSource.DataMember =
         "FK_GrandParentSet_SecondSiblingSet";
      m_SecondSiblingGrid.DataSource = m_SecondSiblingBindingSource;

      m_GrandChildBindingSource.DataSource =
         m_FirstSiblingBindingSource;
      m_GrandChildBindingSource.DataMember =
         "FK_FirstSiblingSet_GrandChildSet";
      m_GrandChildGrid.DataSource = m_GrandChildBindingSource;
   }

   private void InitData( )
   {
     ...
   }
}



The InitData method programmatically populates the data set with several rows per table, with appropriate foreign key values from the child rows to the parent rows to set up the master-details relations. You can see that there is a separate binding source per data grid, and they are set up as described earlier in this section. The top-level binding source for the entire parental hierarchy is set with its data source to the GrandParentSet data table. The first and second sibling binding sources are each set to have the grandparent binding source as their data source, and the data member is set to the data relation’s name that ties the child table to the parent table. For example, for the first sibling this is the FK_GrandParentSet_FirstSiblingSet relation. The grandchild binding source is set to have the first sibling binding source as its data source, and its data member is set to the FK_FirstSiblingSet_GrandChildSet data relation.

You can use the same approach of chaining binding sources for binding to object collections that are hierarchical. Consider the object definitions in Listing 4.3.

LISTING 4.3: Hierarchical Object Definitions


public class ParentObject
{
   private BindingList<ChildObject> m_Children =
      new BindingList<ChildObject>( );
   private string m_Greeting = "Hello there";

   public string Greeting
   {
      get { return m_Greeting; }
      set { m_Greeting = value; }
   }

   public BindingList<ChildObject> Children
   {
      get { return m_Children; }
      set { m_Children = value; }
   }
}

public class ChildObject
{
   private int m_DataItem1 = 42;
   private string m_DataItem2 = "yadda";

   public ChildObject( ) { }
   public ChildObject(int i, string s)
   {
      m_DataItem1 = i;
      m_DataItem2 = s;
   }

   public string DataItem2
   {
      get { return m_DataItem2; }
      set { m_DataItem2 = value; }
   }

   public int DataItem1
   {
      get { return m_DataItem1; }
      set { m_DataItem1 = value; }
   }
}



Because there is a parent-child relationship represented by the Children collection on the parent object, you can use that for master-details data binding through a binding source as well. You could add two grids and two binding sources to a form, create a collection of the parent objects, and then data bind in a similar fashion to what you do when the data collections are tables:

partial class Form1 : Form
{
   ParentObject[ ] pos = new ParentObject[2];
   public Form1( )
   {
      InitializeComponent( );
      // Bind grids
      m_ParentGrid.DataSource = m_ParentBindingSource;
      m_ChildGrid.DataSource = m_ChildBindingSource;

      // Create data objects
      pos[0] = new ParentObject( );
      pos[1] = new ParentObject( );
      pos[0].Children.Add(new ChildObject( ));
      pos[1].Children.Add(new ChildObject(1,"foo"));

      // Bind connectors
      m_ParentBindingSource.DataSource = pos;
      m_ChildBindingSource.DataSource = m_ParentBindingSource;
      m_ChildBindingSource.DataMember = "Children";
   }
}


The key concept here is knowing that you need to set the data source for the parent binding source to be the parent object collection (in this case, an array held by the form), and the data source for the child binding source to reference the parent binding source. Then the data member of the child binding source needs to be the name of the property on the parent object that is a reference to the child collection. In this case, that is the Children property, which is of type BindingList<ChildObject>. BindingList<T> is a generic collection type in .NET 2.0 that is specifically designed for Windows Forms data binding. ChildObject is the type parameter that specifies what type of object the collection will contain. You can use this technique of chaining together binding sources to support arbitrarily deep hierarchies of parent and child data and their bound controls.

Navigating Data Through a Binding Source

In Chapter 3 you saw that you can change the current record in the underlying data source by using a set of Move XXX methods and the Position property. I want to review those again here while I am in the process of making you an expert on binding sources, and point out a few other methods that assist in navigating through the data contained in a binding source. Table 4.1 shows the properties and methods related to navigating through the data in the underlying data collection and describes what each does.

TABLE 4.1: BindingSource Navigation Properties and Methods

Image

Manipulating Data Through a Binding Source

The binding source itself gives you indirect access to the data that is stored in the underlying client-side data source. Once you set the DataSource property on a binding source, you don’t have to maintain a reference to the data source object itself, because one will be maintained by the binding source. You can always retrieve a reference to the data source by casting the DataSource property to the expected type. The only downside of this approach is that you have to make sure that at the point in the code where you are accessing the DataSource reference, you know exactly what type of data collection the binding source is holding onto.

You can also always get to the current data item in the collection through the Current property on the binding source. The Current property returns an object reference, and as long as you know the type of each item in the collection, you can again cast it to the appropriate type and work against its members. Remember, the type of object returned by the Current property will be a DataRowView object any time you are bound to a DataTable or DataView, and will be an instance of whatever object type you have stored in your object collection when dealing with custom objects.

To work with the underlying data for a binding source in a more loosely coupled way, you can access the data indirectly through the binding source without having to cast to the specific type at design time (because the Current property just returns an object reference for the current item without specifying its type). There is an indexer on the BindingSource class, so you can pass an index in to access a particular item and get back an object reference:

object fifthItem = m_BindingSource[4];


You can access the List property, which returns an IList reference. You can then iterate through the items in the collection or index into it through the IList reference. Once you obtain an object reference by using any of these approaches, you could then use PropertyDescriptors (discussed in Chapter 7) to reflect on the objects and obtain information on them without having design-time insight into the specific type of object that the binding source is managing.

Using a Binding Source as a Data Storage Container

You can also use the binding source without actually binding a particular data source to it. If no data source has been set for an instance of a binding source, you can add objects directly to the list that is contained by the binding source. To do this, you use the Add or AddNew methods on the BindingSource class. The Add method inserts an item in the underlying list. If nothing has been added to the list yet (through the Add method or implicitly through setting the DataSource property), then the first item added also determines the type of the objects contained in the List maintained by the binding source. Subsequent attempts to add an item to the list must add the same type of object, or an InvalidOperationException will be thrown because the items in the list must be homogeneous. Setting the DataSource property refreshes the entire collection with whatever collection of data the property is set to, so that results in the loss of any items that have been manually added to the list through the Add method.

The AddNew method lets you add a new item directly and get a reference to the new item back, allowing you to edit its properties. AddNew returns an instance of whatever type of object the binding source is set to contain. If no type has been set before calling AddNew, then an instance of type Object will be added, which is pretty useless. So calling AddNew only really makes sense after you have set a DataSource or you have added other objects of a specific type to the list with the Add method.

The AddNew method also causes the EndEdit method to be called (discussed later in this chapter) and commits any changes to the current row to the underlying data source. The new item becomes the current item after AddNew has been called. Finally, AddNew raises the AddingNew event, which you can use to either initialize the new object to a set of default values, or you can actually create the object that is used for the new object and return it through the event handler, as shown in Listing 4.4.

In Listing 4.4, the OnAddingNew event handler is subscribed to the AddingNew event on the binding source in the form’s constructor. Note that the event subscription in the form constructor is using delegate inference in C#, rather than explicitly creating a new instance of the event delegate type. This new language feature provides a more compact syntax for creating delegates for event handlers or callback method parameters. The AddingNew handler constructs the new object and returns it through the event argument, and then the subsequent code in the form Load event handler modifies the property exposed on that object to some value other than its default.

LISTING 4.4: Handling the AddingNew Event to Create an Object


public partial class Form1 : Form
{
   BindingSource m_CustomObjectSource = new BindingSource( );
   public Form1( )
   {
      InitializeComponent( );
      m_CustomObjectSource.AddingNew += OnAddingNew;
   }

   private void OnFormLoad(object sender, EventArgs e)
   {
      object newObj = m_CustomObjectSource.AddNew( );
      MyCustomObject mco = newObj as MyCustomObject;
      mco.Val = "yadda";
   }

   private void OnAddingNew(object sender, AddingNewEventArgs e)
   {
      e.NewObject = new MyCustomObject( );
   }
}

public class MyCustomObject
{
   string m_Val = "foo";
   public string Val
   {
      get { return m_Val; }
      set { m_Val = value; }
   }
}



You can also remove items from the collection of data contained by the binding source using the Remove and RemoveAt methods. Remove takes an object reference and looks for an instance of that object in the list, and removes it if found. RemoveAt takes an index and removes the item found at that location. You can remove all of the items from the list using the Clear method.

Filling a Binding Source with a Data Reader

Another opportunity that opens up with the binding source that wasn’t really an option before in Windows Forms is the ability to bind controls (indirectly) to data coming from a data reader. If you execute a data reader through a command object, you can set a binding source’s DataSource property to that data reader. The binding source will iterate through the data reader’s contents and use it to quickly populate the binding source’s List collection. Then, as long as you execute the data reader using the CommandBehavior.CloseConnection flag, the connection will close and release back to the connection pool. Or you can close it explicitly or by disposing of the connection. See Appendix D if you are unfamiliar with data readers.

The code in Listing 4.5 shows using a data reader for binding. First the grid that the BindingSource is bound to has its AutoGenerateColumns property set to true in the constructor. This is required any time you will dynamically provide data to a DataGridView control without setting up its columns ahead of time. In the Form.Load event handler, after executing the reader, the code sets the binding source’s data source property to the reader, which causes the binding source to iterate through all the items in the reader and add them to the internal List maintained by the binding source. In this case, the items added to the List of the binding source are instances of the DbDataRecord class from the System.Data.Common namespace. These objects have enough schema information embedded that it’s easy for the DataGridView or other controls to use reflection to extract the column schema information just like it would for a DataTable.

LISTING 4.5: Populating a BindingSource Object with a DataReader


public Form1( )
{
   InitializeComponent( );
   m_Grid.AutoGenerateColumns = true;
}

private void OnFormLoad(object sender, EventArgs e)
{
   SqlConnection conn = new SqlConnection(
      "server=localhost;database=Northwind;trusted_connection=true");
   // SQL Server 2005 Express connection:
   // SqlConnection conn = new SqlConnection(
   //    @"server=.SQLEXPRESS;AttachDbFileName=
   //     C: empNorthwind.mdf;trusted_connection=true");

   SqlCommand cmd = new SqlCommand("SELECT CustomerID, CompanyName,
      ContactName, Phone FROM Customers", conn);
   using (conn)
   {
      conn.Open( );
      SqlDataReader reader = cmd.ExecuteReader( );
      m_CustomersBindingSource.DataSource = reader;
   }
}



Using a data reader in this way has several advantages and disadvantages. The advantage is speed—a data reader is about the fastest possible way to get data from a query into a collection in your application. The disadvantage is the tight coupling it introduces between your presentation tier and your data tier. I would recommend staying away from this approach for any large-scale application where maintainability is a concern, and only use it as a performance optimization technique for those places where you have identified a performance hotspot in your presentation of data.

Sorting, Searching, and Filtering Presented Data with a Binding Source

If the data source bound to the binding source implements the IBindingList or IBindingListView interfaces (covered in detail in Chapter 7), then you may be able to sort, search, or filter the data through the binding source. The data source implementation of the IBindingList interface will have to return true from the IBindingList.SupportsSorting property in order to sort through the binding source. If it does, you can provide a sorting expression to the Sort property, and the data exposed through the binding source will automatically be sorted. This doesn’t require any direct support for sorting in the control(s) to which the binding source is bound. The following example shows setting a sort expression for a binding source bound to a CustomersDataTable.

private void OnBindSortedCustomerGrid(object sender, EventArgs args)
{
   m_CustomersGrid.DataSource = m_CustomersBindingSource;

   CustomersTableAdapter adapter = new CustomersTableAdapter( );
   CustomersDataSet.CustomersDataTable customers = adapter.GetData( );
   m_CustomersBindingSource.DataSource = customers;
   m_CustomersBindingSource.Sort = "ContactName ASC";
}


In this code, a grid is bound to a binding source. The binding source is then bound to a CustomersDataTable instance returned from a table adapter. The Sort property on the binding source is then set to "ContactName ASC", which will sort the data from the table in ascending order based on the ContactName column. The grid will then display the data as sorted, because it sees the data as it is exposed by the binding source, regardless of the physical ordering in the underlying data table. The syntax for sort criteria is the name of the sort property, followed by ASC for ascending or DESC for descending. If no sort direction is specified, ascending is used as the default.

Data sources can use this advanced form of sorting through the IBindingListView interface. If a data source implements this interface and returns true from the IBindingListView.SupportsAdvancedSorting property, then you can pass a more complex sort expression with multiple sort criteria to the Sort property. This lets you sort on multiple columns or properties in the collection. For example, for a CustomersDataTable, you could pass a sort expression of "Region ASC, CompanyName DESC". This would sort first on the Region column in an ascending order, then rows that had the same value for Region would be sorted by the CompanyName values in a descending order.

To search a data source through a binding source, you can call the Find method on the binding source. This method takes a property name and an object reference. The property name indicates which property on each of the collection’s items should be checked, and the object reference contains the value that is being sought in that property. When the first item in the list is found whose property value for the specified property matches the specified value, its index will be returned. For this to work correctly, the underlying data source has to implement IBindingList and should return true from the IBindingList.SupportsSearching property. The following example shows how to use the Find method.

private void SetCurrentItemToSpecificCompany(string companyName)
{
   int index = m_CustomersBindingSource.Find("CompanyName",companyName);
   if (index != -1)
   {
      m_CustomersBindingSource.Position = index;
   }
}


This code searches the CompanyName property on each of the items in the list maintained by the binding source and seeks the one with a value that matches whatever was passed into this method. Note that there are no assumptions about the type of the underlying data source or its objects here. This method would work equally well for a CustomersDataTable or a custom collection of Customer objects, provided that the custom collection properly implements IBindingList with searching support. See Chapter 9 for an example of how to provide this support in your collections.

If the underlying collection implements the IBindingListView interface and returns true from the IBindingListView.SupportsFiltering property, the Filter property on the binding source can be set to a filter expression. When this is done, the data exposed through the binding source will be filtered to only show the matching data. Depending on the capabilities of the data source, this should work similarly to the filtering capabilities of a DataView in ADO.NET (see Appendix D for more details). The specific syntax and complexity supported in the filter expression is determined by the data source. The filter expression is just passed through the binding source to the data source, and the filtering is left up to that data source, as shown in the following example:

private void ShowGermanCustomers( )
{
   m_CustomersBindingSource.Filter = "Country = 'Germany'";
}


This code filters the list exposed by the binding source to only those objects whose Country property (or column in the case of a data table) is equal to Germany. Any bound controls will automatically update to display only those items.

Monitoring the Data with Events

Another important capability of data-binding scenarios, especially when there are layers of decoupling involved, is getting notified when a change to the underlying data source occurs. Table 4.2 shows the events exposed by the BindingSource.

TABLE 4.2: BindingSource Events

Image

Image

You can use events like PositionChanged, ListChanged, and CurrentChanged to control or synchronize the data binding of other controls on the form that aren’t necessarily a strict parent-child relation that could be managed through chaining the binding sources as described earlier.

For example, imagine you had a data-bound combo box control on a form, and whenever a new value is selected in the combo box, you need to switch to a new data source on a second binding source that is controlling the data presented through another set of controls. Perhaps the combo box contains a collection of connection strings or database names. You could handle this situation with the switch on the SelectedIndexChanged event for the combo box. But what if there were multiple controls on the form that could cause the currently selected item in the collection of data sources to change? Using the CurrentChanged event on the binding source for the combo box and other controls, you could simply handle the situation at the binding source level instead of at the individual control level.

You can also use these events to synchronize data binding between collections of data beyond just master-details types of binding. This is demonstrated in Listing 4.7 (on page 166), where the CurrentChanged event updates the display of parent item data when a selection in a collection of child objects is made. You can also use the CurrentChanged event to achieve something like a master-details experience between object collections that are related through a many-to-many relationship, shown later in the section “Synchronizing Many-to-Many Related Collections.”

Restricting Changes to the Data

The binding source can act as a gatekeeper for you to restrict access to the underlying data dynamically without the data source itself having to modify its behavior at all. By setting any of the AllowEdit, AllowNew, and AllowRemove properties defined on the IBindingList interface to false, you can prevent client code from making the respective modifications to the underlying data. Setting these properties makes the data items contained by the binding source appear as if they don’t support the type of modification being attempted.

Any calls from bound controls to the IBindingList interface methods to determine whether their data source supports editing, adding, or removing are given the answer by the binding source instead of from the data source itself. The controls or code can then modify their behavior to present the data in a read-only mode, or disable, add, or remove controls. To restore the binding source to whatever behavior the underlying data source supports for allowing the addition of new items, call the ResetAllowNew method. There are no corresponding reset methods for allowing editing or removing; you have to explicitly set the AllowEdit or AllowRemove properties back to the desired value.

Underneath the Covers of Data Binding for Complex Types

Binding text and integer data to grids and controls rarely presents a challenge once you know the basic approach to Windows Forms data binding. But what does get a little trickier is presenting things like floating point numeric types, dates, and images that are stored in the database. The problem with these types is that their format in the database or in the custom objects doesn’t necessarily map directly to the way that you would present them.

For example, when storing a graphic image in the database, you usually store the raw bytes of the saved image file or object into an image column in the database. To present that image, you need to transform it back into an image type that is compatible with the controls you are using to display it. You also may need to modify the raw stored image before presentation, such as scaling it down to present a thumbnail view of it. If it is a date or floating point numeric type, the database often stores greater precision than you plan to display, so the data needs to be formatted before presentation. You may also have a foreign key column in a table, and rather than just displaying that column value, you may want to retrieve a corresponding value from a column in the parent table for display purposes. For instance, you might want to get the customer name for display instead of just displaying a customer ID if you are displaying a collection of orders in a grid. Database columns can store NULL values for columns that translate to value types in the .NET type system, but because value types can never be null, what should happen when you try to data bind that column to a value type property on a control? The answer is going to depend on the design of the control, but a well-designed control will handle a null value gracefully and document what that behavior will be. You will see examples of how to control that behavior in the next few sections.

For all of these situations, there are easy ways to get what you want if you know where to look. Chapter 6 goes into more detail about handling situations like these for the DataGridView control. In this section, let’s focus on binding these complex types to individual properties on controls (simple data binding, as defined in the last chapter). The key to handling these situations for individual control bindings is to understand how the Binding class works and how it controls the data-binding process.

In Listing 3.4, a Binding object was implicitly created and added to the DataBindings collection on a text box control by using an overload of the Add method on that collection:

private void AddTextBoxDataBindings(CustomersDataSet customers)
{
   DataTable table = customers.Customers;
   m_CustomerIDTextBox.DataBindings.Add("Text", table,
      "CustomerID", true);
}


This code binds the Text property on the TextBox control to the CustomerID column in the data table that is provided as the data source. This is actually equivalent to the following code, where you first explicitly create a Binding object and then add it to the collection:

private void AddTextBoxDataBindings(CustomersDataSet customers)
{
   DataTable table = customers.Customers;
   Binding customerIDBinding = new Binding("Text", table,
      "CustomerID", true);
   m_CustomerIDTextBox.DataBindings.Add(customerIDBinding);
}


The Binding object is a middleman between your data source and your data-bound control. It adapts its behavior for providing values to bound controls and accepting changes back from them based on

•    The capabilities of the data source (determined by the interfaces the data source supports)

•    The types of the data member and the bound control property

•    The properties set on the Binding object itself

The binding object determines what value is set on the bound property of a control when the control is rendered or when the underlying data changes (referred to as formatting the data). It also determines what value is written back to the data member when updates occur (referred to as parsing the data) if the data source supports updating. The Binding object has been significantly enhanced in .NET 2.0, including more built-in capability to automatically handle formatting of data values when data binding occurs. Data binding occurs for a Binding object when it is added to a control’s DataBindings collection.

There are several important overloads of the constructor for the Binding object that have a significant effect on what happens when data binding occurs. The parameters available to the various constructor overloads are shown in Table 4.3. Each of the parameters is also exposed as a property on the Binding class with the same name but PascalCased, which lets you set these values declaratively through properties instead of passing them as arguments to the constructor.

TABLE 4.3: Binding Class Constructor Parameters

Image

Image

The minimum constructor uses the first three parameters listed in Table 4.3. Overloads let you specify any of the additional parameters as needed for your data-binding scenario.

When you enable formatting by passing true in the formattingEnabled parameter to the constructor, or by setting the FormattingEnabled property to true, the binding object automatically performs type conversions between the type of the bound control property and the type of the data member when formatting occurs, and the reverse direction when parsing occurs. If automatic formatting fails, a FormatException will be thrown. Automatic formatting is an alternative to handling the Format and Parse events to manually control conversions. The default value for FormattingEnabled is false, but you should set this to true as a general rule unless you are specifically trying to avoid any changes to the data value from the underlying data source value.

The type conversion process is influenced by the types of the bound control property and data source property, as well as the NullValue, FormatString, and FormatProvider properties on the binding object. If FormattingEnabled is false, then the values for formatting and null values will be ignored. To understand how type conversion works, you need to know the basics of the standard approach to type conversions that are part of the .NET Framework. The sidebar “Type Conversions and Format Providers” discusses how these work.

For simple types like integers and strings, whose normal presentation includes the full contents of the data as they are stored, no type conversion is really needed. But for more complex database types, such as image, datetime, or floating point number, the formatters can transform the raw data into a completely different object type. The next few sections step through some examples of setting up data binding for complex types to help illustrate how this all works. The code for the next few sections is contained in the sample application ComplexTypeBinding in the download code.

Binding an Image Column to a PictureBox Control

Let’s start with a somewhat visually appealing example—binding an image column from a database to a PictureBox control on a form. This example assumes that the data stored in the database are the bytes of a saved bitmap image (or JPEG, GIF, PNG, or any other image type supported by the Bitmap class in .NET). An image column in the database gets stored in a data set as a byte array (byte[ ]). Because there is a built-in type conversion for converting from an Image type to a byte array and vice versa, the simplest way to achieve this is to enable automatic formatting on the binding object used to set up the data binding and let it do the work. The following code shows how to set up the binding object for a PictureBox control to display an image.

m_PhotoPictureBox.DataBindings.Add("Image",
   m_EmployeesBindingSource, "Photo", true);


The code uses the overloaded Add method on the DataBindings collection of the PictureBox control and maps the Image property on the control to the Photo column in the m_EmployeesBindingSource data source. The m_EmployeesBindingSource has been set up with its data source set to the Employees table of the Northwind database. Passing true for the formattingEnabled parameter sets the FormattingEnabled property to true on the binding object. This tells the binding object to perform automatic type conversions and formatting based on the types of the bound control property and the data member as described earlier. When Visual Studio writes the data-binding code for you through drag-and-drop operations from the Data Sources window, it enables formatting in a similar fashion.

Binding a DateTime Column to a DateTimePicker

The DateTimePicker control is designed specifically for displaying date and time information and to give you a richer editing experience for the values by treating each of the parts of the displayed information (such as month, day, year, hours, minutes, and seconds) as individual values that can be set when editing through the control. Additionally, it provides a drop-down calendar control that lets a user select a date from a one-month calendar view (see Figure 4.4).

FIGURE 4.4: ComplexTypeBinding Sample Application with DateTimePicker Drop-Down Selected

ComplexTypeBinding Sample Application with DateTimePicker Drop-Down Selected

The DateTimePicker control had a number of problems in databinding scenarios prior to .NET 2.0, but with the automatic formatting capabilities of the Binding object, it works very nicely with date and time values now. However, it still has problems if the property or column you are data binding to is equal to null or DBNull.

All it takes to get the DateTimePicker data bound is to create the binding object with formatting enabled and add it to the DataBindings collection on the control:

m_BirthDateTimePicker.DataBindings.Add("Value",
   m_EmployeesBindingSource, "BirthDate", true);
m_BirthDateTimePicker.Format = DateTimePickerFormat.Custom;
m_BirthDateTimePicker.CustomFormat = "MM/dd/yyyy";


Note that in the case of the DateTimePicker, the control itself has built-in formatting properties for how it presents the bound DateTime value. To specify what formatting the control should use, you set the Format property to an enumeration value of type DateTimePickerFormat. This enumeration has values of Long, Short, Time, and Custom. Long displays the date and time, Short just the date, Time just the time, and with Custom you can pass a custom formatting string through the CustomFormat property as shown in the code example. (You’ll see in the next section how this works if the control doesn’t directly support formatting.) I should point out that using a custom date format like this one will cause problems if you end up needing to localize your application to other cultures. Effectively this is a hard-coded dependency on the U.S. style for showing dates.

Binding a DateTime Column to a TextBox

Developers frequently choose to use a simple text box control for date and time display and input. In part this is due to the data-binding problems experienced with the DateTimePicker control in .NET 1.1 and to the remaining problems with dealing with nulls. But it can also be to simply have explicit control over the display and input values. Presenting a drop-down calendar as shown in Figure 4.4 doesn’t make a lot of sense if the control is only going to display times. Unfortunately, the DateTimePicker doesn’t give you the ability to disable that functionality, so you may decide to display the time in a TextBox control. Another new control to consider for this scenario is the MaskedTextBox control, which lets you specify allowable patterns for the input into the text box.

The same basic approach applies for binding a DateTime value to a text box: you need to create the binding object with formatting enabled and add it to the DataBindings collection on the control.

Binding hireDateBinding = new Binding("Text",
   m_EmployeesBindingSource, "HireDate",true);
hireDateBinding.FormatString = "d";
hireDateBinding.NullValue = "<unknown>" ;
m_HireDateTextBox.DataBindings.Add(hireDateBinding);


This example explicitly creates an instance of the Binding class first, instead of using one of the overloads of the Add method on the DataBindings collection. The constructor for the Binding class has similar overloads, so you can choose whether to initialize all the relevant formatting properties inline as constructor parameters or to break them out as properties as shown in this code snippet. This example constructs the Binding object with automatic formatting turned on (the formattingEnabled parameter to the constructor set to true) and then sets the FormatString property, which will be passed to the format provider that is being used for the control. In the example, the NullValue property is set to a string that will be displayed in the text box if the underlying bound property or column contains a null or DBNull value.

Because the value being used to set the Text property on the text box is a DateTime from the data source, the DateTimeFormatInfo provider is used. It supports a number of predefined and custom formatting strings as discussed earlier. This code example passes a format string of d, which translates to the short date predefined format. This displays the data only in the format MM/DD/YYYY in the United States, but will display it as DD/MM/YYYY in Europe. You could also pass a custom string such as MM/yy, which would display the date with only two digits each for month and year. Note that these formatting strings are case sensitive: MM will output a two-digit numeric month, and mm will output the minutes as two digits.

Binding a Numeric Column to a TextBox

Setting up binding for a numeric column to a TextBox control is very similar to doing it for a DateTime column. The main difference is in the formatting strings that you use to specify the output format in the text box. You can again provide the format string either through the Binding constructor or by setting the FormatString property on the Binding object. You can also use one of the overloaded Add methods on the DataBindings collection. The following code demonstrates the latter approach.

m_SalaryTextBox.DataBindings.Add("Text", m_EmployeesBindingSource,
   "Salary", true, DataSourceUpdateMode.OnValidation,
   "<not specified>", "#.00");


If you are an astute Northwind user, you know that there isn’t a Salary column in the Employees table that we are using for this example. I added a Salary column to the Employees table in the typed data set after it was generated by the designer, with a column type of decimal. Then, in the sample code Form.Load event, I generated random salary values between 0 and 200,000 after retrieving the data from the database and injecting them into the rows:

Random rand = new Random((int)DateTime.Now.Ticks);
foreach (NorthwindDataSet.EmployeesRow row in employees)
{
   row.Salary = new Decimal(rand.Next(200000));
}


The formattingEnabled parameter to the Add method set to true turns on automatic formatting. You can use the DataSourceUpdateMode enumeration value to specify when automatic formatting occurs (OnValidation, OnPropertyChanged, or Never). The nullValue parameter maps to the NullValue property, and it’s used here to specify that if the bound data member value is null or DBNull, then the text box should display the string <not specified>. Note that this null mapping works in both directions. If someone enters <not specified> in the salary text box, then the value that will be written to the data source will be DBNull.Value. The formatString parameter is the custom format string, which in this case specifies to display the number with two decimal places.

You can see the end result of the code from the previous few sections in Figure 4.5. You can get the code itself from the ComplexTypeBinding sample application in the download. I programmatically set the values of the HireDate and Salary columns to DBNull on the first record, and used the SetHireDateNull and SetSalaryNull methods exposed on the EmployeesDataRow class from the typed data set before data binding. This lets you see the effect of setting the NullValue property on the bindings for those columns.

FIGURE 4.5: ComplexTypeBinding Application in Action

ComplexTypeBinding Application in Action

Automatic Formatting and Parsing Summary

This section summarizes what happens in the automatic formatting and parsing process. In the case of formatting, the source type is the type of the data source property involved in the binding, and the target type is the type of the bound control property. For parsing, the source type is the type of the bound control property, and the target type is the type of the data source property.

•    If the target type of the conversion process is a string, and a format string has been provided, the formatting string is passed to the format provider to obtain the properly rendered version of the target string. Each built-in type has a default format provider defined for it.

•    If the target type isn’t a string or if there isn’t a format provider associated with the source type, then the source and target types are inspected to see if one or the other has a type converter that can convert between the source and target types. If so, that type converter is used, such as the case of converting an image to a byte array or vice versa.

•    If no type converter can be found, and the source or target type is a built-in .NET Framework type, the other type is checked to see if it implements the IConvertible interface. When a type implements this interface, it provides conversions to all the built-in types in the .NET Framework.

•    Finally, if none those conversion processes can be done, a FormatException is thrown.

The whole story is actually even a little more complex than that. There are other conversion attempts made under the covers to use static Parse methods if the target type has a static Parse method defined and the source type is a string, and there are other implicit conversions for common types in a few other places in the formatting pipeline. The bottom line is that if you enable automatic formatting, the .NET Framework is going to try to do everything possible to come up with a sensible type conversion to render the data for you.

Going Beyond Built-In Type Conversion with Binding Events

When there is a built-in conversion and formatting process, the binding approach outlined so far is the easiest and most straightforward approach. However, there are always times when you need to do something a little different than the standard data-binding mechanisms support, so knowing how to go beyond the built-in type conversions is an important skill in your data-binding toolbox. Understanding the limitations of these custom approaches is also important.

For now, let’s just focus on simple data binding. You can take control of the whole data-binding process for individual control properties by handling a number of different events, including events raised by the binding object itself and events raised by the control that is being bound. In fact, you often need to handle both control events and binding events to ensure that edited values in controls are pushed down into the data source before a form is closed.

The two Binding class events of interest are the Format and the Parse events. A number of other Binding class events are simple notifications that you can subscribe to if you are interested in knowing when any of the properties affecting data binding change, such as the FormatString and NullValue properties. As you might suspect from the preceding sections, the Format event is raised when the value is being pulled out of the data source property and before the bound control property is set using that value. The Parse event is the reverse: It is fired when the value has changed in the control property and is going to be written back into the corresponding data source property for two-way data binding.

The Format and Parse events give you explicit control over the values being set while data binding. Both events are declared using the same delegate type, a ConvertEventHandler, and they take two arguments. The first argument follows the pattern for most Windows Forms events and is an object reference that refers to the publisher of the event; the second argument is a ConvertEventArgs parameter, which lets you step in and provide whatever value you want when formatting and parsing occurs.

The ConvertEventArgs parameter has two properties that you will want to use to control the data-binding process. The DesiredType property tells you what type is needed for the object value being set. For the Format event, this represents the type of the property on the bound control and is an instance of the Type class that provides the metadata about the property type. For the Parse event, it specifies the type of the data source property that is being written to. The Value property gives you access to the object that is currently going to be used to try to set that property on the control or the data source. If you do nothing with the value, then the Binding class ends up just trying to set the property value using the current value. Normal type conversions will apply if automatic formatting is turned on (as discussed earlier in the section on automatic formatting). However, the Value property is an object reference, so you can replace the value with anything you like. If you do, whatever you set the Value property to is what will be used to set the value of the control or data source property. The Format and Parse events fire before automatic type conversions are applied. As a result, if you turn on automatic formatting and provide a different value through the Value property, the automatic formatting will be applied to the object that you set as the Value, instead of the one that was pulled out of the data source property or control property.

To demonstrate some of the things you can do through binding events, the download code for this chapter contains a project named BindingEvents, which contains the currency exchange rate application shown in Figure 4.6. To use this application, you first have to create a new database called MoneyDB. There is a script named MoneyDB.sql in the download code for Chapter 4 that you can run from SQL Query Analyzer to create and populate the database with some sample data. There is also an additional application called MoneyDBAdmin that you can use to edit, add, or delete data from the tables in this database.

FIGURE 4.6: Currency Exchange Application

Currency Exchange Application

The BindingEvents application performs numerous forms of data binding to demonstrate most of the concepts discussed so far in this chapter.

The application works on a database that contains two tables: ExchangeRates and Countries. The Countries table contains the name of the country (e.g., United States, United Kingdom, etc.), the currency type name (dollars, pounds, etc.), and an image of the country’s flag. The ExchangeRates table contains exchange rates between two countries with the rate information and the date the rate was based on. The country information is stored in the ExchangeRates table as foreign keys in the Countries table. Figure 4.7 shows the schema for this data in the data set designer diagram. You can see that there are two foreign key relations from ExchangeRates to Countries, one for the CountryFromID column and one for the CountryToID column.

FIGURE 4.7: Currency Exchange Data Set

Currency Exchange Data Set

The user interface shown in Figure 4.6 lets users browse through the records of exchange rate data, with the related country information for each exchange rate included in the form—as if it were all stored as a single table of data. However, the normalized format of the data in the data set requires some additional work to provide this appearance to the user. To support this display, each exchange rate record needs to retrieve two corresponding country records to display their data in place with the exchange data for the From Country and To Country text boxes. This example uses data-binding events to do this; in a later example, I will redo this to use additional BindingSource objects and their events.

In this example, the form is coded so that if a user types in a country name and tabs out of the country text box, the data in the text box is parsed, a lookup in the Countries table is made, and if the country is found, that country’s currency type and flag will be displayed in the controls below the country text box. The code to support this for the two sets of country data controls is identical except for the controls and relations it works against.

There are a number of other controls on the form tied in with the data-binding mechanisms. First, there is a BindingNavigator control, which lets users page through the data records one at a time. (This was described in Chapter 3; it is simply a derived class from the ToolStrip control with controls and handlers for navigating the data. It also includes buttons for adding new records, deleting the current record, and saving changes in the data set.) The Exchange Rate and Exchange Date controls at the bottom of the form have their binding set up exactly as was discussed in the Binding a DateTime Column to a DateTimePicker and Binding a Numeric Column to a TextBox sections.

Here’s what is going on in the binding of the country information at the top of the form. To start with, you need data to work on, so the sample loads the data from both the ExchangeRates and Countries tables into a data set in the form constructor:

public CurrencyExchangeForm( )
{
   InitializeComponent( );
   // Get the data
   CountriesTableAdapter countriesAdapter =
      new CountriesTableAdapter( );
   ExchangeRatesTableAdapter exchangeRatesAdapter =
      new ExchangeRatesTableAdapter( );

   m_ExchangeRatesDataSet = new ExchangeRatesDataSet( );

   countriesAdapter.Fill(m_ExchangeRatesDataSet.Countries);
   exchangeRatesAdapter.Fill(m_ExchangeRatesDataSet.ExchangeRates);

   m_ExchangeRatesBindingSource.DataSource = m_ExchangeRatesDataSet;
   m_ExchangeRatesBindingSource.DataMember =
      m_ExchangeRatesDataSet.ExchangeRates.TableName;
   CreateBindings( );
}


After filling each of the tables using their respective table adapter, the constructor code sets the data source on the binding source to the data set and sets the data member to be the ExchangeRates table. Notice that this uses strongly typed properties of the typed data table here to get the table name through a property, instead of having to code a string literal. Finally, it calls a helper method called CreateBindings, into which the code is separated for setting up the individual control bindings. The CreateBindings method is shown in Listing 4.6.

LISTING 4.6: Creating the Individual Bindings


private void CreateBindings( )
{
   // From Country TextBox
   Binding countryFromBinding = new Binding("Text",
      m_ExchangeRatesBindingSource, "CountryFromID");

   countryFromBinding.Format +=
      new ConvertEventHandler(OnCountryFromFormat);
   countryFromBinding.Parse +=
      new ConvertEventHandler(OnCountryFromParse);
   m_CountryFromTextBox.DataBindings.Add(countryFromBinding);

   // To Country TextBox
   Binding countryToBinding = new Binding("Text",
      m_ExchangeRatesBindingSource, "CountryToID");
   countryToBinding.Format +=
      new ConvertEventHandler(OnCountryToFormat);
   countryToBinding.Parse +=
      new ConvertEventHandler(OnCountryToParse);
   m_CountryToTextBox.DataBindings.Add(countryToBinding);

   // From currency type text box
   Binding currencyFromBinding = new Binding("Text",
      m_ExchangeRatesBindingSource, "CountryFromID");
   currencyFromBinding.Format +=
      new ConvertEventHandler(OnCurrencyFromFormat);
   m_CurrencyTypeFromTextBox.DataBindings.Add(currencyFromBinding);

   // To currency type text box
   Binding currencyToBinding = new Binding("Text",
      m_ExchangeRatesBindingSource, "CountryToID");
   currencyToBinding.Format +=
      new ConvertEventHandler(OnCurrencyToFormat);
   m_CurrencyTypeToTextBox.DataBindings.Add(currencyToBinding);

   // Exchange rate
   Binding exchangeRateBinding = new Binding("Text",
      m_ExchangeRatesBindingSource, "ExchangeRate", true,
      DataSourceUpdateMode.OnValidation, "1.0000", "#.0000");
   m_ExchangeRateTextBox.DataBindings.Add(exchangeRateBinding);

   // Exchange rate date
   Binding exchangeRateDateBinding = new Binding("Value",
      m_ExchangeRatesBindingSource, "ExchangeRateDate", true);
   m_ExchangeRateDateTimePicker.DataBindings.Add(
      exchangeRateDateBinding);
}



You can see from the CreateBindings method that this subscribes to the Format and Parse events on the binding object for each of the country name text boxes, and only the Format event for the currency type text boxes since they are read-only. Also note that both the country name and currency type text boxes for From and To information use CountryFromID and CountryToID from the ExchangeRates table, respectively. Because the country name, currency type, and flag are determined by the foreign key stored in the ExchangeRates table, the data-binding process effectively denormalizes the data back into a flat set of data for display.

Note that there are no data bindings set up for the PictureBox controls that display the flags. Those are manually bound through the Format event handlers for the country name text box, since the two pieces of information are linked through the country ID that is used to bind the country name. I could have used the same approach for populating the currency type text box since it is read-only, but I wanted to demonstrate that you can bind more than one control to the same data member, but handle the bindings completely differently if needed, through separate Format and Parse event handlers.

Handling the Format Event

The Format event for individual binding objects fires each time the property or column in the data source changes. As discussed earlier, the Format event is passed a ConvertEventArgs, which contains the Value that is to be used for setting the bound control property unless you decide to change it. In your event handler, you can transform that value, and you can do other processing or set properties on other controls as well:

void OnCountryFromFormat(object sender, ConvertEventArgs e)
{
   if (e.Value == null || e.Value == DBNull.Value)
   {
      m_FlagFromPictureBox.Image = null;
      return;
   }
   ExchangeRatesDataSet.CountriesRow countryRow =
      GetCountryRow((int)e.Value);
   e.Value = countryRow.CountryName;
   // Now set other control properties based on this same binding
   ImageConverter converter = new ImageConverter( );
   m_FlagFromPictureBox.Image =
      converter.ConvertFrom(countryRow.Flag) as Image;
}


In this Format handler for the From Country text box, the code first checks to see if the value of the event argument is null or DBNull. This happens when paging to a new record that has not been populated yet. If the CountryFromId column is empty in the data set, it will be set to DBNull. However, the null case could happen if the data source were changed to an object collection instead of a data set, so it is best to program defensively. If the value is DBNull or null, the text box itself will just display an empty string, which is fine, but the code will also clear out the picture box containing the flag.

The normal case is that a country ID is passed in for the value. The handler code takes that country ID and calls a helper method that looks up the corresponding row in the Countries table using the DataTable.Select method.

Once the country row has been obtained, the value on the event argument is set to the country name. Doing this changes the value that will be used to set the Text property in the text box once the data-binding process completes. Instead of displaying the CountryFromId column value, it will display the text value set on the event argument. In addition, the Format handler retrieves the Flag column from the country row, uses the ImageConverter class to transform the byte array into an Image object, and sets the Image property on the PictureBox control to that object. This keeps the flag picture box synchronized with the displayed country name, all based on the country code that was originally bound to the country name text box. The ImageConverter class is the same one that is used by the automatic formatting type conversion process described earlier in the chapter.

The Format event handler for the currency type is simpler. It just does the lookup to obtain the country row, and then substitutes the currency type for the country ID that was passed in the value of the event argument:

void OnCurrencyFromFormat(object sender, ConvertEventArgs e)
{
   if (e.Value == null || e.Value == DBNull.Value)
   {
      return;
   }
   ExchangeRatesDataSet.CountriesRow countryRow =
      GetCountryRow((int)e.Value);
   e.Value = countryRow.CurrencyType;
}


Handling the Parse Event

As mentioned, the application also lets users type a country name into the From Country or To Country text boxes, and it will update the country information based on that input. The things that need to be updated are the currency type and flag for the entered country name, and the corresponding country ID that is set in the current ExchangeRates row. You could deal with this kind of situation by handling the TextChanged event on the text box and doing the lookup of the entered country name in that handler, but I wanted to show how you can accomplish this using data-binding mechanisms. The sample has the Parse event on the country name text box intercept the changed country name. The Parse event will be fired when the contents of the TextBox have changed and the focus changes to another control (after Validating and Validated events fire).

void OnCountryFromParse(object sender, ConvertEventArgs e)
{

   // Need to look up the Country information for the country name
   ExchangeRatesDataSet.CountriesRow row =
      GetCountryRow(e.Value.ToString( ));
   if (row == null)
   {

       string error = "Country not found";
       m_ErrorProvider.SetError(m_CountryFromTextBox, error);
       m_CountryFromTextBox.Focus( );
       throw new ArgumentException(error);
   }
   e.Value = row.CountryID;
}


For the Parse event, the Value property of the event argument contains the value of the bound control property. When the parsing process is complete, the value set on the Value property will be used to set the content of the bound column in the data source. So the Parse handler needs to obtain the country ID corresponding to the entered country name, which it does using another helper method. The helper method again uses the DataTable.Select method on the Countries table, this time looking for the entered country name. If the country name is found, the Parse handler substitutes the country ID for the value on the event argument, and that will set the corresponding CountryFromID column in the current ExchangeRates row to which this text box is bound.

If the country name isn’t found, you need to let the user know and prevent an invalid value from being set in the data source. The way you do that is to throw an exception. When you throw an exception from a binding event handler, it terminates the binding process for that control and forces the control to refresh its bound property from the data member (triggering the Format event again). You also want to draw the user’s attention to the problem, so the code also uses an error provider to alert the user of the problem and sets the focus back on the offending text box. The binding object handles the event, so the message that you provide in the thrown exception isn’t important unless you are using instrumentation to monitor thrown exceptions at the runtime level.

Completing the Editing Process

If you coded the application as discussed so far, you will see there is still a problem with the data binding for the currency type text boxes. When you type in a new country name in the From Country or To Country text box and then press Tab, the flag for the country entered will be displayed. However, the currency type text box won’t be updated unless you page to another record and then page back using the binding navigator. This problem is caused by the way a data row works when you edit the data contained in it.

The DataRowView class implements the IEditableObject interface. This interface lets an object support transactional changes to the object. This means that you can start to edit the object by setting values on its properties, and then you can either accept those changes or you can roll them back to the previous values before the object editing started. You commence changing an object like this with the BeginEdit method on the interface. You commit the changes by calling a method on that interface named EndEdit, and you roll the changes back with the CancelEdit method. Until the EndEdit method is called on the object, property value changes on that object are considered transient and aren’t reflected in any other controls bound to that same object. Additionally, if EndEdit isn’t called, those pending changes won’t be persisted if you try to save the data source to its underlying data store.

Don’t confuse EndEdit with the AcceptChanges method on a data set, data table, or data row. Data rows in a data table can maintain both a current version and the original version of the row values that were retrieved when the data set was filled. The IEditableObject caching described earlier goes beyond that capability; it says that there is really a third version when you are editing a row containing the uncommitted edits to the row. Until EndEdit is called on the row, any changes made programmatically or through bound controls to columns in the row aren’t actually reflected in the current version of the row.

For example, say you have two text boxes bound to the same column in a data row, such as the CountryName column. If you change the value in one of the text boxes and tab to the other text box, you won’t see the edited value reflected in the second text box. The changed value from the first text box has been parsed and written to the underlying data member in a transient state, but the changed value hasn’t been committed to the data source yet, so other controls bound to that same data member don’t see the change yet.

For the other text box to see the change and be updated, EndEdit needs to be called on the object, which is most easily done by calling EndEdit on the binding source that you are using for setting up the data binding. When you call EndEdit on a binding source, it calls EndEdit on the current item. The best place to call EndEdit if you want an edited value to be immediately committed is to handle the Validated event on the bound control and put the call to EndEdit in that handler. The Validated event is raised after the entered control value has passed validation and has been parsed, but focus hasn’t yet changed to the next control. Calling EndEdit at this point commits the change; all other controls that are bound to that same data item will be notified that the data source has changed, and they will perform formatting on their respective data members.

EndEdit is also called implicitly if the current item changes. If you have a data navigator like in the BindingEvents application, when you page to another record, EndEdit will be called on the binding source, which calls EndEdit on the current data item. This triggers other data bindings that are bound to properties in that current item to perform the formatting process and display any updates. Likewise, when working with a DataGridView, when you change the current row through a selection in the grid, the EndEdit method is called on the row that was previously selected if any changes had been made to the row.

Image TIP   Call EndEdit on the data item to commit changes that have been made in simple bound controls

Developers are often confused when they change a value in one bound control and other controls bound to the same data source don’t immediately update, or when they try to save the changes to the database and the old values are still persisted. This often happens because EndEdit needs to be called on the current item to commit the changes to the object if the object implements IEditableObject. Editing begins on the current item (through a call to the BeginEdit method on the interface) the first time one of its properties is edited through a data-bound control. (The IEditableObject interface is discussed in Chapters 7 and 9.)

Getting back to the problem with the currency type text boxes on the BindingEvents sample’s form, when a user enters a new country name and tabs to another control, the currency type doesn’t update because when you set the CountryFromID or CountryToID column value in the Parse event handler for the text box, you have made a transient programmatic change to that row. Until the change is committed with a call to EndEdit, other controls bound to that same property won’t be notified that the value has changed.

To fix this problem, the Validated event handlers on the Country From and Country To text boxes need to call EndEdit on the binding source and get it to commit the changes to the current row. This will cause Format to fire again on the currency type text boxes, allowing them to update their contents based on the new CountryFromID or CountryToID:

public CurrencyExchangeForm( )
{

   // Other constructor code ...
   m_CountryFromTextBox.Validated += OnCountryFromValidated;
   m_CountryToTextBox.Validated += OnCountryToValidated;
}
private void OnCountryFromValidated(object sender, EventArgs e)
{
   m_ExchangeRatesBindingSource.EndEdit( );
}


private void OnCountryToValidated(object sender, EventArgs e)
{
   m_ExchangeRatesBindingSource.EndEdit( );
}


Making the User’s Life Easier with AutoComplete

If you run the BindingEvents sample, you will see that if you start to type in a country name that is in the database, the text box controls for From Country and To Country will actually provide AutoComplete functionality—a drop-down list of the available countries based on the characters that have been typed in so far displays. Any time you are going to provide a TextBox or ComboBox input control that is likely to take on repeated or predictable values, you should consider providing AutoComplete functionality for your users. In this case, for example, when users start by typing the letter U, they will immediately get a drop-down with United Kingdom and United States of America in it (using the sample data in the MoneyDB.sql script). They can use the arrow keys to select an item in the list, and the list will continue to refine as they type more characters. When they tab out of the text box, the currently selected item will be accepted and entered as the text in the box. If they press the Esc key, the list disappears and they can type whatever they like.

This slick new feature in Windows Forms 2.0 is easy to use. The following steps show you how to enable AutoComplete.

1.   Select the TextBox control in the designer, and set the AutoCompleteSource property to a value other than None (the default) through the Properties window. Other modes are available, including FileSystem, HistoryList, RecentlyUsedList, and a few others that map to built-in collections of strings that are either provided by the system or are managed by the Framework.

2.   Because you want to provide the list of values yourself, set the AutoCompleteSource property to CustomSource.

3.   Set the AutoCompleteMode to SuggestAppend. This means that as the users type, the drop-down list will display matches and append any missing letters for the selected item when users tab out of the TextBox. You can also set the mode to Suggest or Append if desired.

4.   If you aren’t using one of the built-in sources, you need to write a little code to create the list of strings that will be checked against for suggested values. Create an instance of the AutoCompleteStringCollection class and include each value you want to have in the collection against which the AutoComplete functionality checks for matches.

5.   After you build that collection, set it as the AutoCompleteCustomSource property for the TextBox.

Here’s the code that creates and attaches the custom list of AutoComplete values:

private void BuildAutoCompleteList( )
{
   AutoCompleteStringCollection filterVals =
      new AutoCompleteStringCollection( );
   foreach (ExchangeRatesDataSet.CountriesRow countryRow in
      m_ExchangeRatesDataSet.Countries)
   {
      filterVals.Add(countryRow.CountryName);
   }
   m_CountryFromTextBox.AutoCompleteCustomSource = filterVals;
   m_CountryToTextBox.AutoCompleteCustomSource = filterVals;
}


This method is called from the constructor of the BindingEvents sample application after data binding is set up. You can find the complete code for BindingEvents in the download code for this chapter.

Data Binding Lifecycle

As mentioned earlier in the book, it’s important to keep in mind the direction of data flows in data binding and when the data flow occurs. Formatting sends data from the data member to the control property, and parsing sends data from the bound control property to the data member. But when do these processes happen?

The trigger for formatting is usually when the current item in a data source is being set, or when the property in the current item for a given binding object is changed. The current item in the data source is set when the data first loads, and again at any time the Position property on the CurrencyManager for the data source changes. For a tabular data source, such as a data set, the current item is the current row of the data table. For an object collection, the current item is an object reference to one of the objects in the list. If you use the binding source to separate your bound controls from your data sources as recommended, then you don’t have to worry about the CurrencyManager; just think of it in terms of the Position and Current properties on the binding source. The BindingSource component encapsulates a currency manager and exposes its properties and events in an easy-to-use API, so you almost never need to worry about the currency manager.

You can expect the formatting process to get called whenever

•    You set the data source on a binding source that is bound to controls

•    The current item in the binding source is set to a different position

•    The data member that a binding object is tied to changes in the underlying data source (possibly through programmatic code or through a change from another control bound to that same data member)

The exception is if you set the ControlUpdateMode property on the binding object to Never. This prevents the control from being formatted automatically when the value of the data member changes. Effectively, this makes the data member write-only through data binding. This supports certain advanced error handling scenarios, especially when you want one control to support writing data to the data source, but you don’t want that control to update with the current value of the data member if another control bound to the same item in the data source triggers the formatting process. Normally you will want to leave this property set to its default value of OnPropertyChanged.

The trigger for the parsing process depends on the binding object’s DataSourceUpdateMode property. This property supports three enumerated values: OnValidating, OnPropertyChanged, and Never. The default, OnValidating, means parsing will occur after the Validating event on the bound control fires. For example, if you edit the value in a data-bound TextBox control that has a DataSourceUpdateMode value of OnValidating, and then tab off the control, the order of events is TextBox.Leave, TextBox.Validating, Binding.Parse, and TextBox.Validated. If you set the CausesValidation property on the control to false in conjunction with the OnValidating value, then neither validation nor parsing will ever occur.

If the value of the DataSourceUpdateMode property is set to OnPropertyChanged, then Binding.Parse will fire every time the bound control property changes. For a TextBox, that means it will fire for every character entered. Finally, setting the DataSourceUpdateMode property to Never means the parsing process will never be triggered, making the control a read-only control from a data-binding perspective.

Smarter Child-Parent Data Binding

If you’ve spent any time looking at the BindingEvents application, you are probably thinking that there must be a better way to handle this particular data-binding scenario than what has been presented in the preceding sections—and you’re right. What I’ve showed so far was more to illustrate the use of the formatting and parsing events, not the cleanest way to address the scenario.

If you step back from the existing code and analyze the data-binding scenario, what you really need for the BindingEvents application are three data items that stay synchronized:

•    The ExchangeRates row that is the current record being browsed with the data navigator

•    The Countries row corresponding to the From Country information

•    The Countries row corresponding to the To Country information

The functionality you are trying to achieve here is that when paging through the exchange rate data rows, the corresponding country information should be displayed at the top of the form. And if you edit one of the country names, you want the flag and currency type to update to the entered country.

Countries and ExchangeRates are related by a couple of parent-child relations, so you may be thinking of chaining the binding sources. Unfortunately, that won’t really work here because you are displaying child rows, but you want the parent data items to be synchronized to the child, not the other way around.

There is a fairly straightforward way to do this, and it again relies on events. Whenever the current exchange rate record changes, you want to update the two pieces of country information based on the foreign key columns in the exchange rate table. However, you’d probably rather not have to do so much manual data binding for the controls that contain the two parent data items as you did in the BindingEvents sample. What you really want is to have those two sets of country information controls data bound to the country rows themselves, but to keep those bindings synchronized with the data binding of the exchange rows being browsed.

BindingSource components raise a number of events throughout their data binding lifecycle, as described earlier. The one you would be interested in for this scenario is the CurrentChanged event. If you simply handle the CurrentChanged event for the ExchangeRates binding source, you can add code to set the current record for the country information and you will get exactly the behavior described here.

One other change you might want to consider is making the user interface a little friendlier by replacing the text boxes for From Country and To Country with combo box controls that display all the countries currently in the data set. Selecting the country from the combo box should update all the controls displaying country information on that side of the form and should also update the corresponding foreign key column in the exchange rate row being displayed. This new user interface design is shown in Figure 4.8.

FIGURE 4.8: Updated Currency Exchange Application

Updated Currency Exchange Application

Binding to Multiple Copies of Data

The first trick that some people get hung up on for an application like this is dealing with displaying two sets of data-bound controls that are bound to the same set of data (the two sets of country controls), but wanting them to display different items. Your first instinct might simply be to create a single binding source for the Countries table and bind each of the controls in the two sets to their respective columns in that data source. Unfortunately, if you do this, the From Country will always reflect the same information as the To Country and vice versa. Whichever one is selected last will update the other set of controls to match it.

If you read and understood the discussion of currency managers in the last chapter, you probably already understand the problem. If both sets of controls are bound to the same data source, there is one currency manager created for that data source, and there is only one current item ever in that currency manager. Updating the current item in one of the sets of controls immediately updates the controls in the other set because they are bound to the same data source, and the currency manager for that data source keeps all controls bound to the data source synchronized to the current item. So to fix that problem you need to maintain two separate currency managers for the two sets of controls. You could get two separate copies of the data, and each would have its own currency manager as separate data sources. However, a better approach is to have two separate country binding sources, each bound to the same underlying single Countries table in the data set. This does exactly what’s needed without requiring you to maintain two copies of the data. Because each binding source encapsulates its own currency manager, even if bound to the same set of data, it gives you just the layer of indirection you need.

As shown in Listing 4.7, the revised sample application includes three binding sources corresponding to the three sets of displayed data—one for the exchange rate data, one for the From Country data, and one for the To Country data. Only one set of data is retrieved and used, and the two country binding sources are both bound to the same table in that data set.

LISTING 4.7: Initializing the Three BindingSource Components


public CurrencyExchangeForm( )
{
   InitializeComponent( );
   // Get the data
   CountriesTableAdapter countriesAdapter =
      new CountriesTableAdapter( );
   ExchangeRatesTableAdapter exchangeRatesAdapter =
      new ExchangeRatesTableAdapter( );
   m_ExchangeRatesDataSet = new ExchangeRatesDataSet( );
   countriesAdapter.Fill(m_ExchangeRatesDataSet.Countries);
   exchangeRatesAdapter.Fill(m_ExchangeRatesDataSet.ExchangeRates);

   m_CountriesFromBindingSource.DataSource = m_ExchangeRatesDataSet;
   m_CountriesFromBindingSource.DataMember =

      m_ExchangeRatesDataSet.Countries.TableName;
   m_CountriesFromBindingSource.CurrentChanged +=
      OnCountryFromChanged;

   m_CountriesToBindingSource.DataSource = m_ExchangeRatesDataSet;
   m_CountriesToBindingSource.DataMember =      m_ExchangeRatesDataSet.Countries.TableName;
   m_CountriesToBindingSource.CurrentChanged += OnCountryToChanged;

   m_ExchangeRatesBindingSource.DataSource = m_ExchangeRatesDataSet;
   m_ExchangeRatesBindingSource.DataMember =
      m_ExchangeRatesDataSet.ExchangeRates.TableName;
   m_ExchangeRatesBindingSource.CurrentChanged +=
      OnCurrentExchangeRateChanged;
   CreateBindings( );
}



The code hooks up the data source and member for each binding source, sharing the Countries table in the data set across two binding sources. It also hooks up event handlers to the CurrentChanged event on each binding source and uses those handlers to help enforce synchronization between the different data sources. By using binding sources like this, the data-binding code for the individual controls is lot more straightforward. You no longer need to hook up to the Format and Parse events, and can simply let the normal data-binding mechanisms and automatic formatting do all the work for you. You could even hook up all the rest of the data binding in the designer; the code to do it programmatically is shown in Listing 4.8 so that you can see what is going on at the individual control level.

LISTING 4.8: CreateBindings Method


private void CreateBindings( )
{
   m_CurrencyTypeFromTextBox.DataBindings.Add("Text",
      m_CountriesFromBindingSource, "CurrencyType", true);

   m_CurrencyTypeToTextBox.DataBindings.Add("Text",
      m_CountriesToBindingSource, "CurrencyType", true);

   m_FlagFromPictureBox.DataBindings.Add("Image",
      m_CountriesFromBindingSource, "Flag", true);

   m_FlagToPictureBox.DataBindings.Add("Image",
      m_CountriesToBindingSource, "Flag", true);

   m_ExchangeRateTextBox.DataBindings.Add("Text",
      m_ExchangeRatesBindingSource, "ExchangeRate", true,
      DataSourceUpdateMode.OnValidation, "1.0000", "#.0000");

   m_ExchangeRateDateTimePicker.DataBindings.Add("Value",
      m_ExchangeRatesBindingSource, "ExchangeRateDate", true);

   m_FromCountryCombo.DataSource = m_CountriesFromBindingSource;
   m_FromCountryCombo.DisplayMember = "CountryName";
   m_FromCountryCombo.ValueMember = "CountryID";

   m_ToCountryCombo.DataSource = m_CountriesToBindingSource;
   m_ToCountryCombo.DisplayMember = "CountryName";
   m_ToCountryCombo.ValueMember = "CountryID";

   // Twiddle the position to get the CurrentChanged
   // event to sync things up initially
   m_ExchangeRatesBindingSource.Position = 1;
   m_ExchangeRatesBindingSource.Position = 0;
}



Most of the code in Listing 4.8 just adds data bindings to the individual controls and ties the appropriate control property to the corresponding column in the table through the binding source with automatic formatting turned on. The end of the method shows the data binding setup for the combo boxes. As discussed in Chapter 3, the ComboBox control uses complex binding and lets you specify both a DisplayMember and a ValueMember within the data source. In this case, set the ValueMember to the CountryID column so that you can later use that to update the exchange rate row when the parent item changes.

The code also does a little “twiddle” at the end by setting the position on the exchange rate binding source to one and then back to zero. This is needed to make things work correctly on the initial presentation of the form. The reason for this twiddle is to get the CurrentChanged event (discussed next) to fire again after all the data bindings are set up, because it is the CurrentChanged handler that takes care of synchronizing the three sets of controls.

Updating Parent Data-Bound Controls from Child Data-Bound Controls

The trick to reversing master-details scenarios like this is to synchronize the controls bound to parent data items based on the selection of child data items. In this case, the individual data items are rows in two different tables. Those tables have a parent-child relation from the parent Country table to the child ExchangeRates table based on the foreign key constraints from the CountryFromID and CountryToID columns of the ExchangeRates table to the CountryID column of the Countries table. The CurrentChanged event on the child data source gives you the perfect opportunity to perform that synchronization. The following code shows the handler for the CurrentChanged event on the exchange rates binding source:

private void OnCurrentExchangeRateChanged(object sender, EventArgs e)
{
   // Get the strongly typed row for the exchange rate table
   ExchangeRatesDataSet.ExchangeRatesRow currentRow =
      (ExchangeRatesDataSet.ExchangeRatesRow)
      ((DataRowView)m_ExchangeRatesBindingSource.Current).Row;

   // Get the related parent rows through the properties generated
   // on the typed data rows
   ExchangeRatesDataSet.CountriesRow fromCountryRow =
      currentRow.CountriesRowByFK_ExchangeRates_CountriesFrom;
   ExchangeRatesDataSet.CountriesRow toCountryRow =
      currentRow.CountriesRowByFK_ExchangeRates_CountriesTo;

   // Update the parent row controls based on this record change
   if (fromCountryRow != null && toCountryRow != null)
   {
      m_FromCountryCombo.SelectedValue = fromCountryRow.CountryID;
      m_ToCountryCombo.SelectedValue = toCountryRow.CountryID;
   }
   else // New record
   {
      currentRow.CountryFromID = 0;
      m_CountriesFromBindingSource.Position = 0;
      currentRow.CountryToID = 0;
      m_CountriesToBindingSource.Position = 0;
      currentRow.ExchangeRate = 1.0M;
      currentRow.ExchangeRateDate = DateTime.Now;
      // Commit the changes to notify other controls
      m_ExchangeRatesBindingSource.EndEdit( );
   }
}


The first thing the code does is to obtain a strongly typed reference to the current exchange rate row through the binding source. It does this by casting the Current item to a DataRowView, then using the Row property on that to obtain a DataRow reference to the row, which it then casts to the strongly typed row. After the code gets the current exchange rate row, it uses the strongly typed row properties exposed on the ExchangeRateRow to obtain the two country parent rows. These properties are named by the data set code generation based on the relations in the XSD file that defines the data set. If there is just one parent-child relation between the tables, it will be named for the parent table (e.g., CountriesRow). However, in this case there are two parent-child relations between the Countries table and the ExchangeRates table, so it appends the specific relation name to distinguish between the two (e.g., CountriesRowByFK_ExchangeRates_CountriesTo). These properties simply call the GetParentRow method for you with the appropriate relation name. You could also call this method directly, providing the relation names, but you’d get back an untyped data row and have to do more casting. Thus the parent row properties are a better option.

As long as the parent rows are found, all it takes to synchronize the two sets of parent data is to set the SelectedValue property on the country combo boxes to the corresponding foreign key values from the child row. Alternatively, you could have gone through the individual binding sources for the two sets of country data, done a lookup to find the corresponding item in the data source, then set that as the current item, but that would be a lot more work. Setting the SelectedValue property on the combo box is just like the user selecting the item in the list, which updates the current record in the data source, so this results in much more straightforward code.

The code also handles the situation wherein, if you have paged to an item that has just been added to the exchange rates table, none of the columns are initialized yet, so the code sets some simple defaults. Notice that it also calls the EndEdit method after making those changes. You need to call this method any time you programmatically update values in the data source to get the bound controls to update (discussed earlier in the section “Completing the Editing Process”).

Image NOTE   Working with Strongly Typed Data Sets Requires Tradeoffs

The downside of working with the strongly typed data rows is that you end up having to do a lot of casting to get them into their appropriate type from methods that return loosely typed rows. The tradeoff is that when you access the columns of that row—like the code does at the end of the method—you can get compile-time error information if any of the columns get renamed or go away. That compile-time information has saved me countless hours, and so the tradeoff is well worth the extra casting hassle to me.

You need two more CurrentChanged handlers to get the whole form functioning in an intuitive way that keeps the three sets of data synchronized. Any time the user selects a new country in one of the two sets of country data controls at the top of the form, they are effectively editing the exchange rate item that is currently being displayed (specifically, setting the CountryFromID or CountryToID columns). But those controls are bound to different sets of data. To get those changes pushed down into the exchange rates table, you can handle the CurrentChanged event on each of the binding sources for the two sets of country controls. The following code is for the m_CountriesFromBindingSource. The code for the other country binding source is identical except for the names of the objects involved, which reflect the other set of country controls:

private void OnCountryFromChanged(object sender, EventArgs e)
{
   // Get the current From Country row
   ExchangeRatesDataSet.CountriesRow fromCountryRow =
      (ExchangeRatesDataSet.CountriesRow)
      ((DataRowView)m_CountriesFromBindingSource.Current).Row;

   // Get the current exchange rate row
   ExchangeRatesDataSet.ExchangeRatesRow currentExchangeRateRow
      = (ExchangeRatesDataSet.ExchangeRatesRow)
      ((DataRowView)m_ExchangeRatesBindingSource.Current).Row;

   // Set the foreign key column in the child
   currentExchangeRateRow.CountryFromID = fromCountryRow.CountryID;
}


Again, most of the code here has to do with obtaining and casting the current country and exchange rate rows to the appropriate type so that you can do a strongly typed assignment from the parent row primary key to the child row foreign key.

This code is all included in the BindingParentData sample application in the download code.

Synchronizing Many-to-Many Related Collections

Binding source events also come in handy for managing complex synchronization scenarios when you want to present data that has many-to-many relationships between the data items. In these situations, you might want to present the related items with multiple controls in a way that gives the user a similar experience to master-details data binding.

For example, take a look at the data schema depicted in Figure 4.9. It shows the Orders, Order Details, and Products tables from Northwind. The Order Details table has foreign key relations into both the Orders and Products tables. As a result, in addition to containing other data related to each order detail item, it also forms a many-to-many relationship between Orders and Products. In other words, each Order contains a collection of Products, and each Product belongs to many Orders.

FIGURE 4.9: Many-To-Many Relationship Between Orders and Products

Many-To-Many Relationship Between Orders and Products

Assume you want to present a user interface that lets the user browse through orders and see which products were contained within those orders. You could present the collection of orders in one grid, and when the user selects an order row in the grid, you could present all the products that are part of that order in another grid, as shown in Figure 4.10.

FIGURE 4.10: Browsing Related Many-To-Many Records

Browsing Related Many-To-Many Records

The application shown in Figure 4.10 is named ManyToManyBinding and is contained in the download code for this chapter. It contains a data set with the relations shown in Figure 4.9 and uses a set of binding sources to drive the user interface behavior we are trying for here. Specifically, the form has a grid for orders and a grid for products. In the sample I applied some formatting of the columns in the grid using the designer to get the specific set of columns shown. The form also has three binding sources: one for the Orders grid, one for the products grid, and one is just used to help synchronize the data that will be presented through the other two binding sources.

The code supporting the Orders grid is just like many of the other data-binding examples shown so far. The grid is bound to the binding source, and the binding source is bound to the Orders table in the data set. The code that sets up the data binding for the Products grid actually does much the same, binding the grid to its binding source, and the binding source to the Products table:

void InitDataBindings( )
{
   // Hook the grids to their binding sources
   m_OrdersGrid.DataSource = m_OrdersBindingSource;
   m_ProductsGrid.DataSource = m_ProductsBindingSource;

   // Hook up the orders and products binding sources
   m_OrdersBindingSource.DataSource = m_OrdersProductsDataSet;
   m_OrdersBindingSource.DataMember =
      m_OrdersProductsDataSet.Orders.TableName;
   m_ProductsBindingSource.DataSource =
      m_OrdersProductsDataSet.Products;

   // Set up the order details binding source
   // for master-details binding
   m_OrderDetailsBindingSource.DataSource = m_OrdersBindingSource;
   m_OrderDetailsBindingSource.DataMember = "FK_Order_Details_Orders";

   // Hook up the ListChanged event on the details source
   m_OrderDetailsBindingSource.ListChanged += OnDetailsListChanged;
}


The data binding hook-up code then sets up master-details binding between the Orders binding source and the Order Details binding source, so whenever a new Order row is selected, the data contained by the m_OrderDetailsBindingSource will be updated to just show the order detail rows for the selected order. The code then hooks up an event handler for the ListChanged event on that Order Details binding source so that you can react to that changing list of details, each of which contains a ProductID that identifies the product the detail item represents.

The following code shows the handler for that ListChanged event:

void OnDetailsListChanged(object sender, ListChangedEventArgs e)
{
   if (m_OrderDetailsBindingSource.Count < 1)
      return;
   PropertyDescriptor productIdPropDesc =
      TypeDescriptor.GetProperties(
      m_OrderDetailsBindingSource.Current)["ProductID"];
   // Extract the parent item identifier from each order detail item
   // and add to a filter string
   StringBuilder builder = new StringBuilder( );

   foreach (object detailItem in m_OrderDetailsBindingSource.List)
   {
      int productId = (int)productIdPropDesc.GetValue(detailItem);
      if (builder.Length != 0) // Adding criteria
      {
         builder.Append(" OR ");
      }
      builder.Append(string.Format("ProductID = {0}", productId));
   }
   // Set a filter on the products binding source to limit
   // what is shown in the products grid
   m_ProductsBindingSource.Filter = builder.ToString( );
}


This event handler first has a guard statement to see if there are any detail rows to work with. If not, it does nothing. The event handler next gets the type information for the ProductID property from the current item in the Order Details binding source. I could have simply cast the items to the DataRowView type as in some previous samples, but I wanted to write the code in a way that would work even if the data source was composed of an object hierarchy with many-to-many relations. When you use the TypeDescriptor class to retrieve property descriptor information, it will work whether the underlying collection is a data table or some other custom collection of custom objects. In this case, as long as the objects in the collection have a ProductID property (or column), it will work just fine (see Chapter 7 for more details about property descriptors).

The event handler then loops through each of the Order Details items and extracts the value of the ProductID property using the property descriptor. It uses that value to build up a filter string using the StringBuilder class. Note that any time you are doing string concatenation in a loop like this, you should use the StringBuilder class or String.Format to avoid creating unnecessary temporary strings that make the garbage collector work harder.

Once the filter string has been constructed from the product IDs of all the detail rows, the filter string is set on the Filter property of the binding source. The Filter property, as discussed earlier, restricts the items presented through the binding source based on the filter criteria, as long as the underlying data source supports filtering. As a result of setting that filter, the only rows that will be presented in the Products grid are the ones that have order details related to the current order, which gives us just what we were looking for.

Where Are We?

This chapter covered the BindingSource component in detail and discussed how to use it as an intermediary between your data sources and your data-bound controls. You learned how to set up binding objects to bind control properties to sources, using either automatic formatting or binding events to control the formatting and parsing yourself. It also covered some event handling at the BindingSource level, which gives you more control over related items on a form that don’t have a parent-to-child relationship you can directly bind to.

Some key takeaways for this chapter are the following:

•    You create simple data bindings by using the Binding class to associate a single property on a control with a single property in a data source.

•    To set up master-details data binding, you point the child collection binding source to the parent collection binding source and specify which property within the parent collection identifies the child relation.

•    The BindingSource component is a rich data container that encapsulates a currency manager and a data source. It exposes numerous methods and properties that let you manage the contained data collection, as well as events to let you track what is happening to the data source. You can use these events to support updating controls bound to parent objects when the child objects are selected.

The next chapter shows you how to generate most of the data-binding code covered in this chapter using the designer, which saves an immense amount of time. I think it is important to understand the code that is being generated, which is why I covered how to do it manually first. Now you will learn how to shave days or weeks off your Windows Forms data-binding schedules using Visual Studio 2005.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.154.89