
Pro CSharp 2008 And The .NET 3.5 Platform [eng]
.pdf
812 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Configuring the Data Adapter Using the SqlCommandBuilder
When you are using a data adapter to modify tables in 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 references). By “valid” command objects, I am referring to the set of command objects used in conjunction with the table you are attempting to update (the Inventory table in our example).
To fill up our adapter with the necessary data can entail a good amount of code, especially if we make use of parameterized queries. Recall from Chapter 22 that a parameterized query allows us to build a SQL statement using a set of parameter objects. Thus, if we were to take the long road, we could implement ConfigureAdapter() to manually create three new SqlCommand objects, each of which contains a set of SqlParameter objects. After this point, we could set each object to the
UpdateCommand, DeleteCommand, and InsertCommand properties of the adapter.
Thankfully, Visual Studio 2008 provides a number of designer tools to take care of this mundane and tedious code on our behalf. You’ll see some of these shortcuts in action at the conclusion of this chapter. Rather than forcing you to author the numerous code statements to fully configure a data adapter, let’s take a massive shortcut by implementing ConfigureAdapter() as so:
private void ConfigureAdapter(out SqlDataAdapter dAdapt)
{
// Create the adapter and set up the SelectCommand.
dAdapt = new SqlDataAdapter("Select * From Inventory", cnString);
//Obtain the remaining command objects dynamically at runtime
//using the SqlCommandBuilder.
SqlCommandBuilder builder = new SqlCommandBuilder(dAdapt);
}
To help simplify the construction of data adapter objects, each of the Microsoft-supplied ADO.NET data providers provides a command builder type. 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.
Obviously, doing so requires additional round-trips to the remote database, and therefore it will certainly hurt performance if you use the SqlCommandBuilder numerous times in a single application. Here, we are minimizing the negative effect by calling our ConfigureAdapter() method at the time the InventoryDALDisLayer object is constructed, and retaining the configured SqlDataAdapter for use throughout the object’s lifetime.
In the previous code, notice that we 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 we 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 SQL Select command interacts with only a single table (e.g., no joins).
•The single table has been attributed with a primary key.
•The table must have a column(s) representing the primary key that is included in your SQL Select statement.

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
813 |
Based on the way we constructed our AutoLot database, these restrictions pose no problem. However, in a more industrial-strength database, you will need to consider if this type is at all useful (if not, remember that Visual Studio 2008 will autogenerate a good deal of the required code, as you’ll see at the end of this chapter).
Implementing GetAllInventory()
Now that our data adapter is ready to go, the first method of our new class type will simply use the Fill() method of the SqlDataAdapter object to fetch a DataTable representing all records in the Inventory table of the AutoLot database:
public DataTable GetAllInventory()
{
DataTable inv = new DataTable("Inventory"); dAdapt.Fill(inv);
return inv;
}
Implementing UpdateInventory()
The UpdateInventory() method is very simple:
public void UpdateInventory(DataTable modifiedTable)
{
dAdapt.Update(modifiedTable);
}
Here, the data adapter object will examine the RowState value of each row of the incoming
DataTable. Based on this value (RowState.Added, RowState.Deleted, or RowState.Modified), the correct command object will be leveraged behind the scenes.
■Source Code The AutoLotDAL (Part 2) project is included under the Chapter 23 subdirectory.
Building a Windows Forms Front End
At this point we can build a front end to test our new InventoryDALDisLayer object, which will be a Windows Forms application named WindowsFormsInventoryUI. Once you have created the project, set a reference to your updated AutoLotDAL.dll assembly and import the following namespace:
using AutoLotDisconnectedLayer;
The design of the form consists of a single Label, DataGridView (named inventoryGrid), and Button type (named btnUpdateInventory), which has been configured to handle the Click event. Here is the definition of the form (which does not contain error-handling logic for simplicity; feel free to add try/catch logic if you so choose):
public partial class MainForm : Form
{
InventoryDALDisLayer dal = null;
public MainForm()
{
InitializeComponent();

814 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
//Assume we have an App.config file
//storing the connection string. string cnStr =
ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString;
//Create our data access object.
dal = new InventoryDALDisLayer(cnStr);
// Fill up our grid!
inventoryGrid.DataSource = dal.GetAllInventory();
}
private void btnUpdateInventory_Click(object sender, EventArgs e)
{
// Get modified data from the grid.
DataTable changedDT = (DataTable)inventoryGrid.DataSource;
// Commit our changes. dal.UpdateInventory(changedDT);
}
}
Notice that in this example, I am assuming you have added an App.config file to store the connection string data, within a <connectionStrings> section. To make use of the ConnectionStrings indexer of the ConfigurationManager type, be sure to set a reference to the System.Configuration.dll assembly. Once we create the InventoryDALDisLayer object, we bind the DataTable returned from GetAllInventory() to the DataGridView object. When the end user clicks the Update button, we extract out the modified DataTable from the grid (via the DataSource property) and pass it into our
UpdateInventory() method.
That’s it! Once you run this application, add a set of new rows to the grid and update/delete a few others. Assuming you click the Button control, you will see your changes have persisted into the AutoLot database.
■Source Code The updated WindowsFormsInventoryUI project is included under the Chapter 23 subdirectory.
Navigating Multitabled DataSet Objects
So far, all of this chapter’s examples have operated on 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.
■Note Rather than updating AutoLotDAL.dll yet again in order to account for the Customers and Orders tables, this example isolates all of the data access logic within a new Windows Forms project. However, intermixing UI and data logic in a production-level application is certainly not recommended. The final examples of this chapter leverage various database design tools to decouple the UI and data logic code.

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
815 |
Begin this example by creating a new Windows Forms application named MultitabledDataSetApp. The GUI is simple enough. In Figure 23-13 you can see three DataGridView widgets that hold the data retrieved from the Inventory, Orders, and Customers tables of the AutoLot database. In addition, the initial Button (named btnUpdateDatabase) submits any and all changes entered within the grids back to the database for processing via data adapter objects.
Figure 23-13. The initial UI will display data from each table of the AutoLot database.
Prepping the Data Adapters
To keep the data access code as simple as possible, the MainForm will make use of command builder objects 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
{
// Form wide DataSet.
private DataSet autoLotDS = new DataSet("AutoLot");
//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;

816 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
// Form wide connection string. private string cnStr = string.Empty;
...
}
The constructor does the grunge work of creating your data-centric member variables and filling the DataSet. Here, I am assuming you have authored an App.config file that contains the correct connection string data (and that you have referenced System.Configuration.dll and imported the System.Configuration namespace). Also note that there is a call to a private helper function,
BuildTableRelationship(), as shown here:
public MainForm()
{
InitializeComponent();
//Get connection string from *.config file. cnStr =
ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString;
//Create adapters.
invTableAdapter = new SqlDataAdapter("Select * from Inventory", cnStr); custTableAdapter = new SqlDataAdapter("Select * from Customers", cnStr); ordersTableAdapter = new SqlDataAdapter("Select * from Orders", cnStr);
// Autogenerate commands.
sqlCBInventory = new SqlCommandBuilder(invTableAdapter); sqlCBOrders = new SqlCommandBuilder(ordersTableAdapter); sqlCBCustomers = new SqlCommandBuilder(custTableAdapter);
//Add tables to DS. invTableAdapter.Fill(autoLotDS, "Inventory"); custTableAdapter.Fill(autoLotDS, "Customers"); ordersTableAdapter.Fill(autoLotDS, "Orders");
//Build relations between tables.
BuildTableRelationship();
//Bind to grids
dataGridViewInventory.DataSource = autoLotDS.Tables["Inventory"]; dataGridViewCustomers.DataSource = autoLotDS.Tables["Customers"]; dataGridViewOrders.DataSource = autoLotDS.Tables["Orders"];
}
Building the Table Relationships
The BuildTableRelationship() helper function does the grunt work to add two DataRelation objects into the autoLotDS object. Recall from Chapter 22 that the AutoLot 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", autoLotDS.Tables["Customers"].Columns["CustID"], autoLotDS.Tables["Orders"].Columns["CustID"]);
autoLotDS.Relations.Add(dr);

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
817 |
// Create InventoryOrder data relation object. dr = new DataRelation("InventoryOrder",
autoLotDS.Tables["Inventory"].Columns["CarID"],
autoLotDS.Tables["Orders"].Columns["CarID"]);
autoLotDS.Relations.Add(dr);
}
Note that when creating a DataRelation object, you establish a friendly string moniker with the first parameter (you’ll see the usefulness of doing so in just a minute) as well as the keys used to build the relationship itself. Notice that the parent table (the second constructor parameter) is specified before the child table (the third constructor parameter).
Updating the Database Tables
Now that the DataSet has been filled and disconnected from the data source, you can manipulate each DataTable 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 Update button. The code behind the related Click event should be clear at this point:
private void btnUpdateDatabase_Click(object sender, EventArgs e)
{
try
{
invTableAdapter.Update(carsDS, "Inventory"); custTableAdapter.Update(carsDS, "Customers"); ordersTableAdapter.Update(carsDS, "Orders");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Now run your application and perform various updates. When you rerun the application, you should find that your grids are populated with the recent changes.
Navigating Between Related Tables
To illustrate how a DataRelation allows you to move between related tables programmatically, extend your UI to include a new Button type (named btnGetOrderInfo), a related TextBox (named txtCustID), and a descriptive Label (I grouped these controls within a GroupBox simply for visual appeal). Figure 23-14 shows one possible UI of the application.

818 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Figure 23-14. The updated UI allows the user to look up customer order information.
Using this updated UI, the end user is able to enter the ID of a customer and retrieve all the relevant information about that customer’s order (name, order ID, car order, etc.), which will be formatted into a string type that is eventually displayed within a message box. Ponder the code behind the new Button’s Click event handler:
private void btnGetOrderInfo_Click(object sender, System.EventArgs e)
{
string strOrderInfo = string.Empty; DataRow[] drsCust = null; DataRow[] drsOrder = null;
// Get the customer ID in the text box.
int custID = int.Parse(this.txtCustID.Text);
//Now based on custID, get the correct row in Customers table. drsCust = autoLotDS.Tables["Customers"].Select(
string.Format("CustID = {0}", custID));
strOrderInfo += string.Format("Customer {0}: {1} {2}\n", drsCust[0]["CustID"].ToString(), drsCust[0]["FirstName"].ToString().Trim(), drsCust[0]["LastName"].ToString().Trim());
//Navigate from Customers table to Orders table.
drsOrder = drsCust[0].GetChildRows(autoLotDS.Relations["CustomerOrder"]);
// Get order number.
foreach (DataRow r in drsOrder)
strOrderInfo += string.Format("Order Number: {0}\n", r["OrderID"]);
//Now navigate from Orders table to Inventory table.
DataRow[] drsInv = drsOrder[0].GetParentRows(autoLotDS.Relations["InventoryOrder"]);
//Get car info.
foreach (DataRow r in drsInv)
{
strOrderInfo += string.Format("Make: {0}\n", r["Make"]);

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
819 |
strOrderInfo += string.Format("Color: {0}\n", r["Color"]); strOrderInfo += string.Format("Pet Name: {0}\n", r["PetName"]);
}
MessageBox.Show(strOrderInfo, "Order Details");
}
Let’s break down this code step by step. First, you obtain the correct customer ID from the text box and use it to select the correct row in the Customers table, via the Select() method. Given that Select() returns an array of DataRow objects, you must use double indexing to ensure you fetch the data for the first (and only) member of this array:
// Get the customer ID in the text box.
int custID = int.Parse(this.txtCustID.Text);
// Now based on custID, get the correct row in Customers table. drsCust = autoLotDS.Tables["Customers"].Select(
string.Format("CustID = {0}", custID));
strOrderInfo += string.Format("Customer {0}: {1} {2}\n", drsCust[0]["CustID"].ToString(), drsCust[0]["FirstName"].ToString().Trim(), drsCust[0]["LastName"].ToString().Trim());
Next, you navigate from the Customers table to the Orders table, using the CustomerOrder data relation. Notice that the DataRow.GetChildRows() method allows you to grab rows from your child table. Once you do, you can read information out of the table:
// Navigate from Customers table to Orders table.
drsOrder = drsCust[0].GetChildRows(autoLotDS.Relations["CustomerOrder"]);
// Get order number.
foreach (DataRow r in drsOrder)
strOrderInfo += string.Format("Order Number: {0}\n", r["OrderID"]);
The final step is to navigate from the Orders table to its parent table (Inventory), using the GetParentRows() method. At this point, you can read information from the Inventory table using the Make, PetName, and Color columns, as shown here:
//Now navigate from Orders table to Inventory table.
DataRow[] drsInv = drsOrder[0].GetParentRows(autoLotDS.Relations["InventoryOrder"]);
//Get car info.
foreach (DataRow r in drsInv)
{
strOrderInfo += string.Format("Make: {0}\n", r["Make"]); strOrderInfo += string.Format("Color: {0}\n", r["Color"]); strOrderInfo += string.Format("Pet Name: {0}\n", r["PetName"]);
}
Figure 23-15 shows one possible output when specifying a customer ID with the value of 2 (Matt Walton in my copy of the AutoLot database).

820 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Figure 23-15. Navigating data relations
Hopefully, this last example has you convinced of the usefulness of the DataSet type. Given that a DataSet is completely disconnected from the underlying data source, you can work with an inmemory copy of data and navigate around each table to make any necessary updates, deletes, or inserts. Once you’ve finished, you can submit your changes to the data store for processing. The end result is a very scalable and robust application.
■Source Code The MultitabledDataSetApp project is included under the Chapter 23 subdirectory.
The Data Access Tools of Visual Studio 2008
All of the ADO.NET examples in this text thus far have involved a fair amount of elbow grease, in that we were authoring all data access logic by hand. While we did offload a good amount of said code to a .NET code library (AutoLotDAL.dll) for reuse in later chapters of the book, we were still required to manually create the various objects of our data provider before interacting with the relational database.
To wrap up our examination of the disconnected layer of ADO.NET, we will now take a look at a number of services provided by Visual Studio 2008 that can assist you in authoring data access logic. As you might suspect, this IDE supports a number of visual designers and code generation tools (aka wizards) that can produce a good deal of starter code.

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
821 |
■Note Don’t get lulled into the belief that you will never be required to author ADO.NET logic by hand, or that the wizard-generated code will always fit the bill 100 percent for your current project. While these tools can save you a significant amount of time, the more you know about the ADO.NET programming model, the better, as this enables you to customize and tweak the generated code as required.
Visually Designing the DataGridView
The first data access shortcut can be found via the DataGridView designer. While we have used this widget in previous examples for display and editing purposes, we have not used the associated wizard that will generate data access code on our behalf. To begin, create a brand-new Windows Forms application project named VisualDataGridViewApp. Add a descriptive Label control and an instance of the DataGridView control. When you do, note that an inline editor opens to the right of the UI widget. From the Choose Data Source drop-down box, select the Add Project Data Source link (see Figure 23-16).
Figure 23-16. The DataGridView editor
The Data Source Configuration Wizard launches. This tool will guide you through a series of steps that allow you to select and configure a data source, which will then be bound to the DataGridView using a custom data adapter type. The first step of the wizard simply asks you to identify the type of data source you wish to interact with. Select Database (see Figure 23-17) and click the Next button.