Chapter 18. Using ADO.NET

After completing this chapter, you will be able to:

  • Connect to a database.

  • Execute SQL statements to query the database.

  • Execute SQL statements to update the database.

  • Create disconnected applications, which use a DataSet to cache tables in memory.

  • Create a report displaying data from the database.

ADO.NET is the data access API from Microsoft for the .NET Framework. ADO.NET has been optimized to work with .NET, making it possible for distributed applications and services to exchange data easily and reliably.

ADO.NET offers two distinct programming models, depending on the type of application you need to build:

  • If you require forward-only, read-only access to the data, you can use a DataReader to iterate over the results of a query. As you’ll see, DataReaders are easy to use but require that you remain connected to the database as long as you are using the reader.

  • Alternatively, you can use a DataSet to represent an in-memory cache of data from the data source. You can create a DataSet, load data into it from the database, and then disconnect. If you edit the DataSet, you can also update the database by using the changed data. One major advantage of the DataSet is that you only need to be connected to the database while exchanging data; this can make it a more scalable solution.

In this chapter, you will learn how to use ADO.NET to connect to a data source, execute queries, and perform database update operations. You will also learn how to use a DataSet in a disconnected application. You will see how to fill a DataSet with data from a database and display that data in a grid.

Note

ADO.NET provides access to any kind of relational database. To avoid the need to download and install database engines and deal with complex setups, the examples in this chapter use a Microsoft Access database. However, I want to emphasize that the principles are exactly the same whether you’re using Access or Microsoft SQL Server, and if you write your code correctly, you should only have to change the configuration file to change to another database type.

What is ADO.NET?

ADO.NET is a strategic API from Microsoft for data access in the modern era of distributed, Internet-based applications. ADO.NET contains a set of interfaces and classes with which you can work with data from a wide range of databases, including Microsoft SQL Server, Oracle, Sybase, Access, and so on.

ADO.NET data providers

ADO.NET uses the concept of a data provider to facilitate efficient access to different types of databases. Each data provider includes classes to connect to a particular type of database. The .NET Framework includes six data providers, as shown in the following table:

Data provider

Description

System.Data.SqlClient

Contains classes that give optimized access to SQL Server 7 and later

System.Data.OleDb

Contains classes that give access to SQL Server 6.5 and earlier; also provides access to databases such as Oracle, Sybase, Access, and so on

System.Data.ODBC

Contains classes that give access to Open Database Connectivity (ODBC) data sources

System.Data.OracleClient

Contains classes that give access to Oracle databases

System.Data.EntityClient

Contains classes that support the Entity Framework (discussed in Chapter 24)

System.Data.SqlServerCe

Contains classes that work with SQL Server Compact Edition

In addition, data providers are available for a number of other databases through third-party vendors. Supported databases include MySQL, IBM DB2, Informix, Sybase, SQLite, Firebird, and PostgreSQL.

Provider-independent code

In early versions of ADO.NET, developers had to use provider-dependent classes, such as Sql Connection and SqlCommand. The problem with this approach is that it made it hard to write code that was independent of the data source, burdening you with a large editing job if you needed to switch providers.

Version 2.0 introduced a provider-independent interface by using a series of interfaces and classes whose names begin with “Db,” such as DbConnection and DbCommand. All providers implement these interfaces, making it possible for you to use all providers in the same way. One advantage of this approach is that you can specify provider details in an application configuration file, which makes your code truly independent of the data provider.

It is now strongly recommended that you use the provider-independent interface rather than using the provider-specific classes directly.

ADO.NET namespaces

The classes in ADO.NET are divided into a number of namespaces, as shown in the following table:

Namespace

Description

System::Data

This is the core namespace in ADO.NET. Classes in this namespace define the ADO.NET architecture, and it holds provider-independent classes that can used for any type of data source, such as DataSet.

System::Data::Common

Defines common classes and interfaces for data providers.

System::Data::EntityClient

Defines classes for the Entity Framework data provider.

System::Data::Linq

Defines classes that gives access to relational data through Language-Integrated Query (LINQ)

System::Data::SqlClient

Defines classes for the SQL Server data provider.

System::Data::OleDb

Defines classes for the Object Linking and Embedding, Database (OLE DB) data provider.

System::Data::OracleClient

Defines classes for the Oracle data provider.

System::Data::Odbc

Defines classes for working directly with ODBC.

System::Data::Services

Defines classes used to create Windows Communication Foundation (WCF) data services.

System::Data::Spatial

Defines classes that work with spatial data.

System::Data::SqlTypes

Defines classes that represent native SQL Server data types.

ADO.NET assemblies

Many of the ADO.NET classes are in the System::Data assembly, although some of the newer features (such as LINQ and Entity Framework) have their own assemblies. To use these assemblies, you need to include the appropriate using statements in your application, such as in the following example:

#using <System.Data.dll>          // This assembly contains ADO.NET classes
#using <System.Data.Entity.dll>   // This assembly contains the
                                  // Entity Framework provider classes

Note

If you are creating projects by using Microsoft Visual Studio 2012, the reference to System.Data.dll will be provided for you.

After you have imported the assemblies you require, you can add using directives for the namespaces, as shown in the following example:

using System::Data::SqlClient;

Creating a connected application

In the next few pages, you will create a C++/CLI application that connects to an Access database. You will see how to set up the database connection and provider details in the application configuration file and then use a DbConnection object to establish a connection.

After you are connected, you will create a DbCommand object to represent a SQL statement. You will then perform the following tasks:

  • Use the ExecuteScalar method on DbCommand to execute a statement that returns a single value.

  • Use the ExecuteNonQuery method on DbCommand to execute a statement that updates the database.

  • Use the ExecuteReader method to execute a statement that queries the database. Execute Reader returns a DbDataReader object, which provides fast, forward-only access to the rows in the result set. You will use this DbDataReader object to process the result set.

Note

You can find the sample database for this exercise, blog.mdb, in the sample code files for this book. Before starting the exercise, copy this file to a directory on your hard disk.

Connecting to a database

In this exercise, you will create a new application to perform all the operations described in the preceding section. The first step is to connect to the database.

  1. Start Microsoft Visual Studio 2012 and create a new CLR Console Application project named ConnectedApplication.

  2. In the ConnectedApplication.cpp file, after the using namespace System; statement, add the following statements:

    // Generic ADO.NET definitions
    using namespace System::Data;
    // Provider-independent classes
    using namespace System::Data::Common;
  3. Add an application configuration file to the project. In Solution Explorer, right-click the project name to open the Add New Item dialog box (via the shortcut menu). In the pane on the left, click Utility, and then select Configuration file (app.config) as the file type. Press Add, and a file named app.config will be added to the project and opened in the editor.

  4. Edit the app.config file to add a connection string.

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <connectionStrings>
        <clear/>
        <add name="Blog"
          connectionString=
          "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:path	olog.mdb"
          providerName="System.Data.OleDb" />
      </connectionStrings>
    </configuration>

    Remember to edit the path to the location where you stored the blog.mdb database file.

    The connectionStrings section holds connection string information. The clear element clears out the collection in case any have been inherited from computer configuration settings. In this example, we are defining a connection string, identified by the name Blog, which connects to an Access database file by using the System.Data.OleDb provider.

  5. To work with configuration files, you need to add a reference to System.Configuration.dll. Do this by following the instructions given in the sidebar Referencing external assemblies earlier in the chapter.

  6. Add a using namespace directive for System.Configuration.

    using namespace System::Configuration;
  7. At the top of the main function, retrieve the connection string settings from the .config file.

    ConnectionStringSettings ^settings = ConfigurationManager::ConnectionStrings["Blog"];
    if (settings == nullptr)
    {
        Console::WriteLine("Couldn't get settings");
        return -1;
    }
    Console::WriteLine("Have settings");

    The ConfigurationManager class is responsible for interacting with settings stored in configuration files, and as such, it maintains a collection of ConnectionStringSettings objects, which you can access by using an indexer. If the call returns null, there isn’t an entry with that name in the .config file.

  8. After you have the ConnectionStringSettings object, you can use its ProviderName property to get a DbProviderFactory:

    // Get the factory object for this provider type
    DbProviderFactory ^fac = DbProviderFactories::GetFactory(settings->ProviderName);

    The DbProviderFactory is a factory that creates the various other objects that we need—connections, commands, and so on. You use DbProviderFactory the same way, regardless of the actual provider being used underneath.

  9. After you have the factory, use it to create a connection and open it.

    DbConnection ^conn = nullptr;
    try
    {
        // Create a connection and set its connection string
        conn = fac->CreateConnection();
        conn->ConnectionString = settings->ConnectionString;
    
        conn->Open();
        Console::WriteLine("Connection opened");}
    catch (Exception ^ex)
    {
        Console::WriteLine(ex->Message);
    }
    finally
    {
        if (conn != nullptr) conn->Close();
        Console::WriteLine("Connection closed");
    }

    Just about everything you do with databases can generate an exception, so you should always enclose your database code in a try block. Connections need to be opened before they are used, and it is important to close them afterward so that you free up resources. The best way to do this is to use a finally block, which ensures that the connection is closed whether or not an exception occurs.

  10. Build your application and fix any compiler errors.

  11. Run the application.

    If all is well, on the console, you see the message shown in the figure that follows.

    A screenshot of the console window shows that the connection settings were obtained, and that the connection was opened and closed successfully.

Creating and executing a command

In this exercise, you will create a DbCommand object that represents the following SQL statement:

SELECT COUNT(*) FROM Entries

This statement returns an integer indicating how many rows are in the Entries table. You will execute this statement by using the ExecuteScalar method on the command object.

  1. Continue with the project from the previous exercise.

  2. In the main function, add the following code to the try block, after the statement that opens the database connection:

    // Count the entries
    DbCommand ^cmd = fac->CreateCommand();
    cmd->CommandText = "SELECT COUNT(*) FROM Entries";
    cmd->CommandType = CommandType::Text;
    cmd->Connection = conn;

    This code creates and configures a DbCommand object that encapsulates a SQL statement. The CommandText property defines the SQL to be executed, and CommandType says that this is a SQL command, as opposed to a stored procedure. The Connection property specifies which database connection to use when executing the command.

  3. Add the following code to execute the SQL statement and display the results on the console:

    // Print the result
    int numberOfEntries = (int)cmd->ExecuteScalar();
    Console::WriteLine("Number of entries: {0}", numberOfEntries);
  4. Build your application and fix any compiler errors.

  5. Run the application.

    The message shown in the following figure should appear on the console:

    A screenshot of the console window showing that the blog has 9 entries.

Executing a command that modifies data

In this exercise, you will add a new entry into the database by using the following SQL statement:

INSERT INTO [Entries] ([Date], [Text], [Author])
            VALUES ('Dec 02, 2012', 'Some text', 'Julian')

Note

Some fields in the SQL are surrounded with square brackets in case they have the same name as predefined entities or types within Access.

You will use the ExecuteNonQuery method to execute this statement, which will return an integer to indicate how many rows the statement affected. Because you are inserting a single row, you’d expect this value to be 1.

  1. Continue with the project from the previous exercise.

  2. Find the code you wrote in the previous exercise and add the following statement:

    // Update the prices of products
    cmd->CommandText =
        "INSERT INTO [Entries] ([Date], [Text], [Author])"
        " VALUES ('Dec 02, 2012', 'A blog entry', 'Julian')";

    This code reuses the DbCommand object from the previous exercise but specifies a different SQL statement.

    Tip

    It is a little-known feature of C++ that if the preprocessor sees two string literals on adjoining lines, it will combine them. This is a useful way to split up and format long strings.

  3. Add the following code to execute the SQL statement and display the results on the console:

    int rowsAffected = cmd->ExecuteNonQuery();
    Console::WriteLine("Added {0} rows", rowsAffected);
  4. Build your application and fix any compiler errors.

  5. Run the application.

    The message shown in the following figure should appear on the console:

    A screenshot of the console output, showing the message “Added 1 rows”, indicating that the insert was successful.

Executing queries and processing the results

In the final part of this connected application exercise, you will execute a command that retrieves information from the database by using the following SQL statement:

SELECT * FROM Entries

You will use the ExecuteReader method to execute this statement. This will return a DbDataReader object, which is a fast, forward-only reader that reads through each row in the result set in turn.

  1. Continue with the project from the previous exercise.

  2. Find the code you wrote in the previous exercise and add the following statement:

    // Query the database
    cmd->CommandText = "SELECT * FROM Entries";

    This code reuses the DbCommand object from the previous exercise but specifies a different SQL statement.

  3. Add the following code to execute the SQL statement and return a DbDataReader object:

    DbDataReader ^reader = cmd->ExecuteReader();
  4. Add the code that follows to loop through the results one row at a time. For each row, output the values of all four columns. The first, the record ID, is an integer, but the other three (Date, Text and Author) are all strings.

    Console::WriteLine("
    ------------------------------------");
    while (reader->Read())
    {
        Console::WriteLine("{0}: {1} by {2}", reader->GetInt32(0),
                reader->GetString(1), reader->GetString(3));
        Console::WriteLine("  {0}", reader->GetString(2));
    }
    Console::WriteLine("--------------------------------------");

    The Read method steps through the record set one row at a time. Notice the use of the strongly typed methods GetString and GetInt32.

  5. After the loop, close the reader.

    reader->Close();
  6. Run the application.

    The message shown in the following figure should appear on the console: (You might get different values than what’s shown here.)

    A screenshot of the console output, showing a list of the entries in the blog database table.

Creating a disconnected application

For the rest of the chapter, we’ll turn our attention to disconnected applications. A disconnected application is one that does not have a permanently available connection to the data source. Applications are much more scalable when they only need a database connection to retrieve or send data back, and it is possible for an application such as a website to support many users with only a handful of database connections.

In ADO.NET, the DataSet class represents a disconnected, local data store. The following figure shows the DataSet object model:

A diagram depicting how a DataSet is a little like an in-memory database, in that it can contain multiple tables with relations between them.

A DataSet is an in-memory collection of DataTable objects and the relationships between them. You can create many DataTables in a DataSet to hold the results of more than one SQL query.

Each DataTable has a collection of DataRows and DataColumns. Each DataColumn contains metadata about a column, such as its name, data type, default value, and so on. The DataRow objects actually contain the data for the DataSet.

You can create a DataSet from scratch, creating DataTables, setting up a schema using Data Columns, and then adding DataRows. It is, however, much more common to use a DataSet with a database.

The key to doing this is the data adapter, which sits between the database and the DataSet. The adapter knows how to retrieve data from the database and how to insert and update data. Each provider has its own data adapter class, but as you’d expect, you work with the provider-independent DbDataAdapter type.

The following figure shows how data adapters work with DataSets:

A diagram depicting how a DataAdapter sits in between a database and a DataSet, and is responsible for transferring data to and from the database. When a DataSet has been filled, the DataAdapter is not needed until the data needs to be refreshed, or data uploaded from the DataSet to the database.

Each data adapter works with a single DataTable in a DataSet. You call the Fill method on a data adapter to fill the DataSet with data from the database. You call the Update method on a data adapter to save any changes in the DataSet back to the database.

Internally, the data adapter has four command objects, one each for the select, delete, insert, and update operations, each of which encapsulates a SQL command. The following table describes these command objects:

Command object in a data adapter

Description

SelectCommand

Contains a SQL SELECT statement to retrieve data from the database into the DataSet table

InsertCommand

Contains a SQL INSERT statement to insert new rows from the DataSet table into the database

UpdateCommand

Contains a SQL UPDATE statement to modify existing rows in the database

DeleteCommand

Contains a SQL DELETE statement to delete rows from the database

Disconnected operation using a DataSet

This exercise shows you how to create a DataSet, fill it by using a DataAdapter, and extract data from the tables in the DataSet. The details of setting up the configuration and getting a connection are exactly the same as for the previous exercise, so you will be able reuse a lot of the code.

  1. Start a new CLR Console project project named DataSetApp.

  2. Add an external reference to the System::Configuration assembly by using the Properties dialog box, as detailed in the sidebar Referencing external assemblies earlier in the chapter.

  3. Add using statements to the top of the source file for the assemblies that you are going to be using.

    // ADO.NET namespaces
    using namespace System::Data;
    using namespace System::Data::Common;
    // For reading the configuration data
    using namespace System::Configuration;
    // For printing the content of the DataSet
    using namespace System::IO;
  4. Add an application configuration file to the project. In Solution Explorer, right-click the project name. On the shortcut menu that appears, point to Add, and then click New Item. In the New Item dialog box that opens, in the pane on the left, click Visual C++, and then click Utility. In the center pane, click Configuration file (app.config).

  5. Remember to add the post-build step to the project settings so that app.config will be renamed to match the executable name. You can find details on how to do this in the previous exercise.

  6. Copy the content of the app.config file from the previous exercise, Creating a connected application. Here is the content that you need:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <connectionStrings>
        <clear/>
        <add name="Blog"
          connectionString=
          "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:path	olog.mdb"
          providerName="System.Data.OleDb" />
      </connectionStrings>
    </configuration>

    Remember to edit the path to reflect wherever you have stored the blog.mdb file.

  7. Copy the code to read the connection string settings and create the DbProviderFactory. (The code is given here, but it is exactly the same as for the previous exercise.)

    // Get the connection settings
    ConnectionStringSettings ^settings =
        ConfigurationManager::ConnectionStrings["Blog"];
    if (settings == nullptr)
    {
        Console::WriteLine("Couldn't get settings");
        return -1;
    }
    Console::WriteLine("Connection settings OK");
    
    // Get the factory object for this provider type
    DbProviderFactory ^fac = DbProviderFactories::GetFactory(settings->ProviderName);
  8. Add a try block in which you create a connection, a catch block to handle any errors, and a finally block to close the connection. (Again, I have reproduced the code here, but you should be able to copy it from the previous exercise.)

    DbConnection ^conn = nullptr;
    try
    {
        // Create a connection and set its connection string
        conn = fac->CreateConnection();
        conn->ConnectionString = settings->ConnectionString;
    
        conn->Open();
        Console::WriteLine("Connection opened");
    }
    catch (Exception ^ex)
    {
        Console::WriteLine(ex->Message);
    }
    finally
    {
        if (conn != nullptr)
        {
            conn->Close();
            Console::WriteLine("Connection closed");
        }
    }
  9. With that setup complete, you can begin retrieving data. Start by asking the factory to create a DataAdapter.

    // Create a DataAdapter and set its select command
    DbDataAdapter ^adapter = fac->CreateDataAdapter();
  10. A DataAdapter can have four commands associated with it, but because you are only going to be retrieving data, you only need to set the Select command. Do this by creating a DbCommand object, as in the previous exercise, and then assigning it to the SelectCommand property of the adapter.

    DbCommand ^cmd = fac->CreateCommand();
    cmd->CommandText = "SELECT * FROM Entries";
    cmd->CommandType = CommandType::Text;
    cmd->Connection = conn;
    
    adapter->SelectCommand = cmd;
  11. You can now create a DataSet and ask the adapter to fill it.

    DataSet ^dataset = gcnew DataSet("Blog");
    adapter->Fill(dataset, "Entries");

    The first line creates an empty DataSet called “Blog”. Calling Fill on the adapter causes it to execute its SelectCommand, which creates a DataTable called “Entries”, fills it with the result of the query, and then adds it to the DataSet’s collection of DataTables.

    Giving names to DataSets and DataTables is optional, but as you will see shortly, it is very useful when building XML documents from DataSet data.

    Note

    In a larger application, you could now close the connection to the database because you have the data locally in the DataSet.

  12. Now that you have a DataSet, it would be useful to look at what it contains. The WriteXml function writes the content of a DataSet in XML format to any stream. The XmlTextWriter class provides a useful stream for our purposes because it writes the output to a file in properly formatted form.

    XmlTextWriter ^xwriter = gcnew XmlTextWriter("c:\SbS\dataset.xml", nullptr);
    xwriter->Formatting = Formatting::Indented;

    The first two lines create an XmlTextWriter and ensure that it writes out the XML with indentation. Edit the path to put the XML file in a suitable location. Remember that you need to add a using namespace statement for System::Xml, or use the full name System::Xml::XmlTextWriter.

    Note

    The null second argument to the constructor means that the default UTF-8 encoding will be used. If you want to use another encoding, specify it like this:

    XmlTextWriter ^xwriter = gcnew XmlTextWriter("c:\SbS\dataset.xml",
    Encoding::Unicode);

    The Encoding class is in the System::Text namespace, so you will need to add a using declaration if you don’t want to use the fully qualified name.

  13. Use the table’s WriteXml method to write the data out to the file.

    DataTable ^table = dataset->Tables[0];
    
    table->WriteXml(xwriter, XmlWriteMode::IgnoreSchema);
    xwriter->Close();

    The declaration of the DataTable handle makes the following code simpler and shows how the table created by the adapter is the first one in the DataSet’s Tables collection. Because you gave the table a name when the adapter created it, you could also specify the name here rather than the ordinal. The second argument to WriteXml shows that you only want the data and not the schema.

  14. Build and run the application and then open the XML file in Notepad; you should see that the first few lines look like this:

    <Blog>
      <Entries>
        <ID>2</ID>
        <Date>Jul 01, 2009</Date>
        <Text>A first entry</Text>
        <Author>Julian</Author>
      </Entries>
      <Entries>
        <ID>3</ID>
        <Date>Jun 27, 2009</Date>
        <Text>Second entry</Text>
        <Author>Julian</Author>
      </Entries>
      ...

    The root element has the same name as the DataSet, and each row is named after the table. If you hadn’t assigned names to the DataSet and DataTable, the root element would have been called “NewDataSet” and each row would have been called “Table”.

  15. Change the WriteXml statement so that it includes the schema in the generated data.

    table->WriteXml(xwriter, XmlWriteMode::WriteSchema);

    Build and run the application again; you should see that the output file contains an XML schema that describes the data.

    <Blog>
      <xs:schema id="Blog" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema"
                 xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
        <xs:element name="Blog" msdata:IsDataSet="true"
                     msdata:MainDataTable="Entries" msdata:UseCurrentLocale="true">
          <xs:complexType>
            <xs:choice minOccurs="0" maxOccurs="unbounded">
              <xs:element name="Entries">
                <xs:complexType>
                  <xs:sequence>
                    <xs:element name="ID" type="xs:int" minOccurs="0" />
                    <xs:element name="Date" type="xs:string" minOccurs="0" />
                    <xs:element name="Text" type="xs:string" minOccurs="0" />
                    <xs:element name="Author" type="xs:string" minOccurs="0" />
                  </xs:sequence>
                </xs:complexType>
              </xs:element>
            </xs:choice>
          </xs:complexType>
        </xs:element>
      </xs:schema>
      <Entries>
        <ID>2</ID>
        <Date>Jul 01, 2009</Date>
        <Text>A first entry</Text>
        <Author>Julian</Author>
      </Entries>
      ...

Quick reference

To

Do this

Use ADO.NET classes.

If you are using Visual Studio 2012, you will need to add only a using directive for the appropriate assemblies. For example:

using namespace System::Data;
using namespace System::Data::Common;

Store connection strings in the application configuration file.

Add an application configuration file to the project, and add a post-build event to rename it:

copy app.config $(TargetPath).config

Then, add one or more connection string sections to the .config file. For example:

<?xml version="1.0" encoding="utf-8" ?>
  <configuration>
    <connectionStrings>
      <add name="NWind"
           connectionString=
              "Provider=Microsoft.Jet.OLEDB.4.0;"
              "Data Source=C:SbSBlog.mdb"
           providerName="System.Data.OleDb" />
    </connectionStrings>
</configuration>

Connect to a database.

Obtain the provider factory by using the provider name. For example:

DbProvideFactory ^factory =
    DbProviderFactories::GetFactory(name);

Then create a DbConnection object, and configure its ConnectionString property. For example:

DbConnection ^conn = factory->CreateConnection();
conn->ConnectionString = connString;

Create a SQL command.

Create a DbCommand object and configure its CommandText, CommandType, and Connection properties.

Execute a command.

If the command returns a scalar value, call ExecuteScalar. If the command modifies the database, call Execute NonQuery. If the command performs a query, call ExecuteReader. Assign the result to a DbDataReader object and use this reader to loop through the result set. For example:

DbDataReader ^reader = cmd->ExecuteReader();
while (reader->Read())
{
  Console::Write(reader->GetString(0));
}

Use data in a disconnected application.

Create a DbDataAdapter, add DbCommands to access the database, and then set its connection. Create a DataSet and fill the DataSet by using the data adapter. For example:

DbDataAdapter ^daTitles =
    factory->CreateDataAdapter();
daTitles->SelectCommand = cmd;
daTitles->Connection = conn;
DataSet^ ds = gcnew DataSet("Titles");
daTitles->Fill(ds);
..................Content has been hidden....................

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