When it comes to developing interactive database applications, it's difficult to resist using data-bound controls. Data-bound controls are easy to use, and they also provide many handy, built-in features. You used DataGrid, ListBox, and other data-bound controls in the previous chapters. In this chapter, we discuss the basics of data binding, how to use data-bound controls, and how to develop interactive database applications using these controls with a minimal amount of time and effort.
Both Windows Forms and Web Forms provide a rich set of data-bound controls, which help developers build data-driven Windows and Web applications. In this chapter, we concentrate on Windows Forms. Chapter 16 covers data binding in Web Forms.
So what are data-bound controls? You've already seen the DataGrid and ListBox controls in the previous chapters. You used these controls to display data from a data source. Data-bound controls are Windows controls that represent and manipulate data in Graphical User Interface (GUI) forms. Both Windows Forms and Web Forms provide a variety of flexible and powerful data-bound controls. These data-bound controls vary from a TextBox to a DataGrid.
The process of binding a data source's data to GUI controls is called data binding. Most of the editable Windows controls provide data binding, either directly or indirectly. These controls contain members that connect directly to a data source, and then the control takes care of displaying the data and other details. For example, to view data in a DataGrid control, you just need to set its DataSource property to a data source. This data source could be a DataSet, DataView, array, collection, or other data source. Data-bound controls can display data, and they are smart enough to display properties (metadata) of the stored data such as data relations.
You can divide data binding into two categories: simple data binding and complex data binding. In simple data binding, a control displays data provided by a data feed. In fact, the control itself is not capable of displaying complex data. Setting the Text property of a TextBox or Label control is an example of simple data binding. Complex data binding, on the other hand, allows controls to bind to multiple columns and complex data. Binding an entire database table or multiple columns of a database table to a DataGrid or a ListBox control is an example of complex data binding.
The Binding class, defined in the System.Windows.Forms namespace, represents simple binding between the data source item and a control.
The Binding class constructor, which creates an instance of the Binding class, takes three arguments: a data-bound control's property name, a data source as an Object, and a data member, usually the name of the data source columns as a string. You define the Binding class constructor as follows:
Public Sub New(_ ByVal propertyName As String, _ ByVal dataSource As Object, _ ByVal dataMember As String _ )
In this syntax, dataSource can be a DataSet, DataTable, DataView, DataViewManager, any class that implements IList, and a class object. Listing 7-1 binds the Employees.FirstName column of a DataSet to the Text property of a TextBox.
Listing 7-1: Binding a TextBox Using Binding
Dim ds As DataSet = New DataSet() ds = GetDataSet("Employees") Dim bind1 As Binding bind1 = New Binding("Text", ds, "Employees.FirstName") TextBox1.DataBindings.Add(bind1)
Besides the previous two controls, you can perform simple binding on many controls including Button, CheckBox, CheckedListBox, ComboBox, DateTimePicker, DomainUpDown, GroupBox, HscrollBar, Label, LinkLabel, ListBox, ListView, MonthCalender, NumericUpDown, PictureBox, ProgressBar, RadioButton, RichTextBox, ScrollBar, StatusBar, TextBox, TreeView, and VscrollBar. Listing 7-2 binds the Text property of a ComboBox, Label, and Button control with the DataTable's LastName, City, and Country columns (respectively).
Listing 7-2: Binding Multiple Controls Using Binding
ComboBox1.DataBindings.Add _ (New Binding("Text", ds, "Employees.LastName")) TextBox2.DataBindings.Add _ (New Binding("Text", ds, "Employees.EmployeeID")) Label4.DataBindings.Add( New Binding("Text", ds, "Employees.City")) Button1.DataBindings.Add( New Binding("Text", ds, "Employees.Country"))
The BindingsCollection class represents a collection of Binding objects for a control. You access the BindingsCollection class through the control's DataBindings property. The BindingsCollection class provides members to add, count, and remove Binding objects to the collection. Listing 7-1 and Listing 7-2 used the Add method of BindingsCollection to add a Binding object to the collection.
The BindingsCollection class has three properties: Count, Item, and List. The Count property returns the total number of items in the collection. The Item property returns the Binding object at a specified index, and the List property returns all the items in a collection as an ArrayList.
The Add method of BindingsCollection adds a Binding object to the collection. The Remove method deletes a Binding object from the collection. The RemoveAt method removes a Binding object at the specified index. The Clear method removes all the Binding objects from the collection.
Listing 7-3 counts the total number of Binding objects associated with a control and removes the Binding objects from various controls.
Listing 7-3: Counting and Removing Binding Objects
MessageBox.Show("Total Bindings: " + _ Button1.DataBindings.Count.ToString()) TextBox1.DataBindings.RemoveAt(0) TextBox2.DataBindings.Clear()
The BindingsCollection class is a collection of Binding objects. The index of Binding objects in a collection is 0 based, which means the 0th item of the collection is the first item and (n-1)th item in the collection is the nth item.
The Binding class provides six properties: BindingManagerBase, BindingMemberInfo, Control, DataSource, IsBinding, and PropertyName.
BindingManagerBase represents the BindingManagerBase object, which manages the binding between a data source and data-bound controls.
The BindingMemberInfo property object is a BindingMemberInfo structure that contains the information about the binding. The BindingMemberInfo structure has three properties: BindingField, BindingMember, and BindingPath. The BindingField property returns the data-bound control's property name. BindingMember returns the information used to specify the data-bound control's property name, and BindingPath returns the property name, or the period-delimited hierarchy of property names, that precedes the data-bound object's property.
Listing 7-4 reads the bindings available on all the controls and displays their information by using the BindingMemberInfo property.
Listing 7-4: Reading All the Bindings of a Form
Dim str As String Dim curControl As Control Dim curBinding As Binding For Each curControl In Me.Controls For Each curBinding In curControl.DataBindings Dim bInfo As BindingMemberInfo = _ curBinding.BindingMemberInfo str = "Control: " + curControl.Name str += ", BindingPath: " + bInfo.BindingPath str += ", BindingField: " + bInfo.BindingField str += ", BindingMember: " + bInfo.BindingMember MessageBox.Show(str) Next curBinding Next curControl
The Control and DataSource properties return the control and data source that belong to this binding. The IsBinding property returns True if the binding is active; otherwise it returns False. PropertyName returns the name of the bound control's property that can be used in data binding. Listing 7-5 displays the DataSource and PropertyName properties of a TextBox.
Listing 7-5: Reading a TextBox Control's Binding Properties
If (TextBox1.DataBindings(0).IsBinding) Then Dim ds As DataSet = _ CType(TextBox1.DataBindings(0).DataSource, DataSet) str = "DataSource : " + ds.Tables(0).TableName str += ", Property Name: " + _ TextBox1.DataBindings(0).PropertyName MessageBox.Show(str) End If
In addition to the previously discussed properties, the Binding class also provides two protected methods: OnParse and OnFormat. OnParse raises the Parse event, and OnFormat raises the Format event. The Parse event occurs when the value of a data-bound control is changing, and the Format event occurs when the property of a control is bound to a data value. The event handler for both the Parse and Format events receives an argument of type ConvertEventArgs containing data related to this event, which has two members: DesiredType and Value. DesiredType returns the data type of the desired value, and Value gets and sets the value of the ConvertEventArgs object.
Now let's say you want to convert text and decimal values for a Binding for a TextBox. You write the code in Listing 7-6, where you change the Binding type and add event handlers for the Binding objects for Format and Parse members.
Listing 7-6: Adding Format and Parse Event Handlers
Dim bind2 As Binding = New Binding _ ("Text", ds, "customers.custToOrders.OrderAmount") AddHandler bind1.Format, AddressOf DecimalToCurrencyString AddHandler bind1.Parse, AddressOf CurrencyStringToDecimal Private Sub DecimalToCurrencyString(ByVal sender As Object, _ ByVal cevent As ConvertEventArgs) If Not cevent.DesiredType Is GetType(String) Then Exit Sub End If cevent.Value = CType(cevent.Value, Decimal).ToString("c") End Sub Private Sub CurrencyStringToDecimal(ByVal sender As Object, _ ByVal cevent As ConvertEventArgs) If Not cevent.DesiredType Is GetType(Decimal) Then Exit Sub End If cevent.Value = Decimal.Parse(cevent.Value.ToString, _ NumberStyles.Currency, Nothing) End Sub
This listing uses the Customers table instead of Employees because the Employees table doesn't have any decimal data. If you want to use the Employees table, you could convert a Date type to a String type.
To test this code, you need to create a DataSet from the Employees table of the Northwind database and use it as a data source when constructing a Binding object. Also, don't forget to add a reference to the System.Globalization namespace because the NumberStyle enumeration is defined in this namespace.
The BindingManagerBase class is an abstract base class. You use its functionality through its two derived classes: CurrencyManager and PropertyManager.
By default data-bound controls provide neither data synchronization nor the position of the current item. The BindingManagerBase object provides the data synchronization in Windows Forms and makes sure that all controls on a form are updated with the correct data.
What is data synchronization?
Have you ever developed database applications in Visual Basic 6.0 or Microsoft Foundation Classes (MFC)? In both of those languages, a data-bound control lets you navigate through data from one record to another and update data in the controls available on the form. As you move to the next record, the next row was fetched from the data source and every control was updated with the current row's data. This process is called data synchronization.
OK, now let's say a form has three controls: a TextBox, a Label, and a PictureBox. All three controls support data binding from a DataSet, which is filled with the data from the Employees table. The TextBox control displays FirstName, the Label control displays LastName, and the PictureBox control displays Photo properties (columns) of the DataSet. All of the controls must be synchronized in order to display the correct first name, last name, and photo for the same employee.
CurrencyManager accomplishes this synchronization by maintaining a pointer to the current item for the list. All controls are bound to the current item so they display the information for the same row. When the current item changes, CurrencyManager notifies all the bound controls so that they can refresh their data. Furthermore, you can set the Position property to specify the row in the DataSet or DataTable to which the controls point. Figure 7-1 shows the synchronization process.
Figure 7-1: Synchronization between a data source and data-bound controls
As you learned, the Binding property returns the collection of binding objects as a BindingsCollection object that BindingManagerBase manages. Listing 7-7 creates a BindingManagerBase object for the form and reads all of the binding controls.
Listing 7-7: Reading All Controls Participating in Data Binding
' Get the BindingManagerBase Dim bindingBase As BindingManagerBase = _ Me.BindingContext(ds, "Employees") Dim bindingObj As Binding ' Read each Binding object from the collection For Each bindingObj In bindingBase.Bindings MessageBox.Show(bindingObj.Control.Name) Next bindingObj
To read a form's controls that are participating in data binding, you must make sure that the form's data source and control's data source are the same.
The Count property returns the total number of rows being managed by BindingManagerBase. The Current property returns the current object, and the Position property represents (both gets and sets) the position in the underlying list to which controls bound to this data source point. We use these properties in the following sample examples.
Table 7-1 describes the BindingManagerBase class methods.
Adds a new item to the list
Cancels the current edit operation
Ends the current edit operation
Returns the list of property descriptions for the data source
Deletes the row at the specified index
Resumes data binding
Suspends data binding
Protected. Returns the name of the list
Raises the CurrentChanged event, which occurs when the bound value changes
Pulls data from the data-bound control into the data source
Pushes data from data source into the data-bound control
Updates the binding
Besides the properties and methods discussed previously, the BindingManagerBase class provides two events: CurrentChanged and PositionChanged. The CurrentChanged event occurs when the bound value changes, and the PositionChanged event occurs when the position changes.
CurrencyManager manages a list of Binding objects on a form. It's inherited from the BindingManagerBase class. Besides the functionality provided by indingManagerBase, the CurrencyManager provides two members: a List property and a Refresh method. The List property returns the list of bindings maintained by CurrencyManager as an IList object. To convert an IList to other objects, you need to cast it with the type of the object, which must implement IList. Some of the objects that implement IList are DataView, DataTable, DataSet, Array, ArrayList, and CollectionBase.
You create a CurrencyManager object by using the BindingContext object, which returns either CurrencyManager or PropertyManager, depending on the value of the data source and data members passed to the Item property of BindingContext. If the data source is an object that can only return a single property (instead of a list of objects), the type will be PropertyManager. For example, if you specify a TextBox control as the data source, PropertyManager will be returned. If the data source is an object that implements IList, IListSource, or IBindingList, such as a DataSet, DataTable, DataView, or an Array, CurrencyManager will be returned.
You can create a CurrencyManager from objects such as a DataView and vice versa. For example, the following code creates a CurrencyManager from a DataView and a DataView from a CurrencyManager:
Dim dv As DataView dv = New DataView(ds.Tables("Customers")) Dim curManager1 As CurrencyManager = DataGrid1.BindingContext(dv) Dim list As IList = curManager1.List Dim dv1 As DataView = CType(curManager1.List, DataView) Dim curManager2 As CurrencyManager = Me.BindingContext(ds1)
Unlike CurrencyManager, PropertyManager doesn't provide any additional members besides the members provided by its base class, BindingManagerBase.
Each object inherited from the Control class has a BindingContext object attached to it. BindingContext manages the collection of BindingManagerBase objects for that object such as a form. The BindingContext creates the CurrencyManager and PropertyManager objects, which were discussed previously. Normally you use the Form class's BindingContext to create a CurrencyManager and PropertyManager for a form and its controls, which provide data synchronization.
The Item property of BindingContext returns the BindingManagerBase (either CurrencyManager or PropertyManager). The Contains methods returns True if it contains the specified BindingManagerBase.
Besides the Item and Contains members, the BindingContext has three protected methods: Add, Clear, and Remove. The Add method adds a BindingManagerBase to the collection, the Clear method removes all items in the collection, and the Remove method deletes the BindingManagerBase associated with the specified data source.
Now let's see data binding in action. In this section, you'll develop an application that provides data synchronization. In this application, you'll build a record navigation system. The controls will display records, and then when you click the Move Next, Move Last, Move Previous, and Move First buttons, the controls will display the respective records.
To begin, create a Windows application and design a form that looks like Figure 7-2. For this example, you don't have to place the ReadBindingMemberInfo and Remove controls. Add a ComboBox control, two TextBox controls, a ListBox control, some Label controls, and some Button controls. The Load Data button loads data to the controls and attaches Binding objects to the BindingContext. You should also add four buttons with brackets as the text (<<, <, >, >>), which represents the Move First, Move Previous, Move Next, and Move Last records.
Figure 7-2: Record navigation form
You can create your own form, but to save you some time, you can download the code from the Apress (www.apress.com) or C# Corner (www.c-sharpcorner.com) Web sites. Open the project in Visual Studio .NET (VS .NET) to understand it better.
As usual, first you add some variables to the project, which shown in Listing 7-8. Don't forget to change your server name; the server name in this example is MCB.
Listing 7-8: Record Navigation System Variables
Private ConnectionString As String = "Integrated Security=SSPI;" & _ "Initial Catalog=Northwind;Data Source=MCB;" Private conn As SqlConnection = Nothing Private sql As String = Nothing Private adapter As SqlDataAdapter = Nothing Private ds As DataSet = Nothing
Second, you call the LoadData method on the Load Data button click:
Private Sub LoadBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles LoadBtn.Click LoadData() End Sub
Listing 7-9 shows the LoadData and GetDataSet methods. The GetDataSet method returns a DataSet object from the table name passed in the method. The LoadData method creates bindings for these controls with different DataTable columns.
Listing 7-9: LoadData and GetDataSet Methods
Private Sub LoadData() Dim ds As DataSet = New DataSet() ds = GetDataSet("Employees") Dim bind1 As Binding bind1 = New Binding("Text", ds, "Employees.FirstName") TextBox1.DataBindings.Add(bind1) TextBox2.DataBindings.Add _ (New Binding("Text", ds, "Employees.LastName")) ComboBox1.DataBindings.Add _ (New Binding("Text", ds, "Employees.EmployeeID")) Label4.DataBindings.Add(New Binding("Text", ds, "Employees.City")) Button1.DataBindings.Add(New Binding("Text", ds, "Employees.Country")) ListBox1.DataSource = ds.Tables(0).DefaultView ListBox1.DisplayMember = "Title" End Sub ' object based on various parameters. Public Function GetDataSet(ByVal tableName As String) As DataSet sql = "SELECT * FROM " + tableName ds = New DataSet(tableName) conn = New SqlConnection() conn.ConnectionString = ConnectionString adapter = New SqlDataAdapter(sql, conn) adapter.Fill(ds, tableName) Return ds End Function
The previously discussed steps will load the first row from the Employees table to the controls. Now, the next step is to write code for the move buttons. Listing 7-10 shows the code for all four buttons—Move First, Move Next, Move Previous, and Move Last. As you can see, this code uses the Position and Count properties of BindingManagerBase to set the position of the new record. Binding-Context and other Binding objects manage everything for you under the hood.
Listing 7-10: Move Next, Move Previous, Move First, and Move Last Button Code
Private Sub MoveFirstBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MoveFirstBtn.Click Me.BindingContext(Me.ds, "Employees").Position = 0 End Sub Private Sub MovePrevBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MovePrevBtn.Click Dim idx As Int32 = _ Me.BindingContext(Me.ds, "Employees").Position Me.BindingContext(Me.ds, "Employees").Position = idx - 1 End Sub Private Sub MoveNextBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MoveNextBtn.Click Dim idx As Int32 = _ Me.BindingContext(Me.ds, "Employees").Position Me.BindingContext(Me.ds, "Employees").Position = idx + 1 End Sub Private Sub MoveLastBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MoveLastBtn.Click Me.BindingContext(Me.ds, "Employees").Position = _ Me.BindingContext(Me.ds, "Employees").Count - 1 End Sub
When you run your application, the first record looks like Figure 7-3. Clicking the Move First, Move Next, Move Previous, and Move Last buttons will navigate you through the first, next, previous, and last records of the table (respectively).
Figure 7-3: Record navigation system in action
When I click the move buttons, I don't see the pointer in the ListBox moving. Why not?
The ListBox control doesn't use the same binding method as simple data-bound controls such as TextBox or Label controls. We discuss this in the following section.
You just saw how to implement a record navigation system using simple data binding and simple data-bound controls. In the following section, we show you how to build a record navigation system using complex data-bound controls such as ListBox and DataGrid controls.