
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf
814 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
// The applicationwide connection object. public static SqlConnection cnObj = new
SqlConnection("uid=sa;pwd=;Initial Catalog=Cars;Data Source=(local)");
static void Main(string[] args)
{
...
//Create the adapter and fill DataSet.
SqlDataAdapter dAdapter =
new SqlDataAdapter("Select * From Inventory", cnObj); dAdapter.Fill(dsCarInventory, "Inventory"); ShowInstructions();
//Logic to get user command...
}
...
}
Also note in the code that follows that the ListInventory(), DeleteCar(), UpdateCarPetName(), and InsertNewCar() methods have all been updated to take a SqlDataAdapter as the sole parameter.
Setting the InsertCommand Property
When you are using a data adapter to update a DataSet, the first order of business is to assign the UpdateCommand, DeleteCommand, and InsertCommand properties with valid command objects (until you do so, these properties return null!). By “valid” command objects, I am referring to the fact that the set of command objects you plug into a data adapter will change based on the table you are attempting to update. In this example, the table in question is Inventory. Here is the modified
InsertNewCar() method:
private static void InsertNewCar(SqlDataAdapter dAdpater)
{
// Gather info about new car.
...
//Format SQL Insert and plug into DataAdapter. string sql = string.Format("Insert Into Inventory" +
"(CarID, Make, Color, PetName) Values" + "('{0}', '{1}', '{2}', '{3}')",
newCarID, newCarMake, newCarColor, newCarPetName); dAdpater.InsertCommand = new SqlCommand(sql); dAdpater.InsertCommand.Connection = cnObj;
//Update Inventory Table with new row.
DataRow newCar = dsCarInventory.Tables["Inventory"].NewRow(); newCar["CarID"] = newCarID;
newCar["Make"] = newCarMake; newCar["Color"] = newCarColor; newCar["PetName"] = newCarPetName;
dsCarInventory.Tables["Inventory"].Rows.Add(newCar);
dAdpater.Update(dsCarInventory.Tables["Inventory"]);
}
Once you have created your command object, you plug it into the adapter via the InsertCommand property. Next, you add a new row to the Inventory DataTable maintained by the dsCarInventory object. Once you have added this DataRow back into the DataTable, the adapter will execute the SQL found within the InsertCommand property, given that the RowState of this new row is DataRowState.Added.


816 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Autogenerating SQL Commands Using Command-
Builder Types
You might agree that working with data adapters can entail a fair amount of code, given the need to build each of the four command objects and the associated connection string (or DbConnection-derived object). To help simplify matters, each of the ADO.NET data providers that ships with .NET 2.0 provides a command builder type. Using this type, you are able to automatically obtain command objects that contain the correct Insert, Delete, and Update command types based on the initial Select statement.
The SqlCommandBuilder automatically generates the values contained within the SqlDataAdapter’s
InsertCommand, UpdateCommand, and DeleteCommand properties based on the initial SelectCommand. Clearly, the benefit is that you have no need to build all the SqlCommand and SqlParameter types by hand.
An obvious question at this point is how a command builder is able to build these SQL command objects on the fly. The short answer is metadata. At runtime, when you call the Update() method of a data adapter, the related command builder will read the database’s schema data to autogenerate the underlying insert, delete, and update command objects.
Consider the following example, which deletes a row in a DataSet using the autogenerated SQL statements. Furthermore, this application will print out the underlying command text of each command object:
static void Main(string[] args)
{
DataSet theCarsInventory = new DataSet();
//Make connection.
SqlConnection cn = new
SqlConnection("server=(local);User ID=sa;Pwd=;database=Cars");
//Autogenerate Insert, Update, and Delete commands
//based on existing Select command.
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Inventory", cn);
SqlCommandBuilder invBuilder = new SqlCommandBuilder(da);
//Fill data set. da.Fill(theCarsInventory, "Inventory"); PrintDataSet(theCarsInventory);
//Delete row based on user input and update database.
try
{
Console.Write("Row # to delete: ");
int rowToDelete = int.Parse(Console.ReadLine()); theCarsInventory.Tables["Inventory"].Rows[rowToDelete].Delete(); da.Update(theCarsInventory, "Inventory");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
//Refill and reprint Inventory table.
theCarsInventory = new DataSet(); da.Fill(theCarsInventory, "Inventory"); PrintDataSet(theCarsInventory);
}

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
817 |
In the previous code, notice that you made no use of the command builder object (SqlCommandBuilder in this case) beyond passing in the data adapter object as a constructor parameter. As odd as this may seem, this is all you are required to do (at a minimum). Under the hood, this type will configure the data adapter with the remaining command objects.
Now, while you may love the idea of getting something for nothing, do understand that command builders come with some critical restrictions. Specifically, a command builder is only able to autogenerate SQL commands for use by a data adapter if all of the following conditions are true:
•The Select command interacts with only a single table (e.g., no joins).
•The single table has been attributed with a primary key.
•The column(s) representing the primary key is accounted for in your SQL Select statement.
In any case, Figure 22-19 verifies that the specified row has been deleted from the physical database (don’t confuse the CarID value with the ordinal row number value when you run this example code!).
Figure 22-19. Leveraging autogenerated SQL commands
■Source Code The MySqlCommandBuilder project is found under the Chapter 22 subdirectory.
Multitabled DataSets and DataRelation Objects
Currently, all of this chapter’s examples involved DataSets that contained a single DataTable object. However, the power of the disconnected layer really comes to light when a DataSet object contains numerous interrelated DataTables. In this case, you are able to insert any number of DataRelation objects into the DataSet’s DataRelation collection to account for the interdependencies of the tables. Using these objects, the client tier is able to navigate between the table data without incurring network round-trips.
To illustrate the use of data relation objects, create a new Windows Forms project called MultitabledDataSet. The GUI is simple enough. In Figure 22-20 you can see three DataGridView widgets that hold the data retrieved from the Inventory, Orders, and Customers tables of the Cars database. In addition, the single Button pushes any and all changes back to the data store.

818 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Figure 22-20. Viewing related DataTables
To keep things simple, the MainForm will make use of command builders to autogenerate the SQL commands for each of the three SqlDataAdapters (one for each table). Here is the initial update to the Form-derived type:
public partial class MainForm : Form
{
// Formwide DataSet.
private DataSet carsDS = new DataSet("CarsDataSet");
//Make use of command builders to simplify data adapter configuration. private SqlCommandBuilder sqlCBInventory;
private SqlCommandBuilder sqlCBCustomers; private SqlCommandBuilder sqlCBOrders;
//Our data adapters (for each table).
private SqlDataAdapter invTableAdapter; private SqlDataAdapter custTableAdapter; private SqlDataAdapter ordersTableAdapter;
// Formwide connection object. private SqlConnection cn =
new SqlConnection("server=(local);uid=sa;pwd=;database=Cars");
...
}
The Form’s constructor does the grunge work of creating your data-centric member variables and filling the DataSet. Also note that there is a call to a private helper function, BuildTableRelationship(), as shown here:

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
819 |
public MainForm()
{
InitializeComponent();
// Create adapters.
invTableAdapter = new SqlDataAdapter("Select * from Inventory", cn); custTableAdapter = new SqlDataAdapter("Select * from Customers", cn); ordersTableAdapter = new SqlDataAdapter("Select * from Orders", cn);
// Autogenerate commands.
sqlCBInventory = new SqlCommandBuilder(invTableAdapter); sqlCBOrders = new SqlCommandBuilder(ordersTableAdapter); sqlCBCustomers = new SqlCommandBuilder(custTableAdapter);
//Add tables to DS. invTableAdapter.Fill(carsDS, "Inventory"); custTableAdapter.Fill(carsDS, "Customers"); ordersTableAdapter.Fill(carsDS, "Orders");
//Build relations between tables.
BuildTableRelationship();
//Bind to grids.
dataGridViewInventory.DataSource = carsDS.Tables["Inventory"]; dataGridViewCustomers.DataSource = carsDS.Tables["Customers"]; dataGridViewOrders.DataSource = carsDS.Tables["Orders"];
}
The BuildTableRelationship() helper function does just what you would expect. Recall that the Cars database expresses a number of parent/child relationships, accounted for with the following code:
private void BuildTableRelationship()
{
//Create CustomerOrder data relation object.
DataRelation dr = new DataRelation("CustomerOrder", carsDS.Tables["Customers"].Columns["CustID"], carsDS.Tables["Orders"].Columns["CustID"]);
carsDS.Relations.Add(dr);
//Create InventoryOrder data relation object.
dr = new DataRelation("InventoryOrder", carsDS.Tables["Inventory"].Columns["CarID"], carsDS.Tables["Orders"].Columns["CarID"]);
carsDS.Relations.Add(dr);
}
Now that the DataSet has been filled and disconnected from the data source, you can manipulate each table locally. To do so, simply insert, update, or delete values from any of the three DataGridViews. When you are ready to submit the data back for processing, click the Form’s Update button. The code behind the Click event should be clear at this point:
private void btnUpdate_Click(object sender, EventArgs e)
{
try
{
invTableAdapter.Update(carsDS, "Inventory"); custTableAdapter.Update(carsDS, "Customers");

820 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
ordersTableAdapter.Update(carsDS, "Orders");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Once you update, you will find that each table in the Cars database has been correctly altered.
Navigating Between Related Tables
To illustrate how a DataRelation allows you to move between related tables programmatically, extend your GUI to include a new Button type and a related TextBox. The end user is able to enter the ID of a customer and obtain all the information about that customer’s order, which is placed in a simple message box. The Button’s Click event handler is implemented as so:
private void btnGetInfo_Click(object sender, System.EventArgs e)
{
string strInfo = ""; DataRow drCust = null; DataRow[] drsOrder = null;
//Get the specified CustID from the TextBox. int theCust = int.Parse(this.txtCustID.Text);
//Now based on CustID, get the correct row in Customers table. drCust = carsDS.Tables["Customers"].Rows[theCust];
strInfo += "Cust #" + drCust["CustID"].ToString() + "\n";
//Navigate from customer table to order table.
drsOrder = drCust.GetChildRows(carsDS.Relations["CustomerOrder"]);
// Get order number.
foreach (DataRow r in drsOrder)
strInfo += "Order Number: " + r["OrderID"] + "\n";
//Now navigate from order table to inventory table.
DataRow[] drsInv = drsOrder[0].GetParentRows(carsDS.Relations["InventoryOrder"]);
//Get Car info.
foreach (DataRow r in drsInv)
{
strInfo += "Make: " + r["Make"] + "\n"; strInfo += "Color: " + r["Color"] + "\n"; strInfo += "Pet Name: " + r["PetName"] + "\n";
}
MessageBox.Show(strInfo, "Info based on cust ID");
}
As you can see, the key to moving between data tables is to use a handful of methods defined by the DataRow type. Let’s break this code down step by step. First, you obtain the correct customer ID from the text box and use it to grab the correct row in the Customers table (using the Rows property, of course), as shown here:


822 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
We’re Off to See the (Data) Wizard
At this point in the chapter, you have seen numerous ways to interact with the types of ADO.NET in a “wizard-free” manner. While it is (most definitely) true that understanding the ins and outs of working with your data provider is quite important, it is also true that this can lead to hand cramps from typing the large amount of boilerplate code. To wrap things up, therefore, I’d like to point out a few data-centric wizards you may wish to make use of.
Be aware that I have no intention of commenting on all of the UI-centric data wizards provided by Visual Studio 2005, but to illustrate the basics, let’s examine some additional configuration options of the DataGridView widget. Assume you have created a new Windows Forms application that has a single Form containing a DataGridView control named inventoryDataGridView. Using the designer, activate the inline editor for this widget, and from the Choose Data Source drop-down listbox, click the Add Project Data Source link (see Figure 22-22).
Figure 22-22. Adding a data source
This will launch the Data Source Configuration Wizard. On the first step, simply select the Database icon and click Next. On the second step, click New Connection and establish a connection to the Cars database (using the same set of steps described earlier in this chapter within the “Connecting to the Cars Database from Visual Studio 2005” section). The third step allows you to inform the wizard to store the connection string within an external App.config file (which is generally a good idea) within a properly configured <connectionStrings> element. As the final step, you are able to select which database objects you wish to account for within the generated DataSet, which for your purposes here will simply be the Inventory table (see Figure 22-23).
