
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf
804CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
<?xml version="1.0" standalone="yes"?> <Car_x0020_Inventory>
<Inventory CarID="0"> <Make>BMW</Make> <Color>Black</Color> <PetName>Hamlet</PetName>
</Inventory> <Inventory CarID="1">
<Make>Saab</Make>
<Color>Red</Color> <PetName>Sea Breeze</PetName>
</Inventory> </Car_x0020_Inventory>
■Source Code The SimpleDataSet application is included under the Chapter 22 subdirectory.
Binding DataTables to User Interfaces
Now that you have been exposed to the process of interacting with DataSets in the raw, let’s see a Windows Forms example. Your goal is to build a Form that displays the contents of a DataTable within a DataGridView widget. Figure 22-13 shows the initial UI design.
Figure 22-13. Binding a DataTable to a DataGridView
■Note As of .NET 2.0, the DataGridView widget is the preferred UI control used to bind relational data. Do be aware, however, that the legacy .NET 1.x DataGrid control is still available.
To begin, create a new Windows Forms application named CarDataTableViewer. Add a DataGridView widget (named carInventoryGridView) and descriptive Label to your designer. Next, insert a new C# class into your project (named Car), which is defined as follows:
public class Car
{
// Made public for ease of use.
public string carPetName, carMake, carColor;
public Car(string petName, string make, string color)
{
carPetName = petName;

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
805 |
carColor = color; carMake = make;
}
}
Now, within the Form’s default constructor, populate a List<> member variable with a set of new Car objects:
public partial class MainForm : System.Windows.Forms.Form
{
// Our list of Cars.
private List<Car> arTheCars = new List<Car>();
public MainForm()
{
InitializeComponent();
CenterToScreen();
// Fill the list with some cars. arTheCars.Add(new Car("Chucky", "BMW", "Green")); arTheCars.Add(new Car("Tiny", "Yugo", "White")); arTheCars.Add(new Car("", "Jeep", "Tan"));
arTheCars.Add(new Car("Pain Inducer", "Caravan", "Pink")); arTheCars.Add(new Car("Fred", "BMW", "Pea Soup Green")); arTheCars.Add(new Car("Buddha", "BMW", "Black")); arTheCars.Add(new Car("Mel", "Firebird", "Red")); arTheCars.Add(new Car("Sarah", "Colt", "Black"));
}
}
Like the previous SimpleDataSet example, the CarDataTableViewer application will construct a DataTable that contains four DataColumns to represent the columns of the Inventory table within the Cars database. As well, this DataTable will contain a set of DataRows to represent a list of automobiles. This time, however, you will fill the rows using your generic List<> member variable.
First, add a new member variable named inventoryTable of type DataTable to your Form. Next, add a new helper function to your Form class named CreateDataTable(), and call this method within the Form’s default constructor. The code required to add the DataColumns to the DataTable object is identical to that in the previous example, so I’ll omit it here (consult this book’s code download for complete details). Do note, though, that you are iterating over each member of the List<> to build your row set:
private void CreateDataTable()
{
// Create DataColumns and add to DataTable.
...
//Iterate over the array list to make rows. foreach(Car c in arTheCars)
{
DataRow newRow = inventoryTable.NewRow(); newRow["Make"] = c.carMake; newRow["Color"] = c.carColor; newRow["PetName"] = c.carPetName; inventoryTable.Rows.Add(newRow);
}
//Bind the DataTable to the carInventoryGridView. carInventoryGridView.DataSource = inventoryTable;
}

806 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Notice that the final line of code within the CreateDataTable() method assigns the inventoryTable to the DataSource property. This single property is all you need to set to bind a DataTable to
a DataGridView object. As you might guess, this GUI widget is reading the rows and column collections internally to establish the UI. At this point, you should be able to run your application and see the DataTable within the DataGridView control.
Programmatically Deleting Rows
Now, what if you wish to remove a row from a DataTable? One approach is to call the Delete() method of the DataRow object that represents the row to terminate. Simply specify the index (or DataRow object) representing the row to remove. Assume you update your GUI as shown in Figure 22-14.
Figure 22-14. Removing rows from the DataTable
The following logic behind the new Button’s Click event handler removes the specified row from your in-memory DataTable:
// Remove this row from the DataRowCollection.
private void btnRemoveRow_Click (object sender, EventArgs e)
{
try
{
inventoryTable.Rows[(int.Parse(txtRowToRemove.Text))].Delete(); inventoryTable.AcceptChanges();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The Delete() method might have been better named MarkedAsDeletable(), as the row is not literally removed until the DataTable.AcceptChanges() method is called. In effect, the Delete() method simply sets a flag that says, “I am ready to die when my table tells me to.” Also understand that if a row has been marked for deletion, a DataTable may reject the delete operation via
RejectChanges(), as shown here:
// Mark a row as deleted, but reject the changes.
private void btnRemoveRow_Click (object sender, EventArgs e)
{

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
807 |
inventoryTable.Rows[(int.Parse(txtRemove.Text))].Delete();
// Do more work
... |
|
inventoryTable.RejectChanges(); |
// Restore previous RowState value. |
}
Applying Filters and Sort Orders
You may wish to see a small subset of a DataTable’s data, as specified by some sort of filtering criteria. For example, what if you wish to see only a certain make of automobile from the in-memory Inventory table? The Select() method of the DataTable class provides this very functionality. Update your GUI once again, this time allowing users to specify a string that represents the make of the automobile they are interested in viewing (see Figure 22-15). The result will be placed into a Windows Forms message box.
Figure 22-15. Specifying a filter
The Select() method has been overloaded a number of times to provide different selection semantics. At its most basic level, the parameter sent to Select() is a string that contains some conditional operation. To begin, observe the following logic for the Click event handler of your new button:
private void btnGetMakes_Click (object sender, EventArgs e)
{
// Build a filter based on user input.
string filterStr = string.Format("Make= '{0}' ", txtMakeToGet.Text);
// Find all rows matching the filter.
DataRow[] makes = inventoryTable.Select(filterStr);
// Show what we got! if(makes.Length == 0)
MessageBox.Show("Sorry, no cars...", "Selection error!");
else
{
string strMake = null;
for(int i = 0; i < makes.Length; i++)

808 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
{
DataRow temp = makes[i];
strMake += temp["PetName"] + "\n";
}
MessageBox.Show(strMake, txtMakeToGet.Text + " type(s):");
}
}
Here, you first build a simple filter based on the value in the associated TextBox. If you specify BMW, your filter is Make = 'BMW'. When you send this filter to the Select() method, you get back an array of DataRow types that represent each row that matches the filter (see Figure 22-16).
Figure 22-16. Displaying filtered data
As you can see, filtering logic is standard SQL syntax. To prove the point, assume you wish to obtain the results of the previous Select() invocation alphabetically based on pet name. In terms of SQL, this translates into a sort based on the PetName column. Luckily, the Select() method has been overloaded to send in a sort criterion, as shown here:
// Sort by PetName.
makes = inventoryTable.Select(filterStr, "PetName");
If you want the results in descending order, call Select(), as shown here:
// Return results in descending order.
makes = inventoryTable.Select(filterStr, "PetName DESC");
In general, the sort string contains the column name followed by “ASC” (ascending, which is the default) or “DESC” (descending). If need be, multiple columns can be separated by commas. Finally, understand that a filter string can be composed of any number of relational operators. For example, what if you want to find all cars with an ID greater than 5? Here is a helper function that does this very thing:
private void ShowCarsWithIdLessThanFive()
{
// Now show the pet names of all cars with ID greater than 5.
DataRow[] properIDs;
string newFilterStr = "ID > 5";
properIDs = inventoryTable.Select(newFilterStr); string strIDs = null;
for(int i = 0; i < properIDs.Length; i++)
{
DataRow temp = properIDs[i]; strIDs += temp["PetName"]
+ " is ID " + temp["ID"] + "\n";
}
MessageBox.Show(strIDs, "Pet names of cars where ID > 5");
}

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
809 |
Updating Rows
The final aspect of the DataTable you should be aware of is the process of updating an existing row with new values. One approach is to first obtain the row(s) that match a given filter criterion using the Select() method. Once you have the DataRow(s) in question, modify them accordingly. For example, assume you have a new Button that (when clicked) searches the DataTable for all rows where Make is equal to BMW. Once you identify these items, you change the Make from BMW to Colt:
// Find the rows you want to edit with a filter.
private void btnChangeBeemersToColts_Click(object sender, EventArgs e)
{
// Make sure user has not lost his mind. if (DialogResult.Yes ==
MessageBox.Show("Are you sure?? BMWs are much nicer than Colts!", "Please Confirm!", MessageBoxButtons.YesNo))
{
// Build a filter.
string filterStr = "Make='BMW'"; string strMake = null;
// Find all rows matching the filter.
DataRow[] makes = inventoryTable.Select(filterStr);
// Change all Beemers to Colts!
for (int i = 0; i < makes.Length; i++)
{
DataRow temp = makes[i];
strMake += temp["Make"] = "Colt"; makes[i] = temp;
}
}
}
The DataRow class also provides the BeginEdit(), EndEdit(), and CancelEdit() methods, which allow you to edit the content of a row while temporarily suspending any associated validation rules. In the previous logic, each row was validated with each assignment. (Also, if you capture any events from the DataRow, they fire with each modification.) When you call BeginEdit() on a given DataRow, the row is placed in edit mode. At this point you can make your changes as necessary and call either EndEdit() to commit these changes or CancelEdit() to roll back the changes to the original version, for example:
private void UpdateSomeRow()
{
//Assume you have obtained a row to edit.
//Now place this row in edit mode. rowToUpdate.BeginEdit();
//Send the row to a helper function, which returns a Boolean. if( ChangeValuesForThisRow( rowToUpdate) )
rowToUpdate.EndEdit(); |
// OK! |
else
rowToUpdate.CancelEdit(); // Forget it.
}
Although you are free to manually call these methods on a given DataRow, these members are automatically called when you edit a DataGridView widget that has been bound to a DataTable. For example, when you select a row to edit from a DataGridView, that row is automatically placed in edit mode. When you shift focus to a new row, EndEdit() is called automatically.

810 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Working with the DataView Type
In database nomenclature, a view object is a stylized representation of a table (or set of tables). For example, using Microsoft SQL Server, you could create a view for your current Inventory table that returns a new table containing automobiles only of a given color. In ADO.NET, the DataView type allows you to programmatically extract a subset of data from the DataTable into a stand-alone object.
One great advantage of holding multiple views of the same table is that you can bind these views to various GUI widgets (such as the DataGridView). For example, one DataGridView might be bound to a DataView showing all autos in the Inventory, while another might be configured to display only green automobiles.
To illustrate, update the current UI with an additional DataGridView type named dataGridColtsView and a descriptive Label. Next, define a member variable named coltsOnlyView of type DataView:
public partial class MainForm : Form
{
// View of the DataTable. |
|
DataView coltsOnlyView; |
// I only show red colts. |
...
}
Now, create a new helper function named CreateDataView(), and call this method within the Form’s default constructor directly after the DataTable has been fully constructed, as shown here:
public MainForm()
{
...
//Make a data table.
CreateDataTable();
//Make Views.
CreateDataView();
}
Here is the implementation of this new helper function. Notice that the constructor of each DataView has been passed the DataTable that will be used to build the custom set of data rows.
private void CreateDataView()
{
//Set the table that is used to construct this view. coltsOnlyView = new DataView(inventoryTable);
//Now configure the views using a filter. coltsOnlyView.RowFilter = "Make = 'Colt'";
//Bind to grid.
dataGridColtsView.DataSource = coltsOnlyView;
}
As you can see, the DataView class supports a property named RowFilter, which contains the string representing the filtering criteria used to extract matching rows. Once you have your view established, set the grid’s DataSource property accordingly. Figure 22-17 shows the completed application in action.

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
811 |
Figure 22-17. Displaying filtered data
■Source Code The CarDataTableViewer project is included under the Chapter 22 subdirectory.
Working with Data Adapters
Now that you understand the ins and outs of manipulating ADO.NET DataSets, let’s turn our attention to the topic of data adapters. Recall that data adapter objects are used to fill a DataSet with DataTable objects and send modified DataTables back to the database for processing. Table 22-15 documents the core members of the DbDataAdapter base class.
Table 22-15. Core Members of the DbDataAdapter Class
Members |
Meaning in Life |
SelectCommand |
Establish SQL commands that will be issued to the data store when the Fill() |
InsertCommand |
and Update() methods are called. |
UpdateCommand |
|
DeleteCommand |
|
Fill() |
Fills a given table in the DataSet with some number of records based on the |
|
command object–specified SelectCommand. |
Update() |
Updates a DataTable using command objects within the InsertCommand, |
|
UpdateCommand, or DeleteCommand property. The exact command that is |
|
executed is based on the RowState value for a given DataRow in a given |
|
DataTable (of a given DataSet). |
|
|


CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
813 |
Mapping Database Names to Friendly Names
As you most certainly know, database administrators (DBAs) tend to create table and column names that can be less than friendly to end users. The good news is that data adapter objects maintain an internal strongly named collection (DataTableMappingCollection) of System.Data.Common. DataTableMapping types, accessed via the TableMappings property.
If you so choose, you may manipulate this collection to inform a DataTable about which “display names” it should use when asked to print its contents. For example, assume that you wish to map the DBMS table name “Inventory” to “Current Inventory” for display purposes. Furthermore, say you wish to display the CarID column name as “Car ID” (note the extra space) and the PetName column name as “Name of Car.” To do so, add the following code before calling the Fill() method of your data adapter object (and be sure to “use” the System.Data.Common namespace):
static void Main(string[] args)
{
...
// Now map DB column names to user-friendly names.
DataTableMapping custMap = dAdapt.TableMappings.Add("Inventory", "Current Inventory");
custMap.ColumnMappings.Add("CarID", "Car ID"); custMap.ColumnMappings.Add("PetName", "Name of Car"); dAdapt.Fill(myDS, "Inventory");
...
}
If you were to run this program once again, you would find that the PrintDataSet() method now displays the “friendly names” of the DataTable and DataRow objects, rather than the names established by the database schema.
■Source Code The FillDataSetWithSqlDataAdapter project is included under the Chapter 22 subdirectory.
Updating a Database Using Data Adapter Objects
Not only do data adapters fill the tables of a DataSet on your behalf, but they are also in charge of maintaining a set of core SQL command objects used to push updates back to the data store. When you call the Update() method of a given data adapter, it will examine the RowState property for each row in the DataTable and use the correct SQL commands assigned to the DeleteCommand, InsertCommand, and UpdateCommand properties to push the changes within a given DataTable back to the data source.
To illustrate the process of using a data adapter to push back modifications in a DataTable, the next example will re-engineer the CarsInvertoryUpdater example developed earlier in the chapter to now make use of DataSet and data adapter objects. Given that you have already created a bulk of the application, let’s focus on the changes to the DeleteCar(), UpdateCarPetName(), and InsertNewCar() methods (check out the downloadable code for full details).
The first basic adjustment to make to the application is to define two new static member variables of the Program class to represent your DataSet and connection object. As well, the Main() method will be modified to fill the DataSet with the initial data upon startup:
class Program
{
// The applicationwide DataSet.
public static DataSet dsCarInventory = new DataSet("CarsDatabase");