166 Microsoft ADO.NET 4 Step by Step 4. Just after the “Read the next set, which contains the orders” comment, add the follow- ing code: customerReader.NextResult() Do While (customerReader.Read = True) oneOrder = New OrderInfo oneOrder.ID = CLng(customerReader!ID) oneOrder.OrderDate = CDate(customerReader!OrderDate) oneOrder.OrderTotal = CDec(customerReader!Total) AllOrders.Items.Add(oneOrder) Loop This code accesses the records in the second set of results, the SELECT statement for the OrderEntry table, via the NextResult method call. 5. Run the program. On the Customer Management form, select a customer from the list of customers and then click View Orders. When the View Orders form appears, it in- cludes content from both SELECT statements returned by the stored procedure. Summary This chapter discussed parameters, which are data value objects that help ensure the accuracy and safety of the data being sent to and returned from external data sources. Parameterized queries use special SQL statements that include placeholders for each parameter. Each SqlParameter instance defines the name of the parameter, its data type, and its value. Parameters work with either standard SQL commands or with stored procedures. When using them with stored procedures, you can create both input and output stored procedures, sup- porting two-way communications with these custom database functions. Chapter 10 Adding Standards to Queries 167 Chapter 10 Quick Reference To Do This Create a parameterized query for SQL Server Create a SQL query string that includes @-prefixed placeholders. Create a SqlCommand instance. Assign the SQL query to the SqlCommand object’s CommandText property. Create SqlParameter objects, one for each placeholder in the query, and add them to the command object’s Parameters collection. Set the SqlCommand.Conne ction property. Call one of the command object’s Execute methods. Create a parameterized query for an OLE DB data source Create a SQL query string that includes question marks (?) for placeholders. Create an OleDbCommand instance. Assign the SQL query to the OleDbCommand object’s CommandText property. Create OleDbParameter objects, one for each placehold- er in the query, and add them to the command object’s Parameters collection. Set the OleD bCommand.Connection property. Call one of the command object’s Execute methods. Create an “out” parameter for a stored procedure Create a SqlParameter instance, setting its fields as needed. Set the SqlParameter.Direction property to ParameterDirection.Output. 169 Chapter 11 Making External Data Available Locally After completing this chapter, you will be able to: Load external data into a DataTable or DataSet Return updated DataSet content to an external source Use SQL statements and stored procedures to manage DataSet content The disconnected data experience provided by ADO.NET revolves around the DataSet class and its supporting objects. The last few chapters have introduced ways to access external data with ADO.NET, but none of those features took advantage of the disconnected aspects of the framework. Still, part of the promise of ADO.NET is its ability to manage external data in a disconnected and table-focused way. This chapter introduces the DataAdapter class—the class that fulfills that core data promise. The DataAdapter bridges the simple data connectedness exhibited by the DataReader and joins it with the advanced data management features found in the DataSet. By creating a few simple objects and crafting a minimum number of SQL statements, you can safely give your DataSet the tools needed to keep it and its associated external data source in sync. Understanding Data Adapters Data adapters link your external database tables and your local DataSet-managed tables by issuing SQL statements. Anytime you need to get data from the database into a DataSet, the adapter must perform a “Fill” operation, issuing a SELECT statement and moving the results into local DataTable instances. You can then update the values in those DataTable instances. When it’s time to return changes stored in the DataSet to the database, the data adapter’s “Update” operation sends the relevant INSERT, UPDATE, and DELETE statements to the da- tabase to bring the external data store into line with local changes. Figure 11-1 shows these components working on a single database table, Customer. 170 Microsoft ADO.NET 4 Step by Step Database ADO.NET Customer DataAdapter SELECT Fill Update INSERT UPDATE DELETE DataReader Original Data User Updates Changed Data Mapping Command Objects DataSet / DataTable ID FullName Address Phone … FIGURE 11-1 The data adapter in action. As Figure 11-1 makes clear, the DataAdapter manages a lot of complex activity between the database and a DataSet or DataTable. It is no exaggeration to say that the DataAdapter is possibly the most complex part of ADO.NET, especially when you take advantage of all the flexibility it provides. All the classes introduced so far in this book—from DataSet to SqlParameter, from DataRow to DataReader—come into play when creating instances of a data adapter class. The System.Data.SqlClient.SqlDataAdapter class exposes the SQL Server provider implemen- tation of the adapter. You can also find OLE DB and ODBC variations of the data adapter in the classes System.Data.OleDb.OleDbDataAdapter and System.Data.Odbc.Odb cDataAdapter, respectively. All these classes derive from System.Data.Common.DbDataAdapter, which in turn derives from System.Data.Common.DataAdapter. Note Although the information in this chapter applies generally to all data adapter implemen- tations, this chapter’s code samples and examples focus specifically on the SQL Server provider version. SqlDataAdapter provides three general support features in your application: Record retrieval Populating a DataTable with database records represents the mini- mal functionality of the data adapter. Internally, the SqlDataAdapter uses a D ataReader instance to retrieve records out of the database, so you must provide it with a SELECT statement and a connection string. Stored procedures that return data rows also work; the adapter will correctly process multiple record sets returned by the query. Chapter 11 Making External Data Available Locally 171 Record updating Moving modified data back to external storage is a little more in- volved. Although the “fill” from the database requires only a basic SELECT statement, the “update” operation requires distinct INSERT, UPDATE, and DELETE statements to complete its work. You can write these by hand or use a “command builder” to auto- matically generate these statements based on the original SELECT query. Table and column name mapping The naming needs of your database tables and columns may not always mesh with the needs of your application. Each data adapter includes a mapping layer that automatically renames tables and columns as needed while data is passed between local and remote storage areas. The remainder of this chapter elaborates on these three data adapter features. Moving Data from Source to Memory The SqlDataAdapter.Fill method requests data from SQL Server using a valid SELECT state- ment or a data-selection stored procedure. After it accesses the data through an internal SqlDataReader, it moves the records into the DataTable or DataSet of your choice. Moving Data into a DataTable To move data from a database table into a DataTable instance, set up a new SqlDataAdapter object and call its Fill method, passing it the instance of the DataTable. C# DataTable targetTable = new DataTable(); SqlDataAdapter workAdapter = new SqlDataAdapter( "SELECT * FROM Customer ORDER BY LastName", connectionString); workAdapter.Fill(targetTable); Visual Basic Dim targetTable As New DataTable Dim workAdapter As New SqlDataAdapter( "SELECT * FROM Customer ORDER BY LastName", connectionString) workAdapter.Fill(targetTable) The data adapter uses the constructor arguments to create a new SqlCommand instance. It then assigns this instance to its SelectCommand property, a property that must be set before the SqlDataAdapter can do its data retrieval work. 172 Microsoft ADO.NET 4 Step by Step In addition to the two-string constructor variation shown previously, overloaded versions let you pass in a configured SqlCommand instance, pass in a SQL string and SqlConnection pair, or just leave off the arguments altogether. The SqlDataAdapter class has no connec- tion string or connection properties, so if you don’t provide them with the constructor, you need to include them with a SqlComma nd instance that you assign to the SqlDataAdapter. SelectCommand property directly, as shown here: C# DataTable targetTable = new DataTable(); using (SqlConnection linkToDB = new SqlConnection(connectionString)) { SqlDataAdapter workAdapter = new SqlDataAdapter(); workAdapter.SelectCommand = new SqlCommand( "SELECT * FROM Customer ORDER BY LastName", linkToDB); workAdapter.Fill(targetTable); } Visual Basic Dim targetTable As New DataTable Using linkToDB As New SqlConnection(builder.ConnectionString) Dim workAdapter As New SqlDataAdapter workAdapter.SelectCommand = New SqlCommand( "SELECT * FROM Customer ORDER BY LastName", linkToDB) workAdapter.Fill(targetTable) End Using Neither of the preceding examples opened the connection explicitly. If the command’s con- nection isn’t open yet, the Fill method opens it for you—and closes it when the operation completes. As the data adapter reads the incoming data, it examines the schema of that data and builds the columns and properties of the DataTab le instance as needed. If the DataTable already has matching columns (names and data types), they are used as is. Any new columns are created alongside the preexisting columns. Note You can alter this default behavior, as described in this chapter’s “Table and Column Mapping” section on page 186. The D ataTable.TableName property will be set to “Table,” even if you selected records from a specific table with a different name. To alter the target table’s name, modify its TableName property after the data load or use the table mapping features discussed later in this chapter. Chapter 11 Making External Data Available Locally 173 Because the SqlD ataAdapter.SelectCommand property is a standard SqlCommand instance, you can use any of that command object’s features to access the remote data. This includes adding one or more SqlParame ter objects for @-prefixed placeholders embedded in the SQL statement. Configuring the SqlComm and instance as a stored procedure with associated pa- rameters also works. C# // Call the GetCustomerOrders stored procedure with a // single 'customer ID' argument. string sqlText = "dbo.GetOrdersForCustomer"; SqlCommand commandWrapper = new SqlCommand(sqlText, linkToDB); commandWrapper.CommandType = CommandType.StoredProcedure; commandWrapper.Parameters.AddWithValue("@customerID", ActiveCustomerID); // Retrieve the data. SqlDataAdapter workAdapter = new SqlDataAdapter(commandWrapper); DataTable orders = new DataTable(); workAdapter.Fill(orders); Visual Basic ' Call the GetCustomerOrders stored procedure with a ' single 'customer ID' argument. Dim sqlText As String = "dbo.GetOrdersForCustomer" Dim commandWrapper As New SqlCommand(sqlText, linkToDB) commandWrapper.CommandType = CommandType.StoredProcedure commandWrapper.Parameters.AddWithValue("@customerID", ActiveCustomerID) ' Retrieve the data. Dim workAdapter As New SqlDataAdapter(commandWrapper) Dim orders As New DataTable workAdapter.Fill(orders) Moving Data into a DataSet Moving external data into a waiting DataSet instance is as easy as filling a DataTable. To im- port the data into a DataSet, call the SqlDataAdapter.Fill method, passing it an instance of DataSet. 174 Microsoft ADO.NET 4 Step by Step C# DataSet targetSet = new DataSet(); SqlDataAdapter workAdapter = new SqlDataAdapter( "SELECT * FROM Customer ORDER BY LastName", connectionString); workAdapter.Fill(targetSet); Visual Basic Dim targetSet As New DataSet Dim workAdapter As New SqlDataAdapter( "SELECT * FROM Customer ORDER BY LastName", connectionString) workAdapter.Fill(targetSet) As with a DataTable load, the DataSet version of Fill will auto-build the schema for you. If you want to preconfigure the DataSet schema, you can build its table by hand or call the SqlDataAdapter.FillSchema method just before you call the Fill method. C# // First build the schema using the structure defined // in the data source. workAdapter.FillSchema(targetSet, SchemaType.Source); // Then load the data. workAdapter.Fill(targetSet); Visual Basic ' First build the schema using the structure defined ' in the data source. workAdapter.FillSchema(targetSet, SchemaType.Source) ' Then load the data. workAdapter.Fill(targetSet) Note Passing SchemaType.Mapped as the second argument to FillSchema enables a “mapped” schema build. Schema mapping is discussed on page 186 in the “Table and Column Mapping” section of this chapter. Fill names the first created table in the data set “Table,” as is done when filling a DataTable directly. To alter this default name, specify the new name as a second argument to the Fill method. Chapter 11 Making External Data Available Locally 175 C# workAdapter.Fill(targetSet, "Customer"); Visual Basic workAdapter.Fill(targetSet, "Customer") The Fill(DataSet) method will import multiple tables if its SelectCommand includes a batch of SELECT statements or a stored procedure that returns multiple result sets. The first table created is still named “Table” (by default). Subsequent tables are named numerically, with the second table given the name “Table1,” the third table “Table2,” and so on. Duplicate column names found in any table are treated the same way. The first duplicate column is given a “1” suffix, the second has a “2” suffix, and so on. Note When retrieving multiple tables of data, a call to SqlDataAdapter.FillSchema examines only the schema of the first result set. The schemas of subsequent sets can be imported only as a side effect of the Fill method. Moving Data from Memory to Source After imported data has been modified within a DataTable (with or without a surrounding DataSet), the same SqlDataAdapter that brought the data in can move the changes back out to the source. Setting up the adapter to accomplish that feat is a little more involved than just crafting a SELECT statement but still not overwhelmingly difficult. Configuring the data adapter for the return data trip requires setting up the appropriate data manipulation state- ments and calling the SqlDataAdapter.Update method. Configuring the Update Commands The SqlDataAdapter.SelectCommand property manages the movement of data only from the external source to the local DataSet or DataTable. To move data in the other direction or delete data, you need to set up three distinct properties: InsertCommand, UpdateCommand, and DeleteCommand. Like SelectCommand, these three properties are SqlCommand instances, each containing a SQL statement (or stored procedure), a SqlConnection reference, and parameters. Although parameters are optional in the SelectCommand instance, they are an essential part of the three update commands. The following code sets up selection and data modification properties for a simple table, UnitOfMeasure, which includes an identity field, ID; and two text fields, ShortName and FullName: . 1 1-1 shows these components working on a single database table, Customer. 170 Microsoft ADO. NET 4 Step by Step Database ADO. NET Customer DataAdapter SELECT Fill Update INSERT UPDATE DELETE DataReader Original. im- port the data into a DataSet, call the SqlDataAdapter.Fill method, passing it an instance of DataSet. 1 74 Microsoft ADO. NET 4 Step by Step C# DataSet targetSet = new DataSet(); SqlDataAdapter. SqlDataAdapter can do its data retrieval work. 172 Microsoft ADO. NET 4 Step by Step In addition to the two-string constructor variation shown previously, overloaded versions let you pass in a