Chapter 9. Data Access with ADO.NET 3.5

ADO.NET 1.x was the successor to ActiveX Data Objects 2.6 (ADO). The main goal of ADO.NET 1.x was to enable developers to easily create distributed, data-sharing applications in the .NET Framework. The main goals of ADO.NET today are to improve the performance of existing features in ADO.NET 1.x, to provide easier use and to add new features without breaking backward compatibility.

Throughout this chapter, when ADO.NET is mentioned without a version number after it (that is, 1.x, 2.0, or 3.5), the statement applies to all versions of ADO.NET.

ADO.NET 1.x was built upon industry standards such as XML, and it provided a data-access interface to communicate with data sources such as SQL Server and Oracle. ADO.NET 3.5 builds upon these concepts, while increasing performance. Applications can use ADO.NET to connect to these data sources and retrieve, manipulate, and update data. ADO.NET 3.5 does not break any compatibility with ADO.NET 2.0 or 1.x; it only adds to the stack of functionality.

In solutions that require disconnected or remote access to data, ADO.NET 3.5 uses XML to exchange data between programs or with Web pages. Any component that can read XML can make use of ADO.NET components. A receiving component does not even have to be an ADO.NET component if a transmitting ADO.NET component packages and delivers a data set in an XML format. Transmitting information in XML-formatted data sets enables programmers to easily separate the data-processing and user interface components of a data-sharing application onto separate servers. This can greatly improve both the performance and maintainability of systems that support many users.

For distributed applications, ADO.NET 1.x proved that the use of XML data sets provided performance advantages relative to the COM marshaling used to transmit disconnected data sets in ADO. Because transmission of data sets occurred through XML streams in a simple text-based standard accepted throughout the industry, receiving components did not require any of the architectural restrictions required by COM. XML data sets used in ADO.NET 1.x also avoided the processing cost of converting values in the Fields collection of a Recordset to data types recognized by COM. Virtually any two components from different systems can share XML data sets, provided that they both use the same XML schema for formatting the data set. This continues to be true in ADO.NET 3.5, but the story gets better. The XML integration in ADO.NET today is even stronger, and extensive work was done to improve the performance of the DataSet object, particularly in the areas of serialization and memory usage.

ADO.NET also supports the scalability required by Web-based data-sharing applications. Web applications must often serve hundreds, or even thousands, of users. By default, ADO.NET does not retain lengthy database locks or active connections that monopolize limited resources. This enables the number of users to grow with only a small increase in the demands made on the resources of a system.

In this chapter, you will see that ADO.NET is a very extensive and flexible API for accessing many types of data, and because ADO.NET 3.5 is an incremental change to the previous versions of ADO.NET, all previous ADO.NET knowledge already learned can be leveraged. In fact, to get the most out of this chapter, you should be fairly familiar with earlier versions of ADO.NET and the entire .NET Framework.

This chapter demonstrates how to use the ADO.NET object model in order to build flexible, fast, scalable data-access objects and applications. Specifically, it covers the following:

  • The ADO.NET architecture

  • Some of the specific features offered in ADO.NET, including batch updates, DataSet performance improvements, and asynchronous processing

  • Working with the Common Provider Model

  • Building a data-access component

ADO.NET Architecture

The main design goals of ADO.NET 3.5 are as follows:

  • Customer-driven features that are still backwardly compatible with ADO.NET 1.x

  • Improving performance on your data-store calls

  • Providing more power for power users

  • Taking advantage of SQL Server 2005/2008 features

ADO.NET addresses a couple of the most common data-access strategies used for applications today. When classic ADO was developed, many applications could be connected to the data store almost indefinitely. Today, with the explosion of the Internet as the means of data communication, a new data technology is required to make data accessible and updateable in a disconnected architecture.

The first of these common data-access scenarios is one in which a user must locate a collection of data and iterate through this data just a single time. This is a popular scenario for Web pages. When a request for data from a Web page that you have created is received, you can simply fill a table with data from a data store. In this case, you go to the data store, grab the data that you want, send the data across the wire, and then populate the table. In this scenario, the goal is to get the data in place as fast as possible.

The second way to work with data in this disconnected architecture is to grab a collection of data and use this data separately from the data store itself. This could be on the server or even on the client. Even though the data is disconnected, you want the capability to keep the data (with all of its tables and relations in place) on the client side. Classic ADO data was represented by a single table that you could iterate through; but ADO.NET can be a reflection of the data store itself, with tables, columns, rows, and relations all in place. When you are done with the client-side copy of the data, you can persist the changes that you made in the local copy of data directly back into the data store. The technology that gives you this capability is the DataSet, which is covered shortly.

Although classic ADO was geared for a two-tiered environment (client-server), ADO.NET addresses a multi-tiered environment. ADO.NET is easy to work with because it has a unified programming model. This unified programming model makes working with data on the server the same as working with data on the client. Because the models are the same, you find yourself more productive when working with ADO.NET.

Basic ADO.NET Features

This chapter begins with a quick look at the basics of ADO.NET and then provides an overview of ADO.NET capabilities, namespaces, and classes. It also reviews how to work with the Connection, Command, DataAdapter, DataSet, and DataReader objects.

Common ADO.NET Tasks

Before jumping into the depths of ADO.NET, step back and make sure that you understand some of the common tasks you might perform programmatically within ADO.NET. This section looks at the process of selecting, inserting, updating, and deleting data.

The following example makes use of the Northwind.mdf SQL Server Express Database file. To get this database, search for "Northwind and pubs Sample Databases for SQL Server 2000." You can find this link at www.microsoft.com/downloads/details.aspx?familyid=06616212-0356-46a0-8da2-eebc53a68034&displaylang=en. Once installed, you will find the Northwind.mdf file in the C: SQL Server 2000 Sample Databases directory. To add this database to your ASP.NET application, create an App_Data folder within your project (if it isn't already there) and right-click on the folder and select Add Existing Item. From the provided dialog, you can browse to the location of the Northwind.mdf file that you just installed. If you have trouble getting permissions to work with the database, make a data connection to the file from the Visual Studio Server Explorer. You will be asked to be made the appropriate user of the database and VS will make the appropriate changes on your behalf for this to occur.

Selecting Data

After the connection to the data source is open and ready to use, you probably want to read the data from the data source. If you do not want to manipulate the data, but simply to read it or transfer it from one spot to another, you use the DataReader class.

The following example uses the GetCompanyNameData function to provide a list of company names from the SQL Northwind database.

Imports Microsoft.VisualBasic
Imports System.Collections.Generic
Imports System.Data
Imports System.Data.SqlClient

Public Class SelectingData
    Public Function GetCompanyNameData() As List(Of String)
        Dim conn As SqlConnection
        Dim cmd As SqlCommand
        Dim cmdString As String = "Select CompanyName from Customers"
        conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename=
           |DataDirectory|NORTHWND.MDF;Integrated Security=True;
           User Instance=True") ' Put this string on one line in your code
        cmd = New SqlCommand(cmdString, conn)
        conn.Open()

        Dim myReader As SqlDataReader
        Dim returnData As List(Of String) = New List(Of String)
        myReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)

        While myReader.Read()
            returnData.Add(myReader("CompanyName").ToString())
        End While

        Return returnData
    End Function
End Class

In this example, you create an instance of both the SqlConnection and the SqlCommand classes. Then, before you open the connection, you simply pass the SqlCommand class a SQL command selecting specific data from the Northwind database. After your connection is opened (based upon the commands passed in), you create a DataReader. To read the data from the database, you iterate through the data with the DataReader by using the myReader.Read method. After the List(Of String) object is built, the connection is closed and the object is returned from the function.

Inserting Data

When working with data, you often insert the data into the data source. The next code sample shows you how to do this. This data may have been passed to you by the end user through the XML Web Service, or it may be data that you generated within the logic of your class.

Public Sub InsertData()
    Dim conn As SqlConnection
    Dim cmd As SqlCommand
    Dim cmdString As String = "Insert Customers (CustomerID, _
       CompanyName, ContactName) Values ('BILLE', 'XYZ Company', 'Bill Evjen')"
    conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename=
           |DataDirectory|NORTHWND.MDF;Integrated Security=True;
           User Instance=True") ' Put this string on one line in your code
    cmd = New SqlCommand(cmdString, conn)
    conn.Open()

    cmd.ExecuteNonQuery()
    conn.Close()
End Sub

Inserting data into SQL is pretty straightforward and simple. Using the SQL command string, you insert specific values for specific columns. The actual insertion is initiated using the cmd.ExecuteNonQuery command. This executes a command on the data when you don't want anything in return.

Updating Data

In addition to inserting new records into a database, you frequently update existing rows of data in a table. Imagine a table in which you can update multiple records at once. In the next example, you want to update an employee table by putting a particular value in the emp_bonus column if the employee has been at the company for five years or longer:

Public Function UpdateEmployeeBonus() As Integer
    Dim conn As SqlConnection
    Dim cmd As SqlCommand
    Dim RecordsAffected as Integer
    Dim cmdString As String = "UPDATE Employees SET emp_bonus=1000 WHERE " & _
       "yrs_duty>=5"
    conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename=
           |DataDirectory|NORTHWND.MDF;Integrated Security=True;
           User Instance=True") ' Put this string on one line in your code
    cmd = New SqlCommand(cmdString, conn)
    conn.Open()

    RecordsAffected = cmd.ExecuteNonQuery()
    conn.Close()

    Return RecordsAffected
End Function

This update function iterates through all the employees in the table and changes the value of the emp_bonus field to 1000 if an employee has been with the company for more than five years. This is done with the SQL command string. The great thing about these update capabilities is that you can capture the number of records that were updated by assigning the ExecuteNonQuery command to the RecordsAffected variable. The total number of affected records is then returned by the function.

Deleting Data

Along with reading, inserting, and updating data, you sometimes need to delete data from the data source. Deleting data is a simple process of using the SQL command string and then the ExecuteNonQuery command as you did in the update example. The following bit of code illustrates this:

Public Function DeleteEmployee() As Integer
    Dim conn As SqlConnection
    Dim cmd As SqlCommand
    Dim RecordsAffected as Integer
    Dim cmdString As String = "DELETE Employees WHERE LastName='Evjen'"
    conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename=
           |DataDirectory|NORTHWND.MDF;Integrated Security=True;
           User Instance=True") ' Put this string on one line in your code
    cmd = New SqlCommand(cmdString, conn)
    conn.Open()

    RecordsAffected = cmd.ExecuteNonQuery()
conn.Close()

    Return RecordsAffected
End Function

You can assign the ExecuteNonQuery command to an Integer variable (just as you did for the update function) to return the number of records deleted.

Basic ADO.NET Namespaces and Classes

The six core ADO.NET namespaces are shown in the following table. In addition to these namespaces, each new data provider can have its own namespace. As an example, the Oracle .NET data provider adds a namespace of System.Data.OracleClient (for the Microsoft-built Oracle data provider).

Namespace

Description

System.Data

This namespace is the core of ADO.NET. It contains classes used by all data providers. Its classes represent tables, columns, rows, and the DataSet class. It also contains several useful interfaces, such as IDbCommand, IDbConnection, and IDbDataAdapter. These interfaces are used by all managed providers, enabling them to plug into the core of ADO.NET.

System.Data.Common

This namespace defines common classes that are used as base classes for data providers. All data providers share these classes. Two examples are DbConnection and DbDataAdapter.

System.Data.OleDb

This namespace defines classes that work with OLE-DB data sources using the .NET OleDb data provider. It contains classes such as OleDbConnection and OleDbCommand.

System.Data.Odbc

This namespace defines classes that work with the ODBC data sources using the .NET ODBC data provider. It contains classes such as OdbcConnection and OdbcCommand.

System.Data.SqlClient

This namespace defines a data provider for the SQL Server 7.0 or later database. It contains classes such as SqlConnection and SqlCommand.

System.Data.SqlTypes

This namespace defines a few classes that represent specific data types for the SQL Server database.

ADO.NET has three distinct types of classes commonly referred to as disconnected, shared, and data providers. The disconnected classes provide the basic structure for the ADO.NET Framework. A good example of this type of class is the DataTable class. The objects of this class are capable of storing data without any dependency on a specific data provider. The Shared classes form the base classes for data providers and are shared among all data providers. The data provider classes are meant to work with different kinds of data sources. They are used to perform all data-management operations on specific databases. The SqlClient data provider, for example, works only with the SQL Server database.

A data provider contains Connection, Command, DataAdapter, and DataReader objects. Typically, in programming ADO.NET, you first create the Connection object and provide it with the necessary information, such as the connection string. You then create a Command object and provide it with the details of the SQL command that is to be executed. This command can be an inline SQL text command, a stored procedure, or direct table access. You can also provide parameters to these commands if needed.

After you create the Connection and the Command objects, you must decide whether the command returns a result set. If the command doesn't return a result set, then you can simply execute the command by calling one of its several Execute methods. Conversely, if the command returns a result set, you must decide whether you want to retain the result set for future use without maintaining the connection to the database. If you want to retain the result set, then you must create a DataAdapter object and use it to fill a DataSet or a DataTable object. These objects are capable of maintaining their information in a disconnected mode. However, if you don't want to retain the result set, but rather to simply process the command in a swift fashion, then you can use the Command object to create a DataReader object. The DataReader object needs a live connection to the database, and it works as a forward-only, read-only cursor.

ADO.NET Components

To better support the disconnected model as defined above, the ADO.NET components separate data access from data manipulation. This is accomplished via two main components: the DataSet and the .NET Data Provider. Figure 9-1 illustrates the concept of separating data access from data manipulation.

Figure 9-1

Figure 9.1. Figure 9-1

The DataSet is the core component of the disconnected architecture of ADO.NET. It is explicitly designed for data access independent of any data source. As a result, it can be used with multiple and differing data sources, with XML data, or even to manage data local to an application such as an in-memory data cache. The DataSet contains a collection of one or more DataTable objects made up of rows and columns of data, as well as primary key, foreign key, constraint, and relation information about the data in the DataTable objects. It is basically an in-memory database, but what sets it apart is that it doesn't care whether its data is obtained from a database, an XML file, a combination of the two, or somewhere else. You can apply inserts, updates, and deletes to the DataSet and then push the changes back to the data source, no matter where the data source lives! This chapter offers an in-depth look at the DataSet object family.

The other core element of the ADO.NET architecture is the .NET Data Provider, whose components are designed for data manipulation (as opposed to data access with the DataSet). These components are listed in the following table.

The DataAdapter uses Command objects to execute SQL commands at the data source, both to load the DataSet with data and to reconcile changes made to the data in the DataSet with the data source. You will take a closer look at this later in the detailed discussion of the DataAdapter object.

.NET Data Providers can be written for any data source, though this topic is beyond the scope of this chapter.

Object

Activity

Connection

Provides connectivity to a data source

Command

Enables access to database commands to return and modify data, run stored procedures, and send or retrieve parameter information

DataReader

Provides a high-performance, read-only stream of data from the data source

DataAdapter

Provides the bridge between the DataSet object and the data source

The .NET Framework 3.5 ships with three .NET Data Providers: the SQL Server .NET Data Provider, the Oracle .NET Data Provider, and the OLE DB .NET Data Provider.

Note

Do not confuse the OLE DB .NET Data Provider with generic OLE DB Providers.

The rule of thumb when deciding which data provider to use is to first use a .NET Relational Database Management System (RDBMS)–specific data provider if it is available, and to use the .NET OLE DB Provider when connecting to any other data source. (Most RDBMS vendors are now producing their own .NET Data Providers in order to encourage .NET developers to use their databases.)

For example, if you were writing an application that uses SQL Server, then you would want to use the SQL Server .NET Data Provider. The .NET OLE DB Provider is used to access any data source exposed through OLE DB, such as Microsoft Access, Open DataBase Connectivity (ODBC), and so on. You will be taking a closer look at these later.

.NET Data Providers

.NET Data Providers are used for connecting to a RDBMS-specific database (such as SQL Server or Oracle), executing commands, and retrieving results. Those results are either processed directly (via a DataReader) or placed in an ADO.NET DataSet (via a DataAdapter) in order to be exposed to the user in an ad hoc manner, combined with data from multiple sources, or passed around between tiers. NET Data Providers are designed to be lightweight, to create a minimal layer between the data source and the .NET programmer's code, and to increase performance while not sacrificing any functionality.

Connection Object

To connect to a specific data source, you use a data Connection object. To connect to Microsoft SQL Server 7.0 or later, you need to use the SqlConnection object of the SQL Server .NET Data Provider. You need to use the OleDbConnection object of the OLE DB .NET Data Provider to connect to an OLE DB data source, or the OLE DB Provider for SQL Server (SQLOLEDB) to connect to versions of Microsoft SQL Server earlier than 7.0.

Connection String Format — OleDbConnection

For the OLE DB .NET Data Provider, the connection string format is the same as the connection string format used in ADO, with the following exceptions:

  • The Provider keyword is required.

  • The URL, Remote Provider, and Remote Server keywords are not supported.

Here is an example OleDbConnection connection string connecting to an Oracle database:

Provider=msdaora;Data Source=MyOracleDB;UserId=myUsername;Password=myPassword;

Connection-String Format — SqlConnection

The SQL Server .NET Data Provider supports a connection-string format that is similar to the OLE DB (ADO) connection-string format. The only thing that you need to omit, obviously, is the provider name-value pair, as you know you are using the SQL Server .NET Data Provider. Here is an example of a SqlConnection connection string:

Data Source=(local);Initial Catalog=pubs;Integrated Security=SSPI;

Command Object

After establishing a connection, you can execute commands and return results from a data source (such as SQL Server) using a Command object. A Command object can be created using the Command constructor, or by calling the CreateCommand method of the Connection object. When creating a Command object using the Command constructor, you need to specify a SQL statement to execute at the data source, and a Connection object. The Command object's SQL statement can be queried and modified using the CommandText property. The following code is an example of executing a SELECT command and returning a DataReader object:

' Build the SQL and Connection strings.
Dim sql As String = "SELECT * FROM authors"
Dim connectionString As String = "Initial Catalog=pubs;" _
 & "Data Source=(local);Integrated Security=SSPI;"
' Initialize the SqlCommand with the SQL
' and Connection strings.
Dim command As SqlCommand = New SqlCommand(sql, _
New SqlConnection(connectionString))
' Open the connection.
command.Connection.Open()
' Execute the query, return a SqlDataReader object.
' CommandBehavior.CloseConnection flags the
' DataReader to automatically close the DB connection
' when it is closed.
Dim dataReader As SqlDataReader = _
    command.ExecuteReader(CommandBehavior.CloseConnection)

The CommandText property of the Command object executes all SQL statements in addition to the standard SELECT, UPDATE, INSERT, and DELETE statements. For example, you could create tables, foreign keys, primary keys, and so on, by executing the applicable SQL from the Command object.

The Command object exposes several Execute methods to perform the intended action. When returning results as a stream of data, ExecuteReader is used to return a DataReader object. ExecuteScalar is used to return a singleton value. In ADO.NET, the ExecuteRow method has been added, which returns a single row of data in the form of a SqlRecord object. ExecuteNonQuery is used to execute commands that do not return rows, which usually includes stored procedures that have output parameters and/or return values. (You will learn about stored procedures in a later section.)

When using a DataAdapter with a DataSet, Command objects are used to return and modify data at the data source through the DataAdapter object's SelectCommand, InsertCommand, UpdateCommand, and DeleteCommand properties.

Note

Note that the DataAdapter object's SelectCommand property must be set before the Fill method is called.

The InsertCommand, UpdateCommand, and DeleteCommand properties must be set before the Update method is called. You will take a closer look at this when you look at the DataAdapter object.

Using Stored Procedures with Command Objects

This section offers a quick look at how to use stored procedures, before delving into a more complex example later in the chapter demonstrating how you can build a reusable data-access component that also uses stored procedures. The motivation for using stored procedures is simple. Imagine you have the following code:

SELECT au_lname FROM authors WHERE au_id='172-32-1176'

If you pass that to SQL Server using ExecuteReader on SqlCommand (or any execute method, for that matter), SQL Server has to compile the code before it can run it, in much the same way that VB .NET applications have to be compiled before they can be executed. This compilation takes up SQL Server's time, so it is easy to deduce that if you can reduce the amount of compilation that SQL Server has to do, database performance should increase. (Compare the speed of execution of a compiled application against interpreted code.)

That's what stored procedures are all about: you create a procedure, store it in the database, and because the procedure is recognized and understood ahead of time, it can be compiled ahead of time and ready for use in your application.

Stored procedures are very easy to use, but the code to access them is sometimes a little verbose. The next section demonstrates some code that can make accessing stored procedures a bit more straightforward, but to make things clearer, let's start by building a simple application that demonstrates how to create and call a stored procedure.

Creating a Stored Procedure

To create a stored procedure, you can either use the tools in Visual Studio .NET or you can use the tools in SQL Server's Enterprise Manager if you are using SQL Server 2000, or in SQL Server Management Studio if you are using SQL Server 2005/2008. (Technically, you can use a third-party tool or just create the stored procedure in a good, old-fashioned SQL script.)

This example builds a stored procedure that returns all of the columns for a given author ID. The SQL to do this looks like this:

SELECT
   au_id, au_lname, au_fname, phone,
   address, city, state, zip, contract
FROM
    authors
WHERE
    au_id = whatever author ID you want

The "whatever author ID you want" part is important. When using stored procedures, you typically have to be able to provide parameters into the stored procedure and use them from within code. This is not a book about SQL Server, so this example focuses only on the principle involved. You can find many resources on the Web about building stored procedures (they have been around a very long time, and they are most definitely not a .NET-specific feature).

Variables in SQL Server are prefixed by the @ symbol, so if you have a variable called au id, then your SQL will look like this:

SELECT
    au_id, au_lname, au_fname, phone,
    address, city, state, zip, contract
FROM
    authors
WHERE
    au_id = @au_id

In Visual Studio 2008, stored procedures can be accessed using the Server Explorer. Simply add a new data connection (or use an existing data connection), and then drill down into the Stored Procedures folder in the management tree. A number of stored procedures are already loaded. The byroyalty procedure is a stored procedure provided by the sample pubs database developers. Figure 9-2 illustrates the stored procedures of the pubs database in Visual Studio 2008.

Figure 9-2

Figure 9.2. Figure 9-2

To create a new stored procedure, just right-click the Stored Procedures folder in the Server Explorer and select Add New Stored Procedure to invoke the Editor window.

A stored procedure can be either a single SQL statement or a complex set of statements. T-SQL supports branches, loops, and other variable declarations, which can make for some pretty complex stored procedure code. However, your stored procedure is just a single line of SQL. You need to declare the parameter that you want to pass in (@au_id) and the name of the procedure: usp_authors_Get_By_ID. Here's code for the stored procedure:

CREATE PROCEDURE usp_authors_Get_By_ID
    @au_id varchar(11)
AS
SELECT
    au_id, au_lname, au_fname, phone,
    address, city, state, zip, contract
FROM
    authors
WHERE
    au_id = @au_id

Click OK to save the stored procedure in the database. You are now able to access this stored procedure from code.

Calling the Stored Procedure

Calling the stored procedure is just a matter of creating a SqlConnection object to connect to the database, and a SqlCommand object to run the stored procedure.

The sample code for this chapter demonstrates a solution called Examples.sln, which includes a project called AdoNetFeaturesTest.

Note

For all of the data-access examples in this chapter, you need the pubs database, which can be downloaded from MSDN. In addition, be sure to run the examples.sql file — available with the code download for this chapter — in SQL Server 2005 Management Studio before running the code examples. This creates the necessary stored procedures and functions in the pubs database. You can also use the SQL Server Express Edition of the pubs database, PUBS.MDF, also found on MSDN.

Now you have to decide what you want to return by calling the stored procedure. In this case, you return an instance of the SqlDataReader object. The TestForm.vb file contains a method called GetAuthorSqlReader that takes an author ID and returns an instance of a SqlDataReader. Here is the code for the method:

Private Function GetAuthorSqlReader(ByVal authorId As String) As SqlDataReader
    ' Build a SqlCommand
    Dim command As SqlCommand = New SqlCommand("usp_authors_Get_By_ID", _
        GetPubsConnection())
    ' Tell the command we are calling a stored procedure
    command.CommandType = CommandType.StoredProcedure
    ' Add the @au_id parameter information to the command
    command.Parameters.Add(New SqlParameter("@au_id", authorId))
    ' The reader requires an open connection
    command.Connection.Open()
    ' Execute the sql and return the reader
    Return command.ExecuteReader(CommandBehavior.CloseConnection)
End Function

Notice that in the SqlCommand's constructor call, you have factored out creating a connection to the pubs database into a separate helper method. This is used later in other code examples in your form.

Here is the code for the GetPubsConnection helper method:

Private Function GetPubsConnection() As SqlConnection
    ' Build a SqlConnection based on the config value.
    Return New _
        SqlConnection(ConfigurationSettings.AppSettings("dbConnectionString"))
End Function

The most significant thing this code does is grab a connection string to the database from the application's configuration file, app.config. Here is what the entry in the app.config file looks like:

<appSettings>
    <add key="dbConnectionString" value="data source=(local);initial
         catalog=pubs;Integrated Security=SSPI;" />
</appSettings>

Although the helper method does not do much, it is nice to place this code in a separate method. This way, if the code to get a connection to the databases needs to be changed, then the code only has to be changed in one place.

Accessing a stored procedure is more verbose (but not more difficult) than accessing a normal SQL statement through the methods discussed thus far. The approach is as follows:

  1. Create a SqlCommand object.

  2. Configure it to access a stored procedure by setting the CommandType property.

  3. Add parameters that exactly match those in the stored procedure itself.

  4. Execute the stored procedure using one of the SqlCommand object's Execute*** methods

There is no real need to build an impressive UI for this application, as we're about to add a button named getAuthorByIdButton that calls the GetAuthorSqlRecord helper method and displays the selected author's name. Here is the button's Click event handler:

Private Sub _getAuthorByIdButton_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles _getAuthorByIdButton.Click
    Dim reader As SqlDataReader = Me. GetAuthorSqlReader ("409-56-7008")
    If reader.Read()
        MessageBox.Show(reader("au_fname").ToString() & "  " _
            & reader("au_lname").ToString())
    End If

    reader.Close()
End Sub

This has hard-coded an author ID of 409-56-7008. Run the code now and you should see the result shown in Figure 9-3.

Figure 9-3

Figure 9.3. Figure 9-3

DataReader Object

You can use the DataReader to retrieve a read-only, forward-only stream of data from the database. Using the DataReader can increase application performance and reduce system overhead because only one buffered row at a time is ever in memory. With the DataReader object, you are getting as close to the raw data as possible in ADO.NET; you do not have to go through the overhead of populating a DataSet object, which sometimes may be expensive if the DataSet contains a lot of data. The disadvantage of using a DataReader object is that it requires an open database connection and increases network activity.

After creating an instance of the Command object, a DataReader is created by calling the ExecuteReader method of the Command object. Here is an example of creating a DataReader and iterating through it to print out its values to the screen:

Private Sub TraverseDataReader()

    ' Build the SQL and Connection strings.
    Dim sql As String = "SELECT * FROM authors"
    Dim connectionString As String = "Initial Catalog=pubs;" _
        & "Data Source=(local);Integrated Security=SSPI;"

    ' Initialize the SqlCommand with the SQL query and connection strings.
    Dim command As SqlCommand = New SqlCommand(sql, _
        New SqlConnection(connectionString))
    ' Open the connection.
    command.Connection.Open()
    ' Execute the query, return a SqlDataReader object.
    ' CommandBehavior.CloseConnection flags the
    ' DataReader to automatically close the DB connection
    ' when it is closed.
    Dim reader As SqlDataReader = _
        command.ExecuteReader(CommandBehavior.CloseConnection)
    ' Loop through the records and print the values.
    Do While reader.Read
        Console.WriteLine(reader.GetString(1) & " " & reader.GetString(2))
    Loop
    ' Close the DataReader (and its connection).
    reader.Close()

End Sub

This code snippet uses the SqlCommand object to execute the query via the ExecuteReader method. This method returns a populated SqlDataReader object, which you loop through and then print out the author names. The main difference between this code and looping through the rows of a DataTable is that you have to stay connected while you loop through the data in the DataReader object; this is because the DataReader reads in only a small stream of data at a time to conserve memory space.

Note

At this point, an obvious design question is whether to use the DataReader or the DataSet. The answer depends upon performance. If you want high performance and you are only going to access the data you are retrieving once, then the DataReader is the way to go. If you need access to the same data multiple times, or if you need to model a complex relationship in memory, then the DataSet is the way to go. As always, test each option thoroughly before deciding which one is the best.

The Read method of the DataReader object is used to obtain a row from the results of the query. Each column of the returned row may be accessed by passing the name or ordinal reference of the column to the DataReader; or, for best performance, the DataReader provides a series of methods that enable you to access column values in their native data types (GetDateTime, GetDouble, GetGuid, GetInt32, and so on). Using the typed accessor methods when the underlying data type is known reduces the amount of type conversion required (converting from type Object) when retrieving the column value.

The DataReader provides a nonbuffered stream of data that enables procedural logic to efficiently process results from a data source sequentially. The DataReader is a good choice when retrieving large amounts of data; only one row of data is cached in memory at a time. You should always call the Close method when you are through using the DataReader object, as well as close the DataReader object's database connection; otherwise, the connection will not be closed until the garbage collector gets around to collecting the object.

Note how you use the CommandBehavior.CloseConnection enumeration value on the SqlDataReader.ExecuteReader method. This tells the SqlCommand object to automatically close the database connection when the SqlDataReader.Close method is called.

Note

If your command contains output parameters or return values, they will not be available until the DataReader is closed.

Executing Commands Asynchronously

In ADO.NET, additional support enables Command objects to execute their commands asynchronously, which can result in a huge perceived performance gain in many applications, especially in Windows Forms applications. This can come in very handy, especially if you ever have to execute a long-running SQL statement. This section examines how this functionality enables you to add asynchronous processing to enhance the responsiveness of an application.

The SqlCommand object provides three different asynchronous call options: BeginExecuteReader, BeginExecuteNonQuery, and BeginExecuteXmlReader. Each of these methods has a corresponding "end" method — that is, EndExecuteReader, EndExecutreNonQuery, and EndExecuteXmlReader. Now that you are familiar with the DataReader object, let's look at an example using the BeginExecuteReader method to execute a long-running query.

In the AdoNetFeaturesTest project, I have added a Button and an associated Click event handler to the form that will initiate the asynchronous call to get a DataReader instance:

Private Sub _testAsyncCallButton_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles _testAsyncCallButton.Click

        ' Build a connection for the async call to the database.
        Dim connection As SqlConnection = GetPubsConnection()
        connection.ConnectionString &= "Asynchronous Processing=true;"

        ' Build a command to call the stored procedure.
        Dim command As New SqlCommand("usp_Long_Running_Procedure", _
           connection)

        ' Set the command type to stored procedure.
        command.CommandType = CommandType.StoredProcedure

        ' The reader requires an open connection.
        connection.Open()
' Make the asynchronous call to the database.
        command.BeginExecuteReader(AddressOf Me.AsyncCallback, _
        command, CommandBehavior.CloseConnection)
    End Sub

The first thing you do is reuse your helper method GetPubsConnection to get a connection to the pubs database. Next, and this is very important, you append the statement Asynchronous Processing=true to your Connection object's connection string. This must be set in order for ADO.NET to make asynchronous calls to SQL Server.

After getting the connection set, you then build a SqlCommand object and initialize it to be able to execute the usp_Long_Running_Procedure stored procedure. This procedure uses the SQL Server 2005 WAITFOR DELAY statement to create a 20-second delay before it executes the usp_Authors_Get_All stored procedure. As you can probably guess, the usp_authors_Get_All stored procedure simply selects all of the authors from the authors table. The delay is added simply to demonstrate that while this stored procedure is executing, you can perform other tasks in your Windows Forms application. Here is the SQL code for the usp_Long_Running_Procedure stored procedure:

CREATE PROCEDURE usp_Long_Running_Procedure
AS
SET NOCOUNT ON

WAITFOR DELAY '00:00:20'
EXEC usp_authors_Get_All

The last line of code in the Button's Click event handler is the call to BeginExecuteReader. In this call, the first thing you are passing in is a delegate method (Me.AsyncCallback) for the System.AsyncCallback delegate type. This is how the .NET Framework calls you back once the method is finished running asynchronously. You then pass in your initialized SqlCommand object so that it can be executed, as well as the CommandBehavior value for the DataReader. In this case, you pass in the CommandBehavior.CloseConnection value so that the connection to the database will be closed once the DataReader has been closed. You will look at the DataReader in more detail in the next section.

Now that you have initiated the asynchronous call, and have defined a callback for your asynchronous call, let's look at the actual method that is being called back, the AsyncCallback method:

Private Sub AsyncCallback(ByVal ar As IAsyncResult)
    ' Get the command that was passed from the AsyncState of the IAsyncResult.
    Dim command As SqlCommand = CType(ar.AsyncState, SqlCommand)
    ' Get the reader from the IAsyncResult.
    Dim reader As SqlDataReader = command.EndExecuteReader(ar)
    ' Get a table from the reader.
    Dim table As DataTable = Me.GetTableFromReader(reader, "Authors")
    ' Call the BindGrid method on the Windows main thread,
    ' passing in the table.
    Me.Invoke(New BindGridDelegate(AddressOf Me.BindGrid), _
        New Object() {table})
End Sub

The first line of the code is simply retrieving the SqlCommand object from the AsyncState property of the IAsyncResult that was passed in. Remember that when you called BeginExecuteReader earlier, you passed in your SqlCommand object. You need it so that you can call the EndExecuteReader method on the next line. This method gives you your SqlDataReader. On the next line, you then transform the SqlDataReader into a DataTable (covered later when the DataSet is discussed).

The last line of this method is probably the most important. If you tried to just take your DataTable and bind it to the grid, it would not work, because right now you are executing on a thread other than the main Windows thread. The helper method named BindGrid can do the data binding, but it must be called only in the context of the Windows main thread. To bring the data back to the main Windows thread, it must be marshaled via the Invoke method of the Form object. Invoke takes two arguments: the delegate of the method you want to call and (optionally) any parameters for that method. In this case, you define a delegate for the BindGrid method, called BindGridDelegate. Here is the delegate declaration:

Private Delegate Sub BindGridDelegate(ByVal table As DataTable)

Notice how the signature is exactly the same as the BindGrid method shown here:

Private Sub BindGrid(ByVal table As DataTable)
    ' Clear the grid.
    Me._authorsGridView.DataSource = Nothing
    ' Bind the grid to the DataTable.
    Me._authorsGridView.DataSource = table
End Sub

Here is another look at the call to the form's Invoke method:

Me.Invoke(New BindGridDelegate(AddressOf Me.BindGrid), _
    New Object() {table})

You pass in a new instance of the BindGridDelegate delegate and initialize it with a pointer to the BindGrid method. As a result, the .NET worker thread that was executing your query can now safely join up with the main Windows thread.

DataAdapter Objects

Each .NET Data Provider included with the .NET Framework has a DataAdapter object. The OLE DB .NET Data Provider includes an OleDbDataAdapter object, and the SQL Server .NET Data Provider includes a SqlDataAdapter object. A DataAdapter is used to retrieve data from a data source and populate DataTable objects and constraints within a DataSet. The DataAdapter also resolves changes made to the DataSet back to the data source. The DataAdapter uses the Connection object of the .NET Data Provider to connect to a data source, and Command objects to retrieve data from, and resolve changes to, the data source from a DataSet object.

This differs from the DataReader, in that the DataReader uses the Connection object to access the data directly, without having to use a DataAdapter. The DataAdapter essentially decouples the DataSet object from the actual source of the data, whereas the DataReader is tightly bound to the data in a read-only fashion.

The SelectCommand property of the DataAdapter is a Command object that retrieves data from the data source. A nice, convenient way to set the DataAdapter's SelectCommand property is to pass in a Command object in the DataAdapter's constructor. The InsertCommand, UpdateCommand, and DeleteCommand properties of the DataAdapter are Command objects that manage updates to the data in the data source according to the modifications made to the data in the DataSet. The Fill method of the DataAdapter is used to populate a DataSet with the results of the SelectCommand of the DataAdapter. It also adds or refreshes rows in the DataSet to match those in the data source. The following example code demonstrates how to fill a DataSet object with information from the authors table in the pubs database:

Private Sub TraverseDataSet()
    ' Build the SQL and Connection strings.
    Dim sql As String = "SELECT * FROM authors"
    Dim connectionString As String = "Initial Catalog=pubs;" _
        & "Data Source=(local);Integrated Security=SSPI;"

    ' Initialize the SqlDataAdapter with the SQL
    ' and Connection strings, and then use the
    ' SqlDataAdapter to fill the DataSet with data.
    Dim adapter As New SqlDataAdapter(sql, connectionString)
    Dim authors As New DataSet
    adapter.Fill(authors)

    ' Iterate through the DataSet's table.
    For Each row As DataRow In authors.Tables(0).Rows
        Console.WriteLine(row("au_fname").ToString _
            & " " & row("au_lname").ToString)
    Next

    ' Print the DataSet's XML.
    Console.WriteLine(authors.GetXml())
    Console.ReadLine()

End Sub

Note how you use the constructor of the SqlDataAdapter to pass in and set the SelectCommand, as well as pass in the connection string in lieu of a SqlCommand object that already has an initialized Connection property. You then just call the SqlDataAdapter object's Fill method and pass in an initialized DataSet object. If the DataSet object is not initialized, then the Fill method raises an exception (System.ArgumentNullException).

Ever since ADO.NET 2.0, a significant performance improvement was made in the way that the DataAdapter updates the database. In ADO.NET 1.x, the DataAdapter's Update method would loop through each row of every DataTable object in the DataSet and subsequently make a trip to the database for each row being updated. In ADO.NET 2.0, batch update support was added to the DataAdapter. This means that when the Update method is called, the DataAdapter batches all of the updates from the DataSet in one trip to the database.

Now let's take a look at a more advanced example. Here, you use a DataAdapter to insert, update, and delete data from a DataTable back to the pubs database:

Private Sub _batchUpdateButton_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles _batchUpdateButton.Click

        ' Build insert, update, and delete commands.
' Build the parameter values.
        Dim insertUpdateParams() As String = {"@au_id", "@au_lname", _
            "@au_fname", _
            "@phone", "@address", "@city", "@state", "@zip", "@contract"}

The preceding code begins by initializing a string array of parameter names to pass into the BuildSqlCommand helper method:

' Insert command.
Dim insertCommand As SqlCommand = _
    BuildSqlCommand("usp_authors_Insert", _
    insertUpdateParams)

Next, you pass the name of the stored procedure to execute and the parameters for the stored procedure to the BuildSqlCommand helper method. This method returns an initialized instance of the SqlCommand class. Here is the BuildSqlCommand helper method:

Private Function BuildSqlCommand(ByVal storedProcedureName As String, _
        ByVal parameterNames() As String) As SqlCommand
    ' Build a SqlCommand.
    Dim command As New SqlCommand(storedProcedureName, GetPubsConnection())
    ' Set the command type to stored procedure.
    command.CommandType = CommandType.StoredProcedure
    ' Build the parameters for the command.
    ' See if any parameter names were passed in.
    If Not parameterNames Is Nothing Then
        ' Iterate through the parameters.
        Dim parameter As SqlParameter = Nothing
        For Each parameterName As String In parameterNames
            ' Create a new SqlParameter.
            parameter = New SqlParameter()
            parameter.ParameterName = parameterName
            ' Map the parameter to a column name in the DataTable/DataSet.
            parameter.SourceColumn = parameterName.Substring(1)
            ' Add the parameter to the command.
            command.Parameters.Add(parameter)
        Next
    End If
    Return command
End Function

This method first initializes a SqlCommand class and passes in the name of a stored procedure; it then uses the GetPubsConnection helper method to pass in a SqlConnection object to the SqlCommand. The next step is to set the command type of the SqlCommand to a stored procedure. This is important because ADO.NET uses this to optimize how the stored procedure is called on the database server. You then check whether any parameter names have been passed (via the parameterNames string array); if so, you iterate through them. While iterating through the parameter names, you build up SqlParameter objects and add them to the SqlCommand's collection of parameters.

The most important step in building up the SqlParameter object is setting its SourceColumn property. This is what the DataAdapter later uses to map the name of the parameter to the name of the column in the DataTable when its Update method is called. An example of such a mapping is associating the @au_id parameter name with the au_id column name. As shown in the code, the mapping assumes that the stored procedure parameters all have exactly the same names as the columns, except for the mandatory @ character in front of the parameter. That's why when assigning the SqlParameter's SourceColumn property value, you use the Substring method to strip off the @ character to ensure that it maps correctly.

You then call the BuildSqlCommand method two more times to build your update and delete SqlCommand objects:

' Update command.
Dim updateCommand As SqlCommand = _
    BuildSqlCommand("usp_authors_Update", _
    insertUpdateParams)

' Delete command.
Dim deleteCommand As SqlCommand = _
    BuildSqlCommand("usp_authors_Delete", _
    New String() {"@au_id"})

Now that the SqlCommand objects have been created, the next step is to create a SqlDataAdapter object. Once the SqlDataAdapter is created, you set its InsertCommand, UpdateCommand, and DeleteCommand properties with the respective SqlCommand objects that you just built:

' Create an adapter.
Dim adapter As New SqlDataAdapter()

' Associate the commands with the adapter.
adapter.InsertCommand = insertCommand
adapter.UpdateCommand = updateCommand
adapter.DeleteCommand = deleteCommand

The next step is to get a DataTable instance of the authors table from the pubs database. You do this by calling the GetAuthorsSqlReader helper method to first get a DataReader and then the GetTableFromReader helper method to load a DataTable from a DataReader:

' Get the authors reader.
Dim reader As SqlDataReader = GetAuthorsSqlReader()
' Load a DataTable from the reader.
Dim table As DataTable = GetTableFromReader(reader, "Authors")

Once you have your DataTable filled with data, you begin modifying it so you can test the new batch update capability of the DataAdapter. The first change to make is an insert in the DataTable. In order to add a row, you first call the DataTable's NewRow method to give you a DataRow initialized with the same columns as your DataTable:

' Add a new author to the DataTable.
Dim row As DataRow = table.NewRow

Once that is done, you can set the values of the columns of the DataRow:

row("au_id") = "335-22-0707"
row("au_fname") = "Bill"
row("au_lname") = "Evjen"
row("phone") = "800-555-1212"
row("contract") = 0

Then you call the Add method of the DataTable's DataRowCollection property and pass in the newly populated DataRow object:

table.Rows.Add(row)

Now that there is a new row in the DataTable, the next test is to update one of its rows:

' Change an author in the DataTable.
table.Rows(0)("au_fname") = "Updated Name!"

Finally, you delete a row from the DataTable. In this case, it is the second-to-last row in the DataTable:

' Delete the second to last author from the table
table.Rows(table.Rows.Count - 2).Delete()

Now that you have performed an insert, update, and delete action on your DataTable, it is time to send the changes back to the database. You do this by calling the DataAdapter's Update method and passing in either a DataSet or a DataTable. Note that you are calling the GetChanges method of the DataTable; this is important, because you only want to send the changes to the DataAdapter:

' Send only the changes in the DataTable to the database for updating.
adapter.Update(table.GetChanges())

To prove that the update worked, you get back a new DataTable from the server using the same technique as before, and then bind it to the grid with your helper method to view the changes that were made:

' Get the new changes back from the server to show that the update worked.
      reader = GetAuthorsSqlReader()
      table = GetTableFromReader(reader, "Authors")
      ' Bind the grid to the new table data.
      BindGrid(table)
  End Sub

SQL Server .NET Data Provider

The SQL Server .NET Data Provider uses Tabular Data Stream (TDS) to communicate with the SQL Server. This offers a great performance increase, as TDS is SQL Server's native communication protocol. As an example of how much of an increase you can expect, when I ran some simple tests accessing the authors table of the pubs database, the SQL Server .NET Data Provider performed about 70 percent faster than the OLE DB .NET Data Provider.

The SQL Server .NET Data Provider is lightweight and performs very well, thanks to not having to go through the OLE DB or ODBC layer. What it actually does is establish a network connection (usually sockets-based) and drag data from this directly into managed code and vice versa.

Note

This is very important, as going through the OLE DB or ODBC layers means that the CLR has to marshal (convert) all of the COM data types to .NET CLR data types each time data is accessed from a data source. When using the SQL Server .NET Data Provider, everything runs within the .NET CLR, and the TDS protocol is faster than the other network protocols previously used for SQL Server.

To use this provider, you need to include the System.Data.SqlClient namespace in your application. Note that it works only for SQL Server 7.0 and later. I highly recommend using the SQL Server .NET Data Provider any time you are connecting to a SQL Server 7.0 and later database server. The SQL Server .NET Data Provider requires the installation of MDAC 2.6 or later.

OLE DB .NET Data Provider

The OLE DB .NET Data Provider uses native OLE DB through COM interop to enable data access. The OLE DB .NET Data Provider supports both manual and automatic transactions. For automatic transactions, the OLE DB .NET Data Provider automatically enlists in a transaction and obtains transaction details from Windows 2000 Component Services. The OLE DB .NET Data Provider does not support OLE DB 2.5 interfaces. OLE DB Providers that require support for OLE DB 2.5 interfaces will not function properly with the OLE DB .NET Data Provider. This includes the Microsoft OLE DB Provider for Exchange and the Microsoft OLE DB Provider for Internet Publishing. The OLE DB .NET Data Provider requires the installation of MDAC 2.6 or later. To use this provider, you need to include the System.Data.OleDb namespace in your application.

The DataSet Component

The DataSet object is central to supporting disconnected, distributed data scenarios with ADO.NET. The DataSet is a memory-resident representation of data that provides a consistent relational programming model regardless of the data source. The DataSet represents a complete set of data, including related tables, constraints, and relationships among the tables; basically, it's like having a small relational database residing in memory.

Because the DataSet contains a lot of metadata, you need to be careful about how much data you try to stuff into it, as it consumes memory.

The methods and objects in a DataSet are consistent with those in the relational database model. The DataSet can also persist and reload its contents as XML, and its schema as XSD. It is completely disconnected from any database connections, so it is totally up to you to fill it with whatever data you need in memory.

Ever since ADO.NET 2.0, there have been several new features to the DataSet and the DataTable classes, as well as enhancements to existing features. The features covered in this section are as follows:

  • The binary serialization format option

  • Additions to make the DataTable more of a standalone object

  • The capability to expose DataSet and DataTable data as a stream (DataReader), and loading stream data into a DataSet or DataTable

DataTableCollection

An ADO.NET DataSet contains a collection of zero or more tables represented by DataTable objects. The DataTableCollection contains all of the DataTable objects in a DataSet.

A DataTable is defined in the System.Data namespace and represents a single table of memory-resident data. It contains a collection of columns represented by the DataColumnCollection, which defines the schema and rows of the table. It also contains a collection of rows represented by the DataRowCollection, which contains the data in the table. Along with the current state, a DataRow retains its original state and tracks changes that occur to the data.

DataRelationCollection

A DataSet contains relationships in its DataRelationCollection object. A relationship (represented by the DataRelation object) associates rows in one DataTable with rows in another DataTable. The relationships in the DataSet can have constraints, which are represented by UniqueConstraint and ForeignKeyConstraint objects. It is analogous to a JOIN path that might exist between the primary and foreign key columns in a relational database. A DataRelation identifies matching columns in two tables of a DataSet.

Relationships enable you to see what links information within one table to another. The essential elements of a DataRelation are the name of the relationship, the two tables being related, and the related columns in each table. Relationships can be built with more than one column per table, with an array of DataColumn objects for the key columns. When a relationship is added to the DataRelationCollection, it may optionally add ForeignKeyConstraints that disallow any changes that would invalidate the relationship.

ExtendedProperties

DataSet (as well as DataTable and DataColumn) has an ExtendedProperties property. ExtendedProperties is a PropertyCollection in which a user can place customized information, such as the SELECT statement that is used to generate the result set, or a date/time stamp indicating when the data was generated. Because the ExtendedProperties contains customized information, this is a good place to store extra user-defined data about the DataSet (or DataTable or DataColumn), such as a time when the data should be refreshed. The ExtendedProperties collection is persisted with the schema information for the DataSet (as well as DataTable and DataColumn). The following code is an example of adding an expiration property to a DataSet:

Private Shared Sub DataSetExtended()

    ' Build the SQL and Connection strings.
    Dim sql As String = "SELECT * FROM authors"
    Dim connectionString As String = "Initial Catalog=pubs;" _
        & "Data Source=(local);Integrated Security=SSPI;"

    ' Initialize the SqlDataAdapter with the SQL
    ' and Connection strings, and then use the
    ' SqlDataAdapter to fill the DataSet with data.
Dim adapter As SqlDataAdapter = _
        New SqlDataAdapter(sql, connectionString)
    Dim authors As New DataSet
    adapter.Fill(authors)

    ' Add an extended property called "expiration."
    ' Set its value to the current date/time + 1 hour.
    authors.ExtendedProperties.Add("expiration", _
        DateAdd(DateInterval.Hour, 1, Now))

    Console.Write(authors.ExtendedProperties("expiration").ToString)
    Console.ReadLine()

End Sub

This code begins by filling a DataSet with the authors table from the pubs database. It then adds a new extended property, called expiration, and sets its value to the current date and time plus one hour. You then simply read it back. As you can see, it is very easy to add extended properties to DataSet objects. The same pattern also applies to DataTable and DataColumn objects.

Creating and Using DataSet Objects

The ADO.NET DataSet is a memory-resident representation of the data that provides a consistent relational programming model, regardless of the source of the data it contains. A DataSet represents a complete set of data, including the tables that contain, order, and constrain the data, as well as the relationships between the tables. The advantage to using a DataSet is that the data it contains can come from multiple sources, and it is fairly easy to get the data from multiple sources into the DataSet. In addition, you can define your own constraints between the DataTables in a DataSet.

There are several methods for working with a DataSet, which can be applied independently or in combination:

  • Programmatically create DataTables, DataRelations, and constraints within the DataSet and populate them with data.

  • Populate the DataSet or a DataTable from an existing RDBMS using a DataAdapter.

  • Load and persist a DataSet or DataTable using XML.

  • Load a DataSet from an XSD schema file.

  • Load a DataSet or a DataTable from a DataReader.

Here is a typical usage scenario for a DataSet object:

  1. A client makes a request to a Web service.

  2. Based on this request, the Web service populates a DataSet from a database using a DataAdapter and returns the DataSet to the client.

  3. The client then views the data and makes modifications.

  4. When finished viewing and modifying the data, the client passes the modified DataSet back to the Web service, which again uses a DataAdapter to reconcile the changes in the returned DataSet with the original data in the database.

  5. The Web service may then return a DataSet that reflects the current values in the database.

  6. Optionally, the client can then use the DataSet class's Merge method to merge the returned DataSet with the client's existing copy of the DataSet; the Merge method will accept successful changes and mark with an error any changes that failed.

The design of the ADO.NET DataSet makes this scenario fairly easy to implement. Because the DataSet is stateless, it can be safely passed between the server and the client without tying up server resources such as database connections. Although the DataSet is transmitted as XML, Web services and ADO.NET automatically transform the XML representation of the data to and from a DataSet, creating a rich, yet simplified, programming model.

In addition, because the DataSet is transmitted as an XML stream, non-ADO.NET clients can consume the same Web service consumed by ADO.NET clients. Similarly, ADO.NET clients can interact easily with non-ADO.NET Web services by sending any client DataSet to a Web service as XML and by consuming any XML returned as a DataSet from the Web service. However, note the size of the data; if your DataSet contains a large number of rows, then it will eat up a lot of bandwidth.

Programmatically Creating DataSet Objects

You can programmatically create a DataSet object to use as a data structure in your programs. This could be quite useful if you have complex data that needs to be passed around to another object's method. For example, when creating a new customer, instead of passing 20 arguments about the new customer to a method, you could just pass the programmatically created DataSet object with all of the customer information to the object's method.

Here is the code for building an ADO.NET DataSet object that is comprised of related tables:

Private Sub BuildDataSet()

    Dim customerOrders As New Data.DataSet("CustomerOrders")
    Dim customers As Data.DataTable = customerOrders.Tables.Add("Customers")
    Dim orders As Data.DataTable = customerOrders.Tables.Add("Orders")
    Dim row As Data.DataRow

    With customers
        .Columns.Add("CustomerID", Type.GetType("System.Int32"))
        .Columns.Add("FirstName", Type.GetType("System.String"))
        .Columns.Add("LastName", Type.GetType("System.String"))
        .Columns.Add("Phone", Type.GetType("System.String"))
        .Columns.Add("Email", Type.GetType("System.String"))
    End With

    With orders
        .Columns.Add("CustomerID", Type.GetType("System.Int32"))
        .Columns.Add("OrderID", Type.GetType("System.Int32"))
        .Columns.Add("OrderAmount", Type.GetType("System.Double"))
        .Columns.Add("OrderDate", Type.GetType("System.DateTime"))
    End With

    customerOrders.Relations.Add("Customers_Orders", _
    customerOrders.Tables("Customers").Columns("CustomerID"), _
    customerOrders.Tables("Orders").Columns("CustomerID"))
row = customers.NewRow()
    row("CustomerID") = 1
    row("FirstName") = "Bill"
    row("LastName") = "Evjen"
    row("Phone") = "555-1212"
    row("Email") = "[email protected]"
    customers.Rows.Add(row)

    row = orders.NewRow()
    row("CustomerID") = 1
    row("OrderID") = 22
    row("OrderAmount") = 0
    row("OrderDate") = #11/10/1997#
    orders.Rows.Add(row)

    Console.WriteLine(customerOrders.GetXml())
    Console.ReadLine()

End Sub

Here is what the resulting XML of the DataSet looks like:

<CustomerOrders>
  <Customers>
    <CustomerID>1</CustomerID>
    <FirstName>Bill</FirstName>
    <LastName>Evjen</LastName>
    <Phone>555-1212</Phone>
    <Email>[email protected]</Email>
</Customers>
<Orders>
  <CustomerID>1</CustomerID>
  <OrderID>22</OrderID>
  <OrderAmount>0</OrderAmount>
  <OrderDate>1997-11-10T00:00:00.0000</OrderDate>
</Orders>
</CustomerOrders>

You begin by first defining a DataSet object (CustomerOrders) named CustomerOrders. You then create two tables: one for customers (customers) and one for orders (orders). Then you define the columns of the tables. Note that you call the Add method of the DataSet's Tables collection. You then define the columns of each table and create a relation in the DataSet between the customers table and the orders table on the CustomerID column. Finally, you create instances of Rows for the tables, add the data, and then append the Rows to the Rows collection of the DataTable objects.

If you create a DataSet object with no name, it is given the default name of NewDataSet.

ADO.NET DataTable Objects

A DataSet is made up of a collection of tables, relationships, and constraints. In ADO.NET, DataTable objects are used to represent the tables in a DataSet. A DataTable represents one table of in-memory relational data. The data is local to the .NET application in which it resides, but can be populated from a data source such as SQL Server using a DataAdapter.

The DataTable class is a member of the System.Data namespace within the .NET Framework class library. You can create and use a DataTable independently or as a member of a DataSet, and DataTable objects can be used by other .NET Framework objects, including the DataView. You access the collection of tables in a DataSet through the DataSet object's Tables property.

The schema, or structure, of a table is represented by columns and constraints. You define the schema of a DataTable using DataColumn objects as well as ForeignKeyConstraint and UniqueConstraint objects. The columns in a table can map to columns in a data source, contain calculated values from expressions, automatically increment their values, or contain primary key values.

If you populate a DataTable from a database, then it inherits the constraints from the database, so you don't have to do all of that work manually. A DataTable must also have rows in which to contain and order the data. The DataRow class represents the actual data contained in the table. You use the DataRow and its properties and methods to retrieve, evaluate, and manipulate the data in a table. As you access and change the data within a row, the DataRow object maintains both its current and original state.

You can create parent-child relationships between tables within a database, such as SQL Server, using one or more related columns in the tables. You create a relationship between DataTable objects using a DataRelation, which can then be used to return a row's related child or parent rows.

Advanced ADO.NET Features of the DataSet and DataTable Objects

One of the main complaints developers had about ADO.NET 1.x was related to the performance of the DataSet and its DataTable children — in particular, when they contained a large amount of data. The performance hit comes in two different ways. The first way is the time it takes to actually load a DataSet with a lot of data. As the number of rows in a DataTable increases, the time to load a new row increases almost proportionally to the number of rows. The second way is when the large DataSet is serialized and remoted. A key feature of the DataSet is the fact that it automatically knows how to serialize itself, especially when you want to pass it between application tiers. Unfortunately, the serialization is quite verbose and takes up a lot of memory and network bandwidth. Both of these performance problems have been addressed since ADO.NET 2.0.

Indexing

The first improvement made since ADO.NET 2.0 to the DataSet family was a complete rewrite of the indexing engine for the DataTable, which now scales much better for large DataSets. The addition of the new indexing engine results in faster basic inserts, updates, and deletes, which also means faster Fill and Merge operations. Just as in relational database design, if you are dealing with large DataSets, then it pays big dividends if you first add unique keys and foreign keys to your DataTable. Even better, you don't have to change any of your code at all to take advantage of this new feature.

Serialization

The second improvement made to the DataSet family was adding new options to the way the DataSet and DataTable are serialized. The main complaint about retrieving DataSet objects from Web services and remoting calls was that they were way too verbose and took up too much network bandwidth. In ADO.NET 1.x, the DataSet serializes as XML, even when using the binary formatter. Using ADO.NET, you can also specify true binary serialization by setting the newly added RemotingFormat property to SerializationFormat.Binary, rather than (the default) SerializationFormat.XML. In the AdoNetFeaturesTest project of the Examples solution, I have added a Button (serializationButton) to the form and its associated Click event handler that demonstrates how to serialize a DataTable in binary format:

Private Sub _serializationButton_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles _serializationButton.Click
    ' Get the authors reader.
    Dim reader As SqlDataReader = GetAuthorsSqlReader()
    ' Load a DataTable from the reader
    Dim table As DataTable = GetTableFromReader(reader, "Authors")

This code begins by calling the helper methods GetAuthorsSqlReader and GetTableFromReader to get a DataTable of the authors from the pubs database. The next code block, shown here, is where you are actually serializing the DataTable out to a binary format:

Using fs As New FileStream("c:authors.dat", FileMode.Create)
        table.RemotingFormat = SerializationFormat.Binary
        Dim format As New BinaryFormatter()
        format.Serialize(fs, table)
    End Using

    ' Tell the user what happened.
    MessageBox.Show("Successfully serialized the DataTable!")
End Sub

This code takes advantage of the newly added Using statement for Visual Basic to wrap up creating and disposing of a FileStream instance that will hold your serialized DataTable data. The next step is to set the DataTable's RemotingFormat property to the SerializationFormat.Binary enumeration value. Once that is done, you simply create a new BinaryFormatter instance, and then call its Serialize method to serialize your DataTable into the FileStream instance. You then finish by showing users a message box indicating that the data has been serialized.

DataReader Integration

Another nice feature of the DataSet and DataTable classes is the capability to both read from and write out to a stream of data in the form of a DataReader. You will first take a look at how you can load a DataTable from a DataReader. To demonstrate this, I have added a Button (loadFromReaderButton) and its associated Click event handler to TestForm.vb of the AdoNetFeaturesTest project in the Examples solution:

Private Sub _loadFromReaderButton_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles _loadFromReaderButton.Click

    ' Get the authors reader.
    Dim reader As SqlDataReader = GetAuthorsSqlReader()

    ' Load a DataTable from the reader.
    Dim table As DataTable = GetTableFromReader(reader, "Authors")

    ' Bind the grid to the table.
    BindGrid(table)
    End Sub

This method is a controller method, meaning that it only calls helper methods. It begins by first obtaining a SqlDataReader from the GetAuthorsReader helper method. It then calls the GetTableFromReader helper method to transform the DataReader into a DataTable. The GetTableFromReader method is where you actually get to see the DataTable's new load functionality:

Private Function GetTableFromReader(ByVal reader As SqlDataReader, _
    ByVal tableName As String) As DataTable
    ' Create a new DataTable using the name passed in.
    Dim table As New DataTable(tableName)
    ' Load the DataTable from the reader.
    table.Load(reader)
    ' Close the reader.
    reader.Close()
    Return table
End Function

This method begins by first creating an instance of a DataTable and initializing it with the name passed in from the tableName argument. Once the new DataTable has been initialized, you call the new Load method and pass in the SqlDataReader that was passed into the method via the reader argument. This is where the DataTable takes the DataReader and populates the DataTable instance with the column names and data from the DataReader. The next step is to close the DataReader, as it is no longer needed; and finally, you return the newly populated DataTable.

DataTable Independence

One of the most convenient capabilities in ADO.NET is the inclusion of several methods from the DataSet class in the DataTable class. The DataTable is now much more versatile and useful than it was in the early ADO.NET days. The DataTable now supports all of the same read and write methods for XML as the DataSet — specifically, the ReadXml, ReadXmlSchema, WriteXml, and WriteXmlSchema methods.

The Merge method of the DataSet has now been added to the DataTable as well; and in addition to the existing functionality of the DataSet class, some of the new features of the DataSet class have been added to the DataTable class — namely, the RemotingFormat property, the Load method, and the GetDataReader method.

Working with the Common Provider Model

In ADO.NET 1.x, you could either code to the provider-specific classes, such as SqlConnection, or the generic interfaces, such as IDbConnection. If there was a possibility that the database you were programming against would change during your project, or if you were creating a commercial package intended to support customers with different databases, then you had to use the generic interfaces. You cannot call a constructor on an interface, so most generic programs included code that accomplished the task of obtaining the original IDbConnection by means of their own factory method, such as a GetConnection method that would return a provider-specific instance of the IDbConnection interface.

ADO.NET today has a more elegant solution for getting the provider-specific connection. Each data provider registers a ProviderFactory class and a provider string in the .NET machine.config file. A base ProviderFactory class (DbProviderFactory) and a System.Data.Common.ProviderFactories class can return a DataTable of information about different data providers registered in machine.config, and can return the correct ProviderFactory given the provider string (called ProviderInvariantName) or a DataRow from the DataTable. Instead of writing your own framework to build connections based on the name of the provider, ADO.NET now makes it much more straightforward, flexible, and easy to solve this problem.

Let's look at an example of using the common provider model to connect to the pubs database and display some rows from the authors table. In the AdoNetFeaturesTest project, on the TestForm.vb form, the providerButton button's Click event handler shows this functionality. The code is broken down into six steps. The first step is get the provider factory object based on a configuration value of the provider's invariant name:

Private Sub _providerButton_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles _providerButton.Click
        ' 1. Factory
        ' Create the provider factory from config value.
        Dim factory As DbProviderFactory = DbProviderFactories.GetFactory( _
           ConfigurationSettings.AppSettings("providerInvariantName"))

You are able to get the factory via the DbProviderFactories object's GetFactory method and pass in the string name of the provider invariant that you are storing in the project's app.config file. Here is the entry in the app.config file:

<add key="providerInvariantName" value="System.Data.SqlClient" />

In this case, you are using the SQL Server Data Provider. Once you have the factory object, the next step is to use it to create a connection:

' 2. Connection
' Create the connection from the factory.
Dim connection As DbConnection = factory.CreateConnection()
' Get the connection string from config.
connection.ConnectionString = _
  ConfigurationSettings.AppSettings("dbConnectionString")

The connection is created by calling the DbProviderFactory's CreateConnection method. In this case, the factory is returning a SqlConnection, because you chose to use the System.Data.SqlClient provider invariant. To keep your code generic, you will not be directly programming against any of the classes in the System.Data.SqlClient namespace. Note how the connection class you declare is a DbConnection class, which is part of the System.Data namespace.

The next step is to create a Command object so you can retrieve the data from the authors table:

' 3. Command
 ' Create the command from the connection.
 Dim command As DbCommand = connection.CreateCommand()
 ' Set the type of the command to stored procedure.
 command.CommandType = CommandType.StoredProcedure
 ' Set the name of the stored procedure to execute.
 command.CommandText = "usp_authors_Get_All"

You begin by declaring a generic DbCommand class variable and then using the DbConnection's CreateCommand method to create the DbCommand instance. Once you have done that, you set the command type to StoredProcedure and then set the stored procedure name.

This example uses a DbDataAdapter to fill a DataTable with the authors' data. Here is how you create and initialize the DbDataAdapter:

' 4. Adapter
' Create the adapter from the factory.
Dim adapter As DbDataAdapter = factory.CreateDataAdapter()
' Set the adapter's select command.
adapter.SelectCommand = command

Just as you did when you created your DbConnection instance, you use the factory to create your DbDataAdapter. After creating it, you then set the SelectCommand property's value to the instance of the previously initialized DbCommand instance.

After finishing these steps, the next step is to create a DataTable and fill it using the DataAdapter:

' 5. DataTable
' Create a new DataTable.
Dim authors As New DataTable("Authors")
' Use the adapter to fill the DataTable.
adapter.Fill(authors)

The final step is to bind the table to the form's grid:

' 6.  Grid
' Populate the grid with the data.
BindGrid(authors)

You already looked at the BindGrid helper method in the asynchronous example earlier. In this example, you are simply reusing this generic method again:

Private Sub BindGrid(ByVal table As DataTable)
        ' Clear the grid.
        Me._authorsGridView.DataSource = Nothing
        ' Bind the grid to the DataTable.
        Me._authorsGridView.DataSource = table
 End Sub

The main point to take away from this example is that you were able to easily write database-agnostic code with just a few short lines. ADO.NET 1.x required a lot of lines of code to create this functionality; you had to write your own abstract factory classes and factory methods in order to create instances of the generic database interfaces, such as IDbConnection, IDbCommand, and so on.

Connection Pooling in ADO.NET

Pooling connections can significantly enhance the performance and scalability of your application. Both the SQL Client .NET Data Provider and the OLE DB .NET Data Provider automatically pool connections using Windows Component Services and OLE DB Session Pooling, respectively. The only requirement is that you must use the exact same connection string each time if you want a pooled connection.

ADO.NET now enhances the connection pooling functionality offered in ADO.NET 1.x by enabling you to close all of the connections currently kept alive by the particular managed provider that you are using. You can clear a specific connection pool by using the shared SqlConnection.ClearPool method or clear all of the connection pools in an application domain by using the shared SqlConnection.ClearPools method. Both the SQL Server and Oracle managed providers implement this functionality.

Building a Data-Access Component

To better demonstrate what you have learned so far about ADO.NET, in this section you are going to build a data-access component. This component is designed to abstract the processing of stored procedures. The component you build is targeted at SQL Server, and it is assumed that all data access to the database will be through stored procedures. The idea of only using stored procedures to access data in a database has a number of advantages, such as scalability, performance, flexibility, and security. The only disadvantage is that you have to use stored procedures, and not SQL strings. Through the process of building this component, you will see how stored procedures are implemented in ADO.NET. You will also be building on the knowledge that you have gained from the previous chapters.

This component's main job is to abstract stored procedure calls to SQL Server, and one of the ways you do this is by passing in all of your stored procedure parameter metadata as XML (covered later in this section). The other job of the component is to demonstrate the use of some of the new objects in ADO.NET.

The code for this project is quite extensive, and you will only examine the key parts of it in this chapter. The full source is available in the code download (www.wrox.com).

Let's start with the beginning of the component. The first thing you do is declare your class and the private members of the class:

Option Explicit On
Option Strict On

Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Xml
Imports System.Collections
Imports System.Diagnostics

'' ' <summary>
'' ' This class wraps stored procedure calls to SQL Server.
'' ' It requires that all
'' ' stored procedures and their parameters be defined in an
'' ' XML document before
'' ' calling any of its methods. The XML can be passed in as an XmlDocument
'' ' instance or as a string of XML.  The only exceptions to this rule are
'' ' stored procedures that do not have parameters. This class also caches
'' ' SqlCommand objects. Each time a stored procedure is executed, a SqlCommand
'' ' object is built and cached into memory so that the next time the stored
'' ' procedure is called the SqlCommand object can be retrieved from memory.
'' ' </summary>
Public NotInheritable Class StoredProcedureHelper
Private _connectionString As String = ""
Private _spParamXml As String = ""
Private _spParamXmlDoc As XmlDocument = Nothing
Private _spParamXmlNode As XmlNode = Nothing
Private _commandParametersHashTable As New Hashtable()

Private Const ExceptionMsg As String = _
    "There was an error in the method.  " _
    & "Please see the Windows Event Viewer Application log for details"

You begin with your Option statements. Note that you are using the Option Strict statement. This helps prevent logic errors and data loss that can occur when you work between variables of different types. Next, you import the namespaces that you need for your component. In this case, most of your dependencies are on System.Data.SqlClient. You call your class StoredProcedureHelper to indicate that it wraps calling stored procedures to SQL Server. Next, you declare your private data members. You use the ExceptionMsg constant to indicate a generic error message for any exceptions thrown.

Constructors

Now you get to declare your constructors for the StoredProcedureHelper class. This is where you can really take advantage of method overloading, and it gives you a way to pass data to your class upon instantiation. First, you declare a default constructor:

'' ' <summary>
'' ' Default constructor.
'' ' </summary>
Public Sub New()

End Sub

The default constructor is provided in case users want to pass data to your class through public properties instead of through constructor arguments.

The next constructor you create allows a database connection string to be passed into it. By abstracting the database connection string out of this component, you give users of your component more flexibility regarding how they store and retrieve their database connection strings. Here is the code for the constructor:

`´` <summary>
`´` Overloaded constructor.
`´` </summary>
`´` <param name="connectionString">The connection string to the
`´` SQL Server database.</param>
Public Sub New(ByVal connectionString As String)
    Me._connectionString = connectionString
End Sub

The only difference between this constructor and the default constructor is that you are passing in a database connection string.

In the next constructor, you pass in both a database connection string and a string of XML representing the stored procedure parameters for the stored procedures you want to call:

`´` <summary>
`´` Overloaded constructor.
`´` </summary>
`´` <param name="connectionString">The connection string to the
`´` SQL Server database.</param>
`´` <param name="spParamXml">A valid XML string which conforms to
`´` the correct schema for stored procedure(s) and their
`´` associated parameter(s).</param>
Public Sub New(ByVal connectionString As String, ByVal spParamXml As String)
    Me.New(connectionString)
    Me._spParamXml = spParamXml
    Me._spParamXmlDoc = New XmlDocument
    Try
        Me._spParamXmlDoc.LoadXml(spParamXml)
        Me._spParamXmlNode = Me._spParamXmlDoc.DocumentElement
    Catch e As XmlException
        LogError(e)
        Throw New Exception(ExceptionMsg, e)
    End Try
End Sub

This constructor sets the database connection string by calling the first overloaded constructor. This handy technique enables you to avoid writing duplicate code in your constructors. The constructor then loads the stored procedure parameter configuration into a private XmlDocument instance variable as well as a private XmlNode instance variable.

The remaining constructors enable you to pass in combinations of database connection strings as well as either a valid XmlDocument instance representing the stored procedure parameters or a valid XmlNode instance that represents the stored procedure parameters.

Properties

Now let's look at the properties of your class. Your object contains the following properties: ConnectionString, SpParamXml, and SpParamXmlDoc. These properties are provided as a courtesy in case the user of your object does not want to supply them via a constructor call. The ConnectionString property provides the same functionality as the first overloaded constructor you looked at. The SpParamXml property enables the user of the object to pass in a valid XML string representing the stored procedures' parameter metadata. All of the properties are read-write. The SpParamXmlDoc property enables users to pass in an XmlDocument instance representing the stored procedures' parameter metadata.

Here is the code for the SpParamXml property:

`´` <summary>
`´` A valid XML string which conforms to the correct schema for
`´` stored procedure(s) and their associated parameter(s).
`´` </summary>
Public Property SpParamXml() As String
Get
        Return Me._spParamXml
    End Get
    Set(ByVal Value As String)
        Me._spParamXml = Value
        ' Set the XmlDocument instance to null, since
        ' an XML string is being passed in.
        Me._spParamXmlDoc = Nothing
        Try
            Me._spParamXmlDoc.LoadXml(Me._spParamXml)
            Me._spParamXmlNode = Me._spParamXmlDoc.DocumentElement
        Catch e As XmlException
            LogError(e)
            Throw New Exception(ExceptionMsg)
        End Try
    End Set
End Property

Note that this property resets the XmlDocument instance to Nothing before trying to load the document. This is done in case it was already set in one of the overloaded constructors, or from a previous call to this property. It also sets the XmlNode instance to the DocumentElement property of the XmlDocument instance, thus keeping them both in sync.

Stored Procedure XML Structure

In this case, rather than have the user of this class be responsible for populating the Parameters collection of a Command object, you will abstract it out into an XML structure. The structure is very simple; it basically enables you to store the metadata for one or more stored procedures at a time. This has a huge advantage because you can change all of the parameters on a stored procedure without having to recompile the project. Shown here is the XML structure for the metadata:

<StoredProcedures>
 <StoredProcedure name>
  <Parameters>
   <Parameter name size datatype direction isNullable sourceColumn />
  </Parameters>
 </StoredProcedure>
</StoredProcedures>

Here is what some sample data for the XML structure looks like:

<?xml version="1.0"?>
<StoredProcedures>
 <StoredProcedure name="usp_Get_Authors_By_States">
  <Parameters>
   <Parameter name="@states" size="100" datatype="VarChar"
    direction="Input" isNullabel="True" />
   <Parameter name="@state_delimiter" size="1" datatype="Char"
    direction="Input" isNullabel="True" />
  </Parameters>
 </StoredProcedure>
</StoredProcedures>

The valid values for the direction attribute are Input, Output, ReturnValue, and InputOutput. These values map directly to the System.Data.Parameter enumeration values. The valid values for the data type attribute are BigInt, Binary, Bit, Char, DateTime, Decimal, Float, Image, Int, Money, NChar, NText, NVarChar, Real, SmallDateTime, SmallInt, SmallMoney, Text, Timestamp, TinyInt, UniqueIdentifier, VarBinary, VarChar, and Variant. These values map directly to the System.Data.SqlDbType enumeration values.

Methods

That completes our look at the stored procedure XML structure the class expects, as well as the public properties and public constructors for the class. Now let's turn our attention to the public methods of your class.

ExecSpReturnDataSet

This public function executes a stored procedure and returns a DataSet object. It takes a stored procedure name (String), an optional DataSet name (String), and an optional list of parameter names and values (IDictionary). Here is the code for ExecSpReturnDataSet:

`´` <summary>
`´` Executes a stored procedure with or without parameters and returns a
`´` populated DataSet object.
`´` </summary>
`´` <param name="spName">The name of the stored procedure to execute.</param>
`´` <param name="dataSetName">An optional name for the DataSet instance.</param>
`´` <param name="paramValues">A name-value pair of stored procedure parameter
`´` name(s) and value(s).</param>
`´` <returns>A populated DataSet object.</returns>
Public Function ExecSpReturnDataSet(ByVal spName As String, _
                ByVal dataSetName As String, _
                ByVal paramValues As IDictionary) As DataSet
    Dim command As SqlCommand = Nothing
    Try
        ' Get the initialized SqlCommand instance.
        command = GetSqlCommand(spName)
        ' Set the parameter values for the SqlCommand.
        SetParameterValues(command, paramValues)
        ' Initialize the SqlDataAdapter with the SqlCommand object.
        Dim sqlDA As New SqlDataAdapter(command)

        ' Initialize the DataSet.
        Dim ds As New DataSet()

        If Not (dataSetName Is Nothing) Then
            If dataSetName.Length > 0 Then
                ds.DataSetName = dataSetName
            End If
        End If

        ' Fill the DataSet.
        sqlDA.Fill(ds)

        ' Return the DataSet.
Return ds
    Catch e As Exception
        LogError(e)
        Throw New Exception(ExceptionMsg, e)
    Finally
        ' Close and release resources.
        DisposeCommand(command)
    End Try
End Function

This function uses three main objects to accomplish its mission: SqlCommand, SqlDataAdapter, and DataSet. You first wrap everything in a Try-Catch-Finally block to ensure that you trap any exceptions that are thrown and to properly close and release the SqlCommand and SqlConnection resources. You call a helper method, GetSqlCommand, in order to get a fully initialized SqlCommand instance, to include any SqlParameter objects the SqlCommand may have based on your object's internal XmlDocument. Here is the code for GetSqlCommand and its overload:

`´` <summary>
`´` Initializes a SqlCommand object based on a stored procedure name
`´` and a SqlTransaction instance. Verifies that the stored procedure
`´` name is valid, and then tries to get the SqlCommand object from
`´` cache. If it is not already in cache, then the SqlCommand object
`´` is initialized and placed into cache.
`´` </summary>
`´` <param name="transaction">The transaction that the stored
`´` procedure will be executed under.</param>
`´` <param name="spName">The name of the stored procedure to execute.</param>
`´` <returns>An initialized SqlCommand object.</returns>
Public Function GetSqlCommand(ByVal transaction As SqlTransaction, _
    ByVal spName As String) As SqlCommand

    Dim command As SqlCommand = Nothing

    ' Get the name of the stored procedure.
    If spName.Length < 1 Or spName.Length > 127 Then
        Throw New ArgumentOutOfRangeException("spName", _
            "Stored procedure name must be from 1 - 128 characters.")
    End If

    ' See if the command object is already in memory.
    Dim hashKey As String = Me._connectionString & ":" & spName
    command = CType(_commandParametersHashTable(hashKey), SqlCommand)
    If command Is Nothing Then
        ' It was not in memory.
        ' Initialize the SqlCommand.
        command = New SqlCommand(spName, GetSqlConnection(transaction))

        ' Tell the SqlCommand that you are using a stored procedure.
        command.CommandType = CommandType.StoredProcedure

        ' Build the parameters, if there are any.
        BuildParameters(command)

        ' Put the SqlCommand instance into memory.
Me._commandParametersHashTable(hashKey) = command
    Else
        ' It was in memory, but you still need to set the
        ' connection property.
        command.Connection = GetSqlConnection(transaction)
    End If

    ' Return the initialized SqlCommand instance.
    Return command
End Function

`´` <summary>
`´` Overload. Initializes a SqlCommand object based on a stored
`´` procedure name, with no SqlTransaction instance.
`´` Verifies that the stored procedure name is valid, and then tries
`´` to get the SqlCommand object from cache. If it is not already in
`´` cache, then the SqlCommand object is initialized and placed into cache.
`´` </summary>
`´` <param name="spName">The name of the stored procedure to execute.</param>
`´` <returns>An initialized SqlCommand object.</returns>
Public Function GetSqlCommand(ByVal spName As String) As SqlCommand
    ' Return the initialized SqlCommand instance.
    Return GetSqlCommand(Nothing, spName)
End Function

The difference between this method and its overload is that the first method takes in a SqlTransaction instance argument, and the overload does not require the SqlTransaction instance to be passed in. The overload simply calls the first method and passes in a value of Nothing for the SqlTransaction argument.

This method first performs a check to ensure that the stored procedure name is between 1 and 128 characters long, in accordance with SQL Server's object-naming conventions. If it is not, then you throw an exception. The next step this method performs is to try to get an already initialized SqlCommand object from your object's private Hashtable variable, commandParametersHashTable, using your object's database connection string and the name of the stored procedure as the key. If the SqlCommand is not found, then you go ahead and build the SqlCommand object by calling its constructor and passing in the stored procedure name and a SqlConnection instance returned from the GetSqlConnection helper method. The code then sets the SqlCommand's CommandType property. You should ensure that you pass in the CommandType.StoredProcedure enumeration value, as you are executing a stored procedure.

Once the SqlCommand object is properly initialized, you pass it to the BuildParameters method. You will look at this method in more detail later. After this step, the SqlCommand is fully initialized, and you place it into your object's internal cache (the commandParametersHashTable Hashtable variable). Finally, the SqlCommand is returned to the calling code.

Getting back to the ExecSpReturnDataSet method, now that the SqlCommand object has been properly initialized, you need to set the values of the parameters. This is done via another helper method called SetParameterValues. SetParameterValues takes two arguments: a reference to a SqlCommand object and an IDictionary interface. You are using an IDictionary interface instead of a class such as a Hashtable (which implements the IDictionary interface) in order to make your code more flexible. This is a good design practice and works quite well — for example, in situations where the user of your class has built his or her own custom dictionary object that implements the IDictionary interface. It then loops through the SqlCommand's Parameters collection and sets each SqlParameter's value based on the corresponding name-value pair in the IDictionary object, as long as the parameter's direction is not Output. Following is the code for the SetParameterValues method:

`´` <summary>
`´` Traverses the SqlCommand's SqlParameters collection and sets the values
`´` for all of the SqlParameter(s) objects whose direction is not Output and
`´` whose name matches the name in the dictValues IDictionary that was
`´` passed in.
`´` </summary>
`´` <param name="command">An initialized SqlCommand object.</param>
`´` <param name="dictValues">A name-value pair of stored procedure parameter
`´` name(s) and value(s).</param>
Public Sub SetParameterValues(ByVal command As SqlCommand, _
    ByVal dictValues As IDictionary)
    If command Is Nothing Then
        Throw New ArgumentNullException("command", _
            "The command argument cannot be null.")
    End If
    ' Traverse the SqlCommand's SqlParameters collection.
    Dim parameter As SqlParameter
    For Each parameter In command.Parameters
        ' Do not set Output parameters.
        If parameter.Direction <> ParameterDirection.Output Then
            ' Set the initial value to DBNull.
            parameter.Value = TypeCode.DBNull
            ' If there is a match, then update the parameter value.
            If dictValues.Contains(parameter.ParameterName) Then
                parameter.Value = dictValues(parameter.ParameterName)
            Else
                ' There was not a match.
                ' If the parameter value cannot be null, throw an exception.
                If Not parameter.IsNullable Then
                    Throw New ArgumentNullException(parameter.ParameterName, _
                        "Error getting the value for the " _
                        & parameter.ParameterName & " parameter.")
                End If
            End If
        End If
    Next parameter
End Sub

When traversing the SqlCommand's Parameters collection, if a SqlParameter's value cannot be found in the IDictionary instance, then a check is made to determine whether the SqlParameter's value is allowed to be null or not. If it is allowed, then the value is set to DBNull; otherwise, an exception is thrown.

After setting the values of the parameters, the next step is to pass the SqlCommand object to the SqlDataAdapter's constructor:

' Initialize the SqlDataAdapter with the SqlCommand object.
Dim sqlDA As New SqlDataAdapter(command)

Then try to set the name of the DataSet using the dataSetName method argument:

' Try to set the name of the DataSet.
If Not (dataSetName Is Nothing) Then
    If dataSetName.Length > 0 Then
        ds.DataSetName = dataSetName
    End If
End If

After doing this, you call the Fill method of the SqlDataAdapter to fill your DataSet object:

' Fill the DataSet.
sqlDA.Fill(ds)

You then return the DataSet object back to the caller:

' Return the DataSet.
Return ds

If an exception was caught, then you log the exception data to the Windows Application Log via the LogError private method, and then throw a new exception with your generic exception message. Nest the original exception inside of the new exception via the innerException constructor parameter:

Catch e As Exception
    LogError(e)
    Throw New Exception(ExceptionMsg, e)

In the Finally block, you close and release the SqlCommand object's resources via the DisposeCommand helper method:

Finally
    ' Close and release resources
    DisposeCommand(command)

The DisposeCommand helper function closes the SqlCommand's SqlConnection property and disposes of the SqlCommand object:

`´` <summary>
`´` Disposes a SqlCommand and its underlying SqlConnection.
`´` </summary>
`´` <param name="command"></param>
Private Sub DisposeCommand(ByVal command As SqlCommand)
    If Not (command Is Nothing) Then
        If Not (command.Connection Is Nothing) Then
            command.Connection.Close()
            command.Connection.Dispose()
        End If
        command.Dispose()
    End If
End Sub

BuildParameters

This private method is the heart of this object and does the most work. It is responsible for parsing the stored procedure parameter XML and mapping all of the SqlParameter objects into the Parameters property of the SqlCommand object. Here is the signature of the method:

`´` <summary>
`´` Finds the parameter information for the stored procedure from the
`´` stored procedures XML document and then uses that information to
`´` build and append the parameter(s) for the SqlCommand's
`´` SqlParameters collection.
`´` </summary>
`´` <param name="command">An initialized SqlCommand object.</param>
Private Sub BuildParameters(ByVal command As SqlCommand)

The first thing you do in this method is determine whether any XML is being passed in or not. Here is the code that checks for the XML:

' See if there is an XmlNode of parameter(s) for the stored procedure.
If Me._spParamXmlNode Is Nothing Then
    ' No parameters to add, so exit.
    Return
End If

The last bit of code simply checks whether there is an XmlNode instance of parameter information. If the XmlNode has not been initialized, then you exit the method. It is entirely possible that users of this object may have stored procedures with no parameters at all. You choose an XmlNode object to parse the XML because loading all of the stored procedure XML into memory will not hurt performance; it is a small amount of data. As an alternative, you could use an XmlReader object to load into memory only what you need at runtime.

The next step is to clear the SqlCommand object's Parameters collection:

' Clear the parameters collection for the SqlCommand
command.Parameters.Clear()

You then use the name of the stored procedure as the key in your XPath query of the XML, and execute the following XPath query to get the list of parameters for the stored procedure:

' Get the node list of <Parameter>'s for the stored procedure.
Dim xpathQuery As String = "//StoredProcedures/StoredProcedure[@name='" _
    & command.CommandText & "']/Parameters/Parameter"
Dim parameterNodes As XmlNodeList = Me._spParamXmlNode.SelectNodes(xpathQuery)

This query is executed off the XmlDocument object and returns an XmlNodeList object. You start the loop through the Parameter elements in the XML and retrieve all of the mandatory Parameter attributes:

Dim parameterNode As XmlElement
For Each parameterNode In parameterNodes
    ' Get the attribute values for the <Parameter> element.

    ' Get the attribute values for the <Parameter> element.
' name
    Dim parameterName As String = parameterNode.GetAttribute("name")
    If parameterName.Length = 0 Then
        Throw New ArgumentNullException("name", "Error getting the 'name' " _
            & "attribute for the <Parameter> element.")
    End If

    ' size
    Dim parameterSize As Integer = 0
    If parameterNode.GetAttribute("size").Length = 0 Then
        Throw New ArgumentNullException("size", "Error getting the 'size' " _
            & "attribute for the <Parameter> element.")
    Else
        parameterSize = Convert.ToInt32(parameterNode.GetAttribute("size"))
    End If

    ' datatype
    Dim sqlDataType As SqlDbType
    If parameterNode.GetAttribute("datatype").Length = 0 Then
        Throw New ArgumentNullException("datatype", "Error getting the " _
             & "'datatype' attribute for the <Parameter> element.")
    Else
        sqlDataType = CType([Enum].Parse(GetType(SqlDbType), _
            parameterNode.GetAttribute("datatype"), True), SqlDbType)
    End If

    ' direction
    Dim parameterDirection As ParameterDirection = parameterDirection.Input
    If parameterNode.GetAttribute("direction").Length > 0 Then
        parameterDirection = CType([Enum].Parse(GetType(ParameterDirection), _
            parameterNode.GetAttribute("direction"), True), ParameterDirection)
    End If
End If

Because these attributes are mandatory, if any of them are missing, then you throw an exception. The interesting part of this code is using the Enum.Parse static method to convert the string value from the XML into the correct .NET enumeration data type for the sqlDataType and parameterDirection variables. This is possible because the probable values in your XML for these attributes map directly to the names of their respective enumeration data types in .NET. Next, you get the optional attributes:

' Get the optional attribute values for the <Parameter> element.
' isNullable
Dim isNullable As Boolean = False
Try
    If parameterNode.GetAttribute("isNullable").Length > 0 Then
        isNullable = Boolean.Parse(parameterNode.GetAttribute("isNullable"))
    End If
Catch
End Try

' sourceColumn  -  This must map to the name of a column in a DataSet.
Dim sourceColumn As String = ""
Try
    If parameterNode.GetAttribute("sourceColumn").Length > 0 Then
sourceColumn = parameterNode.GetAttribute("sourceColumn")
    End If
Catch
End Try

These attributes are optional mainly because of their data types. Because isNullable is Boolean, you go ahead and convert it to False if it is missing; and if sourceColumn is missing, then you just ignore it entirely.

Now you are ready to create the SqlParameter object and set its Direction property:

' Create the parameter object.  Pass in the name, datatype,
' and size to the constructor.
Dim sqlParameter As SqlParameter = New SqlParameter(parameterName, _
    sqlDataType, parameterSize)

'Set the direction of the parameter.
sqlParameter.Direction = parameterDirection

You then set the optional property values of the SqlParameter object:

' If the optional attributes have values, then set them.
' IsNullable
If isNullable Then
    sqlParameter.IsNullable = isNullable
End If

' SourceColumn
sqlParameter.SourceColumn = sourceColumn

Finally, you add the SqlParameter object to the SqlCommand object's Parameters collection, complete your loop, and finish the method:

' Add the parameter to the SqlCommand's parameter collection.
        command.Parameters.Add(sqlParameter)
    Next parameterNode
End Sub

Now it's time to look at ExecSpReturnDataReader. This function is almost identical to ExecSpReturnDataSet except that it returns a SqlDataReader object instead of a DataSet object.

ExecSpReturnDataReader

This public function executes a stored procedure and returns a SqlDataReader object. Similar to the ExecSpReturnDataSet method, it takes a stored procedure name (String) and an optional list of parameter names and values (IDictionary). Here is the code for ExecSpReturnDataReader:

`´` <summary>
`´` Executes a stored procedure with or without parameters and returns a
`´` SqlDataReader instance with a live connection to the database. It is
`´` very important to call the Close method of the SqlDataReader as soon
`´` as possible after using it.
`´` </summary>
`´` <param name="spName">The name of the stored procedure to execute.</param>
`´` <param name="paramValues">A name-value pair of stored procedure parameter
`´` name(s) and value(s).</param>
`´` <returns>A SqlDataReader object.</returns>
Public Function ExecSpReturnDataReader(ByVal spName As String, _
    ByVal paramValues As IDictionary) As SqlDataReader
  Dim command As SqlCommand = Nothing
  Try
    ' Get the initialized SqlCommand instance.
    command = GetSqlCommand(spName)

    ' Set the parameter values for the SqlCommand.
    SetParameterValues(command, paramValues)
    ' Open the connection.
    command.Connection.Open()

    ' Execute the sp and return the SqlDataReader.
    Return command.ExecuteReader(CommandBehavior.CloseConnection)
Catch e As Exception
    LogError(e)
    Throw New Exception(ExceptionMsg, e)

End Try

End Function

This function uses two objects to accomplish its mission: SqlCommand and SqlDataReader. The only part where this function differs from ExecSpReturnDataSet is right after you call the SetParameterValues private method. In this case, you have to ensure that the SqlCommand object's SqlConnection is opened because the SqlDataReader requires an open connection. You then call the ExecuteReader method of the SqlCommand object to get your SqlDataReader object, passing in the CommandBehavior.CloseConnection value for the method's behavior argument.

Because this method returns a SqlDataReader object, which requires an open database connection, you do not close the connection in this method. It is up to the caller to close the SqlDataReader and the connection when finished. Because you used the CommandBehavior.CloseConnection value for the behavior argument, the user of the method only has to remember to call the SqlDataReader's Close method in order to close the underlying SqlConnection object.

The next function you are going to look at, ExecSpReturnXmlReader, is almost identical to the last two functions, except that it returns an XmlReader instead of a DataSet or a SqlDataReader.

ExecSpReturnXmlReader

This public function executes a stored procedure and returns an XmlReader instance. The function requires the stored procedure to contain a FOR XML clause in its SQL statement. Once again, it takes a stored procedure name (String) and an optional list of parameter names and values (IDictionary). Here is the code for ExecSpReturnXmlReader:

`´` <summary>
`´` Executes a stored procedure with or without parameters and returns an
`´` XmlReader instance with a live connection to the database. It is
`´` very important to call the Close method of the XmlReader as soon
`´` as possible after using it. Only use this method when calling stored
`´` procedures that return XML results (FOR XML ...).
`´` </summary>
`´` <param name="spName">The name of the stored procedure to execute.</param>
`´` <param name="paramValues">A name-value pair of stored procedure parameter
`´` name(s) and value(s).</param>
`´` <returns>An XmlReader object.</returns>
Public Function ExecSpReturnXmlReader(ByVal spName As String, _
    ByVal paramValues As IDictionary) As XmlReader
    Dim command As SqlCommand = Nothing
    Try
       ' Get the initialized SqlCommand instance.
       command = GetSqlCommand(spName)
       ' Set the parameter values for the SqlCommand.
       SetParameterValues(command, paramValues)

       ' Open the connection.
       command.Connection.Open()
       ' Execute the sp and return the XmlReader.
       Return command.ExecuteXmlReader()
   Catch e As Exception
       LogError(e)
       Throw New Exception(ExceptionMsg, e)
   End Try
End Function

The only difference between this method and ExecSpReturnDataReader is that you call the ExecuteXmlReader method of the SqlCommand object instead of the ExecuteReader method. Like the ExecSpReturnDataReader method, users of this method need to close the returned XmlReader after using it in order to properly release resources.

This method works only with SQL Server 2000 and later.

Next up is the ExecSp method, which needs only the SqlCommand object to get its work done. Its job is to execute stored procedures that do not return result sets.

ExecSp

This public method executes a stored procedure and does not return a value. It takes a stored procedure name (String) and an optional list of parameter names and values (IDictionary) for its arguments. Here is the code for ExecSp:

`´` <summary>
`´` Executes a stored procedure with or without parameters that
`´` does not return output values or a resultset.
`´` </summary>
`´` <param name="transaction">The transaction that the stored procedure
`´` will be executed under.</param>
`´` <param name="spName">The name of the stored procedure to execute.</param>
`´` <param name="paramValues">A name-value pair of stored procedure parameter
`´` name(s) and value(s).</param>
Public Sub ExecSp(ByVal spName As String, ByVal paramValues As IDictionary)
Dim command As SqlCommand = Nothing
    Try
        ' Get the initialized SqlCommand instance.
        command = GetSqlCommand(transaction, spName)
        ' Set the parameter values for the SqlCommand.
        SetParameterValues(command, paramValues)
        ' Run the stored procedure.
        RunSp(command)
    Catch e As Exception
        LogError(e)
        Throw New Exception(ExceptionMsg, e)
    Finally
        ' Close and release resources.
        DisposeCommand(command)
    End Try
End Sub

It is almost identical to the other Exec* functions, except when it executes the stored procedure. The code inside of the private RunSp method opens the SqlCommand's SqlConnection object and then calls the SqlCommand object's ExecuteNonQuery method. This ensures that the SqlCommand does not return any type of DataReader object to read the results. This method is used mostly to execute INSERT, UPDATE, and DELETE stored procedures that do not return any results. It also has an overload that does not include the SqlTransaction argument.

Following is the code for RunSp:

`´` <summary>
`´` Opens the SqlCommand object's underlying SqlConnection and calls
`´` the SqlCommand's ExecuteNonQuery method.
`´` </summary>
`´` <param name="command">An initialized SqlCommand object.</param>
Private Sub RunSp(ByRef command As SqlCommand)
    ' Open the connection.
    command.Connection.Open()

    'a Execute the stored procedure.
    command.ExecuteNonQuery()
End Sub

Finally, the last public function you are going to create is ExecSpOutputValues.

ExecSpOutputValues

This last public function in your component executes a stored procedure and returns an IDictionary object that contains output parameter name-value pairs. It is not meant for stored procedures that return result sets. As with the previous examples, this function takes a stored procedure name (String) and an optional list of parameter names and values (IDictionary) for its arguments. Here is the code for ExecSpOutputValues:

`´` <summary>
`´` Executes a stored procedure with or without parameters and returns an
`´` IDictionary instance with the stored procedure's output parameter
`´` name(s) and value(s).
`´` </summary>
`´` <param name="transaction">The transaction that the stored procedure
`´` will be executed under.</param>
`´` <param name="spName">The name of the stored procedure to execute.</param>
`´` <param name="paramValues">A name-value pair of stored procedure parameter
`´` name(s) and value(s).</param>
`´` <returns>An IDictionary object.</returns>
Public Function ExecSpOutputValues(ByVal transaction As SqlTransaction, _
                                   ByVal spName As String, _
                                   ByVal paramValues As IDictionary) As IDictionary
    Dim command As SqlCommand = Nothing
    Try
        ' Get the initialized SqlCommand instance.
        command = GetSqlCommand(transaction, spName)
        ' Set the parameter values for the SqlCommand.
        SetParameterValues(command, paramValues)

        ' Run the stored procedure.
        RunSp(command)
        ' Get the output values.
        Dim outputParams As New Hashtable()
        Dim param As SqlParameter
        For Each param In command.Parameters
            If param.Direction = ParameterDirection.Output _
                Or param.Direction = ParameterDirection.InputOutput Then
                outputParams.Add(param.ParameterName, param.Value)
            End If
        Next param
        Return outputParams
    Catch e As Exception
        LogError(e)
        Throw New Exception(ExceptionMsg, e)
    Finally
        ' Close and release resources.
        DisposeCommand(command)
    End Try
End Function

This function is almost identical to ExecSp except that after the SqlCommand.ExecuteNonQuery method is called, you iterate through the SqlCommand object's Parameters collection and look for all of the parameters that are output parameters. Next, you take the values of the output parameters and add the name-value pair to the IDictionary instance that you return. This method also has an overload that does not include the SqlTransaction argument.

Using DataSet Objects to Bind to DataGrids

Now that you have built your data-access component, it is time to test it. A nice way to do that is to call the ExecSpReturnDataSet method, take the DataSet object that was created, and then bind the DataSet to a DataGrid. You also get to see how easily the DataSet and the DataGrid control integrate together.

This exercise uses a Windows Application project called SqlServerWrapperTestHarness, added to the Examples solution. It contains references to System, System.Data, System.Drawing, System.Windows.Forms, and System.Xml, as well as a project reference to the SqlServerWrapper project. Added to the project is a form named TestForm.vb with two buttons, one for testing the ExecSpReturnDataSet method and one for testing the ExecSpReturnSqlRecord method. In this example, you will be looking only at the code for testing the ExecSpReturnDataSet method. Figure 9-4 shows what the test form looks like.

Figure 9-4

Figure 9.4. Figure 9-4

Figure 9-5 shows what your references should look like.

Here is the code for the declarations and private members of the form:

Option Explicit On
Option Strict On
Imports SqlServerWrapper
Imports System.Data.SqlClient
Imports System.Xml
Imports System.Configuration

Public Class TestForm
    Inherits System.Windows.Forms.Form

    Private _helper As StoredProcedureHelper = Nothing

These declarations should look pretty familiar by now. Note that you are declaring a private variable (_helper) for the StoredProcedureHelper class that you are using so you can get to the class from other parts of the form instead of just a Button Click event handler.

Figure 9-5

Figure 9.5. Figure 9-5

Next, you initialize the _helper variable in the form's Load event handler:

Private Sub TestForm_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
    ' Set the SQL connection string
    Dim connectionString As String = _
       ConfigurationSettings.AppSettings("dbConnectionString")

    ' Call the SqlServer wrapper constructor and
    ' pass the DB connection string and the stored procedures config.
    helper = New StoredProcedureHelper(connectionString, _
        CType(ConfigurationSettings.GetConfig("StoredProcedureSettings"), _
        XmlNode))
    End Sub

As in the earlier examples, this code begins by retrieving a connection string to the pubs database from the app.config file. You then create a new instance of the StoredProcedureHelper and assign it to the _helper class variable. During the constructor call to the StoredProcedureHelper class, you first pass in your connection string, and then you pass in an XmlNode of the stored procedure metadata for the StoredProcedureHelper class to consume. Note that you are passing the stored procedure metadata in to your class via the GetConfig method of the ConfigurationSettings class. This is because you have created a section inside of your app.config file called StoredProcedureSettings, and you have configured a SectionHandler to let the .NET Framework application configuration functionality consume your XML and give it back to you as an XmlNode. Here is what this section looks like inside of the app.config file:

<configSections>
    <section name="StoredProcedureSettings"
    type="SqlServerWrapper.StoredProcedureSectionHandler, SqlServerWrapper" />
</configSections>
  <StoredProcedureSettings>
    <StoredProcedures>
      <StoredProcedure name="usp_Get_Authors_By_States">
        <Parameters>
          <Parameter name="@states" datatype="VarChar" direction="Input"
           isNullabel="false" size="100" />
          <Parameter name="@state_delimiter" datatype="Char" direction="Input"
           isNullabel="false" size="1" />
        </Parameters>
      </StoredProcedure>
      <StoredProcedure name="usp_Get_Author_By_ID">
        <Parameters>
          <Parameter name="@au_id" datatype="VarChar" direction="Input"
           isNullabel="false" size="11" />
        </Parameters>
      </StoredProcedure>
    </StoredProcedures>
  </StoredProcedureSettings>

This is nice because you don't need to include a separate XML file for your project; you just integrate seamlessly into the app.config file. Note how you are defining what class in what assembly will handle consuming your <StoredProcedureSettings> section in the <section> element. In order for this to work, the class defined must implement the System.Configuration.IConfigurationSectionHandler interface. Here is the code for your section handler:

Option Explicit On
Option Strict On
Imports System
Imports System.Configuration
Imports System.Xml
Imports System.Xml.Serialization
Imports System.Xml.XPath

Public Class StoredProcedureSectionHandler
    Implements IConfigurationSectionHandler

    Public Function Create(ByVal parent As Object, _
        ByVal configContext As Object, _
        ByVal section As System.Xml.XmlNode) As Object _
            Implements IConfigurationSectionHandler.Create
        Return section("StoredProcedures")
    End Function
End Class

This code is pretty simple; you just return the XML node named StoredProcedures to the caller of your handler.

Back to your Button's Click event handler, once you have the StoredProcedureHelper class instance fully initialized, you then create the parameter values for the stored procedure you want to execute and pass these arguments to the ExecSpReturnDataSet method:

' Add the two parameter name-values.
Dim params As New Hashtable
params.Add("@states", "CA")
params.Add("@state_delimiter", "^")

' Execute the sp, and get the DataSet object back.
Dim ds As DataSet = _helper.ExecSpReturnDataSet("usp_Get_Authors_By_States", _
    "", params)

The last step is to actually bind the data to the form's grid:

' Bind the DataGrid to the DataSet object.
dgdAuthors.SetDataBinding(ds.Tables(0), Nothing)

The results should look like the dialog shown in Figure 9-6.

Figure 9-6

Figure 9.6. Figure 9-6

Summary

This chapter took a look at ADO.NET and some of its more advanced features. You have seen and used the main objects in ADO.NET that you need to quickly get up and running in order to build data-access into your .NET applications. You took a fairly in-depth look at the DataSet and DataTable classes, as these are the core classes of ADO.NET.

You also looked at stored procedures, including how to create them in SQL Server and how to access them from your code. Finally, you built your own custom data-access component, which makes it easy to call stored procedures, and separate data-access code from the rest of business-logic code in a .NET application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset