Data Binding


In Windows Forms development, there often comes a time when you need to populate a number of controls with data from a database or other data source. Recall our example from Chapter 12: Data Sets and Designer Support, which used a data set populated from a database. Whenever the data set was changed, such as when the user added a row or deleted a row, we had to repopulate the list boxes so that the display was kept in sync with the data:

 void addRowMenuItem_Click(object sender, EventArgs e) {  // Add a new typed row   CustomerSet.CustomersRow row =   this.customerSet1.Customers.NewCustomersRow();  row.CustomerID = "SELLSB";  ...   this.customerSet1.Customers.AddCustomersRow(row);  // Update list box   PopulateListBox(); } 

Writing code to do this repopulation of the control is boring:

 void PopulateListBox() {  // Don't show any item more than once  customersListBox.Items.Clear();  // Show all rows in the Customers table  foreach( CustomerSet.CustomersRow row in             this.customerSet1.Customers.Rows ) {  // Except don't show the deleted rows  if( (row.RowState & DataRowState.Deleted) !=          DataRowState.Deleted ) {       continue;     }  // On each row, show the ContactTitleName column  customersListBox.Items.Add(row.ContactTitleName);   } } 

This code implements the following intention: "Keep the list box up-to-date by showing the ContactTitleName column from the undeleted rows of the Customers table." But what we'd really like to do is to declare this intention and let some part of WinForms keep a control up-to-date as the underlying data changes. That is the service provided by WinForms data binding .

Bindings and Data Sources

At the heart of the data binding architecture is the idea of a binding. A binding is an association between a control and a data source , which can be any object (although it's often an instance of the DataSet class). For example, binding a data set to a list box requires a binding be established between the list box and the data set. For the list box to be populated with the appropriate data, we must also specify, as part of the binding, the data table and the column name , which is the data member portion of the binding. The data member specifies the name of the data to pull from the data source. Figure 13.1 shows a logical binding from a data set to a list box, using the ContactTitleName from the Customers table as the data member.

Figure 13.1. Complex Data Binding

Figure 13.2 shows the results of establishing this binding.

Figure 13.2. Result of Complex Data Binding

The kind of binding shown in Figures 13.1 and 13.2 is known as complex data binding . The binding is "complex" not because it's hard to use, but because the ListBox control must have specific support for it. Other built-in controls, such as ComboBox and DataGrid, support complex binding, but not all controls do. [1]

[1] ListView is an example of a control that you'd expect to support complex binding but doesn't.

On the other hand, simple data binding works for all controls. Simple data binding is an association from a data source to a specific property of a control. The property can be nearly any public property that a control exposes, such as Text or BackColor. What makes this binding "simple" is that the control doesn't have to do anything to support simple binding; the data binding infrastructure itself sets the value of a control's bound property.

As an example of simple binding, Figure 13.3 shows binding the Text property of a text box to a data set, using Customers.ContactTitleName as the data member.

Figure 13.3. Simple Data Binding

Because text boxes can't show lists of data, only one row from the data set can be displayed at a time, as shown in Figure 13.4.

Figure 13.4. Simple Data Binding

It may not seem useful to be able to see only one row from a data set, but, as you'll see, it's possible to "scroll" through the rows of a data set, allowing a column from each row to be displayed and edited one at a time.

Both of these examples of data binding show binding against a data set, which provides a list of objects to display either all at once (in the case of complex binding) or one at a time (in the case of simple binding). A data set is an example of a list data source , because it provides a list of objects against which to bind. Other list data sources include arrays, collections from the System.Collections namespace, and custom collections (more on that later).

However, data binding can also happen against an item data source, which is an instance of any type that can queried for values to bind against. For example, Figure 13.5 shows a logical binding from a string object to the Text property of a TextBox control.

Figure 13.5. Simple Data Binding to an Item Data Source

Figure 13.6 shows the results of establishing this binding.

Figure 13.6. Simple Binding to a String Object

When you're using data binding, you need to keep track of both the kind of binding you're doing (simple or complex) and the kind of data source to which you're binding (item or list). Those two factors largely determine how data binding works, as you'll see.

Simple Data Binding to Items

The binding scenario in Figure 13.5 shows the simple binding of a TextBox control to a string item data source. You implement simple binding by creating an instance of a Binding object and adding it to the list of bindings exposed by the control via the DataBindings property:

 void Form1_Load(object sender, EventArgs e) {  // Data source   string stringDataSource = "Hello, There";   // Binding (without a data member)   Binding binding = new Binding("Text", stringDataSource, null);   // Bind the control to the data source   // Control: textBox1   // PropertyName: Text   // DataSource: stringDataSource   // DataMember: null   textBox1.DataBindings.Add(binding);  } 

You create the Binding object by passing the name of the control's property as the first argument, the data source object as the second argument, and the optional name of a data member. Then you add the new binding object to the list of the data bindings of the text box, and that's all that's needed to bind the string object's value to the Text property of the control, as shown earlier in Figure 13.6.

The Binding class, which is used to implement simple binding, [2] is from the System.Windows.Forms namespace:

[2] Although simple and complex data binding are logically similar, only simple binding uses instances of the Binding class. The implementation details of complex binding are specific to the control and are hidden from the client of the control, as you'll see later in this chapter.

 class Binding {   // Constructors   public Binding(     string propertyName, object dataSource, string dataMember);   // Properties   public BindingManagerBase BindingManagerBase { get; }   public BindingMemberInfo BindingMemberInfo { get; }  public Control Control { get; }   public object DataSource { get; }  public bool IsBinding { get; }  public string PropertyName { get; }  // Events   public event ConvertEventHandler Format;   public event ConvertEventHandler Parse; } 

As a shortcut to creating a Binding object and adding it to a control's list of bindings, you can use the overload of the Add method (which takes the three Binding constructor arguments) and let the Add method create the Binding object for you:

 // Data source string stringDataSource = "Hello, There";  // Bind the control to the data source,   // letting the Add method create the Binding object   // Control: textBox1   // PropertyName: Text   // DataSource: stringDataSource   // DataMember: null   textBox1.DataBindings.Add("Text", stringDataSource, null);  

Passing null as the data member parameter means that we can obtain the contents of the data source by calling the string object's ToString method. If you'd like to bind to a specific property of a data source, you can do so by passing the name of the property as the data member of the binding. For example, the following code binds the text box's Text property to the Length property of the string object:

 // Bind the control to the Length property of the data source // Control: textBox1 // PropertyName: Text // DataSource: stringDataSource // DataMember: Length textBox1.DataBindings.Add("Text", stringDataSource,  "Length"  ); 

Binding to the Length property of the string instead of binding the string itself works as shown in Figure 13.7.

Figure 13.7. Simple Binding to the Length Property of a String Object

Any public property of a data source can serve as a data member. Similarly, any public property of a control can serve as the property for binding. The following shows an example of binding a data source to the Font property of a text box:

 // Data sources string stringDataSource = "Hello, There";  Font fontDataSource = new Font("Lucida Console", 18);  // Bind the control properties to the data sources // Control: textBox1 // PropertyName: Text // DataSource: stringDataSource // DataMember: null textBox1.DataBindings.Add("Text", stringDataSource, null); // Control: textBox1 // PropertyName: Font // DataSource: fontDataSource // DataMember: null  textBox1.DataBindings.Add("Font", fontDataSource, null);  

Notice that the code provides two different bindings against the same control. A control can have any number of bindings as long as any single property has only one. The results of the combined bindings are shown in Figure 13.8.

Figure 13.8. Binding the Text and Font Properties of a TextBox Control

Notice also that the data from the data source needs to be a string object for the Text property and needs to be a Font object for the Font property. By providing data sources that expose those types directly, we avoided the need for conversion. If the data being bound to isn't already in the appropriate format, a type converter is used, as discussed in Chapter 9: Design-Time Integration. Type converters are also covered in more detail later in this chapter.

Simple Data Binding to Lists

You implement simple binding to a list data source without a data member in the same way as binding to an item data source:

 // Control: textBox1 // PropertyName: Text // DataSource: listDataSource // DataMember: null  string[] listDataSource = { "apple", "peach", "pumpkin" };   textBox1.DataBindings.Add("Text", listDataSource, null);  

When you bind to a list data source, only one item is displayed at a time, as shown in Figure 13.9.

Figure 13.9. Simple Data Binding to a List Data Source

Similarly, you're allowed to bind to a particular property of the objects in the list data source by specifying a data member:

 // Control: textBox1 // PropertyName: Text // DataSource: listDataSource // DataMember: Length textBox1.DataBindings.Add("Text", listDataSource, "Length"); 

Again, this technique displays only a single item at a time, as shown in Figure 13.10.

Figure 13.10. Simple Data Binding to a Property of a List Data Source

Before we look at how to display other items from a list data set using simple binding, let's talk about binding to a data set.

Simple Binding to Data Sets

Although arrays and other kinds of list data sources are useful, the most popular list data source is the data set. You can bind to columns in a table from a data set in two ways:

 // Fill the data set customersAdapter.Fill(customerSet1, "Customers"); ordersAdapter.Fill(customerSet1, "Orders");  // 1. Bind to data set + table.column (good!)  textBox1.DataBindings.Add(   "Text",  customerSet1, "Customers.ContactTitleName"  );  // 2. Bind the table + column (BAD!)  textBox1.DataBindings.Add(   "Text",  customerSet1.Customers, "ContactTitleName"  ); 

Either way you do the binding, what you'll see is shown in Figure 13.11.

Figure 13.11. Simple Binding to a Data Set

Although both techniques of binding to a column seem to display equivalent results, there is an "issue" in .NET 1.x that causes inconsistencies if you mix the two methods . The reason to always use technique 1 (data set + table.column) is that the Designer-generated code uses this technique when you choose a data member in the Property Browser, as shown in Figure 13.12.

Figure 13.12. Adding a Data Binding in the Property Browser

The DataBindings property in the Property Browser allows you to add bindings without writing the code manually. When you choose a column from a data set hosted on your form, the generated code will use the data set + table.column technique:

 void InitializeComponent() {   ...   this.textBox1.DataBindings.Add(     new Binding(       "Text",  this.customerSet1, "Customers.ContactTitleName")  );   ... } 

You can feel free to add bindings in custom code or to use the Property Browser, whichever you find more convenient . But if you also use the data set + table.column technique, you'll avoid the inconsistencies caused by data binding support in WinForms 1.x.

So, as convenient as the Property Browser is for generating the binding code for us, it doesn't seem very useful to show only a single value from a list data source. However, when you understand binding managers (discussed next ), simple binding to list data sources can be very useful.

Binding Managers

For every bound data source, the infrastructure creates a binding manager for you. Binding managers manage a set of bindings for a particular data source and come in two flavors: property managers and currency managers. A property manager is an instance of the PropertyManager class and is created for an item data source. A currency manager is an instance of the CurrencyManager class and is created for a list data source. Both of these managers are implementations of the abstract base class BindingManagerBase:

 abstract class BindingManagerBase {   // Constructors   public BindingManagerBase();   // Properties   public BindingsCollection Bindings { get; }   public int Count { virtual get; }   public object Current { virtual get; }   public int Position { virtual get; virtual set; }   // Events   public event EventHandler CurrentChanged;   public event EventHandler PositionChanged;   // Methods   public virtual void AddNew();   public virtual void CancelCurrentEdit();   public virtual void EndCurrentEdit();   public virtual void RemoveAt(int index);   public virtual void ResumeBinding();   public virtual void SuspendBinding(); } 

It's the job of the binding manager ”whether it's a property binding manager or a currency binding manager ”to keep track of all the bindings between all the controls bound to the same data source. For example, it's the binding manager's job (through the bindings) to keep all the controls up-to-date when the underlying data source changes and vice versa. To access a data source's binding manager, you can retrieve a binding for a bound property and get the binding manager from the binding's BindingManagerBase property:

 void positionButton_Click(object sender, EventArgs e) {  // Get the binding   Binding binding = textBox1.DataBindings["Text"];   // Get the binding manager   BindingManagerBase manager = binding.BindingManagerBase;   // Get current position   int pos = manager.Position;  MessageBox.Show(pos.ToString(), "Current Position"); } 

Notice that after the binding manager is retrieved, the code checks the current position. This is known as the binding manager's currency, which is the current position in its list of items. Figure 13.13 shows how a currency manager maintains the position into a list data source.

Figure 13.13. A Currency Manager Maintaining a Position into a List Data Source

Although only a currency manager manages an actual list, for simplicity the position is pushed into the base binding manager for both the currency manager and the property manager. This allows easy access to the current position for both kinds of binding managers through the BindingManagerBase class. For a property manager, which manages the data source of only a single object, the position will always be zero, as shown in Figure 13.14.

Figure 13.14. A Property Manager Managers Only a Single Item

For a currency manager, the position can be changed, and that will update all the bindings to a new "current" object in a list data source. This arrangement allows you to build controls that let your users "scroll" through a list data source, even when you're using simple binding, as shown in Figure 13.15.

Figure 13.15. Managing Currency

The following code shows how to implement the list data source navigation buttons using the BindingManagerBase Position property:

 void CurrencyForm_Load(object sender, EventArgs e) {   Binding binding = textBox1.DataBindings["Text"];   // Fill the data set   customersAdapter.Fill(customerSet1, "Customers"); } void positionButton_Click(object sender, EventArgs e) {  Binding binding = textBox1.DataBindings["Text"];   BindingManagerBase manager = binding.BindingManagerBase;   // Get current position   int pos = manager.Position;  MessageBox.Show(pos.ToString(), "Current Position"); } void startButton_Click(object sender, EventArgs e) {  // Reset the position   Binding binding = textBox1.DataBindings["Text"];   BindingManagerBase manager = binding.BindingManagerBase;   manager.Position = 0;  } void previousButton_Click(object sender, EventArgs e) {  // Decrement the position   Binding binding = textBox1.DataBindings["Text"];   BindingManagerBase manager = binding.BindingManagerBase;   --manager.Position; // No need to worry about being <0  } void nextButton_Click(object sender, EventArgs e) {  // Increment the position   Binding binding = textBox1.DataBindings["Text"];   BindingManagerBase manager = binding.BindingManagerBase;   ++manager.Position; // No need to worry about being >Count  } void endButton_Click(object sender, EventArgs e) {  // Set position to end   Binding binding = textBox1.DataBindings["Text"];   BindingManagerBase manager = binding.BindingManagerBase;   manager.Position = manager.Count - 1;  } 

Taking this a step further, Figure 13.16 shows two text boxes bound to the same data source but in two different columns.

Figure 13.16. Two Controls Bound to the Same Data Source

After the binding is in place for both controls, changing the position on the binding context for one will automatically affect the other. Figure 13.17 shows the currency manager and the logical bindings for the two text controls.

Figure 13.17. Two Controls Bound to the Same Data Source Sharing the Same Currency Manager

Establishing the bindings for the two text boxes works as you'd expect:

 void InitializeComponent() {   ...  this.textBox1.DataBindings.Add(   new Binding(   "Text", this.customerSet1, "Customers.ContactTitle"));  ...  this.textBox2.DataBindings.Add(   new Binding(   "Text", this.customerSet1, "Customers.ContactName"));  ... } 

A binding manager is per data source, [3] not per binding. This means that when the same data set and data table are used, the two text boxes get routed through the same currency manager to the same data source. As the currency manager's position changes, all the controls bound through the same currency manager get updated appropriately.

[3] Technically, a binding manager is unique per data source only within a binding context , which holds groups of binding managers. Every control can have its own binding context, and that allows two controls in the same container to show the same data source but different currencies. Although it's interesting, the need for multiple binding contexts is rare and not explored further in this book.

Because all controls bound against the same data source share access to the same binding manager, getting the binding manager from either control yields the same binding manager:

 // Check the binding manager  BindingManagerBase manager1 =   textBox1.DataBindings["Text"].BindingManagerBase;   BindingManagerBase manager2 =   textBox2.DataBindings["Text"].BindingManagerBase;  // Assert that these controls are bound to the // same data source System.Diagnostics.Debug.Assert(manager1 == manager2); 

In fact, the binding managers are shared at the container level, so you can go to the containing form's BindingContext collection to get the binding manager:

 BindingManagerBase manager =   this.BindingContext[customerSet1, "Customers"]; // 1: good! 

Access to the binding manager from the BindingContext property is another place where there's an overload that can take a data set and a table name or only a table:

 BindingManagerBase manager =   this.BindingContext[  customerSet1.Customers  ]; // 2: BAD! 

This is another case of technique 1 versus technique 2 for specifying the data source. If you use technique 1, all the binding managers will be shared as planned, keeping everything in sync. However, if you use technique 2, you'll get back a different binding manager, and this means that things won't be in sync when you manipulate the binding manager. This is the issue that data binding has in .NET 1.x, and you'd be wise to avoid it by always sticking to technique 1.

Current Data Row

The Position property is fine when it comes to navigating the rows currently shown, but it's not good for finding the current row in a data table. The problem is that as soon as items are marked as deleted in the underlying data table, they're automatically hidden from view by the binding infrastructure (just what we want to have happen). However, deleted rows are still there at exactly the same offset, and this means that the offsets will be mismatched between the underlying data table and the binding manager, which "counts" rows that haven't been deleted.

Luckily, the BindingManagerBase class provides a Current property, which always provides access to the current row in the data table regardless of the mismatch between the offsets:

 void showButton_Click(object sender, EventArgs e) {   // Get the binding manager   BindingManagerBase manager =     this.BindingContext[customerSet1, "Customers"];  // Get the current row via the view   DataRowView view = (DataRowView)manager.Current;   CustomerSet.CustomersRow row =   (CustomerSet.CustomersRow)view.Row;  MessageBox.Show(row.ContactTitleName); } 

BindingManagerBase's Current property is of type Object, so it must be cast to a specific type. Normally, that type will be the type you'd expect when you set up the binding. For example, when you bind to a string, the Current property will return an object of type string. However, in the case of DataSets, the DataTable data source exposes DataRowView objects instead of DataRow objects (to tailor the view of a row when it's displayed). You'll learn more about DataRowView later in this chapter, but the trick is to get the current row from the Row property of the current DataRowView.

Changes to the Data Set

Adding data to the underlying data works in pretty much the same way it did before we started using data binding:

 void addButton_Click(object sender, EventArgs e) {   // Add a new row   CustomerSet.CustomersRow row =     this.customerSet1.Customers.NewCustomersRow();   row.CustomerID = "SELLSB";   ...   this.customerSet1.Customers.AddCustomersRow(row);  // Controls automatically updated   // (except some controls, e.g. ListBox, need a nudge)   // NOTE: Use C# "as" cast to ask if the binding   //       manager is a currency manager. If the   //       result of the "as" cast is null, then   //       we've got a property manager, which   //       doesn't have a Refresh method   CurrencyManager manager =   this.BindingContext[customerSet1, "Customers"]   as CurrencyManager;   if( manager != null ) manager.Refresh();  } 

Adding a new row to the data set causes most controls to be updated, except for some complex bound controls like the ListBox. For those cases, we cast the BindingManagerBase class to a CurrencyManager to be able to call the CurrencyManager-specific Refresh method, which will bring those pesky list boxes up to speed on the new row.

As far as updates and deletes go, make sure to use the BindingManagerBase class's Current property instead of the Position property to get the current row. Otherwise , you could access the wrong row:

 void updateButton_Click(object sender, EventArgs e) {   // Update the current row  BindingManagerBase manager =   this.BindingContext[customerSet1, "Customers"];   DataRowView view = (DataRowView)manager.Current;   CustomerSet.CustomersRow row =   (CustomerSet.CustomersRow)view.Row;  row.ContactTitle = "CEO";  // Controls automatically updated  } void deleteButton_Click(object sender, EventArgs e) {   // Mark the current row as deleted  BindingManagerBase manager =   this.BindingContext[customerSet1, "Customers"];   DataRowView view = (DataRowView)manager.Current;   CustomerSet.CustomersRow row =   (CustomerSet.CustomersRow)view.Row;  row.Delete();  // Controls automatically updated   // (no special treatment needed to avoid deleted rows!)  } 

Notice that no code is needed to update the controls to take into account updated or deleted rows. This is the power of data binding. When the underlying data source is updated, we don't want to be forced to update the related controls manually. Instead, we bind them and let that happen automatically. [4]

[4] Not all data sources are as good about keeping the bound controls up-to-date as the DataSet-related data sources are. For the requirements of a good custom data source, see the "Custom Data Sources" section later in this chapter.

Replacing the Data Set

Sometimes, after all the data bindings are established, it's necessary to replace a data set with a different data set. For example, if you're calling into a Web service that produces a DataSet as the return type, you might find yourself writing code like this:

 DataSet newDataSet = myWebService.GetDataSet(); this.dataSet1 = newDataSet; // Data bindings lost 

Unfortunately, when you replace the existing data set with a new one, all the data bindings remain with the old data set. As a shortcut, instead of manually moving all the data bindings to the new data set, you can use the Merge method of the DataSet class to bring the data from the new data set into the existing one:

 DataSet newDataSet = myWebService.GetDataSet(); this.dataSet1.Clear(); this.dataSet1.Merge(newDataSet); // Data bindings kept 

The trick here is that all the data bindings continue to be bound to the existing data set, but the data from the existing data set is cleared and replaced with the data from the new data set. For this to work, all the schema information in the new data set, such as the names of the columns, must match the schema information that the bindings are using. Otherwise, you'll get run-time exceptions.

As an optimization when merging data sets, you'll want to temporarily turn off data binding so that you don't have to update all the bound controls after when the data set is cleared and again when the new data is merged:

 DataSet newDataSet = myWebService.GetDataSet();  BindingManagerBase manager =   this.BindingContext[dataSet1, "Customers"];   manager.SuspendBinding();  this.dataSet1.Clear(); this.dataSet1.Merge(newDataSet); // Data bindings kept  manager.ResumeBinding();  

To turn off the updates that bindings cause as the data set is updated, we suspend the binding by using a call to the binding manager's SuspendBinding method. Then, after we clear and merge the data set, to resume updates to the bound controls we resume the binding with the call to ResumeBinding. Temporarily suspending bindings isn't for use only with data sets; it's useful whenever the controls shouldn't be updated until a set of operations on the underlying data source has been performed.

Changes to the Control

Keeping the controls updated when the data source changes is half the story of data binding. The other half is keeping the underlying data source up-to-date as the data in the controls changes. By default, any change in the control's data will be replicated when the position within the currency manager is changed. For example, in Figure 13.18, even though the data in the TextBox has changed, the underlying row (as shown in the message box) has not, in spite of the change in focus that occurs when you move to the Show button.

Figure 13.18. Losing Focus Does Not Trigger an End to the Edit

The reason that the text box hasn't pushed the new data to the current row is that the current edit hasn't yet ended. When data in a control changes, that represents an "edit" of the data. Only when the current edit has ended does the data get replicated to the underlying data source. If you don't like waiting for the currency to change before the current edit takes effect, you can push it through yourself using the EndCurrentEdit method of the BindingManagerBase:

 void textBox1_Leave(object sender, EventArgs e) {  // Force the current edit to end  BindingManagerBase manager =     this.BindingContext[customerSet1, "Customers"];  manager.EndCurrentEdit();  } 

This causes the control whose data is being edited to flush it to the underlying data source immediately. If, on the other hand, you'd like to replace the dirty data with the data currently in the data source, you can use CancelCurrentEdit instead:

 // Cancel the current edit BindingManagerBase manager =   this.BindingContext[customerSet1, "Customers"];  manager.CancelCurrentEdit();  
Custom Formatting and Parsing

Sometimes the data as shown in the control is already formatted automatically the way you would like to see it. However, if you'd like to change the formatting, you can do so by handling the Format event of the Binding object:

 void CurrencyForm_Load(object sender, EventArgs e) {  // Custom-format the ContactTitle   // NOTE: subscribe to this before filling the data set   Binding binding = textBox1.DataBindings["Text"];   binding.Format += new ConvertEventHandler(textBox1_Format);  // Fill the data set   ... }  void textBox1_Format(object sender, ConvertEventArgs e) {   // Show ContactTitle as all caps   e.Value = e.Value.ToString().ToUpper();   }  

The Format event handler gets a ConvertEventArgs parameter:

 class ConvertEventArgs : EventArgs {   // Constructors   public ConvertEventArgs(object value, Type desiredType);   // Properties  public Type DesiredType { get; }   public object Value { get; set; }  } 

The Value property of the ConvertEventArgs class is the unconverted value that will be shown if you don't change it. It's your job to convert the current Value property (which will be of the same type as the underlying data) to a value of the DesiredType (string), formatting it as you please .

If you are binding to a read-only property, this is all that is involved in getting what you need. If you expect the data to be pushed back to the data source, you should also trap the Parse event of the binding. This allows you to undo the formatting as the data is replicated from the control back to the data source:

 void CurrencyForm_Load(object sender, EventArgs e) {   // Custom-format and parse the ContactTitle   // NOTE: subscribe to these before filling the data set   Binding binding = textBox1.DataBindings["Text"];   binding.Format += new ConvertEventHandler(textBox1_Format);  binding.Parse += new ConvertEventHandler(textBox1_Parse);  // Fill the data set   ... }  void textBox1_Parse(object sender, ConvertEventArgs e) {   // Convert ContactTitle to mixed case   e.Value = MixedCase(e.Value.ToString());   }  

Parsing is the opposite of formatting. The type of Value will be string, and DesiredType will be the type needed for the underlying data source. Value will start as the data from the control, and it's your job to convert it.

Complex Data Binding

Complex binding and simple binding are more alike than not. All the considerations discussed so far about using binding managers and keeping the control and data source in sync apply equally well to complex binding as they do to simple binding.

However, unlike simple binding, a control that supports complex binding must do so in a custom fashion. It does so by exposing a custom property to specify a data source (typically called DataSource) and zero or more properties for specifying data members . For example, the list control family of controls ”ListBox, CheckedListBox, and ComboBox ”exposes the following properties to support complex data binding:

  • DataSource . This takes a list data source that implements IList or IListSource. [5] This includes arrays, ArrayList data tables, and any custom type that supports one of these interfaces.

    [5] The implementation of the IListSource interface is what allows a DataTable to expose multiple DataRowView objects as data sources.

  • DisplayMember . This is the name of the property from the data source that the list control will display (defaults to ValueMember, if that's set, or ToString otherwise).

  • ValueMember . This is the name of the property from the data source that the list control will use as the value of the SelectedValue property (defaults to the currently selected item).

  • SelectedValue . This is the value of the ValueMember property for the currently selected item.

  • SelectedItem . This is the currently selected item from the list data source.

  • SelectedItems . This is the currently selected items for a multiselect list control.

For the list controls, you must set at least the DataSource and the DisplayMember:

 this.listBox1.DataSource = this.customerSet1; this.listBox1.DisplayMember = "Customers.ContactTitleName"; 

This is another case of remembering to set the data source to the data set and to set the display member (and value member) to table.column. Otherwise, you can end up with mismatched binding managers. Remembering to make these settings is especially important because, unlike simple binding, complex binding using technique 2 is allowed in the Property Browser, as shown in Figure 13.19.

Figure 13.19. Don't Use the dataset.table + Column Technique to Specify the Data Source

Instead, make sure that you pick a data set for the DataSource property and pick a table.column for the DisplayMember and ValueMember properties, as shown in Figure 13.20.

Figure 13.20. Use the Dataset + table.column Technique to Specify a Data Source

After you've set the data source and the display member, you'll get an automatically populated list control, just as we've been pining for since this chapter began (and as shown in Figure 13.21).

Figure 13.21. Using Data Binding to Populate a List Control

In addition to a nicely filled list box, notice that Figure 13.21 shows the data for the same row in both of the text boxes as in the currently selected list box item. As the list box selection changes, the position is updated for the shared currency manager. Using the VCR-style buttons would likewise change the position and update the list box's selection accordingly .

Also, notice that the status bar is updated with the CustomerID of the current row as the position changes. You do this using the SelectedItem property of the list control, which exposes the currently selected item:

 void listBox1_SelectedIndexChanged(object sender, EventArgs e) {   if( listBox1.SelectedValue == null ) return;  // Get the currently selected row's CustomerID using current item   DataRowView view = (DataRowView)listBox1.SelectedItem;   CustomerSet.CustomersRow row = (CustomerSet.CustomersRow)view.Row;  statusBar1.Text = "Selected CustomerID= " + row.CustomerID; } 

As a convenience, you can set the ValueMember property to designate a property as the value of the selected row. This is useful for primary keys when you want to know what was selected without knowing any of the other details. Using the ValueMember property, you can directly extract the ID of the currently selected row:

 void InitializeComponent() {   ...  this.listBox1.ValueMember = "Customers.CustomerID";  ... } void listBox1_SelectedIndexChanged(object sender, EventArgs e) {   if( listBox1.SelectedValue == null ) return;  // Get the currently selected row's CustomerID   statusBar1.Text = "Selected CustomerID= " + listBox1.SelectedValue;  } 

Data Views

So far we've discussed how to show specific data members, such as specific columns, from a data source. However, it's also useful to filter which rows from a data table are shown as well as to sort the rows. This kind of functionality is provided by a data view , which allows you to transform a data table as it's being displayed in ways that the Format and Parse events can't.

Sorting

You can sort on any number of columns in either ascending or descending order. Setting the sort criteria is a matter of creating an instance of the DataView class, setting the Sort property, and then binding to the view as the data source:

 void CurrencyForm_Load(object sender, EventArgs e) {  // Create a sorting view   DataView sortView = new DataView(customerSet1.Customers);   sortView.Sort = "ContactTitle ASC, ContactName DESC";  // Bind to the view   listBox1.DataSource = sortView;   listBox1.DisplayMember = "ContactTitleName";   // Fill the data set   ... } 

Notice that the DataView object takes a DataTable argument so that it knows where to get its data. Notice also the ASC (default) and DESC designators, which indicate ascending and descending sort order, respectively. Binding to the sorted view of our customers table yields Figure 13.22.

Figure 13.22. Binding to a Sort View

Filtering

Similarly, filtering is a matter of creating an instance of the DataView class, setting the RowFilter property, and binding to the view:

 void CurrencyForm_Load(object sender, EventArgs e) {   // Create a filtering view   DataView filterView = new DataView(customerSet1.Customers);  filterView.RowFilter = "ContactTitle = 'Owner'";  // Bind to the view   listBox1.DataSource = filterView;   listBox1.DisplayMember = "ContactTitleName";   // Fill the data set   ... } 

Binding to this view of our customers data table shows only rows where the ContactTitle column has the value of "Owner," as shown in Figure 13.23.

Figure 13.23. Binding to a Filtered View

The expression language used for filtering is a subset of SQL. You can find a link to the documentation in the description of the DataView class's RowFilter property.

Master-Detail Relations

While we're on the subject of filtering, one of the most popular ways to filter what's shown in one control is based on the current selection in another control. For example, when a customer is selected in the top list box of Figure 13.24, the bottom list box shows only the related orders for that customer.

Figure 13.24. Master-Detail Relations

Recall from Chapter 12: Data Sets and Designer Support that we implemented this functionality by repopulating the orders list box whenever the selection in the customers list box changed. The order list box populating code uses a relation to get the child order rows from the currently selected parent customer row:

 // Get references to the tables DataTable customers = dataset.Tables["Customers"]; DataTable orders = dataset.Tables["Orders"];  // Create the relation   DataRelation relation =   new DataRelation(   "CustomersOrders",   customers.Columns["CustomerID"],   orders.Columns["CustomerID"]);   // Add the relation   dataset.Relations.Add(relation);   ...  void PopulateChildListBox() {   // Clear the list box   ordersListBox.Items.Clear();  // Get the currently selected parent custom row   int index = customersListBox.SelectedIndex;  if( index == -1 ) return;  // Get row from data set   DataRow parent = dataset.Tables["Customers"].Rows[index];  // Enumerate child rows   foreach( DataRow row in  parent.GetChildRows("CustomersOrders")  ) {     ...   } } 

Instead of our writing this code by hand, data binding allows us to bind to a relation. Then as the selection changes in the parent control, the child control is automatically populated with only the related child rows. The format for specifying the data member for a relation is the following:

 parentTable.relationName.childColumn 

Specifying a parent-child relationship in this manner allows us to use a binding to display master-detail data.

 void InitializeComponent() {   ...  this.ordersListBox.DisplayMember =   "Customers.CustomersOrders.OrderID";  ... } 

When we use a typed data set to establish a relation, the Property Browser provides a list of possible relation bindings, as shown in Figure 13.25.

Figure 13.25. Composing a Data Member from a Relation in the Property Browser

In addition, you aren't limited to a single level of master-detail binding. You can have any number of relations from parent to child to grandchild, and so on.

The combination of VS.NET design-time features, typed data sets, and data binding has reduced the amount of handwritten code in the sample shown in Figure 13.24 to the following:

 void MasterDetailForm_Load(object sender, EventArgs e) {   // Fill the data set   customersAdapter.Fill(customerSet1, "Customers");   ordersAdapter.Fill(customerSet1, "Orders"); } void addRowMenuItem_Click(object sender, EventArgs e) {   // Add a new typed row   CustomerSet.CustomersRow row =     this.customerSet1.Customers.NewCustomersRow();   row.CustomerID = "SELLSB";   ...   this.customerSet1.Customers.AddCustomersRow(row);   // Controls automatically updated   // (except some controls, e.g. ListBox, need a nudge)   CurrencyManager manager =     this.BindingContext[customerSet1, "Customers"]     as CurrencyManager;   if( manager != null ) manager.Refresh(); } void updateSelectedRowMenuItem_Click(object sender, EventArgs e) {   // Update the current row   BindingManagerBase manager =     this.BindingContext[customerSet1, "Customers"];   DataRowView view = (DataRowView)manager.Current;   CustomerSet.CustomersRow row = (CustomerSet.CustomersRow)view.Row;   row.ContactTitle = "CEO"; } void deleteSelectedRowMenuItem_Click(object sender, EventArgs e) {   // Mark the current row as deleted   BindingManagerBase manager =     this.BindingContext[customerSet1, "Customers"];   DataRowView view = (DataRowView)manager.Current;   CustomerSet.CustomersRow row = (CustomerSet.CustomersRow)view.Row;   row.Delete(); } 

The rest, including keeping both list boxes up-to-date as the underlying data changes and synchronizing the orders list box based on the selection in the customers list box, is all written for us.



Windows Forms Programming in C#
Windows Forms Programming in C#
ISBN: 0321116208
EAN: 2147483647
Year: 2003
Pages: 136
Authors: Chris Sells

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net