Export (0) Print
Expand All
Build Providers for Windows Forms
Draft a Rich UI: Ground Rules for Building Enhanced Windows Forms Support into Your .NET App
A New Grid Control in Windows Forms
Owner-Drawing in .NET
P2P Comm Using Web Services
Smart Clients: Build A Windows Forms Control To Consume And Render WSRP Portlets
Spice It Up: Sprinkle Some Pizzazz on Your Plain Vanilla Windows Forms Apps
Synchronizing Multiple Windows Forms
Text Rendering: Build World-Ready Apps Using Complex Scripts In Windows Forms Controls
Windows Forms Controls: Z-order and Copying Collections
Winning Forms: Practical Tips For Boosting The Performance Of Windows Forms Apps
Code Samples
Expand Minimize

Field Views on Windows Forms 1.0 and 1.1 Data Binding

 

Steve White
Premier Support for Developers
Microsoft UK

March 2004

Summary: Data binding is a robust, powerful, and extensible infrastructure that is very popular with developers. This article offers best practices, addresses difficulties that developers have with data binding, and provides practical workarounds for some common issues. (31 printed pages)

Applies to:

   Microsoft .NET Framework versions 1.0 and 1.1
   Microsoft Visual C#® 2002 and Visual C# 2003
   Microsoft Visual Basic® .NET 2002 and Visual Basic .NET 2003

Contents

Introduction
Parent-Child Role Correctness in DataRelations
Binding to a DataRelation is Binding to a DataView
When to Call CurrencyManager.Refresh
Distant Relations
CurrencyManager.Current, not TextBox.Text
Consistent Indexing in Control.BindingContext
DataViewManager and When to Use It
Binding ValueMember Using a "This" Property
RowFilter on a Boolean Column
Always Call DataBindings.Clear when Disposing
Summary of Best Practices
Roadmaps and Futures

Introduction

As an Application Development Consultant I am a contact through which some of Microsoft's Premier customers provide feedback about the experiences they have with developer technologies. This article is based on some of that feedback. It specifically deals with ADO.NET, Windows Forms, and data binding in versions 1.0 and 1.1 of the .NET Framework and therefore some knowledge of these topics is assumed. All example code is C# and the text makes reference to the Northwind sample database throughout.

Parent-Child Role Correctness in DataRelations

When constructing a DataRelation object, the order of arguments supplied is important. There's a strong implication of this when you look at one of the overloads of the DataRelation constructor:

   public DataRelation(
      string relationName,
      DataColumn parentColumn,
      DataColumn childColumn,
      bool createConstraints);

In this section, I'll take the classic Customers-Orders example (that is, a master-detail relationship) from the Northwind sample database, and I'll assume we've generated a typed DataSet from the Customers and Orders tables. With versions 1.0 and 1.1 of the .NET Framework we need to manually define the relationship between the two DataTables, so we'll construct a new DataRelation with the above constructor overload. The parentColumn argument should reference the CustomerIDColumn DataColumn in the Customers DataTable, and the childColumn argument should reference the CustomerIDColumn DataColumn in the Orders DataTable. It is clearer to think in terms of parent and child DataColumns rather than parent and child DataTables because it is possible to relate a DataTable to itself. So, the order in which the DataColumn references are supplied determines their parent-child role. As a sanity check, if what is being called the parentColumn is not a primary-key column, or what is being called the childColumn is not a foreign-key column, then the relation being defined is arguably incorrect. The argument for it being incorrect is that referential integrity must be disabled for the 'reversed relation,' and doing so leads to a known issue, which I'll explain.

It is usual that the parent-column's values are unique, and that the child-column contains valid parent-column values. Such referential integrity is enforced if the createConstraints argument is true (the default). These constraints provide both safety and the ability to cascade changes, and their use is highly recommended.

Once a DataRelation is established between two DataTables in a DataSet, a common requirement is to bind Windows Forms controls to the parent and child roles and navigate between them. In the Customers-Orders case, the user might wish to choose a Customer from a DataGrid and have the associated Orders appear in a second DataGrid. This is achieved by binding the DataGrids to the Customers DataTable and the CustomerOrders DataRelation respectively, with code similar to the following:

C# code

   dataGrid1.SetDataBinding(dsCustOrd.Customers, "");
   dataGrid2.SetDataBinding(dsCustOrd.Customers, "CustomerOrders");

Visual Basic .NET code

   dataGrid1.SetDataBinding(dsCustOrd.Customers, "")
   dataGrid2.SetDataBinding(dsCustOrd.Customers, "CustomerOrders")

Conversely, and also common, is the requirement that the user chooses an Order from a DataGrid and sees the Customer's CompanyName appear in a Label or read-only TextBox. This might be thought of as a detail-master navigation. Given the ease of binding to a DataRelation, it is tempting to use code like the following:

C# code

   dataGrid1.SetDataBinding(dsCustOrd.Orders, "");
   dataGrid2.SetDataBinding(dsCustOrd.Orders, "OrderCustomers");

Visual Basic .NET code

   dataGrid1.SetDataBinding(dsCustOrd.Orders, "")
   dataGrid2.SetDataBinding(dsCustOrd.Orders, "OrderCustomers")

To do this, it would be necessary to rename and redefine the DataRelation and supply its related columns in the reverse order. Doing this is as much as saying that the Orders table's column is the parent column, and the Customers table's column is the child column. But as soon as any Customer places more than one Order, the parentColumn will not contain unique values and, if constraints are in place, they will fail. Remember the sanity check—what is being called the parentColumn is not a primary key. In addition, if any Customer places zero Orders, then referential integrity cannot be satisfied and, again, constraints fail. This is because what is being called the childColumn is not a foreign key. The only option is to define the DataRelation without a ForeignKeyConstraint, which leads to the known issue I mentioned above and will describe next.

You can reproduce this issue in a sample using either version 1.0 or 1.1 of the .NET Framework and the Northwind sample database. Drag the Customers and Orders tables onto a Form, then generate and fill a typed DataSet from the two tables. Next, define a DataRelation between the tables. You can either define the correct "CustomerOrders" relation making sure to disable constraints (that is, supply false as the createConstraints argument), or you can define the reversed "OrderCustomers" relation, in which case you will be forced to disable constraints. If you defined your relation correctly, the parent table is Customers. Otherwise Orders is the parent table. Drag two DataGrids onto a Form and bind one to the parent table and the other to the DataRelation. One at a time, delete rows from the parent table. You can use the parent DataGrid to do this, or you can do it in program code using either BindingManagerBase.RemoveAt, or cast CurrencyManager.Current to a DataRowView and then call Delete on it. The issue is that, when the last row is deleted, the child DataGrid does not update. This importance of this issue can only be seen if the last row deleted has child rows, as these are left in the grid.

In the case of all but the last delete, the CurrencyManager.CurrentChanged event is being fired and the child DataGrid is updated by a behind-the-scenes mechanism, which I'll describe in the next section. When the last delete happens, CurrencyManager.CurrentChanged does not fire because there is no CurrencyManager.Current. Nor, because there is no ForeignKeyConstraint, and therefore no DeleteRule, are child rows deleted. In this case 'child rows' means the Customer who placed the deleted Order, and it would be an unusual business requirement to want to delete her. However, if child rows were deleted (something we could do in a handler for the RowDeleting event of the Orders table as in the following code), then the child DataGrid updates in all cases.

C# code

   CurrencyManager cmParent = BindingContext[dataGrid1.DataSource, 
   dataGrid1.DataMember] as CurrencyManager;
   DataRow[] rows = (cmParent.Current as DataRowView).Row.GetChildRows("OrderCustomers");
   foreach (DataRow row in rows)
      dsCustOrd.Customers.Rows.Remove(row);

Visual Basic .NET code

    Dim cmParent As CurrencyManager _
        = DirectCast(BindingContext(dataGrid1.DataSource, _
                 dataGrid1.DataMember), CurrencyManager)
    Dim rows As DataRow() _
        = DirectCast(cmParent.Current, _
        DataRowView).Row.GetChildRows("OrderCustomers")
        For Each row As DataRow In rows
            dsCustOrd.Customers.Rows.Remove(row)
        Next

Apart from that, the only user action I could find that prompts the child grid to update correctly is to set focus to the parent grid. So, programmatically setting focus to the parent DataGrid (that is, dataGrid1.Focus) in a RowDeleted event handler is not an elegant workaround. And it only works for non-read-only DataGrids.

Binding to a DataRelation Is Binding to a DataView

The idea of binding to a DataRelation is a commonly-expressed one, but misleading as well. Consider this line of code:

C# code

   dataGrid2.SetDataBinding(
        Customers.ChildRelations["CustomerOrders"], "");

Visual Basic .NET code

   dataGrid2.SetDataBinding( _
        Customers.ChildRelations("CustomerOrders"), "")

When executed, the code throws an InvalidCastException because the DataRelation class does not fulfill any of the criteria for being a data source (the Binding class overview in the .NET Framework Class Library documentation describes these criteria). The truth about binding a DataGrid to a DataRelation is more involved because there is a lot of .NET Framework runtime support going on. This involves capturing CurrentChanged events on the CurrencyManager of the DataGrid bound to the parent table, accessing the current parent DataRowView, and calling CreateChildView on the DataRowView. The resulting framework-managed DataView (actually a RelatedView) is then bound to the child DataGrid. So, binding to a DataRelation is actually binding to a DataView (and, incidentally, binding to a DataTable is actually binding to its default DataView).

We've seen that you can't rely on the above mechanism to work for a reversed relation, so that's not the way to go when you want to bind to the Customer for a selected Order. The right way to go is to implement one of two similar mechanisms. The first involves using a RowFilter to find the Customer. The second uses a DataRelation to get an Order's parent row (the Customer).

To do this, drag the Northwind Customers and Orders tables onto a Form then generate and fill a typed DataSet from the two tables. Then define a correct DataRelation—CustomerOrders—between them, relating the Customers.CustomerIDColumn and Orders.CustomerIDColumn columns and imposing constraints. Create a DataGrid named dataGrid1, and TextBoxes named txtFilter and txtParentRows (one for each of the two solutions we'll implement). The following code from Form1_Load constructs a class member DataView (dvCustomer) to bind txtFilter to, initializes some other useful members, and sets up event handlers:

C# code

   // Construct the member DataViews.
   dvCustomer = new DataView(dsCustOrd.Customers);
   txtFilter.DataBindings.Add("Text", dvCustomer, "CompanyName");
   dvCurrentOrders = new DataView(dsCustOrd.Orders);

   cmOrders = BindingContext[dataGrid1.DataSource, 
          dataGrid1.DataMember] as CurrencyManager;

   // Register for events.
   cmOrders.CurrentChanged 
         += new EventHandler(CustomerCurrentChanged);
   dsCustOrd.Orders.RowDeleted 
         += new DataRowChangeEventHandler(RowDeleted);

   // Force initial update.
   CustomerCurrentChanged (null, null);

Visual Basic .NET code

    'Construct the member DataViews.
    dvCustomer = New DataView(dsCustOrd.Customers)
    txtFilter.DataBindings.Add("Text", dvCustomer, "CompanyName")
    dvCurrentOrders = New DataView(dsCustOrd.Orders)

    cmOrders = DirectCast(BindingContext(dataGrid1.DataSource, _
            dataGrid1.DataMember), CurrencyManager)

    'Register for events.
    AddHandler cmOrders.CurrentChanged, _
            AddressOf CustomerCurrentChanged
    AddHandler dsCustOrd.Orders.RowDeleted, _
            AddressOf RowDeleted

    'Force initial update.
    CustomerCurrentChanged(Nothing, Nothing)

The DataView dvCustomer's filter can't be set to a meaningful value until dataGrid1 is known to have a valid row selected. This is handled in the CurrencyManager.CurrentChanged event handler:

C# code

private void CustomerCurrentChanged(object sender, EventArgs e)
{
   // Check if unsafe to access CurrencyManager.Current.
   if (cmOrders.Position == -1 || cmOrders.Position >= dvCurrentOrders.Count)
   {
      txtFilter.Text = txtParentRows.Text = dvCustomer.RowFilter = "(null)";
   }
   else
   {
      // Update txtFilter by changing the DataView's row filter.
      DataRowView drv = cmOrders.Current as DataRowView;
      dvCustomer.RowFilter = "CustomerID='" + drv["CustomerID"] + "'";

      // Update txtParentRows by getting Parent Rows for selected Child Row.
      txtParentRows.Text = drv.Row.GetParentRow("CustomerOrders")["CompanyName"].ToString();
   }
}

Visual Basic .NET code

Private Sub CustomerCurrentChanged(ByVal sender As Object, _
        ByVal e As EventArgs)
    ' Check if unsafe to access CurrencyManager.Current.
    If (cmOrders.Position = -1 OrElse _
        cmOrders.Position >= dvCurrentOrders.Count) Then
        dvCustomer.RowFilter = "(null)"
        txtParentRows.Text = "(null)"
        txtFilter.Text = "(null)"
    Else
        ' Update txtFilter by changing the DataView's row filter.
        Dim drv As DataRowView _
            = DirectCast(cmOrders.Current, DataRowView)
        dvCustomer.RowFilter = _
            "CustomerID='" & CStr(drv("CustomerID")) & "'"

        ' Update txtParentRows by getting Parent Rows 
        ' for selected Child Row.
        txtParentRows.Text = drv.Row.GetParentRow( _
            "CustomerOrders")("CompanyName").ToString()
    End If
End Sub

Most of the code in the event handler is concerned with the special cases of the DataGrid being empty or being in the process of editing a new row. The rest is simply a matter of changing the DataView's row filter, or obtaining the DataRowView representing the selected order and querying for its parent row. Either of these two approaches is a solution in itself.

Some further points to note are that CurrencyManager.CurrentChanged does not fire when the last row is deleted from the Orders table, which is something we noted in the previous section. This is a simple issue to solve. Call CustomerCurrentChanged from a handler for the RowDeleted event of the Orders table:

C# code

void RowDeleted(object sender, DataRowChangeEventArgs e)
{
   CustomerCurrentChanged(null, null);
}

Visual Basic .NET code

Private Sub dt_RowDeleted(ByVal sender As Object, _
    ByVal e As DataRowChangeEventArgs)
    CustomerCurrentChanged(Nothing, Nothing)
End Sub

Also note the use of the member DataView dvCurrentOrders, which views the current rows of the Orders table, excluding deleted rows. It is used in CustomerCurrentChanged to check if a new row is being edited by comparing its row count to the CurrencyManager's Position. It is tempting, but incorrect, to use the count of rows in the Orders table in this comparison as that count includes deleted rows. Also note the use of the member CurrencyManager cmOrders, which, once defined, saves verbosity in the code. This could just as easily be a property, which is a best practice I'll discuss later.

Finally, you can safely delete programmatically like this:

C# code

private void btnDelete_Click(object sender, System.EventArgs e)
{
   if (cmOrders.Position != -1)
   {
      DataRowView drv = cmOrders.Current as DataRowView;
      drv.Delete();
   }
}

Visual Basic .NET code

Private Sub btnDelete_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnDelete.Click
    If cmOrders.Position <> -1 Then
        Dim drv As DataRowView _
            = DirectCast(cmOrders.Current, DataRowView)
        drv.Delete()
    End If
End Sub

When to Call CurrencyManager.Refresh

The .NET Framework Class Library documentation indicates that Refresh should be called on a CurrencyManager when the data source doesn't support change notification. But even when the data source is a DataSet, calling Refresh is found to be necessary in apparently arbitrary situations. This does cause some confusion for developers.

To reproduce one such situation (using either version 1.0 or 1.1 of the .NET Framework), define and populate the same Northwind Customers and Orders tables as earlier and define the correct "CustomerOrders" DataRelation between them with constraints. Drag two DataGrids onto a Form and bind one to the Customers table and the other to the DataRelation. Add a button to the form and put some code in its handler to amend the value of the currently-selected Customer's id. Here is some code that does this:

C# code

   CurrencyManager cm = BindingContext[dataGrid1.DataSource, 
   dataGrid1.DataMember] as CurrencyManager;
   DataRowView drv = cm.Current as DataRowView;
   drv["CustomerID"] = someUniqueValue;

Visual Basic .NET code

    Dim cm As CurrencyManager = _
    DirectCast(Me.BindingContext(dataGrid1.DataSource, _
        dataGrid1.DataMember), CurrencyManager)
    Dim drv As DataRowView = DirectCast(cm.Current, DataRowView)
        drv("CustomerID") = someUniqueValue

Naturally, the id's unique constraint prevents you from amending it to anything but another unique value. Once you have amended the customer id, the UpdateRule in the DataRelation's ForeignKeyConstraint cascades the key change down to any orders belonging to this customer. However, you won't be able to see these changes in the DataGrid bound to the DataRelation (the child grid). Not yet, at least. There doesn't seem to be any reason for it, but you have to add the following line of code below the others:

C# code

   cm.Refresh();

Visual Basic .NET code

   cm.Refresh()

Once done, the child grid updates to show the new foreign key values, even if it is read-only. Without the refresh, the child grid clears itself, so evidently some CreateChildView-style operation is happening, but possibly before the primary key change has settled in.

Editing key values in the grid has been another source of furrowed brows. Once the user amends the value in the CustomerID cell, it is necessary to move to another row before the key change propagates to the Orders table. Even then, the change is not seen until the user navigates back to the changed row. This hop may not be convenient for the user who wants to see changes while still on the same row. Unfortunately, subclassing the Windows Forms DataGrid class and overriding ProcessCmdKey or ProcessKeyPreview does not do the trick in this case. Overriding the behavior of Keys.Enter, which by default ends the current edit and selects the following row, seems like a good candidate. You can trap Keys.Enter, and even substitute another key for it, but refreshing the CurrencyManager in the process causes the visual selection cue to disappear. Substituting the key pressed, or using the SendKeys class doesn't help. Forcing the edit to end is impossible due to EndEdit (and all other promising methods) being private.

In any case, it's arguable that any amendment to the behavior of Keys.Enter would frustrate the user at times when she did want normal behavior so some other, more explicit, mechanism for updating the current row and leaving it selected is called for. A simple Button-click handler will suffice to refresh the CurrencyManager and return focus to the grid:

C# code

   CurrencyManager cm = BindingContext[dataGrid1.DataSource, 
       dataGrid1.DataMember] as CurrencyManager;
   cm.Refresh();
   dataGrid1.Focus();

Visual Basic .NET code

    Dim cm As CurrencyManager = _
    DirectCast(Me.BindingContext(dataGrid1.DataSource, _
        dataGrid1.DataMember), CurrencyManager)
    cm.Refresh()
    dataGrid1.Focus()

Distant Relations

Having seen how to bind a pair of DataGrids to either end of a DataRelation, I have been asked how to extend the family and bind DataGrids to a Customers-Orders-OrderDetails hierarchy. The answer is to define a second DataRelation between Orders and OrderDetails. Then bind the DataGrids like this:

C# code

   dataGrid1.SetDataBinding(dsCustOrd.Customers, "");
   dataGrid2.SetDataBinding(dsCustOrd.Customers, "CustomerOrders");
   dataGrid3.SetDataBinding(dsCustOrd.Customers,
       "CustomerOrders.OrderOrderDetails");

Visual Basic .NET code

   dataGrid1.SetDataBinding(dsCustOrd.Customers, "")
   dataGrid2.SetDataBinding(dsCustOrd.Customers, "CustomerOrders")
   dataGrid3.SetDataBinding(dsCustOrd.Customers, _
        "CustomerOrders.OrderOrderDetails")

An equivalent syntax is:

C# code

   dataGrid3.DataSource = dsCustOrd.Customers;
   dataGrid3.DataMember = "CustomerOrders.OrderOrderDetails";

Visual Basic .NET code

   dataGrid3.DataSource = dsCustOrd.Customers
   dataGrid3.DataMember = "CustomerOrders.OrderOrderDetails"

The second argument in each case (and the string assigned to the DataMember property) is a navigation path. The idea of a navigation path is well documented in the .NET Framework Class Library documentation under the topic of the Binding class. The phrase also appears in a dozen other related places in the documentation, but it is neither mentioned nor explained under the topics of DataGrid.SetDataBinding nor DataGrid.DataMember, which are arguably important places for it to be.

Of course, the convenience of loading up tables in their entirety and relating them with DataRelations for binding purposes should be offset against the memory being used. On top of this there is a rather serious memory leak with version 1.0 of the .NET Framework of which you should be aware. The leak and its workaround are described in the Knowledge Base article 317088: "BUG: Memory Leak in Windows Forms Application When You Move Through Master-Detail DataGrid Controls." The reproduction code in the KB article uses the Northwind sample database and leaks about 2.25 MB in the process of choosing each row in the Customers table six times (546 rows in total). That's 4 kilobytes (KB) every time the current row changes (that is, whenever the user or the code moves from one row to another) in the parent table.

However, the amount of memory leaked varies depending on the nature of the data and the depth of the relation hierarchy. I created another two-table example, with far less string data, which leaks only 0.5 kilobyte for the same number of rows visited. I then related a third table to the second and the same test (only visiting the parent table) pushed the leak rate up to 2.25 kilobytes per row.

The KB article's workaround involves binding to a DataView and updating its RowFilter when the CurrencyManager's PositionChanged event fires. This is a technique I recommended in a previous section for detail-master navigation so it seems that, if you don't want to leak memory, you need to apply this technique to master-detail navigation too. If you do adopt the code in the KB article's workaround, I refer you to the following issues with it. First, the references to CustomerOrders2 should be CustomerOrders1. Second, it uses the PositionChanged event, which is not fired during deletions so the child grid does not update. Better to use CurrentChanged. Incidentally, neither event fires when the last row is deleted, but the child grid clears, which is correct, because the ForeignKeyConstraint's DeleteRule deletes the child rows. Third, the event handler code does not work when rows have been deleted as it uses CurrencyManager.Position to index into the Customers table, which still contains the deleted rows. You should either index into a DataView or, more simply, get the key from CurrencyManager.Current as a DataRowView.

Note that the workaround technique is only necessary in applications targeting version 1.0 of the .NET Framework because the bug is fixed in version 1.1.

CurrencyManager.Current, not TextBox.Text

A developer I was working with showed me a sample where changes made programmatically in a TextBox were not being reflected in other controls bound to the same list-based data source. You can reproduce this situation easily and witness other strange behavior by defining and populating a DataTable and binding it to some controls like this:

C# code

   dataGrid1.DataSource = dsCustomers.Customers;
   textBox1.DataBindings.Add("Text", dsCustomers.Customers, "CustomerID");
   textBox2.DataBindings.Add("Text", dsCustomers.Customers, "CustomerID");
   textBox3.DataBindings.Add("Text", dsCustomers.Customers, "CustomerID");

Visual Basic .NET code

   dataGrid1.DataSource = dsCustomers.Customers
   textBox1.DataBindings.Add("Text", dsCustomers.Customers, "CustomerID")
   textBox2.DataBindings.Add("Text", dsCustomers.Customers, "CustomerID")
   textBox3.DataBindings.Add("Text", dsCustomers.Customers, "CustomerID")

Edit the CustomerID in the first row of the DataGrid and, when you move out of that cell, the other controls update correctly. Do the same with any other row and the other controls don't update until the row loses focus. Similarly, if you edit the value in any of the TextBoxes and tab away, the other controls only update if the first row of the data source is current. With any other row current, the other controls do not update when tabbing away from an amended TextBox (unless the control receiving focus is the DataGrid in which case it updates, but the other controls do not). Interestingly, this first-row special case only applies to version 1.0 of the .NET Framework. With version 1.1 all rows behave like the second and subsequent rows of version 1.0.

Let's add programmatic update to an already complicated story. The developer I mentioned wanted to update a TextBox's value in a Button click handler and the code he was using was along the lines of:

C# code

   textBox1.Text = "1"; // or anyVar

Visual Basic .NET code

   textBox1.Text = "1" ' or anyVar

This, frankly, does not work any better than editing the TextBox through its GUI. In fact, in the first-row special case, it works worse because the TextBox is not losing focus after the change to its Text property. The simple solution to the programmatic method is to update CurrencyManager.Current instead of TextBox.Text.

C# code

   CurrencyManager cm = BindingContext[dataGrid1.DataSource, 
dataGrid1.DataMember] as CurrencyManager;
   DataRowView drv = cm.Current as DataRowView;
   drv["CustomerID"] = "1"; // or anyVar

Visual Basic .NET code

    Dim cm As CurrencyManager = _
      DirectCast(Me.BindingContext(dataGrid1.DataSource, _
        dataGrid1.DataMember), CurrencyManager)
    Dim drv As DataRowView = _
      DirectCast(cm.Current, DataRowView)
    drv("CustomerID") = "1" 'or anyVar 

To handle the GUI edit, you can hook the Leave events of all the TextBoxes to the same handler and update CurrencyManager.Current with TextBox.Text.

C# code

   CurrencyManager cm = BindingContext[dataGrid1.DataSource,
        dataGrid1.DataMember] as CurrencyManager;
   DataRowView drv = cm.Current as DataRowView;
   drv["CustomerID"] = (sender as TextBox).Text;

Visual Basic .NET code

    Dim cm As CurrencyManager = _
      DirectCast(Me.BindingContext(dataGrid1.DataSource, _
        dataGrid1.DataMember), CurrencyManager)
    Dim drv As DataRowView = _
      DirectCast(cm.Current, DataRowView)
    drv("CustomerID") = DirectCast(sender, TextBox).Text

These solutions work for version 1.0 and 1.1 of the .NET Framework.

Consistent Indexing in Control.BindingContext

I've seen subtle errors come about in complex Windows Forms applications when a developer breaks a fundamental, but non-obvious, rule. The rule is that you must be consistent in your use of data sources and navigation paths. Any time you call Control.DataBindings.Add, DataGrid.SetDataBinding, DataGrid.DataSource or DataGrid.DataMember, you are specifying a data source and/or a navigation path. When you wish to access the CurrencyManager or PropertyManager that manages your binding, you obtain it from Control.BindingContext by indexing into it. But the index arguments must be the exact same data source reference and navigation path string as you used to create the binding otherwise the BindingContext's indexer is not able to resolve them to the same BindingManagerBase, and so the expected binding information is not be available. Here's a code example:

C# code

   dataGrid1.SetDataBinding(dataSet1.Customer, "");
   CurrencyManager cm1 = BindingContext[dataSet1.Customer, ""]
         as CurrencyManager;
   CurrencyManager cm2 = BindingContext[dataSet1, "Customer"]
         as CurrencyManager;

Visual Basic .NET code

   dataGrid1.SetDataBinding(dataSet1.Customer, "")
   Dim cm1 As CurrencyManager = DirectCast( _
        BindingContext(dataSet1.Customer, ""),CurrencyManager)
   Dim cm2 As CurrencyManager = DirectCast( _
        BindingContext(dataSet1, "Customer"),CurrencyManager)

Comparing the two references cm1 and cm2 reveals them to be different identities, and only variable cm1 references the DataGrid's CurrencyManager. Although the distinction is documented in the article Consumers of Data on Windows Forms in the Visual Studio.NET documentation (and I show a subtle difference later in this section), developer feedback is that the two methods of indexing are logically identical. Whatever your view, the situation needs to be understood. So, the following two bindings reveal the same data in both DataGrids, but the two controls have no currency in common. That is, they will not be synchronized.

C# code

   dataGrid1.SetDataBinding(dataSet1, "Customer");
   dataGrid2.SetDataBinding(dataSet1.Customer, "");

Visual Basic .NET code

   dataGrid1.SetDataBinding(dataSet1, "Customer")
   dataGrid2.SetDataBinding(dataSet1.Customer, "")

The same care must be taken if you want to bind a DataGrid and, say, a TextBox to the same data source. In this case, the data source references must be the same. For example, the following code does not bind the two controls to the same CurrencyManager, therefore they will not be synchronized.

C# code

   dataGrid1.SetDataBinding(dataSet1, "Customer");
   textBox1.DataBindings.Add("Text", dataSet1.Customer, "custid");

Visual Basic .NET code

   dataGrid1.SetDataBinding(dataSet1, "Customer")
   textBox1.DataBindings.Add("Text", dataSet1.Customer, "custid")

It's good practice to use the DataSource and DataMember properties of a DataGrid when obtaining its CurrencyManager. This ensures that you are using the correct indexing arguments.

C# code

   CurrencyManager cm = BindingContext[dataGrid1.DataSource, 
   dataGrid1.DataMember] as CurrencyManager;

Visual Basic .NET code

    Dim cm As CurrencyManager = _
      DirectCast(Me.BindingContext(dataGrid1.DataSource, _
        dataGrid1.DataMember), CurrencyManager)

A similar technique for simple-bound controls is this:

C# code

   BindingManagerBase bmb = textBox1.DataBindings["Text"].BindingManagerBase;

Visual Basic .NET code

    Dim bmb As BindingManagerBase = _
      textBox1.DataBindings("Text").BindingManagerBase

The danger here is that indexing into Control.DataBindings (an instance of ControlBindingsCollection) can return null. Contrast this with indexing into Control.BindingContext which never returns null, but rather silently constructs and returns a new binding manager if necessary. This behavior arguably contributes to the confusion. Encapsulating your binding manager access inside a helper function or property is a good habit to get into:

C# code

   public CurrencyManager CustomerCurrencyManager
   {
      get { return BindingContext[dataSet1.Customer] as CurrencyManager; }
   }

Visual Basic .NET code

    Public ReadOnly Property CustomerCurrencyManager() _
            As CurrencyManager
        Get
            Return DirectCast( _
                BindingContext(dataSet1.Customer), _
                CurrencyManager)
        End Get
    End Property

So, earlier I promised to mention a difference between the two ostensibly identical methods of indexing into Control.BindingContext. Let's first briefly summarize the facts given above. The article Consumers of Data on Windows Forms describes how each CurrencyManager is associated with at most one data source. We've already seen what follows from this—that if you want two bound controls to have the same CurrencyManager, then their data sources must match as a minimum. Additionally, it is impossible to correctly index into Control.BindingContext without the correct data source (because data source and navigation path must match). The key item of interest here is the data source. Consider this code:

C# code

   dataGrid1.SetDataBinding(dataSet1, "Customer");
   dataGrid2.SetDataBinding(dataSet1.Customer, "");

Visual Basic .NET code

   dataGrid1.SetDataBinding(dataSet1, "Customer")
   dataGrid2.SetDataBinding(dataSet1.Customer, "")

The DataGrids are bound to different data sources—one to a DataSet, the other to a DataTable. This alone means they have different CurrencyManagers, even though they will, all else being equal, show the same data. The difference comes when you start sorting (or filtering) the data sources. Binding to a DataTable is really binding to its DefaultView (an instance of DataView), so let's say you apply a sort to that DataView.

C# code

   dataSet1.Customers.DefaultView.Sort = "CustomerID desc";

Visual Basic .NET code

   dataSet1.Customers.DefaultView.Sort = "CustomerID desc"

This causes dataGrid2 to be sorted, but not dataGrid1. dataGrid1 is bound to a DataSet, not a DataTable, therefore it is not affected by the Customers table's sort order. However, the CurrencyManager controlling dataGrid2's data source is bound to a DataView, so there is another way to sort dataGrid2.

C# code

   CurrencyManager cm = BindingContext[dataGrid2.DataSource, 
   dataGrid2.DataMember] as CurrencyManager;
   (cm.Current as DataRowView).DataView.Sort = "CustomerID desc";

Visual Basic .NET code

    Dim cm As CurrencyManager = _
      DirectCast(Me.BindingContext(dataGrid2.DataSource, _
        dataGrid2.DataMember), CurrencyManager)
    DirectCast(cm.Current, DataRowView).DataView.Sort = "CustomerID desc"

We know that dataGrid1 is not bound to a DataTable, but its own CurrencyManager is also bound to a DataView, so we can sort the data using the same technique as in the code snippet above. The question is, if it's not the Customers table's view's sort order we're changing, then which DataView are we affecting? The answer involves the DataViewManager type and is explained in the next section.

Before we move on, though, it's worth mentioning that, although BindingContext[Customers, ""] and BindingContext[Customers.DefaultView, ""] return two different CurrencyManager references, those references are for all intents and purposes the same because both return the same object from their Current property.

DataViewManager and When to Use It

It's not uncommon for developers to express frustration that each time they run into a Windows Forms data binding obstacle, they lose development velocity. One customer I worked with, who was a long way into development, wanted to change tack and hide the complexities of data binding behind a general component that would also perform table joins. At first sight, the DataViewManager class seemed to fit this requirement. The article Providers of Data to Windows Forms in the Visual Studio .NET documentation describes the DataViewManager as "analogous to a DataView, but with relations included." There is an article in the Knowledge Base called HOW TO: Implement a DataSet JOIN helper class in Visual C# .NET that is probably more relevant, but in this section I will show what we found out about DataViewManager in terms of what it can do and its limitations.

The DataViewManager class is all about managing and creating DataViews, either stand-alone or in the context of a DataSet. A DataTable has a default DataView and you can also create new DataViews on it, so what does the DataViewManager do that's so special? In fact, its strength and raison d'être is arguably a counter-intuitive feature until you understand why it's there.

To illustrate, bind DataGrids according to the following code:

C# code

   dataGrid1.SetDataBinding(dataSet1, "Customers");
   dataGrid2.SetDataBinding(dataSet1, "Customers.CustomerOrders");
   dataGrid3.SetDataBinding(dataSet1, "Orders");
   dataGrid4.SetDataBinding(dataSet1.Orders, "");

Visual Basic .NET code

   dataGrid1.SetDataBinding(dataSet1, "Customers")
   dataGrid2.SetDataBinding(dataSet1, "Customers.CustomerOrders")
   dataGrid3.SetDataBinding(dataSet1, "Orders")
   dataGrid4.SetDataBinding(dataSet1.Orders, "")

Right above the binding code, put the following code:

C# code

   dataSet1.DefaultViewManager.DataViewSettings["Orders"].Sort = "EmployeeID asc";

Visual Basic .NET code

   dataSet1.DefaultViewManager.DataViewSettings("Orders").Sort _
         = "EmployeeID asc"

This appears to be setting what the DataSet's default DataViewManager considers to be the sort order of the Orders table. And indeed, when you run the code, all three child grids are initially sorted by EmployeeID. But setting the DataViewSetting's sort order as above only affects dataGrid3 and 4 if you do so before they are data bound. Somehow, a once-only change is being made to the DataViews to which those grids are bound, and any later change to the same DataViewSetting does not affect the two grids. Setting the sort order as above can affect dataGrid2 after binding, but only if you refresh the parent grid's CurrencyManager afterwards (or if you move to another row). Compare those problems to code such as the following, which can be called at any time after binding with no need for refreshes:

C# code

   CurrencyManager cm = BindingContext[dataGridX.DataSource, 
   dataGridX.DataMember] as CurrencyManager;
   (cm.Current as DataRowView).DataView.Sort = "EmployeeID asc";

Visual Basic .NET code

    Dim cm As CurrencyManager = _
      DirectCast(Me.BindingContext(dataGridX.DataSource, _
        dataGridX.DataMember), CurrencyManager)
    DirectCast(cm.Current, DataRowView).DataView.Sort = "EmployeeID asc"

The first piece of feedback here is that it is counter-intuitive; that using the Orders table's name to index into the DataViewSettings (after binding) should only have an effect on the relation. If anything, one would expect it to only have an effect on the table. However, this is what the DataViewManager is all about, and once that's realized we can:

  • Set up relations between tables in a DataSet
  • Set DataViewSettings properties for the tables (relations) using one of its DataViewManagers (most likely the default one)
  • Bind a DataGrid to the DataSet (or, equivalently, to the DataViewManager)
  • Use the DataGrid to navigate through the relational hierarchy with the view filters and sort orders in all their glory

I have mentioned that if you set DataViewSetting properties, then you must do so before binding (or else refresh the parent table's CurrencyManager afterwards). But there are further problems when you want to change any of those DataViewSetting properties at runtime. Take the earlier example, but bind the first DataGrid to the DataSet itself. Then navigate through to the Orders for a Customer (using the relation) and try and alter the sort order programmatically. Without refreshing, the view does not update. Whether you refresh the DataSet's CurrencyManager or that of the Customers table, the parent DataGrid's view is reset back to the collapsed view of the DataTables. The only case in which the parent DataGrid does not collapse is when it is showing the Customers table and you refresh the CurrencyManager of the Customers table. The DataGrid bound to the relation updates correctly no matter what the parent grid is showing and no matter which of the two CurrencyManagers you refresh.

In conclusion, the DataViewManager has a single use. When using a single DataGrid to navigate relations between tables, the DataViewManager can be used to set properties of the views of the relations once only, prior to binding.

Binding ValueMember Using a "This" Property

The Customers and Orders we've seen so far have been rows in tables. But Windows Forms data binding is flexible enough to allow them to be objects. Consider these two simple classes that illustrate a more object-oriented relation between Customer and Order:

C# code

   public class Customer
   {
      public Customer(string id)
      {
         _id = id;
      }
      public string CustomerID
      {
         get { return _id; }
         set { _id = value; }
      } string _id;
   }

   public class Order
   {
      public Order(int id, Customer customer)
      {
         _id = id;
         _customer = customer;
      }
      public int OrderID
      {
         get { return _id; }
         set { _id = value; }
      } int _id;
      public Customer Customer
      {
         get { return _customer; }
         set { _customer = value; }
      } Customer _customer;
   }

Visual Basic .NET code

    Public Class Customer
        Dim m_id As String

        Public Sub New(ByVal id As String)
            m_id = id
        End Sub

        Public Property CustomerID() As String
            Get
                Return m_id
            End Get
            Set(ByVal Value As String)
                m_id = Value
            End Set
        End Property

    End Class

    Public Class Order
        Dim m_id As Integer
        Dim m_customer As Customer

        Public Sub New(ByVal id As Integer, _
            ByVal myCustomer As Customer)
            m_id = id
            m_customer = myCustomer
        End Sub

        Public Property OrderID() As Integer
            Get
                Return m_id
            End Get
            Set(ByVal Value As Integer)
                m_id = Value
            End Set
        End Property

        Public Property Customer() As Customer
            Get
                Return Me.m_customer
            End Get
            Set(ByVal Value As Customer)
                Me.m_customer = Value
            End Set
        End Property

    End Class

We can construct some Customers and then bind an ArrayList of Orders to a ListBox like this:

C# code

   Customer cVINET = new Customer("VINET");
   Customer cTOMSP = new Customer("TOMSP");
   Customer cHANAR = new Customer("HANAR");
   ArrayList orders = new ArrayList();
   orders.Add(new Order(10248, cVINET));
   orders.Add(new Order(10249, cTOMSP));
   orders.Add(new Order(10250, cHANAR));
   listBox1.DataSource = orders;
   listBox1.ValueMember = "OrderID";

Visual Basic .NET code

   Customer cVINET = new Customer("VINET");
   Customer cTOMSP = new Customer("TOMSP");
   Customer cHANAR = new Customer("HANAR");
   ArrayList orders = new ArrayList();
   orders.Add(new Order(10248, cVINET));
   orders.Add(new Order(10249, cTOMSP));
   orders.Add(new Order(10250, cHANAR));
   listBox1.DataSource = orders;
   listBox1.ValueMember = "OrderID";

The ListBox shows only the OrderID. If we want to view or update the Customer for an Order, then we can bind to a ComboBox of Customers like this:

C# code

   comboBox1.Items.Add(cVINET);
   comboBox1.Items.Add(cTOMSP);
   comboBox1.Items.Add(cHANAR);
   comboBox1.ValueMember = "CustomerID";
   comboBox1.DataBindings.Add("SelectedItem", orders, "Customer");

Visual Basic .NET code

   comboBox1.Items.Add(cVINET)
   comboBox1.Items.Add(cTOMSP)
   comboBox1.Items.Add(cHANAR)
   comboBox1.ValueMember = "CustomerID"
   comboBox1.DataBindings.Add("SelectedItem", orders, "Customer")

The ListBox and the ComboBox both bind to the orders data source so they share the same CurrencyManager. Note that the items in the ComboBox are Customer objects, not strings. When the ComboBox's selected item changes, it becomes the Customer object reference of the Order current in the ListBox.

On the way to this solution, I have seen developers implement a This property on the Customer class like so (note that the capitalization is necessary):

C# code

   public Customer This
   {
      get { return this; }
   }

Visual Basic .NET code

    Public ReadOnly Property This() As Customer
        Get
            Return Me
        End Get
    End Property

Then the ComboBox has its ValueMember and DisplayMember set like so:

C# code

   comboBox1.ValueMember = "This";
   comboBox1.DisplayMember = "CustomerID";
 

Visual Basic .NET code

   comboBox1.ValueMember = "This"
   comboBox1.DisplayMember = "CustomerID"

Although this does work, it is not necessary. The feedback has been that it should be possible to use code like the following without implementing any fake 'This' property:

C# code

   comboBox1.ValueMember = "this";

Visual Basic .NET code

   comboBox1.ValueMember = "this"

However, so long as you use SelectedItem as the ComboBox property to bind to, you will be binding to a Customer object.

RowFilter on a Boolean Column

This section describes the reproduction and workaround of a bug in versions 1.0 and 1.1 of the .NET Framework. I have worked on this issue with two different customers, so I imagine that knowing how to workaround it until it is patched will be of interest to others.

Create a DataTable with an identifier column of integer type and a second column of type Boolean, and populate the table with rows, setting all the Boolean fields to false. Bind a ListBox to the table's identifier column. Create a new DataView on the table, setting its RowFilter to select rows where the Boolean column is true, and bind a DataGrid to this. Next, add a Button to the form whose handler sets the Boolean field to true for the row selected in the ListBox.

The grid is initially empty, but click the button and the first row appears. Now put focus into the first cell of the DataGrid. Finally, if you cause the DataGrid to lose focus (say, click the button again), you will see a message box with the error "Index -1 is not non-negative and below total rows count." The cause of this IndexOutOfRangeException can be diagnosed with the aid of some informative Labels and the Reflector tool (Lutz Roeder's excellent decompiler is available at http://www.aisto.com/roeder/dotnet). At the point of the exception, the value of CurrencyManager.Count is 1 and the value of CurrencyManager.Position is -1. This is a state not expected by the code in CurrencyManager.EndCurrentEdit, which I paraphrase here:

   public override void EndCurrentEdit()
   {
      if (this.Count > 0)
      {
         object obj1 = this.List[this.Position];
         . . .
      }
   }

I promised a workaround. The nature of this is to handle the DataGrid's Leave event (that is, before CurrencyManager.EndCurrentEdit is called) and toggle the RowFilter:

C# code

   private void dataGrid1_Leave(object sender, System.EventArgs e)
   {
      DataGridDV.RowFilter = "myBoolColumn = false";
      DataGridDV.RowFilter = "myBoolColumn = true";
   }

Visual Basic .NET code

    Private Sub DataGrid1_Leave(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles DataGrid1.Leave
        DataGridDV.RowFilter = "myBoolColumn = false"
        DataGridDV.RowFilter = "myBoolColumn = true"
    End Sub

Doing so sets the value of CurrencyManager.Position to 0, which is the correct value.

Always Call DataBindings.Clear when Disposing

This is a habit that's worth getting into in order to avoid a problem with versions 1.0 and 1.1 of the .NET Framework. When disposing a data-bound Form, recurse into its Controls and clear their DataBindings collections. If you know which properties of which controls are bound, then there is a more selective method that I'll go into shortly.

The problem I mentioned has to do with bound objects not being garbage-collected. Technically speaking, this is not a memory leak because references on the objects are held by an internal .NET Framework class. However, given that the application code releases all of its references, for all intents and purposes, the objects are leaked for the lifetime of the application domain. Specifically, if an application constructs, shows and closes a Form on which a control is bound to an object, then the object will not be garbage collected unless bindings are cleared. If the object is a substantial one and has references to the form itself through events, then the Form will not be collected either. What happens is that, during binding, the bound object's property is examined through reflection and a reference to the object is placed in a ValueChanged event, which is indirectly cached in a static HashTable. The HashTable has to be explicitly cleared at some point, and a good time to do this is when the bound control's owning Form is disposed. Consider this binding statement:

C# code

   label1.DataBindings.Add("Text", myObj, "Property");

Visual Basic .NET code

   label1.DataBindings.Add("Text", myObj, "Property")

For single controls where the control's bound property is known, clearing the binding is as simple as using code similar to either:

C# code

   label1.DataBindings.Clear();

Visual Basic .NET code

   label1.DataBindings.Clear()

Or:

C# code

   TypeDescriptor.Refresh(label1.DataBindings["Text"].DataSource);

Visual Basic .NET code

   TypeDescriptor.Refresh(label1.DataBindings("Text").DataSource)

For a hierarchy of controls, use the same principle while recursing into the control tree. You should be aware of a further wrinkle when your navigation path refers to a property of a property. So, if you are binding like so:

C# code

   label1.DataBindings.Add("Text", myObj, "Nested.Property");

Visual Basic .NET code

   label1.DataBindings.Add("Text", myObj, "Nested.Property")

Then your disposing logic needs to be slightly more involved:

C# code

   object ds = label1.DataBindings["Text"].DataSource;
   label1.DataBindings.Clear();
   TypeDescriptor.Refresh(ds);

Visual Basic .NET code

   Dim ds As Object = label1.DataBindings("Text").DataSource
   label1.DataBindings.Clear()
   TypeDescriptor.Refresh(ds)

Again, recurse if you want to handle a control tree. To finish this section, I'll give the recommended method for performing a general clearing of bindings courtesy of Mike Taulty, a colleague in the DPE team whose customer first encountered this issue. This code covers all cases known, so although probably not the most performant solution, it is the safest and most general.

C# code

private void ClearBindings(Control c)
{
   Binding[] bindings = new Binding[c.DataBindings.Count];
   c.DataBindings.CopyTo(bindings, 0);
   c.DataBindings.Clear();
   foreach (Binding binding in bindings)
   {
      TypeDescriptor.Refresh(binding.DataSource);
   }
   foreach (Control cc in c.Controls)
   {
      ClearBindings(cc);
   }
}

Visual Basic .NET code

    Private Sub ClearBindings(ByVal c As Control)
        Dim bindings(c.DataBindings.Count) As Binding
        c.DataBindings.CopyTo(bindings, 0)
        c.DataBindings.Clear()
        For Each bind As Binding In bindings
            System.ComponentModel. _
                TypeDescriptor.Refresh(bind.DataSource)
        Next

        For Each cc As Control In c.Controls
            ClearBindings(cc)
        Next
    End Sub

Summary of Best Practices

  • Define parent-child relations with care. If in doubt, let referential integrity guide you. Remember, all good relations need constraints.
  • Use a RowFilter or emulate the .NET Framework mechanism for binding to a DataRelation when navigating from child to parent.
  • Try a CurrencyManager.Refresh if controls don't refresh automatically.
  • When binding to grandchildren, chain the DataRelation names together in your navigation path.
  • If you want to programmatically read or write the bound property of a control, do it using CurrencyManager.Current when possible.
  • Use the DataSource and DataMember properties of a control to get its BindingManagerBase from BindingContext.
  • Understand what the DataViewManager does.
  • Binding to rich object hierarchies is possible and powerful.
  • A RowFilter on a Boolean column needs a workaround.
  • Use the ClearBindings method from your Form's Dispose method to avoid leaks.

Roadmaps and Futures

For an excellent introduction to the data binding technologies, see Knowledge Base article 313482 INFO: Roadmap for Windows Forms Data Binding. Another must-read is the Microsoft Developer Tools Roadmap 2004-2005, which amongst other things discusses the future of Windows Forms data binding in Visual Studio 2005. Current users of the .NET Framework have at their disposal a powerful and extensible infrastructure in data binding. In the future, that's a story that just gets better as it paves the road to Longhorn.

Steve White is an Application Development Consultant working in the Premier Support for Developers team at Microsoft UK. He supports customers using Visual C#, ASP.NET, and Windows Forms. His website, http://home.btconnect.com/stevewhi/, has more information about his interests in music and 3D visualizations, and his disgraceful Penguin paperback.

Show:
© 2014 Microsoft