| |
Upon first appearance, data-binding on Windows Forms looks simple, which it can be, but first impressions can be deceiving. It is easy to get started, but there is more in depth material.
Contents
What is Data-Binding? Simply put, it is the ability to link data to objects. In practice, it is usually used to display and manage data on a form from a database in an easy way. You could have a table with ten fields shown on a form and the Data-Bindings would automatically manage all the changes and updates made to the fields.
Data-Binding and ADO.NET are two mutually exclusive technologies. Most frequently, Data-Binding is performed using ADO.NET. This tutorial's objective is to provide background, examples, and tricks of both technologies.
A huge amount of code is managed by Visual Studio .NET's (VS.NET) Interactive Development Environment (IDE), easily 1,000s of lines of code. Many of the examples presented use source code from the CoadTools' Data Navigation Control, which includes a Contacts database and a simple data-bindings form.
A basic understanding of databases is expected. This tutorial starts with a primer on ADO.NET and shows some of the improvements over classic Active Data Objects (ADO). Microsoft Access database connections will be used. Adapting to an SQL Server is trivial and is occasionally mentioned.
The ADO.NET data structure is in the System.Data namespace.
using System.Data;
using System.Data.OleDb;
using System.Data.SqlClient;
|
First off the bat, you will need a basic background in Microsoft's new and exciting, entirely .NET data system. ADO.NET is a total rewrite of Active Data Objects (ADO) that supports many new features including data separation (mobility), strong type naming, XML schemas, and many new properties, methods, and events.
| Strongly vs. Loosely Typed | One of the major features of ADO.NET is the ability to strongly type a DataSet. This is discussed first because it is a basic Object Oriented concept. An example: Say I have a class that is to store a contact's FirstName and LastName. Loosely typed would specify the two properties as objects. Whereas strongly typed would specify them as strings. Obviously, if you know they are strings, it would be better to specify this. Since databases store a wide range of data types, classic ADO defined all fields as simply 'objects', not the specified data types that were used to set up the database. One of the main features of ADO.NET is that you can strongly type the kind of data being used.
Previously data had to be accessed like this with ADO:
int age = int.Parse(myRecordet.Fields["Age"].ToString());
|
The ADO.NET way is simply:
int age = myDataset.Contacts.Age;
|
The DataSet represents an entire database. It contains a collection of DataTables and relations. The DataSet is an entirely self contained unit. It does not require a constant connection to a database. You can use the VS.NET IDE to easily create a strongly typed DataSet by simply right clicking on your project, choosing 'Add/Add New Item...', then selecting 'DataSet'. Two other (preferred) methods will be described below in Putting it Together.
The DataTable object is representative of a table in a database. It primarily contains a collection of DataRows (records/entries).
The DataRow is a single entry or record in a DataTable. When you make typical changes to the data, the changes get saved in the respective DataRow.
Most of the time you will want to populate your DataSet from a database, and after changes have been made, save the changes back to the database. For this you will need one DataConnection object per physical database and one DataAdapter for each table.
There are two types of data connections: oleDbConnection for OLE Databases (such as Access) and sqlConnection for an SQL Server. Usually only one property is used with the oleDbConnection, a ConnectionString property, which contains a standardized connection string with options and physical database file location.
For each table from your database, you will need a Data Adapter, again in flavors of oleDbDataAdapter and sqlDataAdapter. The Data Adapter maintains the SQL commands to SELECT, INSERT, UPDATE, and DELETE data to/from the database. It has two main methods, Fill & Update, which fill a DataSet from the database and save changes made to a DataSet back to the original database respectfully.
There are two ways to quickly set up a DataSet, Data Connections, Data Adapters, and associated classes.
| First: |
You should create a connection to your database under the 'Server Explorer' (Ctrl-Shift-S).
Simply right click on 'Data Connections', choose 'Add Connection...', select the interface engine from the 'Provider' list (use 'Microsoft Jet 4.x OLE DB Provider' for Access databases), then select the database file location under 'Connection'. |
Then there are two options for creating the ADO.NET objects:
-
Use the 'Data Form Wizard'
Right click on your project, select 'Add New Item...', choose 'Data Form Wizard', and it will take you through all the steps. It will also allow you to add a new connection if you skip the first step above.
-
Drag & Drop
Under 'Data Connections', select a table from your database and drag it onto your form. This will create an oleDbConnection and oleDbDataAdapter for you. Right click on the oleDbDataAdapter and choose 'Generate Dataset...' which will create your project's strongly typed DataSet.
| The IDE Works Hard for You! | There are the basic DataSet, DataRow, and DataTable classes, but when you strongly type a dataset (such as what happens when using the IDE), an XML scheme is created that details specifically what type of data is getting stored in a DataSet. You are essentially re-specifying your data table structures, as originally specified in the database.
The IDE then creates an underlying class file that extends from DataSet, DataRow, and DataTable but with all your specifically named and typed tables and fields (strongly typed). This underlying DataSet extended class, with just one simple table, is easily over 600 lines of code and a mid-sized database can be over 10,000 lines of code, all automatically managed by the IDE.
In the next topic's small relationship example, the IDE manages over 1,300 lines of dataset code for just three tables.
One of the fundamental ideas in today's data systems are relational databases. This simply means that data in one table relates to data in another table. This greatly reduces redundancy.
For example, say I have a table of Projects and a table keeping track of my time worked on these projects called TimeLog. Each project is specified once in the Projects table with fields such as Name, DateStarted, Notes, etc. Each entry in the TimeLog table refers to a specific project. Believe it or not, some old databases would actually store the entire project Name, DateStarted, etc with every TimeLog entry. With relationships, only a small number referring to a specific project is stored with each TimeLog entry.
Microsoft Access has a drag & drop relationship manager under the 'Tools'/'Relationships...' menu. ADO.NET is almost identical. When viewing your DataTables in a DataSet, simply drag & drop the field from one table to the next to create a relationship. Be careful that the relationship goes in the correct direction when using VS.NET. See figure 1 for how this would appear with three tables and two relationships.
 Figure 1: Dataset Relationships
When you populate your dataset, you have to fill the tables in the correct order to maintain referential integrity. In figure 1's example the order would be Clients, Projects, then TimeLog, since each table depends on the previous.
Related data is amazingly easy to work with in a strongly typed dataset. These two examples, one of classic ADO and one of ADO.NET, show the code for when I want to know the client's name of a particular TimeLog entry.
First a function with classic ADO. Notice that you cannot pass an entire row from a table using ADO and there are serval SQL statements involved.
private string ClientName(int TimeLogID)
{
ADODB.RecordsetClass rs = new ADODB.RecordsetClass();
rs.ActiveConnection = adoConnection;
rs.Source = "SELECT * FROM TimeLog WHERE TimeLogID=" + TimeLogID;
int ProjectID = int.Parse(rs.Fields["ProjectID"].ToString());
rs.Source = "SELECT * FROM Projects WHERE ProjectID=" + ProjectID;
int ClientID = int.Parse(rs.Fields["ClientID"].ToString());
rs.Source = "SELECT * FROM Clients WHERE ClientID=" + ClientID;
return rs.Fields["Name"].ToString();
}
|
Another example with a single (complex) SQL statement in ADO.
private string ClientName(int TimeLogID)
{
ADODB.RecordsetClass rs = new ADODB.RecordsetClass();
rs.ActiveConnection = adoConnection;
string sql = "SELECT Clients.Name FROM Clients INNER JOIN (Projects " +
"INNER JOIN TimeLog ON Projects.ProjectID = TimeLog.ProjectID) ON " +
"Clients.ClientID = Projects.ClientID WHERE TimeLog.TimeLogID=" +
TimeLogID;
rs.Source = sql;
return rs.Fields["Name"].ToString();
}
|
And finally, what you have all been waiting for, the beauty of strong typing in ADO.NET. Notice how a TimeLogRow, extended from DataRow as part of the managed strong-typing DataSet, is passed into the function.
private string ClientName(myDataSet.TimeLogRow tlr)
{
return tlr.ProjectsRow.ClientsRow.Name;
}
|
Most of what is covered in this section is available in a simple source code example in the CoadTools' DataNavigator system. This is available here on DevHood at: DataNavigator Custom Control
If you already have your database set up, you can quickly create a data-bound form by using the 'Data Form Wizard'. Just right click on your project, select 'Add', 'Add New Item...', then 'Data Form Wizard'. This will create all your basic bindings and code. Read the sections below if you want to know how it works and how to do it manually.
You can either use Microsoft Access, SQL Server, or some other OLE database. If you do not have any, you can obtain Microsoft's SQL Server 2000 Desktop Edition (MSDE) for free. Download it from Microsoft's and find out more about it on .
If you have SQL Server installed, you can create a database through the 'Server Explorer' in VS.NET. If you have Microsoft Access, you must create one in access. Let us start by making a database called Contacts with one table also named Contacts with the fields shown in figure 2.
 Figure 2: Simple Contacts Database
Now create a connection to the database under 'Server Explorer'. If it is an Access database, right click on 'Data Connections', then 'Add Connection...', select 'Microsoft Jet 4.0 OLE DB Provider' from the 'Provider' tab, then enter the file name on the 'Connection' tab. Having your connection listed in the 'Server Explorer' will make adding the code modules easier.
| Add Data Components to a Windows Form | Next we need to add the data control to a form. Create a new 'Windows Application' project. Then follow these steps. See figure 3 for an example of what these steps create.
 Figure 3: Simple Data Bound Form
-
Add Data Connection & Data Adapters
In the 'Server Explorer', open your database and drag the Contacts table onto your form. This will create the needed data connection and data adapter.
-
Create Your DataSet
Right click on the data adapter and select 'Generate Dataset...'. Click 'OK'. This will create a strongly typed data set for your Contacts table.
-
Add TextBoxes
Now add some TextBox(s) to the form. In the 'Properties' window under '(DataBindings)', then 'Text', add 'DataSet1 - Contacts - FirstName'. Repeat this for the other text boxes but with the remaining fields.
-
Add Buttons
Add eight buttons to the form: Fill, Update, Next, Back, First, Last, Delete, and Add New. Use the code shown below for their click events.
Note, I have renamed the data connection to 'dbConn', the data adapter to 'dbaContacts', and the DataSet to 'dsMain' for ease of reading and better programming practice. The '(DataBindings)' of each of the TextBox(s) must also be updated.
Use this code for the buttons on the form:
private void btnFill_Click(object sender, System.EventArgs e)
{
this.dbaContacts.Fill(dsMain);
}
private void btnUpdate_Click(object sender, System.EventArgs e)
{
BindingContext[dsMain, "Contacts"].EndCurrentEdit();
this.dbaContacts.Update(dsMain.Contacts);
}
private void btnNext_Click(object sender, System.EventArgs e)
{
this.BindingContext[dsMain, "Contacts"].Position++;
}
private void btnBack_Click(object sender, System.EventArgs e)
{
this.BindingContext[dsMain, "Contacts"].Position--;
}
private void btnFirst_Click(object sender, System.EventArgs e)
{
this.BindingContext[dsMain, "Contacts"].Position = 0;
}
private void btnLast_Click(object sender, System.EventArgs e)
{
this.BindingContext[dsMain, "Contacts"].Position =
this.BindingContext[dsMain, "Contacts"].Count - 1;
}
private void btnDelete_Click(object sender, System.EventArgs e)
{
this.BindingContext[dsMain, "Contacts"].RemoveAt(
this.BindingContext[dsMain, "Contacts"].Position);
}
private void btnAddNew_Click(object sender, System.EventArgs e)
{
this.BindingContext[dsMain, "Contacts"].AddNew();
}
|
You have officially created a data-bound form! The data in the text boxes will automatically update when you move through the table and the changes made to the text in the boxes will automatically be saved to the Contacts table in the dataset. Note that you must 'Update' using the data adapter in order for the changes to be saved to the database.
Presented in this section are two more advanced types of bound controls, the DataGrid and the Combo Box.
The System.Windows.Forms assembly contains a powerful data control, the DataGrid. The DataGrid (figure 4) shows a table of data practically just like it looks in the 'Server Explorer' or when viewing the table in Access.
 Figure 4: Simple DataGrid Example
Simply drop a DataGrid from the 'Toolbox' onto your form, and set its DataSource property to the DataSet on the form, and the DataMember property to the table in that DataSet (as in figure 5).
 Figure 5: DataGrid Properties
Parent/Child DataGrids One special use of the DataGrid is to show data relative to another parent DataGrid. For example, say you have both a Contacts and BorrowedItems table. The BorrowedItems table is referencing the Contacts table (similar to what was shown in the Database Relationships topic). Then you would have a parent DataGrid showing the Contacts table and a child DataGrid that should show the BorrowedItems pertaining to the currently selected contact in the first DataGrid. All you have to do is set the DataMember property of the child control to the relationship (ex: 'Contacts.ContactsBorrowedItems').
For more details and an article on how to customize the grid control, see Clay Burch's article on MSDN titled .
Say you add a new table to our Contacts database called Countries and add a field in the Contacts table to reference a row from Countries. Then on your simple data-bindings form you want a drop-down list of countries to select from for each contact. This type of control uses complex-binding.
 Figure 6: Combo Box Complex Binding Properties
Using the example stated above and in figure 6, here is a description of the complex binding properties:
- SelectedValue
Refers to the field in the parent (Contacts) table that holds the value.
- Text
Stores what field contains the text to show (instead of a value/number).
- DataSource
The DataSet on the form.
- DisplayMember
The field to obtain the values in the drop down list.
The same properties can also be applied to the ListBox control, another complex binding control.
|
Topics on the Inner Workings
|
|
Here are some important issues when dealing with ADO.NET and data-bindings. Figure 7 shows an overview of the data-binding classes which are discussed below.

| Figure 7: |
Windows Form Data-Bindings Classes ADO.NET Step by Step, Microsoft Press |
Windows Form controls can be bound to any data source, not just the traditional database tables. Specifically, objects that implement the IList, IBindingListor, and IEditableObject interface. The IList is the easiest of the three, implemented by arrays and collections.
Also, any public property of a control can be bound to a data source. For a complete list of bindings available on a control, look under the '(DataBindings)'/'(Advanced)' properties of the control. The assigned property will be updated just as a common property like the Text field of a TextBox.
| Bindings: Binding, BindingContext, BindingManagerBase, & CurrencyManager Classes | The Binding class represents a single data bind between a data source and a control's property. The BindingContext object is the top-most object in the binding hierarchy and manages the BindingManagerBase objects that control the interaction between a data source and the controls bound to it. It only has one property, Item, that exposes its BindingManagerBase collection. The BindingManagerBase simply keeps track of the Binding objects associated with a DataTable on the form. The CurrencyManager, which inherits from the BindingManagerBase, tracks state information and creates a cursor for the current record being shown.
Previously with ADO, the data cursor (current position in the table) was stored in the Recordset object. This only allowed for one cursor position per record set. With ADO.NET, a CurrencyManager contains this information allowing multiple cursors (one with each CurrencyManager) from a single record set. For example, you can have one DataTable with its row data being shown at different positions on three separate forms through three separate CurrencyManagers.
A unique BindingManagerBase/CurrencyManager is created for each DataTable being used on a form. The BindingContext property of a form is used to gain access to the BindingManagerBase and subsequent CurrencyManager as in the next example.
Properties The three most important properties, Count, Position, and Current, of the CurrencyManager will be demonstrated in this sample code. Note that the Current property returns a DataRowView which is then cast to the strong-typed row from which it came.
BindingManagerBase bmb = this.BindingContext[dsMain, "Contacts"];
Console.WriteLine("Number of Records: " + bmb.Count);
Console.WriteLine("Current Position: " + bmb.Position);
DataRowView drv = (DataRowView) bmb.Current;
dsContacts.ContactsRow row = (dsContacts.ContactsRow) drv.Row;
Console.WriteLine("Current Contact's Name: " +
row.FirstName + " " + row.LastName);
|
Methods The important methods of a BindingManagerBase are AddNew, CancelCurrentEdit, EndCurrentEdit, and RemoveAt. Pay special attention to the RowState when a new record is added. The new record is not actually added to the DataTable until either the position changes (invoking EndCurrentEdit) or EndCurrentEdit is manually called.
private void AddNewRecord()
{
BindingManagerBase bmb = this.BindingContext[dsMain, "Contacts"];
DialogResult result =
MessageBox.Show("Keep changes made to the current record?",
"", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes) bmb.EndCurrentEdit();
else bmb.CancelCurrentEdit();
bmb.AddNew();
dsContacts.ContactsRow row =
(dsContacts.ContactsRow) ((DataRowView) bmb.Current).Row;
Console.WriteLine(row.RowState);
bmb.EndCurrentEdit();
Console.WriteLine(row.RowState);
}
|
Events The two important events in the BindingManagerBase are PositionChanged, which lets you know when the cursor is moved, and CurrentChanged that raises when the current record is being edited.
Each DataRow in a DataTable contains state information in a RowState property. This is how the system keeps track of the changes made so that they can be accepted or rejected and saved back to a database. Here are descriptions of the DataRowState enumerations.
- DataRowState.Added
This row is newly added to the dataset. It did not previously exist. Correlates to an INSERT SQL statement in the Data Adapter.
- DataRowState.Deleted
The row has been deleted, but not yet actually removed from the dataset. Correlates to a DELETE SQL statement.
- DataRowState.Modified
Changes have been made to the row since the last AcceptChanges method was called. Correlates to an UPDATE SQL statement.
- DataRowState.Unchanged
The row's fields/properties have not changed since the dataset was populated or since the last AcceptChanges/RejectChanges method was called.
- DataRowState.Detached
This row does not exist in any DataTable. It is entirely on its own, not yet having been added to a DataTable.
| Bindings: Format & Parse Events | Frequently data should be displayed differently than it is stored in the database. A currency would be stored as a 'decimal' but should be displayed with a dollar sign, commas, and the decimal placed fixed at two digits. For this, two events of the Binding class are exposed, Format & Parse.
Format is raised both when data is pulled from the data source and before it is shown, and also after the Parse event. The Parse event is called if data has changed in a control and before it is written to the data source (to remove formatting). You can see the order of event calling in figure 8.

| Figure 8: |
Order of Format & Parse Events Classes ADO.NET Step by Step, Microsoft Press |
The type of event that is called for both Format & Parse is ConvertEventHandler, which has its own special event arguments, ConvertEventArgs. ConvertEventArgs exposes two properties, DesiredType and Value. Value contains the current data and should be set to the new data format. DesiredType contains the type of data wanted.
If the type of data stored in the database is different than the control's value, determining the direction of events is easy, as shown in the following example. If the data source type is the same as the control's, you will need to use a class variable (such as the Tag property of a TextBox) to keep track if Parse has been called to determine the direction of events.
private void InitTextBox()
{
txtPrice.DataBindings[0].Format += new ConvertEventHandler(txtPrice_Format);
txtPrice.DataBindings[0].Parse += new ConvertEventHandler(txtPrice_Parse);
}
private void txtPrice_Format(object sender, ConvertEventArgs e)
{
if (e.Value is decimal)
e.Value = ((decimal) e.Value).ToString("c");
}
private void txtPrice_Parse(object sender, ConvertEventArgs e)
{
if (e.DesiredType == typeof(decimal))
{
string price = e.Value.ToString();
price = price.Replace("$", "").Replace(",", "");
e.Value = decimal.Parse(price);
}
}
|
| ADO.NET: Accepting and Rejecting DataSet Changes | The final step in updating data is setting a new baseline for the DataRows. Each DataRow contains versions of the data to allow validation and a RowState indicating the type of operation that has been performed since the baseline was set.
Calling AcceptChanges or RejectChanges on a DataSet recursively calls the method on its tables. Each table recursively calls the method on its rows. AcceptChanges resets row's RowState to Unchanged and saves all changes (RowStates of Modified, Changed, or Added, and removes Deleted) to be the new baseline. RejectChanges sets the row's RowState to Unchanged and discards all modifications, reloading the previous baseline.
Data Adapters use the RowState to know which rows need updating in the original database. The DataAdapter's Update method automatically calls AcceptChanges on all associated rows. If you manually call AcceptChanges, the data will not be saved to a database when using Update.
| ADO.NET: Filtering and Sorting Data | For selecting a subset of data from a table (filtering & sorting), there are two techniques available. The simplest, calling the Select method of a DataTable, returns an array of DataRows whereas the DataView system is a separate object structure.
Select Method The Select method of a DataTable is a very easy and quick way to filter and sort data. It returns a DataRow array. Each row in the array can be cast to the specific type of row from your DataTable. Here is an example:
private void ShowByState_Select(string state)
{
DataRow[] results = this.dsMain.
Contacts.Select("State='" + state + "'", "LastName");
foreach (dsContacts.ContactsRow row in results)
Console.WriteLine(row.LastName);
}
|
DataViews DataViews provide a seperate object for filtering and sorting data. The DataViewManager stores DataViews, a DataView stores DataRowViews much like a DataSet holds DataTables and a DataTable holds DataRows. Since DataViews are independent objects, you can create and configure them at design time in VS.NET. You can also bind controls to DataViews which is useful for storing default sorting and filtering criteria.
Note that DataViews are not strongly typed. You can obtain the DataRowView.Row object that returns the original row from the original DataTable and cast this, or just access the field by an indexed name as in the following example.
private void ShowByState_DataView(string state)
{
DataView dv = new DataView(dsMain.Contacts,
"State='" + state + "'", "LastName", DataViewRowState.CurrentRows);
foreach (DataRowView drv in dv)
Console.WriteLine(drv["LastName"].ToString());
}
|
Book Reference Riordan, Rebecca, , Microsoft Press, 2002
Source Code Example For a simple data-bindings on a Windows form example, see Noah's CoadTools' DataNavigator Custom Control.
MSDN Network Online
MSDN Local Library Links NOTE: The MSDN Library links must be copied and manually pasted into IE to order to load.
ADO.NET is not just an upgrade from ADO, but an entirely new data system. It is the result of years of evaluating real-world application of information technologies. Data-bindings make light of creating data access forms and the IDE manages much of the programming for you.
-=[ Know-a-Code ]=-
|
|