Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

.pdf
Скачиваний:
111
Добавлен:
16.08.2013
Размер:
10.35 Mб
Скачать

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.

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

815

Setting the UpdateCommand Property

The modification of the UpdateCarPetName() method is more or less identical. Simply build a new command object and plug it into the UpdateCommand property.

private static void UpdateCarPetName(SqlDataAdapter dAdpater)

{

// Gather info about car to update.

...

// Format SQL Insert and plug into DataAdapter. string sql = string.Format

("Update Inventory Set PetName = '{0}' Where CarID = '{1}'", newPetName, carToUpdate);

SqlCommand cmd = new SqlCommand(sql, cnObj); dAdpater.UpdateCommand = cmd;

DataRow[] carRowToUpdate = dsCarInventory.Tables["Inventory"].Select(

string.Format("CarID = '{0}'", carToUpdate)); carRowToUpdate[0]["PetName"] = newPetName; dAdpater.Update(dsCarInventory.Tables["Inventory"]);

}

In this case, when you select a specific row (via the Select() method), the RowState value of said row is automatically set to DataRowState.Modified. The only other point of interest here is that the Select() method returns an array of DataRow objects; therefore, you must specify the exact row you wish to modify.

Setting the DeleteCommand Property

Last but not least, you have the following update to the DeleteCar() method:

private static void DeleteCar(SqlDataAdapter dAdpater)

{

// Get ID of car to delete.

,,,

string sql = string.Format("Delete from Inventory where CarID = '{0}'", carToDelete);

SqlCommand cmd = new SqlCommand(sql, cnObj); dAdpater.DeleteCommand = cmd;

DataRow[] carRowToDelete = dsCarInventory.Tables["Inventory"].Select(string.Format("CarID = '{0}'", carToDelete));

carRowToDelete[0].Delete();

dAdpater.Update(dsCarInventory.Tables["Inventory"]);

}

In this case, you find the row you wish to delete (again using the Select() method) and then set the RowState property to DataRowState.Deleted by calling Delete().

Source Code The CarsInvertoryUpdaterDS project is included under the Chapter 22 subdirectory.

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:

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

821

//Get the specified CustID from the TextBox. int theCust = int.Parse(this.txtCustID.Text);

//Now based on CustID, get the correct row in the Customers table.

DataRow drCust = null;

drCust = carsDS.Tables["Customers"].Rows[theCust]; strInfo += "Cust #" + drCust["CustID"].ToString() + "\n";

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 customer table to order table.

DataRow[] drsOrder = null;

drsOrder = drCust.GetChildRows(carsDS.Relations["CustomerOrder"]);

//Get order number.

foreach(DataRow r in drsOrder)

strInfo += "Order Number: " + r["OrderID"] + "\n";

Your 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 order table to inventory table.

DataRow[] drsInv = drsOrder[0].GetParentRows(carsDS.Relations["InventoryOrder"]);

foreach(DataRow r in drsInv)

{

strInfo += "Make: " + r["Make"] + "\n"; strInfo += "Color: " + r["Color"] + "\n"; strInfo += "Pet Name: " + r["PetName"] + "\n";

}

Figure 22-21 shows one possible output.

Figure 22-21. 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 in-memory copy of data and navigate around each table to make any necessary updates, deletes, or inserts. Once you’ve finished, you can then submit your changes to the data store for processing.

Source Code The MultitabledDataSetApp project is included under the Chapter 22 subdirectory.

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).

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

823

Figure 22-23. Selecting the Inventory table

Once you complete the wizard, you will notice that the DataGridView automatically displays the column names within the designer. In fact, if you run your application as is, you will find the contents of the Inventory table displayed within the grid’s UI. If you were to examine the code placed in your Form’s Load event, you would find that the grid is populated with the line of code highlighted in bold:

public partial class MainForm : Form

{

public MainForm()

{

InitializeComponent();

}

private void MainForm_Load(object sender, EventArgs e)

{

//TODO: This line of code loads data into

//the 'carsDataSet.Inventory' table.

//You can move, or remove it, as needed. this.inventoryTableAdapter.Fill(this.carsDataSet.Inventory);

}

}

To understand what this line of code is in fact doing, you need to first understand the role of strongly typed DataSet objects.

Strongly Typed DataSets

Strongly typed DataSets (as the name implies) allow you to interact with a DataSet’s internal tables using database-specific properties, methods, and events, rather than via the generalized Tables property. If you activate the View Class View menu option of Visual Studio 2005, you will find that