1. Introduction
  2. Overview of ADO. NET
  3. The ADO.NET Object Model
  4. NET Data Providers
  5. Namespaces in the .NET Framework
  6. Connection Object
  7. DataAdapter Object
  8. Parameter Object
  9. Transaction Object
  10. Command Object
  11. DataReader Object
  12. Disconnected Objects
  13. DataTable Object
  14. DataColumn Object
  15. Creating Connection Objects
  16. Constructors
  17. OLE DB Provider for SQL Server Databases
  18. OLE DB Provider for Oracle Databases
  19. OLE DB Provider for ODBC Drivers
  20. Using Data Links to Build Connection Strings in Code
  21. Opening and Closing Connections
  22. Connection Pooling
  23. Working with Hierarchical Data
  24. Using DataRelation Objects to Validate Your Data
  25. Many-to-Many Relationships
  26. Using DataRelation Objects in Expression-Based DataColumn Objects
  27. What Is a DataView Object?
  28. Working with DataView Objects in Code
  29. Examining All Rows of Data Available Through a DataView
  30. Modifying DataRowView Objects
  31. DataView Object Reference
  32. Working with Strongly Typed DataSet Objects
  33. Working with Null Data
  34. The Stateless Approach—Maintaining No State
  35. Working with XML Data
  36. Using the XmlDataDocument Object
  37. Applying an XSLT Transform
  38. ADO.NET Navigation Control

Microsoft ADO.NET represents a major step forward for Microsoft data access technologies. It gives developers an unprecedented level of control over how their code interacts with their data .ADO.NET is  most powerful and robust data access technology that Microsoft has produced to date,

ADO. NET is a set of libraries included with the Microsoft .NET Framework that help you communicate with various data stores from .NET applications. The ADO.NET libraries include classes for connecting to a data source, submitting queries, and processing results. You can also use ADO.NET as a robust, hierarchical, disconnected data cache to work with data off line. The central disconnected object, the DataSet, allows you to sort, search, filter, store pending changes, and navigate through hierarchical data. The DataSet also includes a number of features that bridge the gap between traditional data access and XML development. Developers can now work with XML data through traditional data access interfaces and vice-versa.

In short, if you’re building an application that accesses data, you should be using ADO.NET.

Microsoft Visual Studio .NET includes a number of data access features you can use to build data access applications. Many of these features can save you time during the development process by generating large amounts of tedious code for you. Other features improve the performance of the applications you build by storing metadata and updating logic in your code rather than fetching this information at run time. Believe it or not, many of Visual Studio .NET’s data access features accomplish both tasks.

Developers who have experience with Microsoft’s previous data access technology, ODBCDirect.,Remote Data Objects,ActiveX Data Objects (ADO), will definitely think why Microsoft has come up with New Model

DAO was initially designed to communicate with local file-based databases. Soon Visual Basic developers wanted to talk to server-based databases such as Microsoft SQL Server and Oracle. DAO allows developers to communicate with such databases, but developers craved more control and better performance. So the Visual Basic development team created RDO to give developers a fast, lightweight data access layer designed to talk to larger server-based databases. Then came the Internet, and developers yearn for a data access model they could use more easily in server-side scripts, that would require fewer lines of code to interact with database. So the End Result ADO was born.

Being honest ADO has served many developers well for the past few years, but it lacks key features of working with XML data. While recent versions of ADO have added XML features, ADO was not built to work with XML data. For example, ADO does not allow you to separate the schema information from the actual data. Microsoft might add more XML features to future releases of ADO, but ADO will never handle XML data as efficiently as ADO.NET does because ADO.NET was designed with XML in mind and ADO was not.

Microsoft built ADO.NET to address these key scenarios, along with others that I’ll cover up next

ADO.NET is designed to combine the best features of its predecessors while adding features requested most frequently by developers—greater XML support, easier disconnected data access, more control over updates, Data relation, transactions and greater update flexibility.

Now that you understand the purpose of ADO.NET and where it fits into the overall Visual Studio .NET architecture, it’s time to take a closer look at the technology. In this Article, we’ll take a brief look at the ADO.NET object model and see how it differs from past Microsoft data access technologies.

ADO.NET is designed to help developers build efficient multi-tiered database applications across intranets and the Internet, and the ADO.NET object model provides the means. Figure 1-1 shows the classes that comprise the ADO.NET object model. A dotted line separates the object model into two halves. The objects to the left of the line are “connected” objects. These objects communicate directly with your database to manage the connection and transactions as well as to retrieve data from and submit changes to your database. The objects to the right of the line are “disconnected” objects that allow a user to work with data offline.

The objects that comprise the disconnected half of the ADO.NET object model do not communicate directly with the connected objects. This is a major change from previous Microsoft data access object models. In ADO, the Recordset object stores the results of your queries. You can call its Open method to fetch the results of a query and call its Update (or UpdateBatch) method to submit changes stored within the Recordset to your database.

The ADO.NET DataSet, which we’ll discuss shortly, is comparable in functionality to the ADO Recordset. However, the DataSet does not communicate with your database. In order to fetch data from your database into a DataSet, you pass the DataSet into the Fill method of a connected ADO.NET object—the DataAdapter. Similarly, to submit the pending changes stored in your DataSet to your database, you pass the DataSet to the DataAdapter object’s Update method.

A .NET data provider is a collection of classes designed to allow you to communicate with a particular type of data store. The .NET Framework includes two such providers, the SQL Client .NET Data Provider and the OLE DB .NET Data Provider. The OLE DB .NET Data Provider lets you communicate with various data stores through OLE DB providers. The SQL Client .NET Data Provider is designed solely to communicate with SQL Server databases, version 7 and later.

Each .NET data provider implements the same base classes—Connection, Command, DataReader, Parameter, and Transaction—although their actual names depend on the provider. For example, the SQL Client .NET Data Provider has a SqlConnection object, and the OLE DB .NET Data Provider includes an OleDbConnection object. Regardless of which .NET data provider you use, the provider’s Connection object implements the same basic features through the same base interfaces. To open a connection to your data store, you create an instance of the provider’s connection object, set the object’s ConnectionString property, and then call its Open method.

Each .NET data provider has its own namespace. The two providers included in the .NET Framework are subsets of the System.Data namespace, where the disconnected objects reside. The OLE DB .NET Data Provider resides in the System.Data.OleDb namespace, and the SQL Client .NET Data Provider resides in System.Data.SqlClient.

Visual Basic .NET
'Open and close a connection using the SQL Client .NET Data Provider.
Dim cnOleDb As New OleDbConnection
cnOleDb.ConnectionString = "Provider=SQLOLEDB;
                           "Data Source=(local);InitialCatalog=Northwind;..."
cnOleDb.Open()
...
cnOleDb.Close()

'Open and close a connection using the SQL Client .NET Data Provider.
Dim cnSql As New SqlConnection
cnSql.ConnectionString = "Data Source=(local);" & _
                         "Initial Catalog=Northwind;..."
cnSql.Open()
...
cnSql.Close()
Visual C#
//Open and close a connection using the OLE DB .NET Data Provider.
OleDbConnection cnOleDb = New OleDbConnection();
cnOleDb.ConnectionString = "Provider=Provider=SQLOLEDB;
                           "Data Source=(local);InitialCatalog=Northwind;...";
cnOleDb.Open();
...
cnOleDb.Close();

//Open and close a connection using the SQL Client .NET Data Provider.
SqlConnection cnSql = New SqlConnection();
cnSql.ConnectionString = "Data Source=(local);" +
                         "Initial Catalog=Northwind;...";
cnSql.Open();
...
cnSql.Close();

No previous Microsoft data access technology has used separate libraries and classes for different data stores. Many developers have asked why Microsoft has made such a major change. There are three main reasons: performance, extensibility, and proliferation.

Better Performance

How does moving to .NET data providers improve performance? When you write ADO code, you’re essentially using the ADO interfaces as a “middleman” when communicating with your data store. You tell ADO which provider you want to use, and ADO forwards your calls to the appropriate provider. The provider performs the requested action and returns the result to you through the ADO library.

.NET data providers don’t involve a middle layer. You communicate directly with the data provider, which communicates with your data store using the data store’s low-level programming interfaces. Communicating with SQL Server using the SQL Client .NET Data Provider in ADO.NET is faster than using ADO and the SQL Server OLE DB provider because one less layer is involved.

Greater Extensibility

When SQL Server 2000 introduced XML features, the ADO development team faced an interesting challenge. In order to add features to ADO that would let developers retrieve XML data from SQL Server 2000, they had to add new interfaces to the OLE DB API and to the SQL Server OLE DB provider.

.NET data providers are more easily extensible. They need to support only the same basic interfaces and can provide additional provider-specific features when appropriate. The SQL Client .NET Data Provider’s Command object (SqlCommand) exposes all of the same methods and properties that its OLE DB .NET Data Provider counterpart does, but it also adds a method to fetch the results of a query as XML.

Proliferation

Microsoft first shipped OLE DB providers for SQL Server, Microsoft Access, and Oracle with the release of the Microsoft Data Access Components (MDAC) version 2.0 in July 1998. Microsoft and other development teams have created native OLE DB providers to communicate with other data stores, but not a whole lot of OLE DB providers are available. If you’re using ADO but aren’t using a Microsoft-built OLE DB provider, there’s a high probability that you’re using an ODBC (OLE DB’s predecessor) driver instead. Many more ODBC drivers are available, primarily because they were easier to develop. Many developers simply found it too difficult to build their own OLE DB providers.

By comparison, a .NET data provider is simple to write. There are far fewer interfaces to implement. Microsoft simplified the process of building providers for ADO.NET so that developers can build .NET data providers more easily. The more .NET data providers there are, the more different data sources you can access via ADO.NET.

Because each .NET data provider implements the same base interfaces, there’s no need for me to cover using these interfaces for every .NET data provider. Instead, I’ll mostly focus on one provider: the OLE DB .NET Data Provider. I’ve chosen to focus on this provider because it’s included with the .NET Framework, provides basic provider-independent features, and is extremely flexible because it can be used against any database that has a native OLE DB provider. The features of the OLE DB .NET Data Provider covered here are also available through other providers and work the same way unless otherwise noted.

Appendix B will cover the features of the other .NET data providers. Article 12 will use the SQL Client Data Provider and the SQL XML .NET Data Provider to demonstrate the use of some of ADO.NET’s XML features.

Until then, if I’m discussing an object that’s common to all managed providers, I’ll generally refer to it by its provider-independent name—for example, DataAdapter rather than OleDbDataAdapter or SqlDataAdapter.

Connected Objects

The ADO.NET object model includes classes designed to help you communicate directly with your data source. I’ll refer to such objects, which appear to the left of the dotted line in Figure 1-1 (shown earlier), as ADO.NET’s “connected” objects. Most of these objects represent basic data access concepts such as the physical connection to the database, a query, and the query’s results.

A Connection object represents a connection to your data source. You can specify the type of data source, its location, and other attributes through the various properties of the Connection object. A Connection object is roughly equivalent to an ADO Connection object or a DAO Database object; you use it to connect to and disconnect from your database. A Connection object acts as a conduit through which other objects, such as DataAdapter and Command objects, communicate with your database to submit queries and retrieve results.

Command objects are similar in structure to ADO Command or DAO QueryDef objects. They can represent a query against your database, a call to a stored procedure, or a direct request to return the contents of a specific table.

Databases support many different types of queries. Some queries retrieve rows of data by referencing one or more tables or views or by calling a stored procedure. Other queries modify rows of data, and still others manipulate the structure of the database by creating or modifying objects such as tables, views, or stored procedures. You can use a Command object to execute any of these types of queries against your database.

Using a Command object to query your database is rather straightforward. You set the Connection property to a Connection object that connects to your database and then specify the text for your query in the CommandText property. You can supply a standard SQL query such as this one:

SELECT CustomerID, CompanyName, ContactName, Phone FROM Customers

You can also supply just the name of a table, view, or stored procedure and use the Command object’s CommandType property for the type of query you want to execute. The Command object offers different ways to execute your query. If the query does not return rows, simply call the ExecuteNonQuery method. The Command object also has an ExecuteReader method, which returns a DataReader object that you can use to examine the rows returned by your query. The SqlCommand includes a third execution method, ExecuteXmlReader, that is similar to ExecuteReader but is designed to handle queries that return results in XML format.

The DataReader is designed to help you retrieve and examine the rows returned by your query as quickly as possible. You can use the DataReader object to examine the results of a query one row at a time. When you move forward to the next row, the contents of the previous row are discarded. The DataReader doesn’t support updating. The data returned by the DataReader is read-only. Because the DataReader object supports such a minimal set of features, it’s extremely fast and lightweight.

At times, you might want to group a number of changes to your database and treat them as a single unit of work. In database programming, that unit of work is called a transaction. Let’s say your database contains banking information and has tables for checking and savings accounts and a user wants to transfer money from a savings account to a checking account. In your code, you’ll want to make sure that the withdrawal from savings and the deposit to checking complete successfully as a single unit or that neither change occurs. You use a transaction to accomplish this.

The Connection object has a BeginTransaction method that you can use to create Transaction objects. You use a Transaction object to either commit or cancel the changes you make to your database during the lifetime of the Transaction object. In our banking example, the changes to both the savings and checking accounts would be included in a single transaction and, therefore, would be either committed or cancelled as a single unit of work.

Say you want to query your Orders table for all the orders for a particular customer. Your query will look something like this:

SELECT CustomerID, CompanyName, CompanyName, Phone FROM Customers
    WHERE CustomerID = 'ALFKI'

The value you use for the CustomerID column in the query’s WHERE clause depends on the customer whose orders you want to examine. But if you use this type of query, you have to modify the text for the query each time you want to examine the orders for a different customer.

To simplify the process of executing such queries, you can replace the value for the CustomerID column with a parameter marker, as shown in the following query:

SELECT CustomerID, CompanyName, CompanyName, Phone FROM Customers
    WHERE CustomerID = ?

Then, prior to executing the query, you supply a value for the parameter. Many developers rely heavily on parameterized queries because they can help simplify your programming and make for more efficient code.

To use a parameterized Command object, you create Parameter objects for each of the parameters in your query and append them to the Command object’s Parameters collection. The ADO.NET Parameter object exposes properties and methods that let you define the data type and value for your parameters. To work with a stored procedure that returns data through output parameters, you set the Parameter object’s Direction property to the appropriate value from the ParameterDirection enumeration.

The DataAdapter object represents a new concept for Microsoft data access models; it has no true equivalent in ADO or DAO, although you can consider the ADO Command and DAO QueryDef objects to be its second cousins, once removed.

DataAdapter objects act as a bridge between your database and the disconnected objects in the ADO.NET object model. The DataAdapter object’s Fill method provides an efficient mechanism to fetch the results of a query into a DataSet or a DataTable so you can work with your data off line. You can also use DataAdapter objects to submit the pending changes stored in your DataSet objects to your database.

The ADO.NET DataAdapter object exposes a number of properties that are actually Command objects. For instance, the SelectCommand property contains a Command object that represents the query you’ll use to populate your DataSet object. The DataAdapter object also has UpdateCommand, InsertCommand, and DeleteCommand properties that correspond to Command objects you use when you submit modified, new, or deleted rows to your database, respectively.

These Command objects provide updating functionality that was automatic (or “automagic,” depending on your perspective) in the ADO and DAO Recordset objects. For example, when you run a query in ADO to generate a Recordset object, the ADO cursor engine asks the databases for metadata about the query to determine where the results came from. ADO then uses that metadata to build the updating logic to translate changes in your Recordset object into changes in your database.

So why does the ADO.NET DataAdapter object have separate UpdateCommand, InsertCommand, and DeleteCommand properties? To allow you to define your own updating logic. The updating functionality in ADO and DAO is fairly limited in the sense that both object models translate changes in Recordset objects into action queries that directly reference tables in your database. To maintain the security and integrity of the data, many database administrators restrict access to the tables in their databases so that the only way to change the contents of a table is to call a stored procedure. ADO and DAO don’t know how to submit changes using a stored procedure; neither provides mechanisms that let you specify your own updating logic. The ADO.NET DataAdapter does.

With a DataAdapter object, you can set the UpdateCommand, InsertCommand, and DeleteCommand properties to call the stored procedures that will modify, add, or delete rows in the appropriate table in your database. Then you can simply call the Update method on the DataAdapter object and ADO.NET will use the Command objects you’ve created to submit the cached changes in your DataSet to your database.

As I stated earlier, the DataAdapter object populates tables in the DataSet object and also reads cached changes and submits them to your database. To keep track of what goes where, a DataAdapter has some supporting properties. The TableMappings collection is a property used to track which table in your database corresponds to which table in your DataSet object. Each table mapping has a similar property for mapping columns, appropriately called a ColumnMappings collection.

You’ve seen that you can use objects in a .NET data provider to connect to a data source, submit queries, and examine their results. However, these connected classes let you examine data only as a forward-only, read-only stream of data. What if you want to sort, search, filter, or modify the results of your queries?

The ADO.NET object model includes classes to provide such functionality. These classes act as an offline data cache. Once you’ve fetched the results of your query into a DataTable (which we’ll discuss shortly), you can close the connection to your data source and continue to work with the data. As mentioned earlier, because these objects do not require a live connection to your data source we call them “disconnected” objects.

Let’s take a look at the disconnected objects in the ADO.NET object model.

The ADO.NET DataTable object is similar to the ADO and DAO Recordset objects. A DataTable object allows you to examine data through collections of rows and columns. You can store the results of a query in a DataTable through the DataAdapter object’s Fill method, as shown in the following code snippet:

Visual Basic .NET
Dim strSQL As String = "SELECT CustomerID, CompanyName FROM Customers" 
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local);..." Dim daCustomers As New OleDbDataAdapter(strSQL, strConn) Dim tblCustomers As New DataTable() daCustomers.Fill(tblCustomers)
Visual C#
string strSQL = "SELECT
    CustomerID, CompanyName FROM Customers"; string strConn = "Provider=SQLOLEDB;Data Source=(local);..."
OleDbDataAdapter daCustomers = New OleDbDataAdapter(strSQL, strConn);
DataTable tblCustomers = New DataTable();
daCustomers.Fill(tblCustomers);

Once you’ve fetched the data from your database and stored it in a Data­Table object, that data is disconnected from the server. You can then examine the contents of the DataTable object without creating any network traffic between ADO.NET and your database. By working with the data off line, you no longer require a live connection to your database, but remember that you also won’t see any changes made by other users after you’ve run your query.

The DataTable class contains collections of other disconnected objects, which I’ll cover shortly. You access the contents of a DataTable through its Rows property, which returns a collection of DataRow objects. If you want to examine the structure of a DataTable, you use its Columns property to retrieve a collection of DataColumn objects. The DataTable class also lets you define constraints, such as a primary key, on the data stored within the class. You can access these constraints through the DataTable object’s Constraints property.

Each DataTable has a Columns collection, which is a container for DataColumn objects. As its name implies, a DataColumn object corresponds to a column in your table. However, a DataColumn object doesn’t actually contain the data stored in your DataTable. Instead, it stores information about the structure of the column. This type of information, data about data, is called metadata. For example, DataColumn exposes a Type property that describes the data type (such as string or integer) that the column stores. DataColumn has other properties such as ReadOnly, AllowDBNull, Unique, Default, and AutoIncrement that allow you to control whether the data in the column can be updated, restrict what can be stored in the column, or dictate how values should be generated for new rows of data.

The DataColumn class also exposes an Expression property, which you can use to define how the data in the column is calculated. A common practice is to base a column in a query on an expression rather than on the contents of a column in a table in your database. For example, in the sample Northwind database that accompanies most Microsoft database-related products, each row in the Order Details table contains UnitPrice and Quantity columns. Traditionally, if you wanted to examine the total cost for the order item in your data structure, you would add a calculated column to the query. The following SQL example defines a calculated column called ItemTotal:

SELECT OrderID, ProductID, Quantity, UnitPrice,
       Quantity * UnitPrice AS ItemTotal
    FROM [Order Details]

The drawback to this technique is that the database engine performs the calculation only at the time of the query. If you modify the contents of the UnitPrice or Quantity columns in your DataTable object, the ItemTotal column doesn’t change.

The ADO.NET DataColumn class defines an Expression property to handle this scenario more elegantly. When you check the value of a DataColumn object based on an expression, ADO.NET evaluates the expression and returns a newly calculated value. In this way, if you update the value of any column in the expression, the value stored in the calculated column is accurate. Here are two code snippets illustrating the use of the Expression property:

Visual Basic .NET
Dim col As New DataColumn()
...
With col
   .ColumnName = "ItemTotal"
   .DataType = GetType(Decimal)
   .Expression = "UnitPrice * Quantity"
End With
Visual C#
DataColumn col = New DataColumn();
col.ColumnName = "ItemTotal";
col.DataType = typeof(Decimal);
col.Expression = "UnitPrice * Quantity";

The Columns collection and DataColumn objects can be roughly compared to the Fields collection and Field objects in ADO and DAO.

The DataTable class also provides a way for you to place constraints on the data stored locally within a DataTable object. For example, you can build a Constraint object that ensures that the values in a column, or multiple columns, are unique within the DataTable. Constraint objects are maintained in a Data­Table object’s Constraints collection.

To access the actual values stored in a DataTable object, you use the object’s Rows collection, which contains a series of DataRow objects. To examine the data stored in a specific column of a particular row, you use the Item property of the appropriate DataRow object to read the value for any column in that row. The DataRow class provides several overloaded definitions of its Item property. You can specify which column to view by passing the column name, index value, or associated DataColumn object to a DataRow object’s Item property. Because Item is the default property of the DataRow object, you can use it implicitly, as shown in the following code snippets:

Visual Basic .NET
Dim row As DataRow
row = MyTable.Rows(0)
Console.WriteLine(row(0))
Console.WriteLine(row("CustomerID"))
Console.WriteLine(row(MyTable.Columns("CustomerID")))
Visual C#
DataRow row;
row = MyTable.Rows[0];
Console.WriteLine(row[0]);
Console.WriteLine(row["CustomerID"]);
Console.WriteLine(row[MyTable.Columns["CustomerID"]]);

Rather than returning the data for just the current row, the DataTable makes all rows of data available through a collection of DataRows. This is a marked change in behavior from the ADO and DAO Recordset objects, which expose only a single row of data at a time, requiring you to navigate through its contents using methods such as MoveNext. The following code snippet is an example of looping through the contents of an ADO Recordset:

Visual Basic “Classic”
Dim strConn As String, strSQL As String
Dim rs As ADODB.Recordset
strConn = "Provider=SQLOLEDB;Data Source=(local);..."
strSQL = "SELECT CustomerID, CompanyName FROM Customers"
Set rs = New ADODB.Recordset
rs.CursorLocation = adUseClient
rs.Open strSQL, strConn, adOpenStatic, adLockReadOnly, adCmdText
Do While Not rs.EOF
    MsgBox rs("CustomerID")
    rs.MoveNext
Loop

To examine the contents of an ADO.NET DataTable, you Loop through the DataRow objects contained in the DataTable object’s Rows property, as shown in the following code snippet:

Visual Basic .NET
Dim strSQL, strConn As String
...
Dim da As New OleDbDataAdapter(strSQL, strConn)
Dim tbl As New DataTable()
da.Fill(tbl)
Dim row As DataRow
For Each row In tbl.Rows
    Console.WriteLine(row(0))
Next row
Visual C#
string strSQL, strConn;
...
OleDbDataAdapter da = New OleDbDataAdapter(strSQL, strConn);
DataTable tbl = New DataTable();
da.Fill(tbl);
foreach (DataRow row in tbl.Rows)
    Console.WriteLine(row[0]);

The DataRow object is also the starting point for your updates. For example, you can call the BeginEdit method of a DataRow object, change the value of some columns in that row through the Item property, and then call the EndEdit method to save the changes to that row. A DataRow object’s CancelEdit method lets you cancel the changes made in the current editing session. A DataRow object also exposes methods to delete or remove an item from the DataTable object’s collection of DataRows.

When you change the contents of a row, the DataRow object caches those changes so that you can submit them to your database at a later time. Thus, when you change the value of a column in a row, the DataRow object maintains that column’s original value as well as its current value in order to successfully update the database. The Item property of a DataRow object also allows you to examine the original value of a column when the row has a pending change.

A DataSet object, as its name indicates, contains a set of data. You can think of a DataSet object as the container for a number of DataTable objects (stored in the DataSet object’s Tables collection). Remember that ADO.NET was created to help developers build large multi-tiered database applications. At times, you might want to access a component running on a middle-tier server to retrieve the contents of many tables. Rather than having to repeatedly call the server in order to fetch that data one table at a time, you can package all the data into a DataSet object and return it in a single call. But a DataSet object does a great deal more than act as a container for multiple DataTable objects.

The data stored in a DataSet object is disconnected from your database. Any changes you make to the data are simply cached in each DataRow. When it’s time to send these changes to your database, it might not be efficient to send the entire DataSet back to your middle-tier server. You can use the GetChanges method to extract just the modified rows from your DataSet. In this way, you pass less data between the different processes or servers.

The DataSet also exposes a Merge method, which can act as a complement to the GetChanges method. The middle-tier server you use to submit changes to your database, using the smaller DataSet returned by the Merge method, might return a DataSet that contains newly retrieved data. You can use the DataSet class’s Merge method to combine the contents of two DataSet objects into a single DataSet. This is another example that shows how ADO.NET was developed with multi-tiered applications in mind. Previous Microsoft data access models have no comparable feature.

You can create a DataSet object and populate its Tables collection with information without having to communicate with a database. In previous data access models, you generally need to query a database before adding new rows locally, and then later submit them to the database. With ADO.NET, you don’t need to communicate with your database until you’re ready to submit the new rows.

The DataSet object also has features that allow you to write it to and read it from a file or an area of memory. You can save just the contents of the DataSet object, just the structure of the DataSet object, or both. ADO.NET stores this data as an XML document. Because ADO.NET and XML are so tightly coupled, moving data back and forth between ADO.NET DataSet objects and XML documents is a snap. You can thus take advantage of one of the most powerful features of XML: its ability to easily transform the structure of your data. For example, you can use an Extensible Stylesheet Language Transformation (XSLT) template to convert data exported to an XML document into HTML.

The tables in your database are usually related in some fashion. For example, in the Northwind database, each entry in the Orders table relates to an entry in the Customers table, so you can determine which customer placed which orders. You’ll probably want to use related data from multiple tables in your application. The ADO.NET DataSet object is designed to handle this through the DataRelation object.

The DataSet class defines a Relations property, which is a collection of DataRelation objects. You can use a DataRelation object to indicate a relationship between different DataTable objects in your DataSet. Once you’ve created your DataRelation object, you can use code such as the following to retrieve an array of DataRow objects for the orders that correspond to a particular customer:

Visual Basic .NET
Dim dsNorthwind As DataSet
Dim rowCustomer, rowOrder As DataRow

'The code for creating the DataSet goes here.

dsNorthwind.Relations.Add("CustomersOrders", _
                      dsNorthwind.Tables("Customers").Columns("CustomerID"), _
                      dsNorthwind.Tables("Orders").Columns("CustomerID"))

For Each rowCustomer In dsNorthwind.Tables("Customers").Rows
    Console.WriteLine("Orders for customer " & rowCustomer("CompanyName"))
    For Each rowOrder In rowCustomer.GetChildRows("CustomersOrders")
        Console.WriteLine(vbTab & rowOrder("OrderID"))
    Next rowOrder
Next rowCustomer
Visual C#
DataSet dsNorthwind;

//Create and initialize DataSet.

dsNorthwind.Relations.Add("CustomersOrders", 
                      dsNorthwind.Tables["Customers"].Columns["CustomerID"], 
                      dsNorthwind.Tables["Orders"].Columns["CustomerID"]);

foreach (DataRow rowCustomer in dsNorthwind.Tables["Customers"].Rows)
{
    Console.WriteLine("Orders for customer " + 
                      rowCustomer["CompanyName"].ToString());
    foreach (DataRow rowOrder in rowCustomer.GetChildRows("CustomersOrders"))
        Console.WriteLine('\t' + rowOrder["OrderID"].ToString());    
}

DataRelation objects also expose properties that allow you to enforce referential integrity. For example, you can set a DataRelation object so that if you modify the value of the primary key field in the parent row, the change cascades down to the child rows automatically. You can also set your DataRelation object so that if you delete a row in one DataTable, the corresponding rows in any child DataTable objects, as defined by the relation, are automatically deleted as well.

Once you’ve retrieved the results of a query into a DataTable object, you can use a DataView object to view the data in different ways. If you want to sort the contents of a DataTable object based on a column, simply set the DataView object’s Sort property to the name of that column. You can also use the Filter property on DataView so that only the rows that match certain criteria are visible.

You can use multiple DataView objects to examine the same DataTable at the same time. For example, you can have two grids on a form, one showing all customers in alphabetical order, and the other showing the rows ordered by a different field, such as state or region. To show each view, you bind each grid to a different DataView object, but both DataView objects reference the same DataTable. This feature prevents you from having to maintain two copies of your data in separate structures.

ADO and DAO allow you to create a Recordset based on the results returned by your query. The data access engine examines the columns of data in the result set and populates the Recordset object’s Fields collection based on this information, setting the name, data type, and so forth.

ADO.NET offers you a choice. You can use just a couple lines of code and let ADO.NET determine the structure of the results automatically, or you can use more code that includes metadata about the structure of the results of your query.

Why would you choose the option that involves writing more code? The main benefits are increased functionality and better performance. But how could having more code make your application run faster? That seems counterintuitive, doesn’t it?

Unless you’re writing an ad-hoc query tool, you’ll generally know what the structure of your query results will look like. For example, most ADO code looks something like the following:

Dim rs as Recordset
'Declare other variables here.



'Initialize variables and establish connection to database.



rs.Open strSQL, cnDatabase, adOpenStatic, adLockOptimistic, adCmdText
Do While Not rs.EOF
    List1.AddItem rs.Fields("UserName").Value
    rs.MoveNext
Loop

In this code snippet, the programmer knows that the query contains a column named UserName. The point is that as a developer, you generally know what columns your query will return and what data types those columns use. But ADO doesn’t know what the results of the query will look like ahead of time. As a result, ADO has to query the OLE DB provider to ask questions such as “How many columns are there in the results of this query?,” “What are the data types for each of those columns?,” “Where did this data come from?,” and “What are the primary key fields for each table referenced in this query?” The OLE DB provider can answer some of these questions, but in many cases it must call back to the database.

To retrieve the results of your query and store this data in a DataSet object, ADO.NET needs to know the answers to such questions. You can supply this information yourself or force ADO.NET to ask the provider for this information. Your code will run faster using the former option because ADO.NET won’t have to ask the provider for this information at run time.

Writing code to prepare the structure for your DataSet can become tedious, even if it improves the performance of your application. Thankfully, Visual Studio .NET includes design-time data-access features that offer the best of both worlds. For example, you can create a DataSet object based on a query, a table name, or a stored procedure, and then a configuration wizard will generate ADO.NET code to run the query and support submitting updates back to your database. We’ll take a close look at many of these Visual Studio features in upcoming Articles.

Visual Studio .NET also helps you simplify the process of building data-access applications by generating strongly typed DataSet. Let’s say we have a simple table named Orders that contains two columns, CustomerID and CompanyName. You don’t have to write code such as shown below.

Visual Basic .NET
Dim ds As DataSet
'Create and fill DataSet.
Console.WriteLine(ds.Tables("Customers").Rows(0)("CustomerID"))
Visual C#
DataSet ds;
//Create and fill DataSet.
Console.WriteLine(ds.Tables["Customers"].Rows[0]["CustomerID"]);

Instead, we can write code like this:

Visual Basic .NET
Dim ds As CustomersDataSet
'Create and fill DataSet.
Console.WriteLine(ds.Customers(0).CustomerID)
Visual C#
CustomersDataSet ds;
//Create and fill DataSet.
Console.WriteLine(ds.Customers[0].CustomerID);

The strongly typed DataSet is simply a class that Visual Studio builds with all the table and column information available through properties. Strongly typed DataSet objects also expose custom methods for such features as creating new rows. So instead of code that looks like the following:

Visual Basic .NET
Dim ds as DataSet
'Code to create DataSet and customers DataTable
Dim rowNewCustomer As DataRow
rowNewCustomer = ds.Tables("Customers").NewRow()
rowNewCustomer("CustomerID") = "ALFKI"
rowNewCustomer("CompanyName") = "Alfreds Futterkiste"
ds.Tables("Customers").Rows.Add(rowNewCustomer)
Visual C#
DataSet ds;
//Code to create DataSet and customers DataTable
DataRow rowNewCustomer;
rowNewCustomer = ds.Tables["Customers"].NewRow();
rowNewCustomer["CustomerID"] = "ALFKI";
rowNewCustomer["CompanyName"] = "Alfreds Futterkiste";
ds.Tables["Customers"].Rows.Add(rowNewCustomer);

We can create and add a new row to our table in a single line of code, such as this:

ds.Customers.AddCustomersRow("ALFKI", "Alfreds Futterkiste")

You can add a number of different types of objects to a Visual Studio .NET project, such as forms, class modules, and code modules. With Visual Basic .NET and C#, you can add a data-bound form. When you add a data-bound form to your project, Visual Studio automatically launches the Data Form Wizard.

To help you build your data-bound form, the wizard prompts you for information about the database, tables in the database, columns in the table, and so forth, in a series of pages. We’ll step through the wizard to build a sample data-bound form.

First in Visual Studio .NET, choose New, Project from the File menu or click the New Project toolbar button to open the New Project dialog box. Select your language of choice in the Project Types pane—Visual Basic or Visual C#—and then select the Windows Application icon, as shown in Figure 2-1. In the text box below the icons, change the name of the project you’re about to create to Article2, and then click OK.

Building a data-bound Web form involves more complex concepts, which we’ll cover in Article 14. For simplicity, we’ll focus on building a data-bound Windows form in this Article.
 

Figure 2-1

Creating a new Windows application

Now that you have your new project set up, you can use the Data Form Wizard to create a new data-bound form. Because using the wizard results in a new form in your project, the wizard is available through the Add New Item dialog box, which you open by choosing Add New Item from the File menu or clicking the Add New Item toolbar button in Visual Studio .NET. Select the Data Form Wizard icon, shown in Figure 2-2, and then click OK.

 

Figure 2-2

Launching the Data Form Wizard from the Add New Item dialog box

The wizard will launch and display a welcome page, shown in Figure 2-3.

 
Figure 2-3
The Data Form Wizard’s welcome page

Click Next to go to the next page, which offers you a choice of using an existing DataSet object or a new one. You haven’t created a DataSet yet, so you’ll create a new one, as shown in Figure 2-4.

 
Figure 2-4
Creating a new DataSet object for your new form

Your DataSet object’s name should indicate the type of data contained in your DataSet. In this example, the DataSet will store customer information and orders tables in the Northwind database. To name the new DataSet, type CustomersAndOrders in the text box and then click Next.

The Data Form Wizard builds strongly typed DataSet objects, which are new class files in your project. Always be sure that the name you type for your new DataSet is a valid class name—that is, one that begins with a letter and includes only letters, numbers, and underscore characters.

Now it’s time to connect the Data Form Wizard to your database. The Choose A Data Connection page, shown in Figure 2-5, allows you to add new connections or select a connection that’s available in the Server Explorer window. If you haven’t already created a connection to your database, click the New Connection button and the OLE DB Data Link Properties dialog box will appear.

 
Figure 2-5
The wizard’s Choose A Data Connection page

On the Connection tab of the Data Link Properties dialog box, you can specify your connection. By default, it is set to connect to Microsoft SQL Server. To connect to your database, specify a server name (or type (local) to communicate with an instance of SQL Server running on your machine), a username, password, and a database name, as shown in Figure 2-6.


Figure 2-6
The Connection tab of the Data Link Properties dialog box

If multiple instances of SQL Server 2000 are installed on a machine, you can indicate which instance you want to use by specifying the server name, a back slash, and then the instance name—for example, MyServerName\MyInstance.

 

Notice that the Allow Saving Password check box is selected in the figure. By default, this check box is deselected. If you enter a password using the default settings, Visual Studio .NET will receive the entire connection string except for the password. As a result, you’ll be prompted for the password at various times while you access the database at design time. I prefer to select this check box to remove the password from the connection string in my code. Select the check box for now. You’ll learn more about this feature in Article 3.

If you want to connect to a database other than SQL Server, click on the Provider tab to select a different OLE DB provider. As shown in Figure 2-7, this tab lists all the OLE DB providers installed on your machine. I’ll discuss using other providers with the Data Link Properties dialog box in more depth in the next Article.


Figure 2-7
The Provider tab of the Data Link Properties dialog box

If you don’t have access to a SQL Server or MSDE database but do have an Access version of the Northwind database, select the Microsoft Jet 4.0 OLE DB provider.

Click Next to move to the Connection tab of the dialog box, as shown in Figure 2-8.


Figure 2-8
Specifying an Access database on the Connection tab of the Data Link Properties dialog box

Type the path to your Access database in the first text box, or click the ellipsis button to the right of the text box to open a dialog box in which you can select the database from your hard drive or network. Type a username and a password in the appropriate text boxes, and select the password check boxes (if desired) for the connection. Click OK.

Selecting Tables from Your Database

The next Data Form Wizard page, shown in Figure 2-9, lists the tables, views, and stored procedures that are available in your database’s schema. Although tables and views are treated as different structures in different database systems, the output from either type of object is mapped to an ADO.NET DataTable. Thus, the wizard refers to all the objects as tables.

Select the tables you want to include in your DataSet in the Available Items list and move them to the Selected Items list by clicking the right arrow button. If you make a mistake and need to remove a table from the Selected Items list, select the table to remove and click the left arrow button. You can also select and deselect tables by double-clicking them.

 
Figure 2-9
Selecting tables in the Data Form Wizard

For the purposes of our sample application, select the Customers and Orders tables from the Northwind database. Then click Next.

Creating Relationships in Your DataSet

If you’ve selected more than one table, the Data Form Wizard will display a page in which you can create relationships between the tables you selected. As you learned in Article 1, you can use relationships to easily locate data in related tables. Relationships also help enforce referential integrity rules by cascading changes from one table to another.

Adding a relationship to your DataSet using the wizard is simple. The most challenging part of the process might be naming the relationship. As a general guideline, combine the name of the parent and child tables (in that order) to create the name.

Here we’ll relate the Customers and Orders tables you selected on the previous page. Name the relationship CustomersOrders.

The data in the two tables are related. Each entry in the Orders table relates to an entry in the Customers table. In other words, each order belongs to a particular customer. Because each customer row has order rows associated with it, the Customers table is the parent table in the relationship.

Select the Customers table as the Parent Table in the relationship and the Orders table as the Child Table in the relationship. The CustomerID defines the relationship between the two tables. Select CustomerID as the key field for each table, as shown in Figure 2-10. Click the right arrow button to add the relationship to the Relations list, and then click Next.

 
Figure 2-10
Creating relationships between tables
Selecting Columns to Display

On the next wizard page (shown in Figure 2-11), you can select the columns to display on the data-bound form. When you selected the tables to include in your DataSet, you didn’t have the option to specify which columns to include. The Data Form Wizard retrieved all rows and all columns in the selected tables.

 
Figure 2-11
Choosing the tables and columns to display on your data-bound form

You can display a single table or two tables in a master/detail relationship. Once you select the table or tables to display, you’ll see all the available columns for the selected tables. By default, all the columns are selected, but you can deselect any columns you don’t want to have appear on the data-bound form.

When this page of the wizard appears, you’ll see that the Customers table is already selected as the parent table and the Orders table is selected as the child table. This behavior is a pleasant result of the wizard’s defaults. The wizard automatically selects the first table by alphabetical order as the parent table. Because “Customers” comes before “Orders,” the Customers table is selected by default. The Customers table has only one related table, Orders, so Orders is selected by default as the child table.

To simplify the display of your form, select just the CustomerID, Company­Name, ContactTitle, and Phone fields from the Customers table and the OrderID, CustomerID, EmployeeID, and OrderDate fields from the Orders table. Then click Next.

Choosing a Display Style

The next wizard page provides a couple of options for showing the contents of the main table on data-bound Windows forms. You can display contents of the main table in a grid, which will allow the user to see multiple rows at the same time, or you can display the rows one at a time in a series of bound controls such as text boxes.

You’ll have more options if you choose to display the contents of the main table one row at a time. The wizard lets you decide whether you want to include buttons that allow the user to navigate back and forth through the contents of the table, cancel pending changes on a row, or add and remove rows. For the purpose of this exercise, select the Single Record In Individual Controls option and all the check boxes that follow as shown in Figure 2-12.

 

]
Figure 2-12

Selecting the Single Record In Individual Controls display style
If you were building a Web application, the wizard would create DataGrid objects, which would convert the data in your tables to HTML tables to display in the browser on your Web form.

That’s it. Click Finish to build your new data-bound form.

Using the New Data-Bound Form

Figure 2-13 shows the data-bound form that the Data Form Wizard created.

 
Figure 2-13
Your new data-bound form

If you build and run the project right now, you won’t see the new data-bound form. Unless you’ve changed the properties for the project, the form that you’ll see when you run the project is the form that was initially included with the project. To change the setting so the project starts with the new form, choose Properties from the Project menu in Visual Studio .NET or right-click on the project in Solution Explorer and choose Properties from the shortcut menu. You’ll see the project’s Property Pages dialog box. Change the project’s Startup Object property to the new data-bound form, DataForm1 as shown in Figure 2‐14. Click OK to save your changes.

 
Figure 2-14
The Property Pages dialog box for the Article2 project
Showing Data in Your New Data-Bound Form

To run the project and see your new data-bound form, press F5, click the Debug menu and then click Start, or click the Start button on the toolbar. You’ll see that the form has labels and text boxes for each field in the Customers table as well as a data-bound grid to display the contents of the Orders table. However, the form does not show any data. All the controls are empty. The form has created a DataSet object with the tables and relationship you defined in the pages of the Data Form Wizard, but that DataSet does not yet contain data. Click the Load button in the upper-left corner of the form to see the data in your controls, as shown in Figure 2-15.

 

Figure 2-15

Viewing the data on the new data-bound form
A Behind-the-Scenes Look at the Wizard-Generated Code

To look at the code that your project runs when you click the Load button, close the form and return to the Visual Studio .NET development environment. Double-click the Load button, and you’ll see that the code in the button’s Click event calls the LoadDataSet procedure. Scroll down to the definition of this procedure and you’ll see that it calls another procedure, FillDataSet. If you examine the code in this procedure, you’ll find that it calls the Fill method on two OleDbDataAdapter objects, as shown in Figure 2-16.

 
Figure 2-16
Wizard-generated code for filling your DataSet

Calling the Fill method of the OleDbDataAdapter object executes the query stored in the SelectCommand property in the DataAdapter and stores the results in the DataSet or DataTable objects specified. The Data Form Wizard created these DataAdapter objects to fetch the contents of each table that you chose to display. The SelectCommand for each DataAdapter contains a query in the following format:

SELECT Field1, Field2, ... , FieldN FROM MyTable

Most of the buttons on the form are self-explanatory—for example, clicking the Add button adds a new customer row and clicking the Delete button deletes the current customer row. The buttons with the arrows are navigation controls that take you to the first, last, previous, and next records.

Click the right arrow button to move to the next customer. The text boxes on the data-bound form will display information about the next customer. Because of the DataRelation you built, the grid will show just the orders for this particular customer. This relation has other features that you can use on the form.

Using the navigation buttons, move to the first customer that has child orders. Unless you’ve modified the contents of the tables in the sample database, the first customer, whose CustomerID setting is ALFKI, will have several orders. Change the value in the text box for the CustomerID field to Chap2. Don’t worry, this won’t affect the contents of the database. Next, move to the next record and then back to this record. Look at the contents of the grid. The CustomerID field for all the orders will be set to Chap2, the value you just entered. The DataRelation object we built instructed the DataSet object to cascade the change to the CustomerID column in the customer row to all the related order rows. If you were to delete the current customer from the DataSet, the DataSet would delete all the related orders as well.

Submitting Changes to Your Database

I mentioned earlier that the change to the data in the DataSet object would not affect the contents of the database. To verify this claim, close the form and rerun the project. Load the data, and then use the navigation buttons to move to the customer that you previously modified. You’ll see that the CustomerID field is still set to its original value rather than to Chap3. As I mentioned in Article 1, the data in the DataSet object is disconnected from the database. Changes you make to the DataSet object do not directly affect the data in your database. ADO.NET does provide features that allow you to submit changes to your database, but how do you use them? In order to submit the changes back to the database, you have to use some additional functionality from the ADO.NET DataAdapter class. We’ll explore that next.

Let’s make another change to a customer. Move to the first customer by clicking on the double left arrow button. Add X to the end of the company name. Click the right arrow button to move to the next row. Then click the left arrow button to move back to the modified row. You’ll see that the change is still stored in the DataSet object. If you were to close the form and rerun the project, you’d lose this modification.

To submit the name change, click the Update button. You might see the mouse cursor momentarily change from an arrow to an hourglass and back while the form runs the code to send the change to your database. After the cursor returns to its normal arrow, close the form and rerun the project. When you click on the Load button, you’ll see the modified company name.

What code did the Update button use to submit the change to the database? To see it, close the form and return to the Visual Studio .NET development environment. Double-click the Update button to see the code contained in its Click event. The wizard-generated code creates two procedures to update your database—UpdateDataSet and UpdateDataSource. The Update button’s click event calls the UpdateDataSet procedure, which, in turn, calls UpdateDataSource. Look at the definition for these procedures of the form, and you’ll see the code shown in Figures 2-17 and 2-18.

 

 Figure 2-17

The UpdateDataSet procedure generated by the Data Form Wizard
 

 Figure 2-18

The UpdateDataSource procedure generated by the Data Form Wizard

This might look like complex code, but it’s really very straightforward. For now, we’ll focus on the general process and look at a couple lines of code at a time. In Article 10 and Article 11, we’ll take a closer look at the process of sending updates to your database. Remember that the wizard generated this code in order to handle whatever changes you make to the data on the form. This code handles modified rows, newly created rows, and deleted rows in each table. The three methods that the wizard uses to control the update process are GetChanges, Update, and Merge.

Rather than discuss these methods in the order in which they appear in the code, I’d like to focus first on the Update method—the most critical part of the process.

In Article 1, you learned that the DataAdapter object acts as a bridge between the DataSet object and the database. The DataAdapter object’s Update method submits changes stored in the DataSet to the database. Each DataAdapter object corresponds to one of the DataTable objects in our DataSet. In order to submit changes stored in both DataTable objects, you have to call the Update method on both DataAdapter objects.

When you call the Update method on a DataAdapter object, you must specify what data you want to submit to the database. The DataAdapter object is flexible and can accept a number of different structures in its Update method. The code generated by the Data Form Wizard uses a DataSet object, but you can also submit a DataTable object or an array of DataRow objects to the Update method.

The DataAdapter object examines the contents of the data structure to determine which rows it can handle. For example, the DataAdapter that the Data Form Wizard built based on the Customers table will look only at the DataTable that corresponds to the Customers table. The DataAdapter object knows which table to examine because of its TableMappings collection, which I described briefly in Article 1.

When the DataAdapter object detects a modified row, it determines the type of change—insert, update, or delete—and submits it to the database based on this type. If the row has been modified, the DataAdapter object executes the DataCommand stored in its UpdateCommand property, using the current contents of the row. Similarly, the DataAdapter object uses its InsertCommand to submit new rows and its DeleteCommand to delete rows.

Isolating Modified Rows

The code in the form’s UpdateRowSource function calls the GetChanges method on the DataSet. The GetChanges method generates a new DataSet object named objDataSetChanges that contains only modified rows. The GetChanges method accepts an optional parameter that you can use to indicate whether you want all changes or just a specific type of change—inserts, updates, or deletes.

There’s actually no need for the Data Form Wizard to use the GetChanges method to create a new DataSet object that contains only modified rows. If you call a DataAdapter object’s Update method and the DataSet you supply as a parameter contains unmodified rows, those rows are simply ignored. So when would you use GetChanges?

The Data Form Wizard generates two-tier applications. The client application communicates directly with your database. If you build a multi-tiered application that uses Web services or COM+ components that run on a middle-tier server, you’ll want to limit the amount of data that you pass back and forth between machines. The less data you have to pass across the wire, the faster your application will run.

If the client application has a DataSet that contains modified rows to submit to the database in such a multi-tiered application, the client sends data to the middle. There’s no need to send unmodified rows back to the middle tier. Intelligent use of the GetChanges method can dramatically improve the performance of multi-tiered ADO.NET applications.

The Data Form Wizard builds two-tier applications, but the code that it generates is appropriate for multi-tiered applications.

When the DataAdapter object examines a modified row and successfully submits the pending change to the database, it marks the row as no longer containing a pending change. This way, the DataAdapter object doesn’t send the same change to the database over and over again on subsequent calls to its Update method.

Earlier we discussed the DataSet class’s GetChanges method. The code uses the DataSet object returned by the GetChanges method in the call to the DataAdapter object’s Update method.

When the updates succeed, the DataAdapter objects mark the appropriate rows in the objDataSetChanges as having successfully updated the database. But the objDataSetChanges is separate from the main DataSet for the form. Somehow, we need to merge the changes that the DataAdapter objects made to the objDataSetChanges back into our main DataSet.

The DataSet class has a Merge method that you can use to merge data from two DataSet objects. If the rows in the DataSet objects represent different rows, ADO.NET simply places all the rows into the DataSet whose Merge method you called. In this case, the rows in the objDataSetChanges reference the same data in our main DataSet. We want the rows in the objDataSetChanges to overwrite the corresponding rows in the main DataSet. ADO.NET compares the primary key values stored in the rows to determine which rows represent the same row of data. By default, ADO.NET overwrites the row in the DataSet whose Merge method you’ve called. Thus, the changes that the DataAdapter objects make to the dsDelta DataSets are passed along to our main DataSet, and we can handle subsequent updates successfully.

Before moving on, I want to draw your attention to another feature of the Visual Studio .NET development environment. Developers who’ve used earlier versions of Visual Studio products might be a little surprised when they look at the designer for the data-bound form in the development environment. One of the first questions that Visual Basic 6 developers ask is, “What is that section below my form?”

If you look below the form in Figure 2-19, you’ll see the component tray. Visual Studio .NET allows you to drag items from the Toolbox onto designers. Many developers use the Toolbox to draw buttons on forms. But not all components are visible at run time.

 

Figure 2-19

The Visual Studio .NET component tray

In Visual Basic 6, for example, you can place timer controls and common dialog box controls on a form. These controls aren’t really visible at run time, however. They are components whose properties you can set at design time using the Properties window, but they don’t have a visual user interface element. When you place such a control on a form in Visual Basic 6, you see an icon on the form, but when you run the form you don’t actually see the control.

Visual Studio .NET places such nonvisual components in the component tray, which is located under the form. To access their properties from the Properties window, you can click items in the component tray. In the new form’s components tray, you’ll see the items for our DataConnection, DataAdapter, and DataSet objects.

You can drag components from the Data tab of the Toolbox onto the form or the component tray. Some components, such as the DataAdapter, are associated with configuration wizards. Drag an OleDbDataAdapter onto the form, and you’ll see a wizard that helps you set properties on your new DataAdapter that allow you to communicate with your database. The wizard generates code based on your input in a similar fashion as the Data Form Wizard. The object variables in your component tray are initialized in a hidden area of the form’s code. Open the section of the code marked “Windows Form Designer generated code,” shown in Figure 2-20. This section contains all designer-generated code. Scroll down past the object declarations in this region, and you’ll see the InitializeComponent procedure that contains the code that the DataAdapter Configuration Wizard generates. Creating components using these rapid application development (RAD) features can save you a great deal of development time.

 

Figure 2-20

The region of the class that contains “hidden” designer-generated code
The Data Form Wizard as a Starting Point

Congratulations! You’ve just built a working database application.

Of course, if building database applications were this easy, this would be the final Article of the book. This application is very simplistic. You probably would not want to deploy it on a network with a large number of users. Imagine every user selecting every row and every column in your database. Yikes! That’s a lot of network traffic.

The Data Form Wizard is more valuable as a tool for creating a starting point than as a tool for creating finished solutions. It’s a great tool for learning about ADO.NET because you can look at the code it generates, as you’ve done in this Article.

Before moving to the next Article to learn more about the DataSet object, take a few minutes to review the code the Data Form Wizard generated in our simple application. Don’t worry about understanding what all that code does right now. Simply look at the amount of code that’s there.

Although the wizards in Visual Studio .NET might not generate an entire application that you can deploy as-is, they can help shorten development time by generating a lot of code that you can use in your application. In subsequent Articles, I’ll cover many of these tools and look at the code they generate, pointing out why the code might or might not be suitable for your applications.

Getting Connected: Using a .NET Data Provider

Part of building a database application requires that you connect to your data source and manage that connection. In the ADO.NET object model, the Connection object represents a connection to your data source.

This Article will serve as your guide to the ADO.NET Connection object by covering the major features of the object. In the process, we’ll learn how to create and use Connection objects both in code and through the Visual Studio .NET development environment.

Throughout the Article, we’ll focus on the more generic of the two Connection objects that reside in the Microsoft .NET Framework, the OleDbConnection object. Unless otherwise noted, the features of the object are generic and apply to all Connection objects regardless of their .NET Data Provider.

Code snippets assume that you have used the appropriate construct to reference the System.Data, System.Data.OleDb, and System.Data.SqlClient namespaces. The following lines of code (Visual Basic .NET and Visual C#. NET, respectively) must appear at the beginning of your code modules. For more information on these constructs, please see the documentation of your language of choice.

In Visual Basic .NET, the code would look like this:

Imports System.Data
Imports System.Data.OleDb
Imports System.Data.SqlClient

In Visual C#. NET, the code would look like this:

using System.Data;
using System.Data.OleDb;
using System.Data.SqlClient;

You can use the properties of the Connection object to specify user credentials and the location of your data source. The Connection object’s methods allow you to control when you connect to and disconnect from your data source. You can also use the Connection object as a starting point for creating Command and Transaction objects. Let’s look at how you can create and use connections in your application by working with the Connection object in code.

Creating Connection Objects

There are two ways to create connections using OleDbConnection at runtime. You can simply create a new uninitialized OleDbConnection object, as shown here:

Visual Basic .NET
Dim cn As OleDbConnection
cn = New OleDbConnection()
Visual C# .NET
OleDbConnection cn;
cn = New OleDbConnection();

Or, you can initialize an OleDbConnection object using the class’s constructor.

The Microsoft .NET Framework supports constructors, a feature not available in classic Component Object Model (COM) programming. You can think of a constructor as a class method that you call when you initialize an object. The constructor generally accepts parameters that correspond to the most commonly used property or properties on the class. For example, the OleDbConnection class defines a constructor that accepts a value for the ConnectionString property of the OleDbConnection object that it creates.

The following code snippets are equivalent. For each language example, there are two code segments. The first instantiates an OleDbConnection and then initializes it, the second initializes the object as it’s created by passing a parameter to its constructor:

Visual Basic .NET
Dim strConn As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As OleDbConnection
cn = New OleDbConnection()
cn.ConnectionString = strConn

is equivalent to

Dim strConn As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As OleDbConnection
cn = New OleDbConnection(strConn)
Visual C# .NET
string strConn;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn;
cn = New OleDbConnection();
cn.ConnectionString = strConn;

is equivalent to

string strConn;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn;
cn = New OleDbConnection(strConn);

Visual Basic .NET and C# also let you initialize variables as you declare them. We can combine this feature with the constructor to simplify the code snippets above to declare, instantiate, and initialize our objects in a single line of code, as shown here:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;" 
Dim cn As New OleDbConnection(strConn)
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
Connection Strings

In the code snippets above, we supplied a connection string for our new OleDbConnection objects. A connection string consists of a series of name-value pairs delimited by semicolons:

strConn = "Setting1=Value1;Setting2=Value2;..."

The settings and values depend on the data source you want to connect to, as well as on the technology you’re using to connect to your data source.

The OLE DB .NET data provider is extremely flexible when it comes to connecting to databases, and it provides a variety of ways to build a connection string. Let’s take a quick look at building connection strings for the three most commonly used OLE DB providers: the Microsoft OLE DB providers for Microsoft Access, Microsoft SQL Server, and Oracle databases.

If you’re connecting to a SQL Server database, you can specify the native OLE DB provider, the location of your SQL Server, and the database you want to use, as well as a username and password:

Provider=SQLOLEDB;Data Source=MyServer;Initial Catalog=MyDatabase;
    User ID=MyUID;Password=MyPassword;

Starting with SQL Server 2000, you can have multiple instances of SQL Server installed on the same machine. You can specify the instance you want to connect to by using the following syntax in the Data Source attribute:

Provider=SQLOLEDB;Data Source=MyServer\MyInstance;
    Initial Catalog=MyDatabase;User ID=MyUID;Password=MyPassword;

If you want to connect to SQL Server using your network credentials, use the Integrated Security attribute and omit the username and password:

Provider=SQLOLEDB;Data Source=MyServer;Initial Catalog=MyDatabase;
    Integrated Security=SSPI;

Some old habits are hard to break. When connecting to SQL Server using the previous technology (ODBC), you can use your network credentials by using the Trusted_Connection attribute. The SQL Server OLE DB provider accepts this same attribute as an alias for Integrated Security. I continue to use this slightly older syntax primarily because the value Yes is easier to remember than SSPI:

Provider=SQLOLEDB;Data Source=MyServer;
    Initial Catalog=MyDatabase;Trusted_Connection=Yes;

See the Microsoft Data Access SDK for the full list of options available through this provider.

Developers who want to use ADO.NET with Oracle databases need to do a little more than just install ADO.NET and build a connection string. Both the Microsoft OLE DB Provider for Oracle and the Microsoft ODBC Driver for Oracle communicate with Oracle’s client components rather than directly with the Oracle database. In order to use ADO.NET with Oracle, you have to install the appropriate version of the Oracle client utilities (SQL*Net) and create a database alias. Then you can use a connection string such as this:

Provider=MSDAORA;Data Source=MyDatabaseAlias;
    User ID=MyUID;Password=MyPassword;

If you’re looking to learn more about any of the above database provider options, see the documentation for these OLE DB providers in the Microsoft Data Access SDK.

If you’re connecting to an Access database, you can use the OLE DB provider for Access databases, Microsoft Jet 4.0 OLE DB Provider. To use this provider, you specify the provider name and version and the location of your database in the connection string, as follows:

Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Path\To\MyDatabase.MDB;

If you don’t specify the entire path to your database, ADO will look for the database in your application’s working path. You can also use relative paths. For example, if your database is in the Data subdirectory of your application, you can use the following connection string:

Provider=Microsoft.Jet.OLEDB.4.0;
    Data Source=Data\MyDatabase.MDB;

A number of other options are available when you connect using the Jet OLE DB provider. See the Microsoft Data Access SDK for the exhaustive list. I’ll show you examples of the two most commonly used options here. One option is connecting to an Access database that uses Jet security:

Provider=Microsoft.Jet.OLEDB.4.0;
    Data Source=C:\...\MySecure.MDB;
    Jet OLEDB:System database=C:\...\MySystem.MDW;
    User ID=MyUserName;Password=MyPassword;

Another option is connecting to an Access database that has a database password:

Provider=Microsoft.Jet.OLEDB.4.0;
    Data Source=C:\...\MyPasswordProtected.MDB;
    Jet OLEDB:Database Password=MyPassword;

Developers who have used ADO might be familiar with the OLE DB Provider for ODBC Drivers, which is often referred to by its code name, Kagera. Until version 2, this was the only provider included with the Microsoft Data Access Components. Kagera acts as a bridge from OLE DB to the earlier data access technology, ODBC, by translating OLE DB API calls to ODBC API calls. This provider enabled developers to use ADO to talk to ODBC drivers.

Using the OLE DB .NET data provider to talk to this OLE DB provider to then talk to ODBC drivers might sound overly complex. It is. This is why the Microsoft development team developed the ODBC .NET data provider, which I’ll cover in Appendix A.

If you want to communicate with your data source through an ODBC driver, use the ODBC .NET data provider. Attempts to use Kagera with the OLE DB .NET data provider will generate an exception.

Using Data Links to Build Connection Strings in Code

If you want to build connection strings in code, you can use the same user interface that Visual Studio 6 and Visual Studio .NET use—the Data Links dialog box. This tabbed dialog box lets you select an OLE DB provider and then enter values for a data source, user name, password, and other provider-specific attributes. You might remember this dialog box from when we created a new Connection using the Data Form Wizard in Article 2.

To use the Data Links dialog box in your Visual Studio .NET application, you must first add a reference to the Data Link’s library. From your project, right-click on your project in Solution Explorer and choose Add Reference. Click on the COM tab of the Add Reference dialog box and add references to Microsoft ActiveX Data Objects 2.7 Library (commonly referred to as ADO) and the Microsoft OLE DB Service Component 1.0 Type Library. See Figure 3-2.

These libraries contain COM components. When you add a reference to them, Visual Studio .NET will ask whether you want a wrapper generated for the libraries. For the purposes of this sample, click Yes. For more information on COM interoperability, see the MSDN documentation.
 

You can then use the following code to launch the Data Links dialog box and retrieve the connection string that it returns based on the user’s input:

Visual Basic .NET
Dim objDataLink As New MSDASC.DataLinks()
Dim cn As New ADODB.Connection()

objDataLink.PromptEdit(cn)
Console.WriteLine(cn.ConnectionString)
Visual C# .NET
MSDASC.DataLinks objDataLink = New MSDASC.DataLinksClass();
ADODB.Connection cn = New ADODB.ConnectionClass();
object objCn = (object) cn;
objDataLink.PromptEdit(ref objCn);
Console.WriteLine(cn.ConnectionString);

If, like me, you have trouble remembering the different connection string attributes, the Data Links dialog box will simplify your work by letting you quickly set options and then examine the resulting connection string. This approach lets me worry about more important things, like what Peter Gammons has to say about my beloved Red Sox in his columns on ESPN’s Web site.

Using Data Links to Build Connection Strings Manually

You don’t have to write code to examine the connection strings that the Data Links dialog box builds. You simply create a file with a .udl extension, and it will be associated with the Data Links dialog box. You then double-click on the file and set the appropriate properties on the dialog box’s tabs. The file is a simple text file that you can examine in a text editor such as Notepad. Voila! You have your new connection string.

Rather than hard-code a connection string in your application or build one dynamically, you can reference a Data Link file in your connection string. In this way, you can let the installation program (or the user, if you’re trusting by nature) build the appropriate connection string and store it in the Data Link file.

To use a Data Link file in a connection string, you can use a name-value pair in the string, such as the following:

File Name=MyDataLink.udl;

If you don’t specify a full path to the data link file, the OLE DB .NET data provider will look in the current working directory for your application. You can also use relative paths in your connection string:

File Name=SettingsSubDir\MyDataLink.udl;

Once you have a OleDbConnection object with a valid connection string, you should open the connection so you can communicate with your data store. To open the connection, you simply call its Open method:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();

To close a Connection object, you simply call its Close method:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
...
cn.Close()
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
...
cn.Close();

Closing the Connection object will not close the actual connection to your data source if you’re using connection pooling.

Connection Pooling

Opening and closing database connections is expensive. If you’re developing a multi-tiered application, connection pooling will probably improve your application’s performance.

Connection pooling is a fairly simple concept. Imagine a multi-tiered application, such as the one shown in Figure 3-3.

 

Figure 3-3

A simple multi-tiered application

Whenever a client application actively communicates with the middle-tier server, that server creates a business object that connects to and queries a database. Each business object maintains its own connection. When the middle tier creates a new business object, the business object creates a new Connection object. When the middle tier releases an existing business object, the business object closes and releases its connection.

Generally, the business object will close its connection in its clean-up code. As I mentioned earlier, database connections are expensive. Rather than close the database connection, what if we stored it in a pool? Then, when a new business object starts up, it will check the pool for an existing connection. If the pool contains an open connection, the business object can use it. Otherwise, the business object can create a new connection. Figure 3-4 shows an example of such an application.

Connection pooling in ADO.NET is that simple. In fact, in some ways it’s even simpler. The .NET data providers included with ADO.NET each implement connection pooling. When you request a new connection, the .NET data provider examines the credentials you’ve supplied (database location, user name, and so forth) and searches the pool for an open connection with matching credentials. If it locates such a connection, it hands you that connection. Otherwise, it creates and returns a new connection.

 
Figure 3-4
A multi-tiered application with connection pooling

When you close a connection object, the .NET data provider doesn’t really close the actual database connection. It marks the connection object as closed but stores the database connection in a pool. If the database connection is not reused within a specified amount of time (60 seconds by default), the .NET data provider closes the connection.

This is the simplest part. Connection pooling is turned on by default. The following code snippet opens and closes the same Connection object five times. Because connection pooling is turned on by default, the actual connection to the database isn’t actually closed when you call the Close method. Instead, the database connection is sent to the pool where it is later reused.

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
Dim intCounter As Integer
For intCounter = 1 To 5
    cn.Open()
    cn.Close()
Next
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
for (int intCounter = 1; intCounter <= 5; intCounter++)
{
    cn.Open();
    cn.Close();
}

Connection pooling is handled on a separate thread. As a result, the code might create a second connection to your database. When the code reaches the call to the Open method in the second iteration of the For Loop, the connection pooling routine might not have finished storing the initial database connection to the pool. You can add the following line of code between the call to the Connection object's Close method and the end of the For Loop to suspend the current thread and allow other threads to execute. The code will then use only a single connection:

System.Threading.Thread.Sleep(0)

OK, here’s the not-quite-so-simple part. Sometimes you won’t want to use connection pooling. For example, if you’re working with a classic two-tiered application in which the client application communicates directly with the database, you probably won’t want to use connection pooling. Connection pooling is turned on by default, so how do you turn it off?

The OleDbConnection class implements a ReleaseConnectionPool method. You could try to use this method in conjunction with the Collect method on the global garbage collection object to truly close the physical connection to your database. However, there’s a more elegant way. You can add the following attribute to your OLE DB connection string:

OLE DB Services=-4;

When you use this connection string attribute, the OLE DB .NET data provider will mark your connection so that it doesn’t participate in connection pooling. When you call the Close method on your OleDbConnection object, you’ll close the actual connection to your database.

If you’re using the SqlConnection object, you can add the following connection string attribute to tell the .NET data provider that you don’t want to pool the connection:

Pooling=False;

There are many ways to check the number of connections to SQL Server, but some are more elegant than others. I prefer using SQL Profiler or Performance Monitor to watch the number of connections to my databases. You can also use Enterprise Manager or you can check the results of repeated calls to a system stored procedure.

Many of the classes in the ADO.NET object model, such as the Connection class, expose a Dispose method. Calling this method on an object allows the object to free its resources prior to garbage collection. If you don’t call the Dispose method explicitly, the object will release its resources when the common language runtime’s garbage collection routine instructs the object to release its resources.

Releasing an open Connection by letting the object go out of scope or by setting the object variable to Nothing or null (depending on your language of choice) won’t close the connection to your data source until the object is cleaned up by garbage collection. Calling Dispose on a Connection marked as open will implicitly call its Close method.

As a general rule, if an object exposes a Dispose method, you should call it when you want to release the object’s resources.

You can use Connection objects to create other objects—Command objects and Transaction objects. Creating objects in this fashion might save you a couple lines of code.

The Command object, which I’ll cover in Article 4, is the object you use to query your data source. In order to execute a query, you must set the Command object’s Connection property to a Connection object. The Connection object provides a CreateCommand method that you can use to simplify the process. This method returns a new Command object that’s already initialized to use your Connection object.

The following code snippets are equivalent:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim cmd As New OleDbCommand()
cmd.Connection = cn

is equivalent to

Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim cmd As OleDbCommand = cn.CreateCommand()
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbCommand cmd = New OleDbCommand();
cmd.Connection = cn;

is equivalent to

string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbCommand cmd = cn.CreateCommand();

I occasionally use the CreateCommand method in my Visual Basic .NET code when I want to create and use a Command object that I’ll use once. Using CreateCommand along with a With block allows you to create and use a Command object without explicitly using a variable name:

Visual Basic .NET
With cn.CreateCommand()
    .CommandText = "CREATE TABLE MyTable ..."
    .ExecuteNonQuery()
    .Dispose()
End With

C# offers somewhat similar functionality through the using statement. One major difference between the two languages is that the C# code implicitly calls the Dispose method on the OleDbCommand object at the end of the block, while the Visual Basic .NET With block does not. Thus, I’ve added the call to the OleDbCommand object’s Dispose method in the Visual Basic .NET code:

Visual C# .NET
using (OleDbCommand cmd = cn.CreateCommand() {
    cmd.CommandText = "CREATE TABLE MyTable ...";
    cmd.ExecuteNonQuery();
}

Whether it’s wise to use such programming constructs is debatable. Some might say that the preceding code snippet is simple and elegant. Others might argue that it lends itself to sloppy code that might be difficult to maintain. Personally, I think it’s pretty slick. In the course of writing this book, I’ve written countless code snippets in Visual Studio .NET against the OLE DB, SQL, and ODBC .NET data providers. If I change the definition of the Connection object from an OleDbConnection to a SqlConnection (along with the connection string, of course), the code to create and execute the Command does not need to be changed. Sometimes there’s something to be said for stupid coding tricks.

Starting Transactions

You can also use the Connection object to start transactions. The Connection object’s BeginTransaction method returns a new open Transaction object on your connection. We’ll discuss the Transaction object in detail in Article 10.

The following code snippets are equivalent:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim txn As New OleDbTransaction()
txn.Connection = cn
txn.Begin()

is equivalent to

Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim txn As OleDbTransaction = cn.BeginTransaction()
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" + 
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbTransaction txn = New OleDbTransaction();
txn.Connection = cn;
txn.Begin();

is equivalent to

string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" + 
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbTransaction txn = cn.BeginTransaction();

The OleDbConnection lets you retrieve schema information about your database through the GetOleDbSchemaTable method. You supply a value from the OleDbSchemaGuid enumeration to specify the type of schema information you want, such as tables, columns, and procedures.

The GetOleDbSchemaTable method also requires a parameter called Restrictions, which acts as a filter on the schema information that the method returns. For example, rather than retrieving information for all columns in your database, you can retrieve information for just the columns in a particular table. The Restrictions parameter contains an array of values. Each schema type allows a different set of restrictions.

If you want to retrieve information about all of the columns in all of the tables in your database, you should omit the Restrictions parameter, as shown in the following code:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim tbl As DataTable
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, Nothing)
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
DataTable tbl;
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null);

However, if you want to retrieve the columns from just a specific table, you should use the Restrictions parameter and supply the name of the table whose columns you want to examine. The MSDN documentation for the Tables member of the OleDbSchemaGuid enumeration states that the Restrictions array for the member should have the following structure:

{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME"}

Therefore, you can use the following code to retrieve just the columns from the Customers table:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim objRestrictions As Object()
objRestrictions = New Object() {Nothing, Nothing, "Customers", Nothing}
Dim tbl As DataTable
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, objRestrictions)
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
string strRestrictions;
object[] objRestrictions;
objRestrictions = New object[] {null, null, "Customers", null};
DataTable tbl;
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, objRestrictions);

For information on the Restrictions parameter for a particular OleDbSchema­Guid value, see the MSDN documentation for that value.

The GetOleDbSchemaTable method returns a DataTable (a structure we’ll examine in detail in Article 6) that contains the schema information you requested. The structure of the DataTable that the method returns depends on the type of schema you requested. We can use the following code to Loop through the rows in the schema table of column information that we just retrieved:

Visual Basic .NET
...
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, strRestrictions)
Console.WriteLine("Columns in Customers table:")
For Each row In tbl.Rows
    Console.WriteLine(vbTab & row("COLUMN_NAME").ToString())
Next row
Visual C# .NET
...
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, strRestrictions);
Console.WriteLine("Columns in Customers table:");
foreach(DataRow row in tbl.Rows)
    Console.WriteLine("\t" + row["COLUMN_NAME"].ToString());

You can build a fairly simple application that uses the GetOleDbSchema­Table method to display schema information about your database (tables, views, stored procedures, and so forth), much like Server Explorer does.

The GetOleDbSchemaTable method relies on functionality in the OLE DB provider that your OleDbConnection is using. Not all OLE DB providers support all schema methods. If you request a schema that your OLE DB provider doesn’t support, you’ll throw a trappable exception.

Visual Studio .NET Design-Time Features

The Visual Studio .NET development environment includes features that make it easy to create Connections at design time and use those Connections at run time.

The ADO.NET development team built ADO.NET to help you build fast, scalable database applications. The Visual Studio .NET development team built a number of features into Visual Studio .NET to help you build those database applications faster. One such feature is Server Explorer.

Server Explorer lets you examine various operating system services and integrate them into your applications. As you can see in Figure 3-5, Server Explorer lists many of the common operating system services such as event logs, message queues, and performance counters. You can also drag items from Server Explorer onto design surfaces such as forms and components in Visual Studio .NET to easily create components at design time that you can access at run time.


Figure 3-5
Visual Studio .NET Server Explorer

For example, if you want to access or add items to the machine’s event log from your application, you can drag and drop an item from the Event Logs portion of Server Explorer onto a designer and then add code to your application to call that item.

Server Explorer also lets you interact with your database from within the Visual Studio development environment. SQL Server is one of the services that Server Explorer makes available through its server-based interface. There’s also a separate category toward the top of Server Explorer called Data Connections.

 we looked at the user interface that the Data Form Wizard displayed to request connection information. When we entered that connection information, Visual Studio added an item to the Data Connections area in Server Explorer. You can see this in Figure 3-6 below.

The items available beneath a connection depend on the type of database you’ve specified. Most databases support tables, views, and stored procedures. Some databases, such as recent versions of Oracle and SQL Server, support database diagrams and functions. Oracle also supports synonyms and packages. All of this information is available in Server Explorer.

You can expand a table to see the list of columns in the table. Click on a column, and you can see properties of the column in the Properties window. If you want to view the contents of a table or view, you can right-click on the item and choose Retrieve Data From Table. Server Explorer will display the contents of the table or view in a grid. The data might be updateable if you have permission to modify the data, and Server Explorer can retrieve the key information necessary to submit changes.


Figure 3-6
Data Connections in Server Explorer

If you’re using the Enterprise Edition of Visual Studio .NET, you can also manage your SQL Server and Oracle databases. You can modify, create, and delete SQL Server and Oracle tables, views, stored procedures, database diagrams, and functions.

Adding a Data Connection to Server Explorer

You can add a data connection to Server Explorer by clicking the button at the top of Server Explorer that shows a yellow cylinder with an electrical cord attached. You can see in Figure 3-7 the same Data Link dialog box that you saw when we added a connection using the Data Form Wizard.

In diagrams, databases are represented almost universally by cylinders. I’m not entirely sure why, but my technical editor, Dail, informed me that this is “likely because a cylinder is the old flowchart symbol for mass storage—chosen because long, long ago random-access mass-storage devices used drums rather than disks.” He also claimed not to be old enough to remember such drives before saying “Now go away and let me drink my Geritol in peace.” He gets a little cranky when he hasn’t had his afternoon nap.

Adding connections to Server Explorer is straightforward in most, but not all, cases. To understand why things aren’t always so simple, we’ll need to discuss some low-level data access technologies, both past and present. The next couple of pages might look like condensed alphabet soup as a result. This information is geared toward developers who want to use ODBC drivers or .NET data providers other than those built for OLE DB or SQL Server.

The Data Link dialog box was originally designed as an interface for building connection strings in Visual Studio 6. The central data access technology in version 6 is ADO, which is based on a lower-level data access technology called OLE DB.

The Providers tab of the Data Link dialog box displays a list of OLE DB providers built to help you connect to a particular type of database such as SQL Server, Oracle, or Microsoft Access.

Before Microsoft developed OLE DB, the most prevalent data access technology was ODBC, OLE DB’s predecessor. Developers communicated with their databases through ODBC drivers. In fact, many developers still build applications that rely on ODBC drivers because some databases have ODBC drivers but not an OLE DB provider.

The first OLE DB provider that Microsoft developed acted as a bridge between the two technologies by translating OLE DB calls to ODBC calls. This allows developers to use ADO, which uses OLE DB providers, with ODBC drivers. You’ll see this provider listed on the Providers tab of the Data Link dialog box as Microsoft OLE DB Provider For ODBC Drivers. If you select this provider and then click on the Connection tab, you can select an ODBC data source or enter an ODBC connection string.

So far, we’ve discussed how to use the Data Link dialog box to connect to a data source using OLE DB and ODBC. But what about .NET data providers? As I mentioned earlier, the Data Link dialog box was built for Visual Studio 6. As of the release of Visual Studio .NET, there is no user interface for building connection strings for .NET data providers in general. Thus, you can add Data Connections to Server Explorer only if the desired data source has an OLE DB provider or ODBC driver.

Saving Password Information

You can enter a password in the Data Link dialog box and click the Test button to verify that you can connect successfully. For security reasons, the Data Link dialog box will not save the password that you entered into the new connection string unless you select the Allow Saving Password check box. If you select this check box, you’ll see the warning shown in Figure 3-8.

 
Figure 3-8
The warning that appears if you try to save a password into a connection string

If you choose to save the password into the connection string, the password information will be used both at design time by Server Explorer and at run time by the applications you build. Thus, you won’t have to enter the password again when Server Explorer connects to your database. However, this also means that if you use this connection string in your application, the password will be built into the application.

Microsoft doesn’t recommend storing the password in your application in this fashion because it is not secure. Visual Studio .NET builds managed code, which can be decompiled. This means that users can decompile your application and find the connection string. If this possibility makes you uncomfortable, you should omit the password from the connection string, and prompt the user for the password—or use integrated security, which we’ll discuss next.

Integrated Security

Who says you need a password to connect to your database? Some database systems, such as SQL Server, allow you to connect using your network identity. This feature is known by a number of different names—network authentication, Windows authentication, Windows NT authentication, or integrated security. It’s a fairly simple concept. Rather than relying on the user to specify a username and password, the database asks the network to identify the user. The database then checks its user list to determine whether that user has permission to connect.

For more information on using SQL Server with integrated security, look under Administering SQL Server\ Managing Security\ Security Levels\ Authentication Modes in SQL Server Books Online.

Adding Connections to Your Application

Now that you’ve seen how to add data connections to Server Explorer, let’s talk about how you can use Server Explorer to add a Connection to your application. It’s as simple as dragging and dropping. Just select the desired connection in Server Explorer, and drag-and-drop it onto the designer in your project. You’ll see a new Connection in the component tray for the designer.

The Component Tray

Many Visual Studio .NET project items—such as Windows forms, component classes, Web forms, and ASP.NET Web services—allow you to interact with controls and components in a visual fashion. If you’re working with a simple Windows form, you can place visual controls such as buttons and text boxes on the form. You can set the properties of these controls in the Properties window. This feature is old hat to developers who’ve used development tools such as Visual Basic or Microsoft Visual InterDev.

But what if you want to add a component that, unlike a control on a form, has no user interface? Previous versions of Visual Basic treated such components as controls, and those components appeared as icons on your forms, but only at design time. Visual Studio .NET takes a slightly different approach. Components that have no user interface appear in a tray below the designer—the component tray. The component tray lets you interact with these components and set their properties at design time in the Properties window.

Using Your New Connection

When you drag-and-drop a data connection from Server Explorer onto a designer, you create a Connection object in the designer’s component tray. The type of Connection will depend on the type of data connection you selected in Server Explorer. Dragging and dropping a SQL Server connection creates a SqlConnection, while dragging and dropping a connection in Server Explorer that uses any other data source creates an OleDbConnection.

As of this writing, you cannot drag-and-drop connections from Server Explorer that use ODBC drivers. You’ll receive an error stating that the OLE DB provider for ODBC drivers cannot be used. Look for the ability to create an OdbcConnection object, the Connection object for the ODBC .NET Data Provider, in this fashion in a future release.

Figure 3-9 shows what you’ll see if you drag-and-drop a SQL Server data connection from Server Explorer onto a designer such as a Windows form:

 
Figure 3-9
The new SqlConnection1 object appears in the Visual Studio .NET components tray.

Because we dragged a SQL Server connection onto the designer, we received a SqlConnection. Visual Studio .NET automatically names new components based on the class name. This new SqlConnection is already initialized. Its ConnectionString property is set to the connection string for the corresponding data connection in Server Explorer.

Creating Connections Using the Toolbox

You can also create Connections by dragging and dropping items from the Data tab in the Visual Studio .NET Toolbox. This lets you specify which .NET data provider’s Connection you want to use. Figure 3-10 shows a new OleDbConnection created by dragging and dropping the OleDbConnection from the Toolbox.

 
Figure 3-10
Setting the connection string using the Properties window

Note that none of the properties are set…yet. The main property for a Connection object, regardless of the .NET data provider you’re using, is the ConnectionString property. You can select the ConnectionString property in the Properties window and type a value for the property by hand. Or, you can ask Visual Studio .NET for a little help.

You can select the ConnectionString property in the Properties window and view a drop-down list of available connections from Server Explorer. (Figure 3-9 shows an example.) At the end of the list, you’ll see a <New Connection…> entry. Select this entry, and you’ll launch the Data Link dialog box.

Now that we’ve created a new connection at design time, let’s use it at run time. Drag a button object onto your Windows form, and double-click the button to launch the code editor. You’ll see that Visual Studio .NET creates a procedure to handle the button’s click event. Add the following code to that event:

Visual Basic .NET
SqlConnection1.Open()
MessageBox.Show("Connection opened successfully!")

SqlConnection1.Close()
MessageBox.Show("Connection closed successfully!")
Visual C# .NET
sqlConnection1.Open();
MessageBox.Show("Connection opened successfully!");

sqlConnection1.Close();
MessageBox.Show("Connection closed successfully!");
If you used an OleDbConnection instead of a SqlConnection in your project, change SqlConnection1 in the preceding code to OleDbConnection1.

Now launch your project and click the button. You’ll see dialog boxes that tell you that you successfully opened and closed the connection to your database.

Where’s the Code?

So far, we’ve built a fairly simplistic application. We created a SqlConnection at design time and added a button and some code to use it at run time. Figure 3-11 shows the code we added. But how did the SqlConnection get in the application?

 
Figure 3-11
Adding code

Visual Studio .NET translates the objects that you create at design time, such as the SqlConnection in the component tray and the button on the form, into code. It then stores this code in the component’s Initialize Component procedure. This procedure is hidden by default in the region marked Windows Form Designer Generated Code, as shown earlier in Figure 3-11.

There are two main reasons why this code is hidden by default. First, developers generally want to focus on the code they’ve written rather than the code the designers have generated. You can click on the plus sign to the left to expand the region if you choose. Second, and more important, Microsoft strongly cautions against changing code in this region.

Most of the Visual Studio .NET designers that generate the code in this region are reentrant. The button we drew on the form is a simple example of this reentrancy. As you can see in Figure 3-12, the Windows Forms Designer translated the button we drew at design time into run-time code.

 
Figure 3-12
Code generated by the Windows Forms Designer

If you close and reopen the project and display the form in the Windows Forms Designer, the designer will use this code to draw the button on your form. Microsoft strongly cautions against changing code in this region because the designers that read the code might not be able to interpret the changes you make, so you might not be able to edit your component within the designer.

Will you get a warning ahead of time? Probably not. Will you know ahead of time whether the designer will successfully interpret the changes you’ve made in this region? Probably not. If you’re going to make changes in this fashion, you should definitely create a backup of your project ahead of time.

While writing this book I’ve wrestled with the structure of the reference-based Articles, 3 through 8. I wanted to provide developers with a complete guide to ADO.NET but also wanted the text to flow smoothly. At this point I expect that you understand the role of the Connection object in database applications and are comfortable using its major features. The following section is intended to fill in some of the blanks and cover the less-frequently-used but still important features of the object.

Read it now or save it for later. This reference material does not introduce new characters, nor does it contain any unexpected plot twists that will affect your ability to understand subsequent Articles.

Without further ado, let’s take a closer look at the properties and methods of the OleDbConnection object.

The only property on the OleDbConnection class that’s not read-only is the ConnectionString property. This property accepts a connection string that the OleDbConnection will use to try to connect to your data source when you call the Open method. Table 3-1 contains the properties you’ll use most often when working with an OleDbConnection object.

Table 3-1  Commonly Used Properties of the OleDbConnection Object
Property Data Type Description
ConnectionString String Controls how the OleDbConnection object will connect to your data source
ConnectionTimeout Int32 Specifies how long, in seconds, the OleDbConnection will try to connect to your data source (read-only)
Database String Returns the name of the database you are, or will be, connected to (read-only)
DataSource String Returns the location of the database you are, or will be, connected to (read-only)
Provider String Returns the name of the OLE DB provider the OleDbConnection will use to connect to your data source (read-only)
ServerVersion String Returns the version of your data source (read-only)
State ConnectionState Indicates the current state of the OleDbConnection (open or closed) (read-only)

The ConnectionString property controls how the Connection object will attempt to connect to your data source. You can set this property only when your Connection is not connected to your data source. When it’s connected to your data source, the property is read-only.

ConnectionTimeout Property

The ConnectionTimeout property indicates the amount of time, in seconds, that the OLE DB provider will wait on an attempt to connect to your data source before timing out.

This property is read-only because not all OLE DB providers support this feature. For example, the Microsoft OLE DB provider for SQL Server supports this feature, while the Microsoft OLE DB providers for Jet and Oracle databases do not.

So how do you tell the OLE DB provider how you long you want to wait before timing out? By using the connection string attribute Connect Timeout. Here’s an example of a connection string that uses the Microsoft OLE DB Provider for SQL Server and the Connect Timeout attribute:

"Provider=SQLOLEDB;Data Source=(local)\NetSDK;Initial Catalog=Northwind;
    Trusted_Connection=Yes;Connect Timeout=11;"

If you set a value for the Connect Timeout attribute in your connection string and the OLE DB provider you’re using doesn’t support this feature, you’ll throw an exception when you call the Open method on the OleDbConnection.

Database and DataSource Properties

The terms database and data source are often used interchangeably, as they are in this book. But the Connection object exposes each as a separate property. So how do they differ?

In our discussion of connection strings, you might have noticed that each connection string used the Data Source attribute followed by the location of the data source we wanted to connect to. Similarly, the Connection object’s DataSource property contains the location of the data source referenced in our connection string. If you’re working with a server-based data store, such as SQL Server or Oracle, the DataSource property will return the name of the machine acting as the server. For file-based databases, such as Access, the DataSource property will return the location of the data file.

When we discussed connection strings that use the SQL Server OLE DB provider, you learned how to specify an instance of SQL Server in a connection string as part of the Data Source attribute. If you use a connection string that includes information specifying the desired instance of SQL Server, that information will be returned in the DataSource property, just as it appears in the connection string.

So, what information does the Database property return? This property is designed for data sources that support multiple databases, such as SQL Server. When we examined SQL Server connection strings, we specified the database on the server we wanted to connect to in the Initial Catalog attribute of the connection string.

The SQL Server ODBC driver supports the same functionality in the Database attribute. For the SQL Server OLE DB provider, the two attributes are interchangeable.

The OleDbConnection class exposes the Provider property to let you determine the OLE DB provider specified in the connection string. The OdbcConnection property exposes a similar property, Driver, which returns the name of the ODBC driver specified in the connection string. Because the SqlConnection class supports connecting only to SQL Server databases, there is no need for such a property.

ServerVersion Property

Most database systems introduce new features with each successive version. SQL Server 2000, for example, supports returning the results of a query as XML. For this reason, you can check the ServerVersion property to ensure you don’t make unsupported calls to a server. The SourceVersion property returns a string containing the version of the database to which you’re connected. Developers with a SQL Server background might be familiar with the SELECT @@Version query. The SourceVersion property returns a subset of the information returned by SELECT @@Version—the database’s version number.

Let’s say your application communicates with different SQL Server databases, using the OLE DB, ODBC or SQL .NET data provider, and there are certain queries that you can run only against servers running SQL Server 2000 or later. In such a situation, you can use the following code to determine whether to run your query:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
If cn.ServerVersion >= "08" Then
    'Run your query here.
End If
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
if (cn.ServerVersion >= "08") {
    //Run your query here.
}

The Connection object’s State property returns the current state of the connection as a member of the ConnectionState enumeration in the System.Data namespace. Table 3-2 contains the constants, values, and descriptions of possible connection states.

Table 3-2  Connection State Constants
Constant Value Description
Broken 16 The connection is broken. A connection is considered broken if it has been opened but is then unable to communicate with the data store for whatever reason (such as network problems or the server being reset). Not used in initial release of ADO.NET.
Closed 0 The connection is closed.
Connecting 2 The connection is in the process of connecting. Not used in initial release of ADO.NET.
Executing 4 The connection is executing a query. Not used in initial release of ADO.NET.
Fetching 8 The connection is busy fetching data. Not used in initial release of ADO.NET.
Open 1 The connection is open.

The ConnectionState enumeration contains a number of values that aren’t used in the initial release of ADO.NET. Currently, the Connection object’s State property will return either Open or Closed. Future releases might support combinations of these values to indicate that a connection is open but is currently executing a query.

You can use the Connection object’s StateChange event to determine when the value of the State property changes.

Methods of the OleDbConnection Object

Table 3-3 lists the OleDbConnection object’s methods. Methods such as GetType and ToString that are common to most objects in the .NET Framework are omitted. Take a look at the table to familiarize yourself with these methods. I discuss each one following the table.

Table 3-3  Commonly Used Methods of the OleDbConnection Object
Method Description
BeginTransaction Begins a transaction on the connection
ChangeDatabase Changes the current database on an open connection
Close Closes the connection
CreateCommand Creates an OleDbCommand for the current connection
GetOleDbSchemaTable Retrieves schema information from the data source
Open Opens the connection
ReleaseObjectPool Releases the connection from the OLE DB connection pool

If you want to start a transaction on your connection—to lock data or to ensure that you can commit or roll back a series of changes to your data store—call the BeginTransaction method on the Connection object. This method returns a new Transaction object, a class we’ll discuss in depth in Article 10 when we discuss updating your database.

Developers who’ve used the connection objects in ADO, RDO, or DAO might expect methods of the Connection object to commit or roll back a transaction. In the ADO.NET object model, the BeginTransaction method generates a new Transaction object. When you want to commit or roll back a transaction, call Commit or Rollback on the Transaction object.

Because BeginTransaction creates a new transaction, associates it with the connection that created it, and initializes the transaction, using this method of the Connection object can simplify your code slightly. The following code snippets are functionally equivalent:

Visual Basic .NET
Dim txn As OleDb.OleDbTransaction = cn.BeginTransaction()

is equivalent to

Dim txn As New OleDb.OleDbTransaction()
txn.Connection = cn
txn.Begin()
Visual C# .NET
OleDbTransaction txn = cn.BeginTransaction();

is equivalent to

OleDbTransaction txn = New OleDbTransaction();
txn.Connection = cn;
txn.Begin();

Earlier in the Article, we talked about SQL Server’s ability to support multiple databases on a single server. If you’re working with SQL Server, you can change the database you’re communicating with by executing a query such as this one:

USE Northwind

ADO.NET also offers a simpler method for changing the database. The Connection object has a ChangeDatabase method that simplifies the process. The following code snippets are equivalent:

Visual Basic .NET
Dim cn As New OleDbConnection(strConn)
cn.Open()
...
cn.ChangeDatabase("Northwind")

is equivalent to

Dim cn As New OleDbConnection(strConn)
cn.Open()
...
Dim cmd As OleDbCommand = cn.CreateCommand()
cmd.CommandText = "USE Northwind"
cmd.ExecuteNonQuery()
Visual C# .NET
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
...
cn.ChangeDatabase("Northwind");

is equivalent to

OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
...
OleDbCommand cmd = cn.CreateCommand();
cmd.CommandText = "USE Northwind";
cmd.ExecuteNonQuery();
Close Method

To close a Connection, you call the object’s Close method. Remember that if you’re using connection pooling, you’re simply sending the physical connection to your data source to the pool.

Calling the Close method on a Connection object that’s already marked as closed will not generate an exception.

You can also create new Command objects by using the Connection class’s CreateCommand method. This method accepts no arguments and returns a new Command object whose Connection property is set to the Connection object that created it.

The following code snippets are functionally equivalent:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
Dim cmd As OleDb.OleDbCommand = cn.CreateCommand()

is equivalent to

Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
Dim cmd As New OleDb.OleDbCommand()
cmd.Connection = cn
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
OleDbCommand cmd = cn.CreateCommand();

is equivalent to

string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
OleDbCommand cmd = New OleDbCommand();
cmd.Connection = cn;

The OleDbConnection lets you retrieve schema information about your database through the GetOleDbSchemaTable method. You supply a value from the OleDbSchemaGuid enumeration to specify the type of schema information you want, such as tables, columns, and procedures.

The GetOleDbSchemaTable method also requires a parameter called Restrictions, which acts as a filter on the schema information returned by the method. For example, rather than retrieving information for all columns in your database, you can retrieve information for just the columns in a particular table. The Restrictions parameter contains an array of values. Each schema type allows a different set of restrictions.

If you want to retrieve information about all of the columns in all of the tables in your database, you should omit the Restrictions parameter, as shown in the following code:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim tbl As DataTable
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, Nothing)
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
DataTable tbl;
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null);

However, if you want to retrieve the columns from just a specific table, use the Restrictions parameter and supply the name of the table whose columns you want to examine. The MSDN documentation for the Tables member of the OleDbSchemaGuid enumeration states that the Restrictions array for the member should have the following structure:

{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME"}

You can use the following code to retrieve just the columns from the Customers table:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim objRestrictions As Object()
objRestrictions = New Object() {Nothing, Nothing, "Customers", Nothing}
Dim tbl As DataTable
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, objRestrictions)
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
object[] objRestrictions;
objRestrictions = New object[] {null, null, "Customers", null};
DataTable tbl;
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, objRestrictions);

For information on the Restrictions parameter for a particular OleDbSchema­Guid value, see the MSDN documentation for that value.

The GetOleDbSchemaTable method returns a DataTable (a structure we’ll examine in detail in Article 6), which contains the schema information you requested. The structure of the DataTable that the method returns will depend on the type of schema you requested. You can use the following code to Loop through the rows in the schema table of column information that we just retrieved:

Visual Basic .NET
...
Dim row as DataRow
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, objRestrictions)
Console.WriteLine("Columns in Customers table:")
For Each row In tbl.Rows
    Console.WriteLine(vbTab & row("COLUMN_NAME").ToString())
Next row
Visual C# .NET
...
tbl = cn.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, objRestrictions);
Console.WriteLine("Columns in Customers table:");
foreach(DataRow row in tbl.Rows)
    Console.WriteLine("\t" + row["COLUMN_NAME"].ToString());

You can build a fairly simple application that uses the GetOleDbSchema­Table method to display schema information about your database (such as tables, views, and stored procedures) similar to how Server Explorer displays that information.

The GetOleDbSchemaTable method relies on functionality in the OLE DB provider that your OleDbConnection is using. Not all OLE DB providers support all schema methods. If you request a schema that your OLE DB provider doesn’t support, you’ll throw a trappable exception.

Open Method

To open a connection to your data source, call the Connection object’s Open method. The Connection object will attempt to connect to your data source based on the information provided in the object’s ConnectionString property. If the attempt to connect fails, the Connection object will throw an exception.

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
Try
    cn.Open()
Catch ex As Exception
    Console.WriteLine("Attempt to connect failed!" & vbCrLf & ex.Message)
End Try
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
try 
{
    cn.Open();
}
catch (Exception ex) 
{
    Console.WriteLine("Attempt to connect failed!\n" + ex.Message);
}

Calling the Open method on a Connection object that’s already opened will close and then reopen the connection. If connection pooling is enabled, this scenario might generate an additional connection to your data source. When the initial connection is closed, it’s sent to the pool. However, because pooling is handled on another thread, the initial connection might not be available when the Connection object requests a connection to the data source based on the connection string.

ReleaseObjectPool Method

The ReleaseObjectPool method can help you manage OLE DB connection pooling within your components. Calling ReleaseObjectPool on your connection, or on the OleDbConnection class itself, releases your reference to the pool.

In all honesty, you’ll rarely need to use this method. With the Visual Studio .NET beta, most developers wanted to use this method to truly close a physical connection to the data store rather than to simply send the physical connection to the pool. In such cases, you’re better off creating your connection so it won’t be pooled in the first place. To do that, include the following snippet in the connection string:

OLE DB Services=-4;

If you use this attribute and value in your connection string, your connection to the data source will be closed rather than pooled when you call the Close method on the OleDbConnection class.

Events of the OleDbConnection Object

The OleDbConnection object exposes two events, InfoMessage and StateChange, as described in Table 3-4.

Table 3-4  Events of the OleDbConnection Object
Event Description
InfoMessage Fires when the connection receives an informational message from the data source
StateChange Fires when the State property of the connection changes
InfoMessage Event

Some database systems, such as SQL Server, support informational messages. SQL Server lets you send messages to the client via the PRINT command. These messages are not returned as errors, nor are they included with the results of a query.

You can use the Connection object’s InfoMessage event to trap for such messages. The following code snippet shows how you can log informational messages.

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
AddHandler cn.InfoMessage, AddressOf cn_InfoMessage
cn.Open()
With cn.CreateCommand()
    .CommandText = "PRINT 'Hello ADO.NET!'"
    .ExecuteNonQuery()
End With

Public Sub cn_InfoMessage(ByVal sender As Object, _
           ByVal e As System.Data.OleDb.OleDbInfoMessageEventArgs)
    Console.WriteLine("InfoMessage event occurred")
    Console.WriteLine(vbTab & "Message received: " & e.Message)
End Sub
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.InfoMessage += New OleDbInfoMessageEventHandler(cn_InfoMessage);
cn.Open();
OleDbCommand cmd = cn.CreateCommand();
cmd.CommandText = "PRINT 'Hello ADO.NET'";
cmd.ExecuteNonQuery();

static void cn_InfoMessage(object sender, OleDbInfoMessageEventArgs e)
{
    Console.WriteLine("InfoMessage event occurred");
    Console.WriteLine("\tMessage received: " + e.Message);
}
  SQL Server also supports generating informational messages using the RAISERROR command. Errors created with this command are treated as informational messages if the error’s severity level is 10 or below. For more information, see SQL Server Books Online.

The Connection object’s StateChange event fires whenever the value of its State property changes. This event can prove handy if you display the current state of your connection in, say, a status bar at the bottom your application’s main form.

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
AddHandler cn.StateChange, AddressOf cn_StateChange
cn.Open()
cn.Close()

Public Sub cn_StateChange(ByVal sender As Object, _
                          ByVal e As System.Data.StateChangeEventArgs)
    Console.WriteLine("StateChange event occurred")
    Console.WriteLine(vbTab & "From " & e.OriginalState.ToString)
    Console.WriteLine(vbTab & "To " & e.CurrentState.ToString)
End Sub
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.StateChange += New StateChangeEventHandler(cn_StateChange);
cn.Open();
cn.Close();

static void cn_StateChange(object sender, StateChangeEventArgs e)
{
    Console.WriteLine("StateChange event occurred");
    Console.WriteLine("\tFrom " & e.OriginalState.ToString());
    Console.WriteLine("\tTo " & e.CurrentState.ToString());
}

Command objects let you execute many different types of queries. Some Command objects retrieve data in the form of a result set, and others modify the content or structure of the data store. Let’s look at how to create Command objects and use them to perform a variety of tasks.

You can create a Command object in three ways. The first way is to simply create a new instance of an object using the new keyword and then set the appropriate properties. Or, you can use one of the available constructors to specify the query string and a Connection object. The third way is to call the CreateCommand method of the Connection object (which we examined in Article 3), as shown here:

Visual Basic .NET
Dim strConn, strSQL As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
strSQL = "SELECT CustomerID, CompanyName FROM Customers"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim cmd As OleDbCommand

cmd = cn.CreateCommand()
cmd.CommandText = strSQL

cmd = New OleDbCommand()
cmd.CommandText = strSQL
cmd.Connection = cn

cmd = New OleDbCommand(strSQL, cn)
Visual C# .NET
string strConn, strSQL;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
strSQL = "SELECT CustomerID, CompanyName FROM Customers";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbCommand cmd;

cmd = cn.CreateCommand();
cmd.CommandText = strSQL;

cmd = New OleDbCommand();
cmd.CommandText = strSQL;
cmd.Connection = cn;

cmd = New OleDbCommand(strSQL, cn);

Queries that don’t return a result set are generally referred to as action queries—a term we’ll use occasionally in this text. There are two main categories of action queries:

  • Data manipulation language (DML) queries  Also known as query-based updates (QBUs), these modify the contents of your database. Here are a few examples:
    UPDATE Customers SET CompanyName = 'NewCompanyName'
           WHERE CustomerID = 'ALFKI'
    
    INSERT INTO Customers (CustomerID, CompanyName)
           VALUES ('NewID', 'NewCustomer')
    
    DELETE FROM Customers WHERE CustomerID = 'ALFKI'
  • Data definition language (DDL) queries  These modify the structure of your database, as shown in the following examples:
    CREATE  TABLE Table1 (Field1 int NOT NULL 
                         CONSTRAINT PK_Table1 PRIMARY KEY, 
                         Field2 varchar(32))
    
    ALTER VIEW View1 AS SELECT Field1, Field2 FROM Table1
    
    DROP PROCEDURE StoredProcedure1

To execute an action query, you create a Command object, set its Connection property to an open connection, set its CommandText property to the query string you want to submit, and call the Command’s ExecuteNonQuery method, as shown here:

Visual Basic .NET
Dim cn As New OleDbConnection()
cn.ConnectionString = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                      "Initial Catalog=Northwind;Trusted_Connection=Yes;"
cn.Open()
Dim cmd As OleDbCommand = cn.CreateCommand()
cmd.CommandText = "UPDATE Customers SET CompanyName = 'NewCompanyName'" & _
                  " WHERE CustomerID = 'ALFKI'"
cmd.ExecuteNonQuery()
Visual C# .NET
OleDbConnection cn = New OleDbConnection();
cn.ConnectionString = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                      "Initial Catalog=Northwind;Trusted_Connection=Yes;";
cn.Open();
OleDbCommand cmd = cn.CreateCommand();
cmd.CommandText = "UPDATE Customers SET CompanyName = 'NewCompanyName' " + 
                  "WHERE CustomerID = 'ALFKI'";
cmd.ExecuteNonQuery();
  Despite what the ExecuteNonQuery method’s name implies, action queries are valid queries. They simply don’t return rows. I’m not entirely sure why the method is called ExecuteNonQuery. (I just work here.)

Executing an action query is often only half the battle. When you execute the following query, there are two possible outcomes, success or failure:

CREATE TABLE NewTable (NewTableID int NOT NULL
                       CONSTRAINT PK_NewTable PRIMARY KEY,
                       OtherField varchar(32))

The query either successfully creates your new table or fails, perhaps because a table of the same name already exists, you didn’t use the right syntax for the query, or you don’t have an open connection to your database. The point here is that if you execute the query and it doesn’t generate an error, you successfully created your new table.

With action queries that are designed to modify or delete an existing row, you need to do more than simply execute the query successfully. Let’s take another look at the query we executed in a code snippet on the previous page (to change the company name for a specific customer):

UPDATE Customers SET CompanyName = 'NewCompanyName'
         WHERE CustomerID = 'ALFKI'

In some cases, executing this query might not modify that customer’s company name—for example, if another user has deleted this row from your table. The database will execute the query, but because no rows satisfy the criteria in the WHERE clause, the query will not modify any rows. To the database, this outcome does not constitute failure.

So, how can you tell whether the query modified one row? If you submit this query in a tool such as Microsoft SQL Server Query Analyzer, you’ll see a message like this one:

(1 row(s) affected)

Keep in mind that if the action query doesn’t modify any rows because no rows satisfied the criteria in the WHERE clause, no error occurs.

The Command lets you retrieve this information by returning the number of rows affected by your query as the return value of the ExecuteNonQuery method, as shown here:

Visual Basic .NET
Dim strConn As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim cmd As OleDbCommand = cn.CreateCommand()
cmd.CommandText = "UPDATE Customers SET CompanyName = 'NewCompanyName'" & _
                  " WHERE CustomerID = 'ALFKI'"
Dim intRecordsAffected As Integer = cmd.ExecuteNonQuery()
If intRecordsAffected = 1 Then
    Console.WriteLine("Update succeeded")
Else
    'Assume intRecordsAffected = 0
    Console.WriteLine("Update failed")
End If
Visual C# .NET
string strConn;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbCommand cmd = cn.CreateCommand();
cmd.CommandText = "UPDATE Customers SET CompanyName = 'NewCompanyName' " + 
                  "WHERE CustomerID = 'ALFKI'";
int intRecordsAffected = cmd.ExecuteNonQuery();
if (intRecordsAffected == 1)
    Console.WriteLine("Update succeeded");
else
    //Assume intRecordsAffected = 0
    Console.WriteLine("Update failed");

In the code, we assume that if the query didn’t modify one row, it modified zero rows. But there are other possible return values from ExecuteNonQuery. If you execute anything other than a DML query, ExecuteNonQuery will return -1. There are also situations in which a DML query will modify more than one row.

However, in the code snippet, we used the table’s primary key field in the WHERE clause. Because the Customer table uses the CustomerID field as its primary key, no two rows can have the same value for the CustomerID field. We therefore know that the query cannot modify more than one row.

What if you want to execute a query that returns a result set? The Command object has an ExecuteReader method that returns a DataReader object that you can use to examine the results of your query.

The DataReader object is similar to other reader objects in the .NET Framework, such as the XmlReader, TextReader, and StreamReader objects. Each of these objects is an efficient, lightweight object that lets you examine (in a read-only fashion) the data that the object exposes. The TextReader object, for example, has methods that let you read the contents of a text file one line at a time. Similarly, the DataReader exposes properties and methods that let you Loop through the results of your query.

Developers who’ve worked with RDO and ADO or the lower-level ODBC and OLE DB API might be familiar with the term firehose cursor. This is the mechanism that databases use to return the results of a query as quickly as possible. Firehose cursors forgo functionality in favor of performance. Once you’ve read one row from the result set and moved on to the next row, the previous row is no longer available. The results come at you fast and furious, like water from a fire hose—hence the name. The ADO.NET DataReader lets you access the firehose cursor directly. To create a DataReader, you simply call the Execute­Reader method on a Command object.

The following code snippet shows how to examine the results of a simple query using a DataReader object:

Visual Basic .NET
Dim strConn, strSQL As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
strSQL = "SELECT CustomerID, CompanyName FROM Customers"
Dim cmd As New OleDbCommand(strSQL, cn)
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
while rdr.Read()
    Console.WriteLine(rdr("CustomerID") & " – " & rdr("CompanyName"))
End While
rdr.Close()
Visual C# .NET
string strConn, strSQL;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" + 
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
strSQL = "SELECT CustomerID, CompanyName FROM Customers";
OleDbCommand cmd = New OleDbCommand(strSQL, cn);
OleDbDataReader rdr = cmd.ExecuteReader();
while (rdr.Read())
    Console.WriteLine(rdr["CustomerID"] + " – " + rdr["CompanyName"]);
rdr.Close();

Note that the code calls the Read method before reading the first row of the result set because the first row is not available immediately after you call ExecuteReader. This represents a change from previous object models such as ADO. The DataReader that the Command object returns does not make the first row of data available until you call the Read method.

The first time you call the Read method, the DataReader moves to the first row in the result set. Subsequent calls to the Read method move to the next row. The method also returns a Boolean value to indicate whether the Data­Reader had another row available. So, if Read returns True, the DataReader moved to the next available row. When the Read method returns False, you’ve reached the end of the results.

Fetching Faster

The DataReader has a default parameterized property named Item. The preceding code snippet implicitly used the Item property to access the values stored in the CustomerID and CompanyName columns in the result set. This code is inefficient, however. We can improve the performance of the code in two ways.

We supplied the name of the column in our code snippet. In order to return the value stored in that column, the DataReader has to locate the column in its internal structure based on the string we supplied. Remember that in our code snippet, we ask the DataReader to perform that string-based lookup for each row in the result set. Supplying the index, or ordinal, for the column will improve the performance of our code.

This coding technique can be applied to nearly all objects that expose collections. I’ve recommended this technique to many developers who were writing ADO code and looking for ways to improve performance. Most developers have agreed that this technique would improve performance, but some have hesitated for fear it might limit their flexibility.

The order of the columns in your result set won’t change unless you change the query string or you make a change to the structure of your database object (table, view, or stored procedure) and you’re retrieving all columns that your database object returns. In the vast majority of applications, you can hard-code the index values for each column into your application without encountering problems.

However, you might encounter situations where you’ll know the column name but not its index. The DataReader offers an elegant way to determine a column’s index based on its name. The GetOrdinal method accepts a string that represents the column name and returns an integer to denote that column’s ordinal. This method is a welcome addition to the ADO.NET object model because it can help you improve performance without losing the flexibility that comes with string-based lookups.

The following code snippet improves on our original DataReader code snippet. It uses the GetOrdinal method to get the ordinal values for the two columns we want to examine and then uses those values to examine the contents of each row. This improves performance because we perform a string-based search of the collection only once per column. In our original code snippet, we performed the string-based search each time we fetched data from a column.

Visual Basic .NET
...
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
Dim intCustomerIDOrdinal As Integer = rdr.GetOrdinal("CustomerID")
Dim intCompanyNameOrdinal As Integer = rdr.GetOrdinal("CompanyName")
while rdr.Read()
    Console.WriteLine(rdr(intCustomerIDOrdinal) & " – " & _
                      rdr(intCompanyNameOrdinal))
End While
rdr.Close()
Visual C# .NET
...
OleDbDataReader rdr = cmd.ExecuteReader();
int intCustomerIDOrdinal = rdr.GetOrdinal("CustomerID");
int intCompanyNameOrdinal = rdr.GetOrdinal("CompanyName");
while (rdr.Read())
    Console.WriteLine(rdr[intCustomerIDOrdinal] + " – " + 
                      rdr[intCompanyNameOrdinal]);
rdr.Close();

But we’re not done yet.…

The DataReader also exposes a series of methods that return data in the different .NET Framework data types (such as string, 32-bit integer, decimal, and double). Currently, our code snippet implicitly uses the Item property, which returns the contents of the specified column in the generic Object data type. To display the value stored in the console window, the console has to convert the generic object to a string. Because the CustomerID and CompanyName columns contain string data, we can use the GetString method of the DataReader to return the contents of the columns as a string, as shown here:

Visual Basic .NET
...
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
Dim intCustomerIDOrdinal As Integer = rdr.GetOrdinal("CustomerID")
Dim intCompanyNameOrdinal As Integer = rdr.GetOrdinal("CompanyName")
while rdr.Read()
    Console.WriteLine(rdr.GetString(intCustomerIDOrdinal) & " – " & _
                      rdr.GetString(intCompanyNameOrdinal))
End While
rdr.Close()
Visual C# .NET
...
OleDbDataReader rdr = cmd.ExecuteReader();
int intCustomerIDOrdinal = rdr.GetOrdinal("CustomerID");
int intCompanyNameOrdinal = rdr.GetOrdinal("CompanyName");
while (rdr.Read())
    Console.WriteLine(rdr.GetString(intCustomerIDOrdinal) + " – " + 
                      rdr.GetString(intCompanyNameOrdinal));
rdr.Close();

You should always use the type-specific Get method that corresponds to the data returned by the column in the result set. Let’s say we want to query the sample Northwind database for the OrderID and ProductID columns in the Order Details table and display the results in a list box. Even though the list box will display the data as strings, we need to use the GetInt32 method to fetch the contents of each of the columns. After we retrieve the data into an integer, we can convert the data to a string and display it in the list box.

Fetching Multiple Results

Some databases, such as SQL Server, allow you to execute a batch of queries that return multiple results. Let’s say we want to issue the following query against the sample Northwind database:

SELECT CustomerID, CompanyName, ContactName, Phone FROM Customers;
SELECT OrderID, CustomerID, EmployeeID, OrderDate FROM Orders;
SELECT OrderID, ProductID, Quantity, UnitPrice FROM [Order Details]

In our previous DataReader code snippets, we looped through the results of our query until the Read method returned False. Using that same code with a batch query will Loop through the results of the only first query in the batch.

The DataReader exposes a NextResult method that lets you move to the results of the next row-returning query. The NextResult method is similar to the Read method in that it returns a Boolean value to indicate whether there are more results. However, unlike with the Read method, you should not call this method initially.

When the Read method returns False, you can check to see whether there are additional results to fetch by calling the NextResult method. When the Next­Result method returns False, there are no more result sets. The following code snippet shows how to use the NextResult method to fetch the results of a batch query:

Visual Basic .NET
...
cn.Open()
Dim strSQL As String
strSQL = "SELECT CustomerID, CompanyName FROM Customers;" & _
         "SELECT OrderID, CustomerID FROM Orders;" & _
         "SELECT OrderID, ProductID FROM [Order Details]"
Dim cmd As New OleDbCommand(strSQL, cn)
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
Do
    Do While rdr.Read()
        Console.WriteLine(rdr(0) & " – " & rdr(1))
    Loop
    Console.WriteLine()
Loop while rdr.NextResult()
Visual C# .NET
...
cn.Open();
string strSQL = "SELECT CustomerID, CompanyName FROM Customers;" +
                "SELECT OrderID, CustomerID FROM Orders;" +
                "SELECT OrderID, ProductID FROM [Order Details]";
OleDbCommand cmd = New OleDbCommand(strSQL, cn);
OleDbDataReader rdr = cmd.ExecuteReader();
do
{
    while (rdr.Read())
        Console.WriteLine(rdr[0] + " – " + rdr[1]);
    Console.WriteLine();
} while (rdr.NextResult());

Developers often encountered a problem when they used ADO with SQL Server to retrieve the result set generated by a stored procedure. If you call a SQL Server stored procedure using the SQL Server OLE DB provider and the stored procedure executes an action query prior to the row returning query, your Recordset will be marked as closed instead of containing the results of the row-returning query.

This behavior is actually by design. The closed Recordset corresponds to the action query. More precisely, it corresponds to the informational message “n row(s) affected” that the query returns. You have to call NextRecordset to move to the results of the next query. Or you can add the statement SET NOCOUNT ON in your stored procedure to suppress these messages, which allows ADO to move immediately to the results of the first row-returning query.

This behavior is not restricted to stored procedures. You can create a similar situation by executing batch queries. Let’s take a look at such a batch query with ADO 2.x and Visual Basic “Classic.”

Visual Basic “Classic”
Dim cn As ADODB.Connection, rs As ADODB.Recordset
Dim strConn As String, strSQL As String
Dim intRecordsAffected As Integer

strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Set cn = New ADODB.Connection
cn.Open strConn

strSQL = "INSERT INTO Customers ...;" & _
         "SELECT CustomerID, CompanyName FROM Customers WHERE ...;" & _
         "UPDATE Customers SET CompanyName = ... WHERE ...;" & _
         "SELECT CustomerID, CompanyName FROM Customers WHERE ..."

Set rs = cn.Execute(strSQL, intRecordsAffected, adCmdText)
Do Until rs Is Nothing
    Debug.Print "rs.State = " & rs.State & vbTab & _
                "intRecordsAffected = " & intRecordsAffected
    Set rs = rs.NextRecordset(intRecordsAffected)
Loop

Initially, the Recordset is closed and the intRecordsAffected variable contains 1. These results correspond to the INSERT query, which does not return rows and modifies one record in the database. Once we call NextRecordset, the Recordset is open and contains the results of the first SELECT query. Because the SELECT query doesn’t modify any records in the database, the intRecords­Affected variable returns -1. The second call to NextRecordset returns a closed Recordset, and intRecordsAffected now contains the number of records affected by the UPDATE query. Calling NextRecordset again returns the results of the second SELECT query and sets intRecordsAffected to -1. The last call to Next­Recordset returns a Recordset set to Nothing, indicating that there are no more results to process.

ADO.NET handles this same batch query differently. The ADO.NET development team saw this scenario as one of the top developer frustrations with ADO. To simplify the process of working with batch queries, the DataReader automatically moves you to the results of the first row-returning query. I think most developers will be pleased with this change in behavior. Unfortunately, it comes with a trade-off.

As a result of this change, the DataReader does not provide a way for you to determine the number of rows modified by each individual action query. The RecordsAffected property on the DataReader acts as a running total. Probably the best way to explain this behavior is by showing an example:

Visual Basic .NET
Dim strConn, strSQL As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
strSQL = "INSERT INTO Customers ...;" & _
         "SELECT CustomerID, CompanyName FROM Customers WHERE ...;" & _
         "UPDATE Customers SET CompanyName = ... WHERE ...;" & _
         "SELECT CustomerID, CompanyName FROM Customers WHERE ..."
Dim cmd As New OleDbCommand(strSQL, cn)
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
Do
    Console.WriteLine("RecordsAffected = " & rdr.RecordsAffected)
    Do While rdr.Read()
        Console.WriteLine(vbTab & rdr.GetName(0) & " – " & rdr.GetValue(0))
    Loop
    Console.WriteLine()
Loop while rdr.NextResult()
Visual C# .NET
string strConn, strSQL;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
strSQL = "INSERT INTO Customers ...;" +
         "SELECT CustomerID, CompanyName FROM Customers WHERE ...;" +
         "UPDATE Customers SET CompanyName = ... WHERE ...;" +
         "SELECT CustomerID, CompanyName FROM Customers WHERE ...";
OleDbCommand cmd = New OleDbCommand(strSQL, cn);
OleDbDataReader rdr = cmd.ExecuteReader();
do
{
    Console.WriteLine("RecordsAffected = " + rdr.RecordsAffected);
    while (rdr.Read())
        Console.WriteLine("\t" + rdr.GetName[0].ToString() + 
                          " – " + rdr.GetValue[0].ToString());
    Console.WriteLine();
}
while (rdr.NextResult());

You probably noticed that the ADO.NET code looks similar to the ADO code. The results, however, are slightly different. When we create the Data­Reader by calling the ExecuteReader method on the Command, the DataReader is ready to return the results of the first SELECT query immediately. When we call NextResult, we move to the results of the second SELECT query. The second call to NextResult returns False because there are no more row-returning queries to process, and we leave the Loop.

The other major change in behavior from ADO is the behavior of the RecordsAffected property of the DataReader. Let’s assume that the INSERT query and the UPDATE query each modify one record in the database. The RecordsAffected property will return the sum of the records affected by all action queries preceding the row-returning query that the DataReader is currently fetching.

So, when the ExecuteReader method returns the DataReader, its Records­Affected property will return 1. After we call the NextResult method, the Records­Affected property will return 2.

Keep in mind that non-DML action queries (such as CREATE PROCEDURE and DROP TABLE) return -1 for the number of records they affect because they’re not designed to affect records.

If you need to determine the number of rows affected by individual queries using ADO.NET, split the batch into its individual queries and execute each query separately.

In the ADO.NET object model, it’s vitally important that you close your Data­Reader objects as quickly as possible. As of this writing, a Connection object that has an open DataReader is considered blocked. If you try to open a second DataReader before closing the first one, you’ll receive an exception whose text indicates that the operation “requires an open and available connection.”

Developers who have some experience with ADO might be surprised by this restriction, but those who’ve used RDO might not. Different Microsoft data access technologies have handled this scenario differently.

If you try to open two firehose cursors against a SQL Server database using ADO, everything will just work and you won’t receive an error. This is because the OLE DB specification states that when the current connection is blocked, the OLE DB provider will perform the requested action on a new connection.

RDO developers might recognize the error message “Connection is busy with results from another hstmt.” ODBC does not do any behind-the-scenes work to try to help you out. If you try to use a connection that’s busy, you’ll simply receive an error message.

Which of these approaches (raising an error or performing the desired action on a new connection) is better? Developers, both inside and outside of Microsoft, can’t seem to agree. In fact, each successive Microsoft data access technology has handled the scenario differently than its predecessor: VBSQL raises an error, DAO/Jet creates a new connection, RDO raises an error, ADO creates a new connection, and ADO.NET raises an error. As they say in New England, “If you don’t like the weather, just wait a while.”

I believe VBSQL predates DAO/Jet, but carbon dating is difficult and the results are not 100 percent accurate.

The DataReader is built for performance. Regardless of the restriction that an open DataReader blocks a Connection, you should pull the results of your query off the wire as quickly as possible after issuing the query. If you need to move back and forth between the results of separate queries, you should use a DataSet or consider storing the results of your queries in a business object of some sort.

What if you want to execute a query and retrieve a single cell (one row, one column) of data? Here are two examples of queries that return a single value:

SELECT COUNT(*) FROM Customers
SELECT CompanyName FROM Customers WHERE CustomerID = 'ALFKI'

Using a DataReader or a DataSet to retrieve this single value is probably overkill. The Command object has a method specifically designed for such queries: ExecuteScalar. This method returns the value through the generic object data type, which you can then convert to the desired data type, as shown here:

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim cmd As OleDbCommand = cn.CreateCommand()

cmd.CommandText = "SELECT COUNT(*) FROM Customers"
Dim intCustomers As Integer = CInt(cmd.ExecuteScalar())

cmd.CommandText = "SELECT CompanyName FROM Customers " & _
                  "WHERE CustomerID = 'ALFKI'"
Dim strCompanyName As String = Convert.ToString(cmd.ExecuteScalar)
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbCommand cmd = cn.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM Customers";
int intCustomers = Convert.ToInt32(cmd.ExecuteScalar());

cmd.CommandText = "SELECT CompanyName FROM Customers " +
                  "WHERE CustomerID = 'ALFKI'";
string strCompanyName = Convert.ToString(cmd.ExecuteScalar());

The ExecuteScalar method is a great example of a feature that offers a better solution to a coding scenario that you might not have even realized was fairly inefficient.

Let’s say you’re building an application that lets a user examine the orders that your customers have placed. If you have 20 customers, you won’t want to write 20 separate functions to return the orders for each specific customer. Instead, you should build a parameterized function that accepts information about the customer and returns the orders for that customer. Similarly, you can build queries that accept parameters. Let’s look at the query that you’d use in the parameterized function to return the orders for a particular customer:

SELECT OrderID, CustomerID, EmployeeID, OrderDate
       FROM Orders WHERE CustomerID = ?

The question mark is a parameter marker—the standard way of denoting a parameter in a query.

  The SQL Server .NET data provider doesn’t support the generic parameter marker ?; it requires named parameters that use the @ prefix instead. In the following query, @CustomerID is the named parameter:
    SELECT OrderID, CustomerID, EmployeeID, OrderDate
           FROM Orders WHERE CustomerID = @CustomerID

We’ll discuss executing parameterized queries with the SQL Server .NET data provider in more detail in Appendix A.

By simply changing the value of the parameter, we can use this query to retrieve the orders for any customer. The ADO.NET Parameter object is the structure you create to store the parameter information. The Command object also has a Parameters collection. The following code snippet creates a Command, adds a Parameter to its Parameters collection, supplies a value for the Parameter, and executes the Command to pull back information for the orders for a particular customer:

Visual Basic .NET
Dim strConn, strSQL As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim cn As New OleDbConnection(strConn)
cn.Open()
strSQL = "SELECT OrderID, CustomerID, EmployeeID, OrderDate " & _
         "FROM Orders WHERE CustomerID = ?"
Dim cmd As New OleDbCommand(strSQL, cn)
cmd.Parameters.Add("@CustomerID", OleDbType.WChar, 5)
cmd.Parameters(0).Value = "ALFKI"
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
Visual C# .NET
string strConn, strSQL;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
strSQL = "SELECT OrderID, CustomerID, EmployeeID, OrderDate " +
         "FROM Orders WHERE CustomerID = ?";
OleDbCommand cmd = New OleDbCommand(strSQL, cn);
cmd.Parameters.Add("@CustomerID", OleDbType.WChar, 5);
cmd.Parameters[0].Value = "ALFKI";
OleDbDataReader rdr = cmd.ExecuteReader();

Once you’ve executed the query using the ExecuteReader method, you can fetch the results of the query using the DataReader object, just as you do with standard queries that don’t use parameters.

Using parameterized queries can greatly simplify your programming. Changing the value of a parameter is much easier than programmatically concatenating query strings, especially because when you supply values in a parameter you don’t need to worry about delimiting them. For example, if you wanted to search for an employee whose last name is O’Leary without using parameters, you would need to build a query string that looks something like this:

SELECT EmployeeID, LastName, FirstName FROM Employees
       WHERE LastName = 'O''Leary'

Because you need to surround the literal value you’re searching for with quotes, you have to replace the single quotes in the value with two consecutive single quotes. Developers who’ve tried to put double quotes into a string in their code can relate.

Calling a Stored Procedure

Let’s say we have a stored procedure that can return a row of data. Here’s an example of a SQL Server stored procedure designed to do just that:

CREATE PROCEDURE GetCustomer (@CustomerID nchar(5)) AS
    SELECT CustomerID, CompanyName, ContactName, ContactTitle
           FROM Customers WHERE CustomerID = @CustomerID
RETURN
  Some databases, such as Oracle, cannot return a result set from a stored procedure call in this fashion. For more information on fetching a result set from an Oracle stored procedure using ADO.NET, please see the Microsoft Knowledge Base.

How do we call this stored procedure from a Command? One option is to use the Command object’s CommandType property. You can set this property to any value in the CommandType enumeration: Text, TableDirect, or StoredProcedure. The property is set to Text by default. Setting CommandType to StoredProcedure tells the Command that you’re calling a stored procedure. The Command object will combine the value stored in its CommandText property with the information in its Parameters collection to generate the syntax to call your stored procedure, as shown here:

Visual Basic .NET
...
Dim cn As New OleDbConnection(strConn)
cn.Open()

Dim cmd As OleDbCommand = cn.CreateCommand()
With cmd
    .CommandText = "GetCustomer"
    .CommandType = CommandType.StoredProcedure
    .Parameters.Add("@CustomerID", OleDbType.WChar, 5)
    .Parameters(0).Value = "ALFKI"
End With

Dim rdr As OleDbDataReader = cmd.ExecuteReader()
If rdr.Read() Then
    Console.WriteLine(rdr("CompanyName"))
Else
    Console.WriteLine("No customer found")
End If

rdr.Close()
cn.Close()
Visual C# .NET
...
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();

OleDbCommand cmd = cn.CreateCommand();
cmd.CommandText = "GetCustomer";
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@CustomerID", OleDbType.WChar, 5);
cmd.Parameters[0].Value = "ALFKI";

OleDbDataReader rdr = cmd.ExecuteReader();
if (rdr.Read())
    Console.WriteLine(rdr["CompanyName"]);
else
    Console.WriteLine("No customer found");

rdr.Close();
cn.Close();

The standard way to call the stored procedure is to use the following syntax:

{? = CALL MyStoredProc(?, ?, ?)}

The initial parameter marker represents the return value of the procedure call and can be omitted if you’re not going to use the value returned. Leaving off the return parameter changes the query’s syntax to this:

{CALL GetCustomer(?)}

I prefer using this syntax in my code rather than relying on the CommandType property. If you want to query a table, view, or stored procedure that contains odd characters such as spaces in its name, you must surround the object name with delimiters. With an OleDbCommand object, setting the CommandType property to TableDirect or StoredProcedure doesn’t surround the object name with delimiters in such cases. You need to add the delimiters yourself. This behavior is not consistent for all Command objects. For example, the SqlCommand object will correctly delimit object names automatically. Of course, if you don’t put spaces in your table and stored procedure names, this won’t be an issue for you (nudge, nudge).

My advice is to avoid relying on the CommandType property and instead use the appropriate syntax for your query in the CommandText property, as shown here:

Visual Basic .NET
Dim cmd As New OleDbDataAdapter()
cmd.CommandText = "{CALL GetCustomer(?)}"
cmd.CommandType = CommandType.Text
Visual C# .NET
OleDbDataAdapter cmd = New OleDbDataAdapter();
cmd.CommandText = "{CALL GetCustomer(?)}";
cmd.CommandType = CommandType.Text;

Developers with some SQL Server experience might be used to using the EXEC syntax for calling stored procedures in tools such as Query Analyzer. You’re welcome to use this syntax with your Command objects, but keep in mind that not all databases support this syntax. You might need to change your syntax slightly if you need to query a different type of database.

Retrieving Data from Output Parameters

Not all stored procedures return information through a result set. Many procedures return information through output parameters. Let’s say our SQL Server GetCustomer stored procedure looks like the following instead:

CREATE PROCEDURE GetCustomer (@CustomerID nchar(5),
                              @CompanyName nvarchar(40) OUTPUT, 
                              @ContactName nvarchar(30) OUTPUT,
                              @ContactTitle nvarchar(30) OUTPUT) AS
    SELECT @CompanyName = CompanyName, @ContactName = ContactName,
           @ContactTitle = ContactTitle
           FROM Customers WHERE CustomerID = @CustomerID
    IF @@ROWCOUNT = 1
        RETURN 0
    ELSE
        RETURN -1

How can we use a Command to retrieve data from the output parameters? The Parameter object has a Direction property that accepts values from the ParameterDirection enumeration: ReturnValue, Input, InputOutput, and Output. The default for the property is Input. In order to retrieve information from this stored procedure, we need to set the Direction property on the parameters that aren’t input-only.

The stored procedure uses the return parameter to indicate success or failure. So, in our code snippet we’ll examine the value of the return parameter to determine whether we successfully located the desired customer in the table:

Visual Basic .NET
...
Dim cn As New OleDbConnection(strConn)
cn.Open()

Dim cmd As OleDbCommand = cn.CreateCommand()
With cmd
    .CommandText = "{? = CALL GetCustomer(?, ?, ?, ?)}"

    .Parameters.Add("@RetVal", OleDbType.Integer)
    .Parameters.Add("@CustomerID", OleDbType.WChar, 5)
    .Parameters.Add("@CompanyName", OleDbType.VarWChar, 40)
    .Parameters.Add("@ContactName", OleDbType.VarWChar, 30)
    .Parameters.Add("@ContactTitle", OleDbType.VarWChar, 30)

    .Parameters("@ContactTitle").Direction = ParameterDirection.Output
    .Parameters("@RetVal").Direction = ParameterDirection.ReturnValue
    .Parameters("@CustomerID").Value = "ALFKI"
    .Parameters("@CompanyName").Direction = ParameterDirection.Output
    .Parameters("@ContactName").Direction = ParameterDirection.Output

    .ExecuteNonQuery()
    If Convert.ToInt32(.Parameters("@RetVal").Value) = 0 Then
        Console.WriteLine(.Parameters("@CompanyName").Value)
    Else
        Console.WriteLine("Customer not found")
    End If
End With
Visual C# .NET
...
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();

OleDbCommand cmd = cn.CreateCommand();
cmd.CommandText = "{? = CALL GetCustomer(?, ?, ?, ?)}";

cmd.Parameters.Add("@RetVal", OleDbType.Integer);
cmd.Parameters.Add("@CustomerID", OleDbType.WChar, 5);
cmd.Parameters.Add("@CompanyName", OleDbType.VarWChar, 40);
cmd.Parameters.Add("@ContactName", OleDbType.VarWChar, 30);
cmd.Parameters.Add("@ContactTitle", OleDbType.VarWChar, 30);

cmd.Parameters["@ContactTitle"].Direction = ParameterDirection.Output;
cmd.Parameters["@RetVal"].Direction = ParameterDirection.ReturnValue;
cmd.Parameters["@CustomerID"].Value = "ALFKI";
cmd.Parameters["@CompanyName"].Direction = ParameterDirection.Output;
cmd.Parameters["@ContactName"].Direction = ParameterDirection.Output;

cmd.ExecuteNonQuery();
if (Convert.ToInt32(cmd.Parameters["@RetVal"].Value) == 0)
    Console.WriteLine(cmd.Parameters["@CompanyName"].Value);
else
    Console.WriteLine("Customer not found");

The Command object has a Transaction property that you must set in order to execute your Command within a Transaction. In the previous Article, you saw how to create a Transaction object using the Command object’s BeginTransaction method. The following code snippet shows how you can execute a Command on that Transaction.

Visual Basic .NET
...
cn.Open()
Dim txn As OleDbTransaction = cn.BeginTransaction()
Dim strSQL As String = "INSERT INTO Customers (...) VALUES (...)"
Dim cmd As New OleDbCommand(strSQL, cn, txn)
Dim intRecordsAffected As Integer = cmd.ExecuteNonQuery()
If intRecordsAffected = 1 Then
    Console.WriteLine("Update succeeded")
    txn.Commit()
Else
    'Assume intRecordsAffected = 0
    Console.WriteLine("Update failed")
    txn.Rollback()
End If
Visual C# .NET
...
cn.Open();
OleDbTransaction txn = cn.BeginTransaction();
string strSQL = "INSERT INTO Customers (...) VALUES (...)";
OleDbCommand cmd = New OleDbCommand(strSQL, cn, txn);
int intRecordsAffected = cmd.ExecuteNonQuery();
if (intRecordsAffected == 1)
{
    Console.WriteLine("Update succeeded");
    txn.Commit();
}
else
{
    //Assume intRecordsAffected = 0
    Console.WriteLine("Update failed");
    txn.Rollback();
}

Don’t forget to call the Commit or Rollback method on the Transaction object (depending on whether you want to save or discard the actions performed within the transaction).

Creating Commands in Visual Studio .NET

Visual Studio .NET can save you time, effort, and quite a few headaches by helping you create and configure Command objects quickly and easily. Let’s look at the Visual Studio .NET design-time features for working with Command objects.

The primary starting point for creating Command objects in Visual Studio .NET is the Toolbox. On the Data tab, you’ll find Command objects for each of the .NET data providers. To create an OleDbCommand, you simply drag-and-drop the OleDbCommand item from the Toolbox onto your design surface or to the design surface’s components tray. You’ll have a new Command object in your designer’s components tray, as shown in Figure 4-1.

As you learned earlier in this Article, you must specify a Connection and a query string in order to create a useful Command object. Visual Studio .NET can help you set the Connection and CommandText properties of your new Command at design time.

Specifying a Connection

Once you’ve added a Command to your components tray, you’ll want to set its Connection property to a Connection object. If you select the Command object’s Connection property in the Properties window, you’ll get a drop-down list of choices. You can opt to use an existing Connection, create a new Connection, or leave the Connection property blank (none). Figure 4-2 shows a Windows form with an OleDbConnection and an OleDbCommand in its components tray. The OleDbCommand is selected, and the Properties window shows how you can set the object’s Connection property to the existing OleDbConnection.

If you select New from the drop-down list, Visual Studio .NET will open the Data Link Properties dialog box, where you can create a new connection string. Visual Studio .NET will add a new Connection object using the connection string you build and set your Command object’s Connection property to this new Connection object.

Using Query Builder

Visual Studio .NET simplifies the process of building your query string through Query Builder. Select a Command object in your designer’s components tray, and then select the object’s CommandText property in the Properties window. You’ll see a button that indicates that this property has its own property page. Click this button to invoke the Query Builder dialog box, shown in Figure 4-3.

Query Builder offers a simple graphical user interface to help you build queries. When it launches, Query Builder prompts you to select the tables, views, and functions you want to access in your query, as shown in Figure 4-3. As you select objects, you’ll see them added to the design surface behind the Add Table dialog box.

After you specify the tables you want to access in your query, Query Builder helps you graphically select the columns, apply filters and sorting order, and so forth. In Figure 4-4, we’ve selected the Customers table and specified the columns from the table that we want to fetch in the query. Notice that we’ve also specified a parameter for the CustomerID column.

The Query Builder dialog box has three design panes. The top pane provides a simple way to select columns. The second pane simplifies the process of adding filters and sort orders to the result set. The third pane contains the text of the query you’ve built. Changes you make in one pane affect the other two. You can right-click in the text pane and choose Verify from the shortcut menu to find out whether the query you’ve built is valid.

Another handy command on the shortcut menu, Run, runs the query you’ve built and displays the results in the bottom pane. If you’ve created a parameterized query, Query Builder will display a dialog box to let you specify values for the parameters. Query Builder also contains logic that enables you to edit the data in the results pane to modify the contents of your database.

Using Your New Command Object in Code

We’ve successfully created and configured our Command object based on the following query:

SELECT CustomerID, CompanyName, ContactName, ContactTitle
       FROM Customers WHERE CustomerID LIKE ?

Before we add code to execute our Command and fetch its results, drag-and-drop a list box item from the Toolbox onto your form. Modify its size to take up most of the form. We’ll use this list box to display the contents of the CompanyName column for the rows we retrieve.

Double-click on the form to enter the form’s Load event. Now we want to execute the Command, fetch the results through a DataReader, and display the contents of the CompanyName column in our list box. Even though we specified a parameter for the CustomerID column, we can supply the wildcard character % so the query will return all customers.

Visual Basic .NET developers can add the code shown in Figure 4-5.

 
Figure 4-5
Using the Command object at run time in Visual Basic .NET

Visual C# .NET developers can add the following code:

//Open the connection.
oleDbConnection1.Open();

//Specify the wildcard for the parameter to retrieve all customers.
oleDbCommand1.Parameters[0].Value = "%";

//Execute the query and display all CompanyNames in the list box.
OleDbDataReader rdr = oleDbCommand1.ExecuteReader();
while (rdr.Read())
    listBox1.Items.Add(rdr["CompanyName"]);

//Close the DataReader and Connection.
rdr.Close();
oleDbConnection1.Close();

Visual C# .NET developers, don’t forget to add using System.Data.OleDb; to the using block at the top of the form.

Run the project, and you’ll see the list box filled with the names of the companies in the Customers table.

Dragging and Dropping from Server Explorer

If you’re basing a Command on a stored procedure call, you can create and configure your Command object by dragging and dropping the stored procedure from Server Explorer onto your design surface. Dragging and dropping a SQL Server stored procedure creates a SqlCommand, and using a stored procedure from other data sources creates an OleDbCommand.

In Figure 4-6, we’ve dragged the SQL Server Northwind database’s Cust­OrderHist stored procedure onto a Windows form. You can see in the Properties window that the CommandText, CommandType, and Connection properties are set so that you can call this stored procedure easily through code.

  Visual Studio .NET adds delimiters to the name of the stored procedure so that you don’t run into problems calling stored procedures that have odd characters such as spaces in their names.

Visual Studio .NET has also populated the new Command object’s Parameters collection. Select the Parameters collection in the Properties window, and then click the button to the right to launch its property page, as shown in Figure 4-7.

Visual Studio .NET’s data tools query your database for schema information about the stored procedure in order to populate the Command object’s Parameters collection. However, many databases, such as SQL Server, don’t differentiate between input/output and output-only parameters. As a result, if you want to call a stored procedure that uses output parameters, you might need to set the direction for those parameters by hand in the Properties window.

  Dragging and dropping a table or view from Server Explorer creates a DataAdapter rather than a Command. We’ll discuss DataAdapter objects in the next Article.
Command, DataReader, and Parameter Object Reference

Now that we’ve examined the major features of the Command, DataReader, and Parameter objects, let’s examine the properties and methods of each object.

Table 4-1 lists the properties of the OleDbCommand object that you’re most likely to use.

Table 4-1  Commonly Used Properties of the OleDbCommand Object
Property Data Type Description
CommandText String The text of the query that you want to execute.
CommandTimeout Int32 Time (in seconds) that the adapter will wait for the query to execute before timing out. (Default = 30 seconds.)
CommandType CommandType Specifies the type of query to execute. (Default = Text.)
Connection OleDbConnection The connection to your data store that the Command will use to execute the query.
Parameters OleDbParameterCollection A collection of parameters for the query.
Transaction OleDbTransaction Specifies the transaction to use for the query.
UpdatedRowSource UpdateRowSource Controls how the results of the query will affect the current DataRow if the Command is used by calling the Update method of a DataAdapter. (Default = Both.) See Article 11 for more information on using this property when submitting pending changes to your database.
CommandTimeout Property

The CommandTimeout property determines how long, in seconds, the Command will wait for the results of your query before timing out. By default, this property is set to 30. If the query does not complete by the time specified in the CommandTimeout property, the Command will throw an exception.

Keep in mind that once the query starts returning results, the query won’t time out. Let’s say you want to use a DataAdapter to fetch the contents of a table into a DataSet. For the sake of argument, let’s imagine that your table is so absurdly large that the process of fetching its contents takes more than 30 seconds, the default value for the Command object’s CommandTimeout property. Because the Command that the DataAdapter uses retrieved the first row in less than the time specified in the CommandTimeout property, the query won’t time out no matter how long it takes to retrieve the contents of the table—a minute, a day, or a year.

CommandType Property

ADO.NET can simplify the process of setting the text for your query through the CommandType property. You can set this property to any of the values in the CommandType enumeration (available in System.Data), which are described in Table 4-2.

Table 4-2  Members of the CommandType Enumeration
Constant Value Description
Text 1 The Command will not modify the contents of the CommandText property.
StoredProcedure 4 The Command will build a query to call a stored procedure using the value of the CommandText property as a stored procedure name.
TableDirect 512 The Command will prepend "SELECT * FROM " to the contents of the CommandText property.

By default, this property is set to Text. Using this default setting, the Command will use whatever text you’ve specified in the CommandText property to execute your query. In my opinion, you should leave the property set to the default. Here’s why.

If you set the property to TableDirect, the Command will prepend "SELECT * FROM " to the contents of the CommandText property when you execute the query. This means that the Command will fetch all rows and all columns from the table—if the query succeeds.

If you query a table that has a space in its name, such as the sample Northwind database’s Order Details table, the query will fail unless you surround the table name with a delimiter that the database can handle. I try to use square brackets rather than having to jump through hoops to embed double quote characters into my strings. Setting CommandType to TableDirect will not delimit your table name automatically. You still have to do that work yourself.

Of course, you can avoid such problems by not using spaces in the names of your tables, columns, views, and stored procedures. Seriously, how many times have database developers said, “Thank goodness I was able to put a space in that object name”?

  The constant name TableDirect is a bit of a misnomer and might lead ADO developers to believe that it maps to adCmdTableDirect in ADO’s CommandTypeEnum. That’s a perfectly logical assumption, but the constant actually maps to adCmdTable. Despite what the constant’s name implies, setting CommandType to TableDirect doesn’t make the Command fetch the contents of your table through the low-level interfaces that the Jet and SQL Server OLE DB providers support.

The StoredProcedure constant simplifies the process of calling a stored procedure, as shown here:

Visual Basic .NET
Dim cmd As New OleDbCommand()
cmd.CommandType = CommandType.StoredProcedure
cmd.CommandText = "MyStoredProc"

cmd.CommandType = CommandType.CommandText
cmd.CommandText = "{CALL MyStoredProc}"
Visual C# .NET
OleDbCommand cmd = New OleDbCommand();
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "MyStoredProc";

cmd.CommandType = CommandType.CommandText;
cmd.CommandText = "{CALL MyStoredProc}";

The code snippet shows the standard syntax for calling stored procedures: {CALL MyStoredProc}. SQL Server also supports the EXEC MyStoredProc syntax. In fact, the SQL Server OLE DB provider will translate calls that use the CALL syntax and actually use the EXEC syntax when communicating directly with the database. For this reason, you might want to use the EXEC syntax to try to get your code to run just a tiny bit faster. I avoid using this syntax because I often have to write code that’s back-end independent. Later in the Article, we’ll look at how to call parameterized stored procedures, which can add a touch of complexity to the syntax.

Like the TableDirect constant, setting CommandType to StoredProcedure doesn’t delimit the stored procedure name when you execute your query. For that reason, I prefer leaving CommandType as Text and using the CALL syntax in the CommandText property.

Parameters Property

The Parameters property returns an OleDbParameterCollection, which contains a collection of OleDbParameter objects. We’ll examine the properties and methods of the OleDbParameter object later in the Article.

Transaction Property

You use the Command object’s Transaction property to execute your Command within a transaction. If you’ve opened a Transaction object on your Connection and try to execute your Command without associating it with that Transaction via this property, the Command object will generate an exception.

The UpdatedRowSource property is designed to help you refetch data for the row you’re updating using a DataAdapter and Command objects that contain updating logic. Table 4-3 lists the values accepted by UpdatedRowSource. We’ll discuss the use of this property in Article 11.

Table 4-3  Members of the UpdateRowSource Enumeration
Constant Value Description
Both 3 Command will fetch new data for the row through both the first returned record and output parameters.
FirstReturnedRecord 2 Command will fetch new data for the row through the first returned record
None 0 Command will not fetch new data for the row upon execution.
OutputParameters 1 Command will fetch new data for the row through output parameters
Methods of the OleDbCommand Object

Now let’s take a look at the methods of the OleDbCommand object, which are listed in Table 4-4.

Table 4-4  Commonly Used Methods of the OleDbCommand Object 
Method Description
Cancel Cancels the execution of the query
CreateParameter Creates a new parameter for the query
ExecuteNonQuery Executes the query (for queries that do not return rows)
ExecuteReader Executes the query and retrieves the results in an OleDbData­Reader
ExecuteScalar Executes the query and retrieves the first column of the first row, designed for singleton queries such as

“SELECT COUNT(*) FROM MyTable WHERE...”

Prepare Creates a prepared version of the query in the data store
ResetCommandTimeout Resets the CommandTimeout property to its default of 30 seconds

You can use the Cancel method to cancel the execution of a query. If the Command object whose Cancel method you’ve called is not currently executing a query, the Cancel method does nothing.

The Cancel method also causes the Command object to discard any unread rows on a DataReader object. The following sample code fetches the results of a simple query. The code displays the results, followed by the number of rows retrieved. In the code, there’s a call to the Cancel method that’s commented out. Remove the comment character(s) and re-run the code to demonstrate that the Cancel method discards the results of the query.

Visual Basic .NET
Dim strConn As String = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
                        "Initial Catalog=Northwind;Trusted_Connection=Yes;"
Dim strSQL As String = "SELECT CustomerID FROM Customers"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim cmd As New OleDbCommand(strSQL, cn)
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
Dim intRowsRetrieved As Integer
'cmd.Cancel()
Do While rdr.Read
    Console.WriteLine(rdr.GetString(0))
    intRowsRetrieved += 1
Loop
Console.WriteLine(intRowsRetrieved & " row(s) retrieved")
rdr.Close()
cn.Close()
Visual C# .NET
string strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" +
                 "Initial Catalog=Northwind;Trusted_Connection=Yes;";
string strSQL = "SELECT CustomerID FROM Customers";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbCommand cmd = New OleDbCommand(strSQL, cn);
OleDbDataReader rdr = cmd.ExecuteReader();
int intRowsRetrieved = 0;
//cmd.Cancel();
while (rdr.Read())
{
    Console.WriteLine(rdr.GetString(0));
    intRowsRetrieved++;
}
Console.WriteLine(intRowsRetrieved + " row(s) retrieved");
rdr.Close();
cn.Close();

The ExecuteNonQuery method executes the query without creating a Data­Reader to fetch the rows returned by the query. Use ExecuteNonQuery if you want to issue an action query or don’t want to examine the rows returned by the query. Values for return and output parameters are available upon completion of the call to ExecuteNonQuery.

ExecuteNonQuery returns an integer to indicate the number of rows modified by the query you’ve executed. If you’re using batch queries, see the discussion of batch queries and the return value of ExecuteNonQuery earlier in this Article.

ExecuteReader Method

If you want to examine the row(s) returned by a query, use the Command object’s ExecuteReader method to return that data in a new DataReader object. We discussed the basic use of this method earlier in the Article. However, there are some interesting options on the method.

The Command object’s ExecuteReader method is overloaded and can accept a value from the CommandBehavior enumeration. Table 4-5 describes each of these options.

Table 4-5  Members of the CommandBehavior Enumeration
Constant Value Description
CloseConnection 32 Closing the DataReader will close the connection.
KeyInfo 4 Causes the DataReader to fetch primary key information for the columns in the result set.
SchemaOnly 2 The DataReader will contain only column information without actually running the query.
SequentialAccess 16 Values in the columns will be available only sequentially. For example, after you examine the contents of third column, you won’t be able to examine the contents of the first two columns.
SingleResult 1 The DataReader will fetch the results of only the first row-returning query.
SingleRow 8 The DataReader will fetch only the first row of the first row-returning query.
CloseConnection

If you supply CloseConnection when calling the Execute­Reader method, when you call the Close method on the DataReader, the Data­Reader will call the Close method on the Connection with which it is associated.

This feature can be extremely handy if you’re building business objects and passing data from one object to another. You might encounter situations in which you want a business object to return a DataReader to the calling object rather than returning the data in a DataTable or some other structure. In such cases, you might want the calling object to be able to close the Connection object after it’s done reading the results of the query from the DataReader.

But what if you don’t trust the calling object? You might not want to hand it a direct connection to the database. Using CloseConnection can simplify this scenario without compromising the security and architecture of your application.

KeyInfo and SchemaOnly

When we look at methods of the DataReader object, we’ll discuss the GetSchemaTable method. This method returns metadata about the columns in the DataReader—column names, data types, and so on. Such information can be helpful if you’re building code-generation tools. If you’re going to use the DataReader object’s GetSchemaTable method, you should also look at the KeyInfo and SchemaOnly options of the Command’s ExecuteReader method.

If you call ExecuteReader and use the SchemaOnly value in the Options parameter, you’ll retrieve schema information about the columns but you won’t actually execute the query.

Using KeyInfo in the Options parameter forces the DataReader to fetch additional schema information from your data source to indicate whether the columns in the result set are part of the key columns in the tables in your data source.

If you use the SchemaOnly option, you don’t need to include KeyInfo as well. The key information will be included in the schema automatically.

If you use the SequentialAccess option when calling ExecuteReader, the columns of data will be available only sequentially through the DataReader. For example, if you look at the contents of the second column, the contents of the first column will no longer be available.

Use of the SequentialAccess value might increase the performance of your DataReader slightly, depending on the data source you’re using.

SingleRow and SingleResult

If you’re interested in examining only the first row or first result set returned by your query, you might want to use SingleRow or SingleResult when calling ExecuteReader.

Supplying SingleRow in the Options parameter will create a DataReader that contains, at most, one row of data. If you issue a query that returns 10 rows of data but you use SingleRow in your call to ExecuteReader, only the first row of data will be available through the DataReader. All other rows will be discarded. Similarly, using SingleResult causes subsequent result sets to be discarded.

ExecuteScalar Method

The ExecuteScalar method is similar to ExecuteReader except that it returns the first column of the first row of the result set in a generic Object data type. If the query returns more than one cell of data, this data is discarded.

If your query returns a single cell of data, like the following query does, you can improve the performance of your code by using ExecuteScalar.

SELECT COUNT(*) FROM MyTable

One of the major benefits of stored procedures is that they generally run faster than dynamic queries. This is because the database system can prepare an execution plan for them ahead of time. It’s sort of like the difference between script code and compiled code. Script code is often more flexible because you can generate it at run time, but compiled code runs faster.

Most database systems support the notion of a “prepared” query. You can think of a prepared query as a temporary stored procedure. If you’re going to execute the same query multiple times, you might get better performance by preparing the query.

To prepare a Command, you simply call its Prepare method.

If you’re building multi-tiered applications, prepared queries aren’t likely to improve the performance. In fact, I don’t recommend using prepared queries in such situations. With multi-tiered applications, the code in your server components will likely connect, run a query or two, and then disconnect. Most multi-tiered applications take advantage of connection pooling at the middle-tier level.

Simply put, a connection pool will hold onto a connection for a brief amount of time. If your code requests a connection that matches one in a pool, your code will receive an open connection from the pool rather than opening a new one. This process can greatly improve the performance of your code at the middle tier.

However, if your connections are constantly recycled through connection pooling rather than being truly closed, your database won’t have a chance to discard all of the temporary stored procedures that it created for your prepared queries. Recent versions of SQL Server have changed how these temporary stored procedures are stored in order to better handle this scenario, but you’re better off not preparing your queries if you’re building multi-tiered applications.

ResetCommandTimeout Method

Calling the ResetCommandTimeout method resets the Command object’s CommandTimeout property to its default value of 30 seconds. If you find yourself wondering “Why would I need a property to do that?,” you’re not alone.

Properties of the OleDbDataReader Object

Now let’s look at the properties of the OleDbDataReader (Table 4-6).

Table 4-6  Commonly Used Properties of the OleDbDataReader Object 
Property Data Type Description
Depth Int32 Indicates the depth of nesting for the current row (read-only).
FieldCount Int32 Returns the number of fields contained by the DataReader (read-only).
IsClosed Boolean Indicates whether the DataReader is closed (read-only).
Item Object Returns the contents of a column for the current row (read-only).
RecordsAffected Int32 Indicates the number of records affected by the queries submitted (read-only).

The Depth property and the GetData method are reserved for queries that return hierarchical data. These features are not supported in the current release of ADO.NET.

FieldCount Property

The FieldCount property returns an integer to indicate the number of columns of data in the result set.

The IsClosed property returns a Boolean value to indicate whether the Data­Reader object is closed.

Item Property

The DataReader object’s Item property is similar, in form and function, to the DataRow object’s Item property. You can supply the name of a column as a string or the integer position of a column, and the property will return the value stored in that column in the generic object data type.

If you know the data type of the column, you’ll get better performance by calling the Get<DataType> method (such as GetInteger or GetString) instead.

You can use the RecordsAffected property to determine the number of rows that your action query (or queries) modified. If you want to execute a single action query, use the ExecuteNonQuery method of the Command object instead. The ExecuteNonQuery method returns the number of rows the action query affected.

If you’re executing a batch of queries and you want to determine the number of rows affected, see the section on batch queries earlier in the Article.

Methods of the OleDbDataReader Object

And now for your programming pleasure, Table 4-7 presents the methods of the OleDbDataReader that you’re most likely to encounter.

Table 4-7  Commonly Used Methods of the OleDbDataReader Object 
Method Description
Close Closes the DataReader.
Get<DataType> Returns the contents of a column in the current row as the specified type based on its ordinal.
GetBytes Retrieves an array of bytes from a column in the current row.
GetChars Retrieves an array of characters from a column in the current row.
GetData Returns a new DataReader from a column.
GetDataTypeName Returns the name of the data type for a column based on its ordinal.
GetFieldType Returns the data type for a column based on its ordinal.
GetName Returns the name of a column based on its ordinal.
GetOrdinal Returns the ordinal of a column based on its name.
GetSchemaTable Returns the schema information (column names and data types) of the DataReader as a DataTable.
GetValue Returns the value of a column based on its ordinal.
GetValues Accepts an array that the DataReader will use to return the contents of the current column. This call returns a 32-bit integer that indicates the number of entries returned in the array.
IsDBNull Indicates whether a column contains a Null value.
NextResult Moves to the next result.
Read Moves to the next row.

The Read method accesses the next row of data. Remember that the first row in the result set will not be available through the DataReader until you call the Read method. The first time you call the Read method, the DataReader will move to the first row in the result set. Subsequent calls to Read will move to the next row of data.

The Read method also returns a Boolean value to indicate whether there are any more results for the query. The sample code we examined earlier continually examines results until the Read method returns False.

The GetValue method is similar to the Item property. Supply an integer, and the GetValue method will return the contents of that column in the generic object type. The GetValue method and the various Get<DataType> methods accept only integers for the column index and do not perform string-based lookups such as the Item property.

The DataReader is designed for speed; referencing an item in a collection by its ordinal value is faster than having the collection locate the item by its name.

The DataReader also offers methods that return specific data types. If you know that a column contains string data, you can call the GetValue method of the DataReader and convert the data to a string or simply call the GetString method, as shown here:

Visual Basic .NET
Dim strCompanyName As String
Dim rdr As OleDbDataReader
...
strCompanyName = rdr.GetString(intCompanyNameIndex)
'or
strCompanyName = rdr.GetValue(intCompanyNameIndex).ToString
Visual C# .NET
string strCompanyName;
OleDbDataReader rdr;
...
strCompanyName = rdr.GetString(intCompanyNameIndex);
//or
strCompanyName = rdr.GetValue(intCompanyNameIndex).ToString();

The DataReader has methods to return each of the data types available in the .NET Framework—GetByte, GetChar, GetDateTime, and so on.

The GetValues method lets you store the contents of a row in an array. If you want to retrieve the contents of each column as quickly as possible, using the GetValues method will provide better performance than checking the value of each column separately.

The DataAdapter uses a DataReader to fetch data from your database to store the results in DataTables. To provide the best performance possible, the DataAdapter objects in the .NET data providers included in Visual Studio .NET use the DataReader object’s GetValues method. Here’s a simple example of how to use GetValues:

Visual Basic .NET
Dim rdr As OleDbDataReader = cmd.ExecuteReader()
Dim aData(rdr.FieldCount – 1) As Object
while rdr.Read
    rdr.GetValues(aData)
    Console.WriteLine(aData(0).ToString)
End While
Visual C# .NET
OleDbDataReader rdr = cmd.ExecuteReader();
object[] aData = New object[rdr.FieldCount];
while (rdr.Read())
{
    rdr.GetValues(aData);
    Console.WriteLine(aData[0].ToString());
}
  Visual Basic .NET and Visual C# .NET create arrays differently. The preceding code snippets take this difference into account. For example, Dim aData(4) As Object creates an array of length 5 (0 to 4) in Visual Basic and Visual Basic .NET, but object[] aData = new object[4]; creates an array of length 4 (0 to 3) in Visual C# .NET.

If you’re working with batch queries that return multiple result sets, use the NextResult method to move to the next set of results. Like the Read method, NextResult returns a Boolean value to indicate whether there are more results.

The sample code under "Questions That Should Be Asked More Frequently" shows how to use a DataReader to examine the contents of a batch query. It also shows how to use the NextResult method in a Loop.

When you’re using DataReader objects, it’s important that you Loop through the results and close the DataReader as quickly as possible. Your Connection object is blocked from performing any other work while a live firehose cursor is open on the connection. If you try to use a Connection that has an open DataReader on it, you’ll receive an exception that states that the operation “requires an open and available connection.”

  Some databases allow you to have multiple queries with pending results on the same connection. In the initial release of ADO.NET, having an open DataReader on a connection prevents you from performing any other operations on that connection until you’ve closed the DataReader object, regardless of whether the database supports having multiple queries with pending results on the same connection. This behavior might change in a future release of ADO.NET.

Developers who have some experience with ADO might be surprised by this restriction, but those who’ve used RDO might not. Various Microsoft data access technologies have handled this scenario differently.

If you try to open two firehose cursors against a SQL Server database using ADO, everything will work and you won’t receive an error. This is because the OLE DB specification states that when the current connection is blocked, the OLE DB provider will perform the requested action on a new connection.

RDO developers might recognize the error message “Connection is busy with results from another hstmt.” ODBC does not do any behind-the-scenes work to try to help you out. If you try to use a connection that’s busy, you simply receive an error message.

Which of these approaches (raising an error or performing the desired action on a new connection) is better? Developers, both inside and outside of Microsoft, can’t seem to agree. In fact, each successive Microsoft data access technology has handled the scenario differently than its predecessor. VBSQL raises an error, DAO/Jet creates a new connection, RDO raises an error, ADO creates a new connection, and ADO.NET raises an error.

GetName, GetOrdinal, and GetDataTypeName Methods

The DataReader has methods that you can use to learn more about the results returned by your query. If you want to determine the name of a particular column, you can call the GetName method. If you already know the column name you want to access but don’t know its ordinal position within the result set, you can pass the column name into the GetOrdinal method to retrieve its ordinal position. The GetDataTypeName method accepts an integer denoting the ordinal position of the column and returns the data type for that column as a string.

The DataReader object’s GetSchemaTable method is similar to the DataAdapter object’s FillSchema method. Each method lets you create a DataTable containing DataColumn objects that correspond to the columns returned by your query. The GetSchemaTable method accepts no parameters and returns a new Data­Table. The DataTable contains a DataColumn for each column returned by your query, but the Rows collection of the DataTable is empty. GetSchemaTable populates the new DataTable with schema information only.

The data that GetSchemaTable returns might be a little difficult to grasp initially. The GetSchemaTable method returns a DataTable with a predefined structure. Each DataRow in the DataTable returned by this method corresponds to a different column in the query results, and the DataColumn objects represent properties or attributes for those columns.

The following code snippet prints the name and database data type for each column the query returns.

Visual Basic .NET
Dim strConn, strSQL As String
strConn = "Provider=SQLOLEDB;Data Source=(local)\NetSDK;" & _
          "Initial Catalog=Northwind;Trusted_Connection=Yes;"
strSQL = "SELECT OrderID, CustomerID, EmployeeID, OrderDate FROM Orders"
Dim cn As New OleDbConnection(strConn)
cn.Open()
Dim cmd As New OleDbCommand(strSQL, cn)
Dim rdr As OleDbDataReader = cmd.ExecuteReader
Dim tbl As DataTable = rdr.GetSchemaTable
Dim row As DataRow
For Each row In tbl.Rows
    Console.WriteLine(row("ColumnName").ToString & " - " & _
                      CType(row("ProviderType"), OleDbType).ToString)
Next row
Visual C# .NET
string strConn, strSQL;
strConn = "Provider=SQLOLEDB;Data Source=(local)\\NetSDK;" + 
          "Initial Catalog=Northwind;Trusted_Connection=Yes;";
strSQL = "SELECT OrderID, CustomerID, EmployeeID, OrderDate FROM Orders";
OleDbConnection cn = New OleDbConnection(strConn);
cn.Open();
OleDbCommand cmd = New OleDbCommand(strSQL, cn);
OleDbDataReader rdr = cmd.ExecuteReader();
DataTable tbl = rdr.GetSchemaTable();
foreach (DataRow row in tbl.Rows)
    Console.WriteLine(row["ColumnName"] + " – " +
                      ((OleDbType) row["ProviderType"]).ToString());
  The code snippet for each language converts the integer stored in the ProviderType column to the OleDbType enumeration.

Various .NET data providers use different table schemas in the DataTable returned by GetSchemaTable. For example, the DataTable returned by the SQL Server .NET data provider DataReader object’s GetSchemaTable method includes columns not available through the OLE DB .NET data provider.

The Depth property and the GetData method are reserved for queries that return hierarchical data. These features are not supported in the current release of ADO.NET.

The Parameter has six constructors. The ParameterCollection has six overloaded Add methods that you can use to create a Parameter and append it to the collection. You can also use the CreateParameter method on the Command. So many choices.

Which method of creating a Parameter is right for you? That depends on which properties on the Parameter you want to set. One of the constructors for OleDbParameter that lets you supply values for the ParameterName, OleDbType, Size, Direction, IsNullable, Precision, Scale, SourceColumn, SourceVersion, and Value properties. Think about the properties you want to set and then use the constructor that provides the functionality you need.

Properties of the OleDbParameter Object

Table 4-8 lists the commonly used properties of the OleDbParameter object.

Table 4-8  Commonly Used Properties of the OleDbParameter Object 
Property Name Data Type Description
DataType Type Specifies the data type for the parameter object.
DbType OleDbType Specifies the database data type for the ­parameter.
Direction ParameterDirection Specifies the direction for the parameter—input, output, input/output, or return.
IsNullable Boolean Indicates whether the parameter can accept Null.
OleDbType OleDbType Specifies the OLE DB data type for the ­parameter.
ParameterName String Specifies the name of the parameter.
Precision Byte Specifies the precision for the parameter.
Scale Byte Specifies the numeric scale for the parameter.
Size Int32 Specifies the size of the parameter.
SourceColumn String Specifies the name of the column in the DataSet that this parameter references. See Article 10 for more information on binding query parameters to DataSet objects.
SourceVersion DataRowVersion Specifies version (current or original) of the column in the DataSet that this parameter references. See Article 10 for more information on binding query parameters to DataSet objects.
Value Object Specifies the value for the parameter.

Generally speaking, the ParameterName property of the Parameter is designed solely to let you locate the desired Parameter in a Command object’s Parameters collection. If you’re calling a stored procedure with the OLE DB .NET data provider, for example, you don’t need to have the ParameterName property on your Parameter objects match the names of the parameters in your stored procedure. But setting the ParameterName property on your Parameter objects can make your code easier to read.

  The SQL Server .NET data provider matches your Parameter objects to the parameter markers in your query based on the Parameter­Name property of the Parameter objects. So, if you use
    SELECT OrderID, CustomerID, EmployeeID, OrderDate
           FROM Orders WHERE CustomerID = @CustomerID

as your query, you need to set the ParameterName property of your Parameter to @CustomerID.

If you’re calling a stored procedure and you want to use output or return parameters, you should set the Direction property of your Parameter to one of the values listed in Table 4-9.

Table 4-9  Members of the ParameterDirection Enumeration
Constant Value Description
Input 1 Default value. The parameter is input-only.
Output 2 The parameter is output-only.
InputOutput 3 The parameter is input/output.
ReturnValue 6 The parameter will contain the return value of a stored procedure.

Because the default value for Direction is Input, you need to explicitly set this property only on Parameter objects that are not input-only.

Most code generation tools will query your database for parameter information, including the direction of the parameters. Even if you’re using a robust code generation tool, such as the ones included in Visual Studio .NET, you might still need to modify the Direction value of your Parameter objects in some cases.

Why, you ask? Most databases support the use of input, output, and input-output parameters on stored procedures, but not all databases have language constructs to let you explicitly specify the direction for your stored procedure parameters. SQL Server, for example, supports the OUTPUT keyword in stored procedure definitions to specify that the parameter can return a value. However, the definition of the parameter in the stored procedure is the same regardless of whether the parameter is input/output or output-only. As a result, code generation tools cannot determine whether the parameter is input/output or output-only. The Visual Studio .NET tools assume that the parameter is input/output. If you want an output-only parameter, you must set the direction explicitly in your code.

Value Property

Use the Value property to check or set the value of your Parameter. This property contains an Object data type. As a result, you might need to convert the data in order to store it in a data type such as a string or integer.

SourceColumn and SourceVersion Properties

The SourceColumn and SourceVersion properties control how the Parameter fetches data from a DataRow when you submit pending changes to your database by calling the Update method on the DataAdapter.

I’ll cover this feature in much more depth in Article 10 when I cover updating your database.

The Parameter is the only class in the ADO.NET object model that requires you to use the data types used by your database.

For example, when you retrieve the CustomerID field from the Customers table into a DataSet using a DataAdapter, you don’t need to know whether the field in the database is a fixed length or a variable length, nor do you need to know whether the field in the database can handle Unicode data. The data type for the DataColumn object is simply a string.

The DataColumn object’s DataType controls the data type that ADO.NET will use to store the contents of the column and accepts a .NET type as returned by the GetType or typeof function, depending on your language of choice. This data type has a loose connection to the data type that the database uses to store the data. String-based database data types (such as char and varchar) are mapped to the .NET data type String, noninteger numeric database data types (money, decimal, numeric) are mapped to the .NET data type Decimal, and so forth.

The data type for the Parameter must be more precise. In the earlier code snippet, we used the query

SELECT OrderID, CustomerID, EmployeeID, OrderDate
    FROM Orders WHERE CustomerID = ?

with a parameter whose data type is wchar (the w stands for wide to indicate that the string handles double-byte Unicode characters rather than single-byte ANSI characters) and whose length is 5. If we don’t use the appropriate data type for the parameter, the database might not handle the information stored in the parameter the way we expect.

Each Parameter exposes a DbType property and a data type property that’s specific to the .NET data provider. For example, the OleDbParameter has an OleDbType property and the SqlParameter has a SqlDbType property.

The DbType and OleDbType properties are closely related. Setting the value of one of these properties affects the value of the other. For example, if you set the DbType property of an OleDbParameter to DbType.Int32, you’re implicitly setting the OleDbType to OleDbType.Integer. Similarly, if you set the OleDbType to OleDbType.UnsignedTinyInt, you’re implicitly setting the DbType property to DbType.Byte.

When you define the structure for a table in a database, some data types require that you specify additional information beyond simply the name of the data type. Binary and character-based columns often have a maximum size. If you’re using a Parameter with such data, you must set the Size property to the desired size. Numeric data types often let you specify the scale (number of digits) and precision (number of digits to the right of the decimal point).

What Is a DataAdapter Object?

The DataAdapter class acts as a bridge between the connected and disconnected halves of the ADO.NET object model. You can use a DataAdapter to pull data from your database into your DataSet. The DataAdapter can also take the cached updates stored in your DataSet and submit them to your database. Article 10 will cover updating your database with DataAdapter objects. In this Article, we will focus on using DataAdapter objects to fetch data from your database.

When I describe the DataAdapter object to database programmers, most nod their heads and say that it sounds similar to the ADO Command object, the RDO rdoQuery object, and the DAO QueryDef object—all of which let you submit queries to your database and store the results in a separate object.

But there are some major differences between the DataAdapter and its predecessors, as I’ll detail in the following sections.

ADO, RDO, and DAO all support disconnected data. Each object model can store the results of a query in a disconnected structure. For example, you can use an ADO Command object to fetch data into a Recordset that’s disconnected from the Connection object. However, none of these object models provided disconnected functionality in their initial release. As a result, their query-based objects were never truly designed for disconnected data.

The DataAdapter is designed to work with disconnected data. Perhaps the best example of this design is the Fill method. You don’t even need a live connection to your database to call the Fill method. If you call the Fill method on a DataAdapter whose connection to your database is not currently open, the DataAdapter opens that connection, queries the database, fetches and stores the results of the query into your DataSet, and then closes the connection to your database.

There Is No Direct Connection Between the DataAdapter and the DataSet

You fill a DataTable in your DataSet by passing your DataSet as a parameter to the DataAdapter object’s Fill method, as shown here:

Visual Basic .NET
OleDbDataAdapter.Fill(DataSet)
Visual C# .NET
OleDbDataAdapter.Fill(DataSet);

Once this call completes, there is no connection between the two objects. The DataSet does not maintain a reference, internally or externally, to the DataAdapter, and the DataAdapter does not maintain a reference to the DataSet. Also, the DataSet contains no information indicating where the data originated—no connection string, no table name, and no column names. Thus, you can pass DataSet objects from your middle-tier server to your client applications without divulging any information about the location or structure of your database.

The DataAdapter Contains the Updating Logic to Submit Changes Stored in Your DataSet Back to Your Database

The DataAdapter acts as a two-way street. You can use a DataAdapter to submit a query and store its results in a DataSet, and you can use it to submit pending changes back to your database. This is a major change from previous data access models.

For example, in ADO, you use a Command, explicitly or implicitly, to fetch the results of your query into your Recordset object. When you want to update your database, you call the Update method of the Recordset. The Command object is not involved in the update process.

With ADO.NET, you use the DataAdapter object’s Update method to submit the changes stored in your DataSet to your database. When you call the Update method, you supp