Data Binding


In WinForms 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:

 
 Sub addRowMenuItem_Click(sender As Object, e As EventArgs)   ' Add a new typed row   Dim row As CustomerSet.CustomersRow = _       Me.customerSet1.Customers.NewCustomersRow()   row.CustomerID = "SELLSB"   ...   Me.customerSet1.Customers.AddCustomersRow(row)   ' Update list box   PopulateListBox() End Sub 

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

 
 Sub PopulateListBox()   ' Don't show any item more than once   customersListBox.Items.Clear()   ' Show all rows in the Customers table   Dim row As CustomerSet.CustomersRow   For Each row In Me.customerSet1.Customers.Rows       ' Except don't show the deleted rows       If Not((row.RowState And DataRowState.Deleted) = _           DataRowState.Deleted)  Then           customersListBox.Items.Add(row.ContactTitleName)       End If   Next End Sub 

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:

 
 Sub Form1_Load(sender As Object, e As EventArgs)   ' Data source   Dim stringDataSource As String = "Hello, There"   ' Binding (without a data member)   Dim myBinding As Binding = New Binding("Text", stringDataSource, Nothing)   ' Bind the control to the data source   ' Control: textBox1   ' PropertyName: Text   ' DataSource: stringDataSource   ' DataMember: Nothing   textBox1.DataBindings.Add(myBinding) End Sub 

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 Overloads Sub New(propertyName as String, dataSource As Object, _       dataMember As String)   ' Properties   Public ReadOnly Property BindingManagerBase() _       As BindingManagerBase   Public ReadOnly Property BindingMemberInfo() As BindingMemberInfo   Public ReadOnly Property Control() As Control   Public ReadOnly Property DataSource() As Object   Public ReadOnly Property IsBinding() As Boolean   Public ReadOnly Property PropertyName() As String   ' Events   Public Event Format As ConvertEventHandler   Public Event Parse As ConvertEventHandler End Class 

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 Dim stringDataSource As String = "Hello, There" ' Bind the control to the data source, ' letting the Add method create the Binding object ' Control: textBox1 ' PropertyName: Text ' DataSource: stringDataSource ' DataMember: Nothing textBox1.DataBindings.Add("Text", stringDataSource, Nothing) 

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 Dim stringDataSource As String = "Hello, There" Dim fontDataSource As Font = New Font("Lucida Console", 18) ' Bind the control properties to the data sources ' Control: textBox1 ' PropertyName: Text ' DataSource: stringDataSource ' DataMember: Nothing textBox1.DataBindings.Add("Text", stringDataSource, Nothing) ' Control: textBox1 ' PropertyName: Font ' DataSource: fontDataSource ' DataMember: Nothing textBox1.DataBindings.Add("Font", fontDataSource, Nothing) 

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 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: Nothing Dim listDataSource() As String = { "apple", "peach", "pumpkin" } textBox1.DataBindings.Add("Text", listDataSource, Nothing) 

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:

 
 Sub InitializeComponent()   ...   Me.textBox1.DataBindings.Add(_       New Binding(_       "Text", Me.customerSet1, _       "Customers.ContactTitleName"))   ... End Sub 

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:

 
 MustInherit Class BindingManagerBase   ' Constructors   Public Sub New()   ' Properties   Public ReadOnly Property Bindings() As BindingsCollection   Public MustOverride ReadOnly Property Count() As Integer   Public MustOverride ReadOnly Property Current() As Object   Public MustOverride Property Position() As Integer   ' Events   Public Event CurrentChanged As EventHandler   Public Event PositionChanged As EventHandler   ' Methods   Public MustOverride Sub AddNew()   Public MustOverride Sub CancelCurrentEdit()   Public MustOverride Sub EndCurrentEdit()   Public MustOverride Sub RemoveAt(index As Integer)   Public MustOverride Sub ResumeBinding()   Public MustOverride Sub SuspendBinding() End Class 

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:

 
 Sub positionButton_Click(sender As Object, e As EventArgs)   ' Get the binding   Dim myBinding As Binding = textBox1.DataBindings("Text")   ' Get the binding manager   Dim manager As BindingManagerBase = myBinding.BindingManagerBase   ' Get current position   Dim pos As Integer = manager.Position   MessageBox.Show(pos.ToString(), "Current Position") End Sub 

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 BindingManager Base 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 Manages 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:

 
 Sub CurrencyForm_Load(sender As Object, e As EventArgs)   Dim myBinding As Binding = textBox1.DataBindings("Text")   ' Fill the data set   customersAdapter.Fill(customerSet1, "Customers") End Sub Sub positionButton_Click(sender As Object, e As EventArgs)   Dim myBinding As Binding = textBox1.DataBindings("Text")   Dim manager As BindingManagerBase = myBinding.BindingManagerBase   ' Get current position   Dim pos As Integer = manager.Position   MessageBox.Show(pos.ToString(), "Current Position") End Sub Sub startButton_Click(sender As Object, e As EventArgs)   ' Reset the position   Dim myBinding As Binding = textBox1.DataBindings("Text")   Dim manager As BindingManagerBase = myBinding.BindingManagerBase   manager.Position = 0 End Sub Sub previousButton_Click(sender As Object, e As EventArgs)   ' Decrement the position   Dim myBinding As Binding = textBox1.DataBindings("Text")   Dim manager As BindingManagerBase = myBinding.BindingManagerBase   manager.Position = manager.Position  1 ' No need to worry about being < 0 End Sub Sub nextButton_Click(sender As Object, e As EventArgs)   ' Increment the position   Dim myBinding As Binding = textBox1.DataBindings("Text")   Dim manager As BindingManagerBase = myBinding.BindingManagerBase   manager.Position = manager.Position + 1 ' No need to worry about > Count End Sub Sub endButton_Click(sender As Object, e As EventArgs)   ' Set position to end   Dim myBinding As Binding = textBox1.DataBindings("Text")   Dim manager As BindingManagerBase = myBinding.BindingManagerBase   manager.Position = manager.Count  1 End Sub 

Taking this a step further, Figure 13.16 shows two text boxes bound to the same data source but 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:

 
 Sub InitializeComponent   ...   Me.textBox1.DataBindings.Add(_       New Binding(_           "Text", Me.customerSet1, "Customers.ContactTitle"))   ...   Me.textBox2.DataBindings.Add(_       New Binding(_           "Text", Me.customerSet1, "Customers.ContactName"))   ... End Sub 

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 Dim manager1 As BindingManagerBase = _   textBox1.DataBindings("Text").BindingManagerBase Dim manager2 As BindingManagerBase = _   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:

 
 Dim manager As BindingManagerBase = _   Me.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:

 
 Dim manager As BindingManagerBase = _   Me.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:

 
 Sub showButton_Click(sender As Object, e As EventArgs)   ' Get the binding manager   Dim manager As BindingManagerBase = _       Me.BindingContext(customerSet1, "Customers")   ' Get the current row via the view   Dim view As DataRowView = CType(manager.Current, DataRowView)   Dim row As CustomerSet.CustomersRow = _       CType(view.Row, CustomerSet.CustomersRow)   MsgBox(row.ContactTitleName) End Sub 

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:

 
 Sub addButton_Click(sender As Object, e As EventArgs)   ' Add a new row   Dim row As CustomerSet.CustomersRow = _       Me.customerSet1.Customers.NewCustomersRow()   row.CustomerID = "SELLSB"   ...   Me.customerSet1.Customers.AddCustomersRow(row)   ' Controls automatically updated   ' (except some controls need a nudge, e.g. listbox)   ' Note: Check to make sure that the binding manager is   '        also a currency manager. If not, we've got a   '       property manager, which doesn't have a   '       Refresh method   If TypeOf Me.BindingContext(customerSet1, "Customers") _       Is CurrencyManager Then       Dim manager As CurrencyManager = _           Me.BindingContext(customerSet1, "Customers")       manager.Refresh()   End If End Sub 

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:

 
 Sub updateButton_Click(sender As Object, e As EventArgs)   ' Update the current row   Dim manager As BindingManagerBase = _       Me.BindingContext(customerSet1, "Customers")   Dim view As DataRowView = CType(manager.Current, DataRowView)   Dim row As CustomerSet.CustomersRow = _       CType(view.Row, CustomerSet.CustomersRow)   row.ContactTitle = "CEO"   ' Control automatically updated End Sub Sub deleteButton_Click(sender As Object, e As EventArgs)   ' Mark the current row as deleted   Dim manager As BindingManagerBase = _       Me.BindingContext(customerSet1, "Customers")   Dim view As DataRowView = CType(manager.Current, DataRowView)   Dim row As CustomerSet.CustomersRow = _       CType(view.Row, CustomerSet.CustomersRow)   row.Delete()   ' Controls automatically updated   ' (no special treatment needed to avoid deleted rows!) End Sub 

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:

 
 Dim newDataSet As DataSet = myWebService.GetDataSet() Me.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:

 
 Dim newDataSet As DataSet = myWebService.GetDataSet() Me.dataSet1.Clear() Me.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:

 
 Dim newDataSet As DataSet = myWebService.GetDataSet() Dim manager As BindingManagerBase = _   Me.BindingContext(dataSet1, "Customers") manager.SuspendBinding() Me.dataSet1.Clear() Me.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:

 
 ' Fires when textBox1 loses focus Sub textBox1_Leave(sender As Object, e As EventArgs)   ' Force the current edit to end   Dim manager As BindingManagerBase = _       Me.BindingContext(customerSet1, "Customers")   manager.EndCurrentEdit() End Sub 

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 Dim manager As BindingManagerBase = _   Me.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:

 
 Sub CurrencyForm_Load(sender As Object, e As EventArgs)   ' Custom-format the ContactTitle   ' NOTE: subscribe to this before filling the data set   Dim myBinding As Binding = textBox1.DataBindings("Text")   AddHandler myBinding.Format, AddressOf textBox1_Format   ' Fill the data set   ... End Sub Sub textBox1_Format(sender As Object, e As ConvertEventArgs)   ' Show ContactTitle as all caps   e.Value = e.Value.ToString().ToUpper() End Sub 

The Format event handler gets a ConvertEventArgs parameter:

 
 Class ConvertEventArgs   Inherits EventArgs   ' Constructors   Public Overloads Sub New(value As Object, desiredType As Type)   ' Properties   Public ReadOnly Property DesiredType() As Type   Public Property Value() As Object End Class 

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:

 
 Sub CurrencyForm_Load(sender As Object, e As EventArgs)   ' Custom-format and parse the ContactTitle   ' NOTE: subscribe to these before filling the data set   Dim myBinding As Binding = textBox1.DataBindings("Text")   AddHandler myBinding.Format, AddressOf textBox1_Format   AddHandler myBinding.Parse, AddressOf textBox1_Parse   ' Fill the data set   ... End Sub Sub textBox1_Parse(sender As Object, e As ConvertEventArgs)   ' Convert ContactTitle to mixed case   e.Value = MixedCase(e.Value.ToString()) End Sub 

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:

 
 Me.listBox1.DataSource = Me.customerSet1 Me.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:

 
 Sub listBox1_SelectedIndexChanged(sender As Object, e As EventArgs)   If listBox1.SelectedValue Is Nothing Then Exit Sub   ' Get the currently selected row's CustomerID using current item   Dim view As DataRowView = CType(listBox1.SelectedItem, _ DataRowView)   Dim row As CustomerSet.CustomersRow = CType(view.Row, _       CustomerSet.CustomersRow)   statusBar1.Text = "Selected CustomerID= " & row.CustomerID End Sub 

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:

 
 Sub InitializeComponent()   ...   Me.listBox1.ValueMember = "Customers.CustomerID"   ... End Sub Sub listBox1_SelectedIndexChanged(sender As Object, e As EventArgs)   If listBox1.SelectedValue Is Nothing Then Exit Sub   ' Get the currently selected row's CustomerID   statusBar1.Text = "Selected CustomerID= " & listBox1.SelectedValue End Sub 

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:

 
 Sub CurrencyForm_Load(sender As Object, e As EventArgs)   ' Create a sorting view   Dim sortView As DataView = New DataView(customerSet1.Customers)   sortView.Sort = "ContactTitle ASC, ContactName DESC"   ' Bind to the view   listBox1.DataSource = sortView   listBox1.DisplayMember = "ContactTitleName"   ' Fill the data set   ... End Sub 

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:

 
 Sub CurrencyForm_Load(sender As Object, e As EventArgs)   ' Create a filtering view   Dim filterView As DataView = New DataView(customerSet1.Customers)   filterView.RowFilter = "ContactTitle = 'Owner'"   ' Bind to the view   listBox1.DataSource = filterView   listBox1.DisplayMember = "ContactTitleName"   ' Fill the data set   ... End Sub 

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 reference to the tables Dim customers As DataTable = mydataset.Tables("Customers") Dim orders As DataTable = mydataset.Tables("Orders") ' Create the relation Dim relation As DataRelation = _   New DataRelation(_       "CustomersOrders", _       customers.Columns("CustomerID"), _       orders.Columns("CustomerID")) ' Add the relation mydataset.Relations.Add(relation) ... Sub PopulateChildListBox()   ' Clear the list box   ordersListBox.Items.Clear()   ' Get the currently selected parent custom row   Dim index As Integer = customersListbox.SelectedIndex   If index = -1 Then Exit Sub   ' Get row from data set   Dim parent As DataRow = mydataset.Tables("Customers").Rows(index)   ' Enumerate child rows   Dim row As DataRow   For Each row In parent.GetChildRows("CustomersOrders")       ...   Next End Sub 

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.

 
 Sub InitializeComponent()   ...   Me.ordersListBox.DisplayMembmer = _       "Customers.CustomersOrders.OrderID"   ... End Sub 

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:

 
 Sub MasterDetailForm_Load(sender As Object, e As EventArgs)   ' Fill the data set   customersAdapter.Fill(customerSet1, "Customers")   ordersAdapter.Fill(customerSet1, "Orders") End Sub Sub addRowMenuItem_Click(sender As Object, e As EventArgs)   ' Add a new typed row   Dim row As CustomerSet.CustomersRow = _       Me.customerSet1.Customers.NewCustomersRow()   row.CustomerID = "SELLSB"   ...   Me.customerSet1.Customers.AddCustomersRow(row)   ' Controls automatically updated   ' (except some controls, e.g. listBox, need a nudge)   If TypeOf Me.BindingContext(customerSet1, "Customers") _       Is CurrencyManager Then       Dim manager As CurrencyManager = _           Me.BindingContext(customerSet1, "Customers")       manager.Refresh()   End If End Sub Sub updateSelectedRowMenuItem_Click(sender As Object, e As EventArgs)   ' Update the current row   Dim manager As BindingManagerBase = _       Me.BindingContext(customerSet1, "Customers")   Dim view As DataRowView = CType(manager.Current, DataRowView)   Dim row As CustomerSet.CustomersRow = _       CType(view.Row, CustomerSet.CustomersRow)   row.ContactTitle = "CEO" End Sub Sub deleteSelectedRowMenuItem_Click(sender As Object, e As EventArgs)   ' Mark the current row as deleted   Dim manager As BindingManagerBase = _       Me.BindingContext(customerSet1, "Customers")   Dim view As DataRowView = CType(manager.Current, DataRowView)   Dim row As CustomerSet.CustomersRow = _       CType(view.Row, CustomerSet.CustomersRow)   row.Delete() End Sub 

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 Visual Basic .NET
Windows Forms Programming in Visual Basic .NET
ISBN: 0321125193
EAN: 2147483647
Year: 2003
Pages: 139

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