Cutting Edge: Data Binding Between Controls in ...

We were unable to locate this content in de-de.

Here is the same content in en-us.

From the February 2002 issue of MSDN Magazine
MSDN Magazine
Data Binding Between Controls in Windows Forms
Dino Esposito
Download the code for this article: Cutting0202.exe (129KB)
D
ata binding is a very powerful feature of most applications, and Windows® Forms and Web Forms applications are clearly no exception. Data binding is the process of retrieving data from a source and dynamically associating it with a property of a visual element. Depending on the context in which the element will be displayed, you can map the element to an HTML tag, a Microsoft® .NET Web control, or a Windows Forms control. Generally speaking, data binding is not a platform-specific feature, but this does not mean that different application models cannot have different implementations of it and even offer a different set of features.
      In this column, I will discuss some underpinnings of the Windows Forms implementation of data binding. I'll do that by including both practical hands-on examples and insights on the Windows Forms object model. To demonstrate how to take full advantage of the built-in data binding mechanism, I'll discuss a Windows Forms-specific feature that combines the best capabilities of the Windows Forms DataGrid control (do not confuse it with the DataGrid Web server control that I've covered in past issues) and two key ADO.NET objects, the DataSet and the DataRelation. These three elements make .NET Windows-based applications capable of implementing master/details schemas in a rather codeless and declarative way.
      The Windows Forms DataGrid control has the ability to detect relationships within the data of a bound DataSet and can automatically provide for a hierarchical view of this data. This aspect of data binding is fully covered in the Data Points column in this issue of MSDN® Magazine. By contrast, I'll show you the structure of the form-binding context and how to develop custom controls that automatically retrieve their parent and synchronize with its currently selected item to provide a hierarchical view. To illustrate my points, I'll start from one of the samples in the Microsoft .NET Framework SDK. Under the samples\quickstart\winforms\samples\data\masterdetails folder, there is an interesting application that connects to a Web Service and downloads a DataSet made up of customers and related orders. The data is packed in a single DataSet using two distinct tables—Customers and Orders. Once on the client, the rows in the tables are correlated on the employeeid column by creating an appropriate DataRelation object (see the Data Points column in this issue). So far, so good—but the most interesting part is still to come.
      The Windows Forms client application contains two DataGrid controls. One acts as the master grid and displays information about the customers. The other plays the role of the detail grid and shows all the information about the orders that the selected customer has issued. What's amazing is that in all the source code there is no place where the two DataGrid controls are explicitly connected to each other. No instruction ever tells one of them about the existence of the other. Yet the detail grid automatically displays the orders that relate to the customer currently selected in the master grid. Is there a reasonable explanation for this cool, apparently magical behavior? In this column, I'll try to unveil the mystery and untangle the details behind automatic data binding in Windows Forms applications.

Collecting Clues and Traces

      Although I've spoken of master and detail DataGrid controls, in no place in the .NET Framework sample code are the two grids ever assigned these roles. The only visible link that ties the two grids together is the content of the DataMember property. The de facto master grid is bound to the actual master table—Employees.
master.DataSource = dataset;
master.DataMember = "Employees";
      The de facto details grid is bound to a navigation path which ends with the name of the relation connecting the two tables. A relation represents a parent/child relationship between two DataTable objects. In ADO.NET, a relation is rendered using a DataRelation object. Assuming EmployeesToOrders is the name of the relation, then the details grid is bound to the dataset as follows:
details.DataSource = dataset;
details.DataMember = "Employees.EmployeesToOrders";
      The ADO.NET DataRelation object provides an effective way to keep two logically related tables synchronized on the value of a common field. The DataRelation object has no direct counterpart in the ADO object model. However, if you have ever dealt with the ADO data shaping feature and know how it works, you will understand ADO.NET relations. A data relation can be established only between two columns belonging to distinct tables in the same DataSet object. The two columns can have different names, but must contain the same data type. To be effective, a DataRelation object must be stored in the DataSet's Relations collection.

Figure 1 Initial Outline of Grid2Grid
Figure 1 Initial Outline of Grid2Grid

      Once a data relation has been created, each row on the parent table points to an array of rows on the child table. So, for instance, each customer row points to an array of orders—all the orders that the particular customer made at a given time. A custom implementation of a master/details schema would be needed to retrieve and display this collection of child rows. But now, as the .NET Framework sample shows, this functionality is built into the Windows Forms DataGrid control. I've written a small Windows Forms application to reproduce this behavior. In Figure 1, you can see the outline of the resulting form as it appears in Visual Studio .NET.

Reproducing the Behavior

      The form layout includes two DataGrid controls named masterGrid and detailsGrid which are placed at the top and the bottom, respectively. Aside from the status bar, no other control participates in the application. In Figure 2, you can see the core part of the code.
      To make sure that no system features could slip in unnoticed, I turned off the automatic configuration of data and connection controls. The ConfigureDataAdapter method shows how the data is actually accessed from the sample Northwind database. The SELECT command is a semicolon-separated batch of two SQL commands. As a result, the final DataSet has two tables, Employees and Orders. During the form initialization stage, the command is executed and the two tables then become related.
DataColumn dc1, dc2;
    dc1 =   
   ds.Tables["Employees"].Columns["employeeid"];
dc2 = ds.Tables["Orders"].Columns["employeeid"];
DataRelation rel;
rel = new DataRelation("EmployeesToOrders", 
      dc1, dc2);
ds.Relations.Add(rel);
The relation associates orders with employees based on the common employeeid field. The two DataGrid controls are bound to data like this:
masterGrid.DataSource = ds; 
masterGrid.DataMember = "Employees";
detailGrid.DataSource = ds;
detailGrid.DataMember =    
    "Employees.EmployeesToOrders";
The result of running the code is shown in Figure 3. At startup, the first row is selected on the topmost grid—the one bound to the master table. The contents of the bottommost grid—the one bound to the output of the relation—is the collection of the orders issued by the given employee. As you select a new employee on the top grid, the contents of the child grid change accordingly. This behavior is easily reproducible. Let's have a closer look at how this works.

Figure 3 Data Relation in Action
Figure 3 Data Relation in Action

      The first question I tried to answer was whether there was a way to detect the change of the current cell on one of the grids. This issue, seemingly unrelated to the master/child problem, turned out to be of some importance. Although the DataGrid control exposes a CurrentCellChanged event, it is only useful for detecting changes that occur interactively. In other words, the following event handler fires your code only when the currently selected cell on the master grid changes in response to a user click.
   masterGrid.CurrentCellChanged += 
   new EventHandler(CurrentCellChanged);
      At startup, the two grids are still synchronized by something else. The following code makes the whole sample applet much more user friendly.
   masterGrid.CurrentCellChanged += new   
       EventHandler(CurrentCellChanged);
   Form1.Activated += new 
       EventHandler(CurrentCellChanged);
  
You can see the results in Figure 4. The CurrentCellChanged event handler updates the caption of the bottommost grid to reflect the name of the employee selected in the master grid. You need to hook up the form's Activated event to make sure the code fires before the form is actually displayed.

Figure 4 Grid2Grid Enhanced
Figure 4 Grid2Grid Enhanced

The Relation is Key

      Based on the code in Figure 2, how would you describe the contents of the details DataGrid control? It looks like the control is bound to the subset of rows that result from the parent table via the relation. How can the details grid retrieve these rows? The only confirmed link is the data member string and the relation. Any attempt to reproduce the same master/details schema using controls other than the DataGrid is destined to fail miserably. Nothing out of the ordinary seems to be present in the code for the master grid, so let's focus on the details grid instead.
      So far I've looked at the navigation path as a keyword that had a special meaning to ADO.NET. Instead, it could be simple syntax recognized by the DataGrid and used to pass two distinct bits of information in a single shot. The navigation path TableName.RelationName could therefore be split into two parts: the name of the parent table and the name of the relationship. To get records out of a relation, though, what you really need is not the table name but a living instance of the DataRow object lying behind the currently selected row in the parent grid.
      The structure of the string assigned to the DataGrid's DataMember property determines whether the control has to work as the details grid or the master grid. If DataMember takes the form of TableName.RelationName, then the DataGrid control is designed to retrieve its contents based on the contents of another parent control. The DataGrid and its parent control share the same data source object. In addition, the DataMember property of the parent binds to the table with the name that matches TableName. In the sample application, this is the master DataGrid control. Not all controls are good candidates to play the role of the master control. They must be data-bound and expose the DataMember property. This works for the Windows Forms DataGrid control, but any list control (such as ListBox or ListView) could be adapted to work as the master. In this case, adapting a control means creating a new custom control with enhanced capabilities. Likewise, the child control can be a DataGrid or any other custom control (more on this later). But why does a control need to have an active role in this kind of automatic master/detail schema?
      The Windows Forms data binding mechanism allows controls to query for the data-bound control linked to a member of a given data source. Through this mechanism, the details DataGrid can retrieve an indirect reference to the parent DataGrid and register for an ad hoc event that fires whenever the currently selected row changes. The details grid, whose DataMember takes the form of TableName.RelationName, requests the binding manager for the control bound to the given member (TableName) within its same DataSet data source. In this way, two DataGrid controls that had no knowledge of each other can automatically start working together.
      Next, I'll look at what happens if two or more controls are bound to the same data source and data member.

Binding Contexts

      I modified the sample application to accommodate a second DataGrid control whose data binding settings are identical to the master grid.
masterGrid.DataSource = ds; 
masterGrid.DataMember = "Employees";
anotherMasterGrid.DataSource = ds; 
anotherMasterGrid.DataMember = "Employees";
What happens to the details grid? Well, it's not affected whatsoever by the number of potential masters (see Figure 5). No matter which master grid you click, the details control correctly detects the change and properly refreshes. This means that the data binding mechanism is a bit more versatile and complex. The details control won't get a reference to a living instance of another control, but it obtains a reference to a component called the binding manager.

Figure 5 A Second Master Grid is Added
Figure 5 A Second Master Grid is Added

      The binding manager is specific to a data source no matter how many controls actually bind to it. The binding manager presents itself to child controls as a single entity to connect to. Then it fires a control-independent set of events to indicate selection changes in any of the masters. Once a details control has gotten a reference to the binding manager, it registers for the CurrentChanged event. When the event fires, the details control retrieves arguments that represent the data item that corresponds to the currently selected row—the parent DataRow object. At this point, the linkage is pretty much complete. A control that holds the parent DataRow object and a relation name can easily retrieve the array of rows that constitute the details view. The base .NET class impersonating the binding manager is BindingManagerBase. A control can query for the binding manager through the BindingContext property, a property inherited from the base Control class. The BindingContext property returns an object of type BindingContext.
      Actually, BindingContext is just a special collection object, as its declaration demonstrates.
public class BindingContext : ICollection, IEnumerable
The BindingContext collection manages all binding objects that are bound to the same data source and data member. Each binding object is represented by an instance of classes that inherit from BindingManagerBase. BindingManagerBase is an abstract class; the actual classes that you will be working with are CurrencyManager and PropertyManager.
      The PropertyManager class keeps track of a simple binding between a data-bound control property and a data source scalar value. A more sophisticated role is played by the CurrencyManager class. CurrencyManager handles complex data binding and maintains bindings between a data source and all the list controls that bind to it or one of its member tables. The CurrencyManager class takes care of synchronizing the controls bound to the same data source and provides a uniform interface for clients to access the current item for the list. Both classes have properties like Current and Position. The PropertyManager's Current property, on the other hand, indicates the current property of a data-bound object. For example, if you have the Text property of a TextBox bound to a particular row/column pair of a data source, the result you get is the current value of the Text property.
      The CurrencyManager's Current property indicates the data item behind the currently selected row in the user interface of the data-bound list control. If you have a list control (such as a DataGrid or ListBox) bound to a DataTable, Current returns the DataRow object corresponding to the control's item. The data type of Current depends on the type of the data source; it will be a DataRowView if a DataView is used or it will be a string if the data source is an array of strings. The current item in the data-bound master control can be interactively set by the user or set programmatically by the application using the CurrencyManager class's Position property. Position is a get/set property that indicates the zero-based index of the current row in the bound data source. Count is the property that lets applications know about the number of rows that can be found in the bound data source.
      Let's recap the facts that make automatic master/details schemas possible with Windows Forms DataGrid controls. Any DataGrid control whose DataMember property is in the form TableName.RelationName behaves like the child of a parent/child relationship. Needless to say, this works the way it should as long as an ADO.NET data relation exists with the given name in the DataSet that the grid is using.
      The child DataGrid, which is a list control, utilizes its own binding context to get the CurrencyManager for all controls in the form bound to the same data source. The child grid registers to handle the CurrentChanged event on the binding manager. The event passes the DataRow object selected in the parent grid as an argument. At this point, the child grid uses the ADO.NET relation to retrieve the child rows to show. To accomplish this task, the details grid does not need to know about the parent control.
      The next step is demonstrating that any Microsoft .NET control whatsoever can be adapted to work as the child of an automatic master/details schema.

Creating a DetailsListView Control

      The DataGrid is not the only type of Windows Forms control that can support this automatic master/details feature. While it's the only control in the .NET Framework to provide this feature, custom ad hoc controls are easy to write. Let's enhance a ListView control to make it work as the child of a master DataGrid. The ListView control is not data-bound, but this does not mean that it can't work in a data-oriented, master/details relationship. Figure 6 shows the source code of a class called DetailListView, which is built atop the ListView control and adds the support needed to make the control work in a master/details architecture. The new class is declared as follows:
public class DetailListView : ListView
{
    public DetailListView()
    {
        View = View.Details; 
        FullRowSelect = true;
    }
•••
}
      For simplicity, I managed to make the control always work in Details mode. The user interface is always column-based regardless of the option you select from the Properties box in the Visual Studio project. The control class is project-specific and does not appear in the toolbox. To put it on the main form of a sample application, just drop it on the form and configure it like an ordinary ListView. When you're finished, open the wizard-generated region of code in the form body and replace occurrences of the class System.Windows.Forms.ListView with the name of your class—in this case, DetailListView.
      The DetailListView control is given two extra properties: DataSource and DataMember. The implementation of the control you find in the source code is not a full implementation of data binding; it is limited to providing the features that allow the control to support automatic master/details. This means, for example, that DataMember is always expected to take a string like TableName.RelationName. Another assumption is that the parent control is bound to a DataTable or a DataView object.
      When either DataSource or DataMember are modified, a special accessor code runs. It splits the data member in two parts: the parent table name and the relation name.
Array a = m_DataMember.Split(sep); 
m_ParentTable = a.GetValue(0).ToString();
m_RelationName = a.GetValue(1).ToString();
      If both properties are non-null, the protected method AutoBind is called. AutoBind looks up the binding context collection of the ListView. Bear in mind that BindingContext is available on any control derived from Control and is not linked to any support for data binding.
private void AutoBind()
{
  CurrencyManager cm;
  cm = (CurrencyManager) BindingContext[DataSource, m_ParentTable];
  if (cm != null)
  {
    cm.CurrentChanged += new EventHandler(CurrentChanged);
    RefreshList(cm);
  }
}
First, the control retrieves the binding manager for the specified data source and table. The BindingContext collection returns an instance of the CurrencyManager class. In this case, you can be sure about the return type.
      You could also replace CurrencyManager with BindingManagerBase. The DetailListView class then registers a handler for the CurrentChanged event and refreshes its user interface. The code for the event handler is pretty straightforward and just passes control down to the filling procedure.
void CurrentChanged(Object sender, EventArgs e)
{
    RefreshList((CurrencyManager) sender);
}
      The most interesting part of the code is in RefreshList.
DataRowView drv = (DataRowView) cm.Current;    
DataRow row = drv.Row;
DataRow [] rgChildRows = row.GetChildRows(m_RelationName);
RefreshList takes an argument of type CurrencyManager. The Current property of this object evaluates to the data item object currently selected in the master control. As I explained earlier, the CurrencyManager hides the details of the parent controls from the caller. Regardless of the type and number of the controls bound to the specified data source, CurrencyManager represents them all by supplying a common programming interface. The data item that the DetailListView control gets from the binding manager is an instance of the data object behind the user interface row displayed by the parent control. For example, if the parent control is a DataGrid, then each displayed row is a DataGridItem object. Although the DataGridItem object represents a row in the grid, it has nothing whatsoever to do with the underlying data object that supplies the text shown in the cells. Each row in the DataGrid is bound to a data object and the collection of all the individual data objects forms the overall data source.
      Once the RefreshList method has retrieved the DataRow object for the currently selected parent row, it can finally exploit the existing data relation. The DataRow's GetChildRows method just returns an array of DataRow objects representing the child view. RefreshList extracts column information from these rows and then populates the ListView.
Items.Clear();
foreach(DataRow r in rgChildRows)
{
    ListViewItem lvi = new ListViewItem();
    lvi.Text = r[0].ToString();
    for(int i=1; i<dt.Columns.Count; i++)  
        lvi.SubItems.Add(r[i].ToString());
     Items.Add(lvi); 
}
      Columns are dynamically added by looking at the structure of the table used to create the layout of child rows, as shown in Figure 7. The code also automatically right-aligns the text for columns which contain numeric data. Figure 8 shows the final results.

Figure 8 The Final Result
Figure 8 The Final Result

Information Drilldown

      A lot of applications use a two-step mechanism to show potentially lengthy and rich data. Typically, applications show a scrollable list of key fields—say, ID and last name—and then leave the user free to click rows and have the rest of the available information drilled down. Basically, two synchronized data controls share the same DataSet, but display a different set of columns. The Windows Forms data binding engine easily allows for this, as the next sample application demonstrates.
      The idea is to have a DataGrid show employee names and a new, custom control that binds to the selected row in the grid and displays all the columns. The DataSet has only one table, which is filled by a very simple query command.
String strCmd = "SELECT * FROM Employees";
SqlDataAdapter da;
da = new SqlDataAdapter(strCmd, connString);
DataSet ds = new DataSet();
da.Fill(ds, "Employees");
You bind the DataGrid to the data source with the same code that was shown earlier.
masterGrid.DataSource = ds;
masterGrid.DataMember = "Employees";
      Unless you explicitly define some custom table and column styles, the DataGrid would simply mirror the contents of the table. To force it to show only a subset of the columns, configure the grid as shown in Figure 9. You first create a DataGridTableStyle object and map it to the name of the bindable DataSet table. Notice that the match is case-sensitive, so make sure that the value of MappingName exactly matches the name of the DataTable. At this time, you could also set some graphical styles that would override others set on the DataGrid control itself. These settings are specific to the table being displayed and are automatically reset when that table is detached from the grid.
      The second step requires you to create as many columns as needed. The .NET Framework provides only two types of columns, TextBox and Boolean, but supports custom columns you might want to write.
DataGridTextBoxColumn col1;
col1 = new DataGridTextBoxColumn();
      The grid column must be mapped to one of the table columns. You use the MappingName property and, again, watch for case-sensitivity. Each column has to be added to the GridColumnStyles collection of the table-style object.
col1.MappingName = "employeeid";
gridStyle.GridColumnStyles.Add(col1);  
When you're finished configuring columns, you add the table-style object to the grid.
   masterGrid.TableStyles.Add(gridStyle);
      Showing only a few columns out of the DataTable does not affect the automatic master/details capabilities of Windows Forms data binding. I wrote a row viewer control (called DataRowViewer) that displays the current values of all fields in the table. The DataRowViewer inherits from the ListView class. It features two columns, Name and Value, and adds as many rows as there are columns in the table. The DataRowViewer control includes the standard piece of code I discussed earlier, which makes it capable of detecting controls bound to the same data source. Thus it can automatically work as the child in a parent/child relationship. I based the user interface of this control on the ListView control because I wanted it to work with any data source. Of course, this is arbitrary. You can rewrite the DataRowViewer control as a form, making use of any combination of child controls (TextBox, ComboBox, CheckBox) to view and edit the various data fields. Notice that the DataRowViewer class has been built into the Visual Studio project as a true control, so you'll find it available from the toolbox as well. Figure 10 shows the controls in action in the sample app.

Figure 10 DataRowViewer Control in Action
Figure 10 DataRowViewer Control in Action

DataGrid versus DataGrid

      The .NET Framework comes with two flavors of the DataGrid control, one for Windows Forms applications and one for Web Forms. Aside from the fact that they both provide grid functionality, the two controls are completely unrelated. They have distinct sets of features and capabilities, belong to different namespaces, and can be used in different models of applications.
      Automatic master/detail relationships are made possible by the data binding mechanism of Windows Forms applications where the BindingContext class plays a key role.
      Web controls do not have the notion of a binding context for a good reason. At the root of the master/details applications, there is a selection event. When this happens over the Web from within a typical ASP.NET application, there is a round-trip between the selection event and the subsequent page refresh. The binding context is invalidated and would need to rebuild each time the page is loaded. Although possible in theory, this could create useless overhead. Automatic master/detail relationships are still possible in Web Forms applications to some extent. However, it utilizes other classes and techniques, which means it will will make good fodder for next month's column.

Send questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Building Web Solutions with ASP.NET and ADO.NET (Microsoft Press), he now spends most of his time teaching classes on ASP.NET and ADO.NET for Wintellect (http://www.wintellect.com). Get in touch with Dino at dinoe@wintellect.com.

Page view tracker