7. Working with Typed Data Sets
All the data sets we have worked with thus far
have been undefined until runtime. That is, not only are their instances
not created until runtime, which is true of any class, but their very
structure—what tables constitute the data set, what columns constitute
the tables—is not specified until runtime. Yet in several cases that
structure was known at design time. The multi SELECT example
that we just saw is such a case. The five tables and their selected
columns could be determined by examining the underlying database. In
fact, if the stored procedure’s SELECT statements had been specific in their column lists, the resulting data set structure would be even more apparent.
Typed
data sets allow us to provide this structure specification at design
time; enabling us to define a class that inherits from DataSet
and that specifies the structure of our specific data set. Defining the
class is quite easy to do. Instead of our writing the entire class,
Visual Studio lets us connect to a source database; specify the desired
tables, columns, and relationships; and have the class code generated
for us. We get the benefits of a strongly typed data set, such as
IntelliSense help and structure-specific routines. For instance,
OurDataSet.Tables["Products"]
becomes
And in coding the second line, the moment we enter “OurDataSet.”, the Products table, being a property of the typed data set, shows in the IntelliSense drop-down list.
Typed data sets, like all data sets, call hold
data that is retrieved from a data source or data that is created
programmatically; but they are most commonly used for holding data that
has been retrieved from a database, especially since the underlying
database’s structure propagates into the application’s data structure.
This common association between databases and typed data sets is the
reason why we waited until we covered the System.Data data provider classes before beginning our discussion of typed data sets.
To illustrate the benefits of typed data sets
we’ll write a simple program, one that retrieves sales data from five
different tables, all located in Microsoft’s Northwind sample database.
We start by creating a new C# Smart Device
application within Visual Studio. We then go to the Product menu and
select Add New Item, giving us the Add New Item dialog shown in Figure 8. We specify the name for our typed data set class, SalesData, and choose Add.
Note that if we had added the data set by using
the Data tab of the Toolbox, we would have created an untyped data set,
which is not what we want. We must use the dialog shown in Figure 8 to have Visual Studio generate a typed data set class for us.
Visual Studio will add an .xsd file and some .cs
files to our project. When we view the .xsd file in the Designer, we are
given some choices. We choose to right-click and then pick Add | Table
Adapter, and are presented with the first of a series of dialogs that
will lead us through the definition of one data table and its associated
data adapter, connection, and command objects. Since our data set will
contain five data tables, we will pass through this series of dialogs
five times.
The first of these dialogs is shown in Figure 9
and lets us specify the underlying database. The next few let us write
the command that will retrieve the database data into the data table. In
our case, with the Designer’s help, we write the command shown in Figure 6.24. We then accept all defaults for the remaining dialogs and arrive at the situation shown in Figure 11: a data set that contains one table, a five-column Employee
table. Note that Visual Studio has detected the presence of a primary
key constraint in the database table and has included that constraint in
the definition of the data table.
We’ll define four more data tables but show the
dialogs for only one of them, as the one aspect left to be illustrated
is relationships. When we complete the definition of the first of our
four remaining data tables, the Orders data table, Visual Studio detects a foreign key relationship between Employees and Orders and adds it to the typed data set, as shown in Figure 12.
We add three more table adapters, generating
three more data tables and their respective data adapters, and arrive at
the typed data set shown in Figure 13.
The code for our typed data set class, written by
Visual Studio, is in the SalesData.Designer.cs file, which appears in
the Solution Explorer when you expand the SalesData.xsd file icon. It
contains the code that exposes the data tables as properties of the data
set; routines for navigating relationships such as ourEmployeeRow.GetOrdersRows(); routines for finding a row given its primary key value, such as ourEmployeeTable.FindByEmployeeId(); and others.
What neither the class itself, nor any other
class in the project, contains is code to create an instance of our data
set and code to fill that instance with data from the database. That
code we must write ourselves. Fortunately, there is sufficient code
already in the typed data set class to make our share relatively small
and easy to write.
For the moment, we will put all the necessary code required to create and load the data set in the LoadDataset menu item’s Click event handler, as shown in Listing 9.
Listing 9. Code to Create and Load a Typed Data Set
SalesData dsetSales = new SalesData();
EmployeesTableAdapter daptEmployee = new EmployeesTableAdapter();
daptEmployee.Fill(dsetSales.Employees);
CustomersTableAdapter daptCustomer = new CustomersTableAdapter();
daptCustomer.Fill(dsetSales.Customers);
ProductsTableAdapter daptProduct = new ProductsTableAdapter();
daptProduct.Fill(dsetSales.Products);
OrdersTableAdapter daptOrder = new OrdersTableAdapter();
daptOrder.Fill(dsetSales.Orders);
Order_DetailsTableAdapter daptDetail =
New Order_DetailsTableAdapter();
daptDetail.Fill(dsetSales.Order_Details);
dgrdOrders.DataSource =
dsetSales.Employees
.FindByEmployeeID(1).GetOrdersRows();
|
This
code creates a data set containing five empty tables, creates five
table adapters and uses them to fill the five tables, and then selects
all orders for the employee whose Employee ID is 1 and binds those
orders to a data grid, resulting in the display shown in Figure 14.
When Visual Studio wrote the code for the data
set class, it defined a class for each table adapter, which it placed
inside a namespace named SalesDataTableAdapters; that is, the name of the data set suffixed by TableAdapters. That namespace was placed inside the project’s default namespace, in our case, TypedDataset. Thus, the code shown in Listing 6.19 requires the following line at the start of the code file:
using TypedDataset.SalesDataTableAdapters;
Now let us consider a small enhancement that will
make our data set class more functional and more flexible. The added
functionality will be a Load method in the data set class that
will pull the data in from the database, allowing us to remove all
knowledge of, and dependence upon, the table adapters from the form
class’s code. The added flexibility will be the option to specify the
source database at runtime rather than being bound to the database that
was specified at design time. This database designation will be made when the Load method is executed; that is, the connection string value will be a parameter of the Load method.
Thus, we move the code for loading the data set from the form’s menu event handler into the data set’s newly defined Load method. The best location for the Load
method is in SalesData.cs, not SalesData.Designer.cs. When the Designer
wrote the data set class, it spread the code over the two files
mentioned in the preceding sentence. SalesData.cs contains a skeleton
partial class and is the place for our code. The other file is where the
Designer writes its share of the class; we should not modify that file.
The contents of SalesData.cs are shown in Listing 10, and the revised menu handler is shown in Listing 11.
Listing 10. Partial Class Code for the Typed Data Set
using System;
using System.Data;
using System.Data.SqlClient;
using TypedDataset.SalesDataTableAdapters;
namespace TypedDataset
{
public partial class SalesData
{
public void Load(string connectionString)
{
EmployeesTableAdapter daptEmployee =
new EmployeesTableAdapter();
CustomersTableAdapter daptCustomer =
new CustomersTableAdapter();
ProductsTableAdapter daptProduct =
new ProductsTableAdapter();
OrdersTableAdapter daptOrder =
new OrdersTableAdapter();
Order_DetailsTableAdapter daptDetail =
new Order_DetailsTableAdapter();
daptEmployee.Connection.ConnectionString =
daptCustomer.Connection.ConnectionString =
daptProduct.Connection.ConnectionString =
daptOrder.Connection.ConnectionString =
daptDetail.Connection.ConnectionString =
connectionString;
daptEmployee.Fill(this.Employees);
daptCustomer.Fill(this.Customers);
daptProduct.Fill(this.Products);
daptOrder.Fill(this.Orders);
daptDetail.Fill(this.Order_Details);
}
}
}
|
Listing 11. Revised Menu Selection Handler
private void mitemLoadDataset_Click(object sender,
EventArgs e)
{
string connectionString =
string.Format("{0};{1};{2};",
"Data Source=RIMROCK",
"Initial Catalog=Northwind",
"Integrated Security=SSPI");
SalesData dsetSales = new SalesData();
dsetSales.Load(connectionString);
dgrdOrders.DataSource =
dsetSales.Employees
.FindByEmployeeID(1).GetOrdersRows();
}
|
So,
we now have a data set containing five data tables and five data
adapters that support those tables. Thus, we could update the data in
the data tables and propagate the changes back to the data source by
using the same methods that were available with the untyped data source.
All of this raises the question of combining the
last two subjects that we just discussed. Can we have a typed data set
that uses a multi SELECT stored procedure to retrieve multiple
tables of information for display only? The answer is yes, but we must
do a few steps slightly differently.
Since
we are dealing with multiple result sets of read-only data from a
single command, a data adapter is of no benefit. In design mode, when we
right-click on the typed data set’s design surface, we should choose
new table rather than new table adapter.
The bad news is, the Designer will give us less help designing a table
than designing a table adapter; the good news is, we will need to write
less code.
We can write less code because the Designer always writes a Load method for typed data sets; not one that we could use before but one that we can use now. This Load method takes two parameters: one is the data reader that will pull the result sets of the multi SELECT
stored procedure, and another is an array referencing the data tables
to be filled.
Note
Programming with ADO.NET for Microsoft
SQL Server is similar to programming for Microsoft SQL Server CE. The
namespaces are different and the class names are different, but their
PMEs are mostly the same. The major differences are caused by the
different capabilities of the two engines, not by the classes
themselves. Connectivity, security, stored procedures, advanced data
integrity, concurrency, and differences in schema definition combine to
make programming for SQL Server different from programming for SQL
Server CE.