
Pro CSharp 2008 And The .NET 3.5 Platform [eng]
.pdf

C H A P T E R 2 3
ADO.NET Part II: The Disconnected
Layer
This chapter picks up where the previous one left off and digs deeper into the .NET database APIs. Here, you will be introduced to the disconnected layer of ADO.NET. When you use this facet of ADO.NET, you are able to model database data in memory within the calling tier using numerous members of the System.Data namespace (most notably, DataSet, DataTable, DataRow, DataColumn,
DataView, and DataRelation). By doing so, you are able to provide the illusion that the calling tier is continuously connected to an external data source, while in reality it is simply operating on a local copy of relational data.
While it is completely possible to use this “disconnected” aspect of ADO.NET without ever making a literal connection to a relational database, you will most often obtain populated DataSet objects using the data adapter object of your data provider. As you will see, data adapter objects function as a bridge between the client tier and a relational database. Using these objects, you are able to obtain DataSet objects, manipulate their contents, and send modified rows back for processing. The end result is a highly scalable data-centric .NET application.
To showcase the usefulness of the disconnected layer, you will be updating the AutoLotDAL.dll data library created in Chapter 22 with new namespaces that make use of disconnected types. As well, you will come to understand the role of data binding and various UI elements within the Windows Forms API that allow you to display and update client-side local data. We wrap things up by examining the role of strongly typed DataSet objects and see how they can be used to expose data using a more object-oriented model.
Understanding the Disconnected Layer of ADO.NET
As you saw in the previous chapter, working with the connected layer allows you to interact with a database using the primary connection, command, and data reader objects. With this handful of types, you are able to select, insert, update, and delete records to your heart’s content (as well as invoke stored procedures). However, you have seen only half of the ADO.NET story. Recall that the ADO.NET object model can be used in a disconnected manner.
Using the disconnected types, it is possible to model relational data via an in-memory object model. Far beyond simply modeling a tabular block of rows and columns, the types within System. Data allow you to represent table relationships, column constraints, primary keys, views, and other database primitives. Furthermore, once you have modeled the data, you are able to apply filters, submit in-memory queries, and persist (or load) your data in XML and binary formats. You can do all of this without ever making a literal connection to a DBMS (hence the term disconnected layer).
While you could indeed use the disconnected types without ever connecting to a database, you will most often still make use of connection and command objects. In addition, you will leverage a
specific object, the data adapter (which extends the abstract DbDataAdapter), to fetch and update
783

784CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
data. Unlike the connected layer, data obtained via a data adapter is not processed using data reader objects. Rather, data adapter objects make use of DataSet objects to move data between the caller and data source. The DataSet type is a container for any number of DataTable objects, each of which contains a collection of DataRow and DataColumn objects.
The data adapter object of your data provider handles the database connection automatically. In an attempt to increase scalability, data adapters keep the connection open for the shortest amount of time possible. Once the caller receives the DataSet object, the calling tier is completely disconnected from the database and left with a local copy of the remote data. The caller is free to insert, delete, or update rows from a given DataTable, but the physical database is not updated until the caller explicitly passes the DataSet to the data adapter for updating. In a nutshell, DataSets allow the clients to pretend they are indeed always connected, when in fact they are operating on an inmemory database (see Figure 23-1).
Figure 23-1. Data adapter objects move DataSets to and from the client tier.
Given that the centerpiece of the disconnected layer is the DataSet type, the first task of this chapter is to learn how to manipulate a DataSet manually. Once you understand how to do so, you will have no problem manipulating the contents of a DataSet retrieved from a data adapter object.
Understanding the Role of the DataSet
As said, a DataSet is an in-memory representation of relational data. More specifically, a DataSet is a class type that maintains three internal strongly typed collections (see Figure 23-2).
Figure 23-2. The anatomy of a DataSet
The Tables property of the DataSet allows you to access the DataTableCollection that contains the individual DataTables. Another important collection used by the DataSet is the DataRelationCollection. Given that a DataSet is a disconnected version of a database schema, it can be used to programmatically represent the parent/child relationships between its tables. For example, a relation can be created between two tables to model a foreign key constraint using the DataRelation type. This object can then be added to the DataRelationCollection through the


786 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Table 23-2. Continued
Methods |
Meaning in Life |
Clone() |
Clones the structure of the DataSet, including all DataTables, as well as all |
|
relations and any constraints. |
Copy() |
Copies both the structure and data for this DataSet. |
GetChanges() |
Returns a copy of the DataSet containing all changes made to it since it |
|
was last loaded or since AcceptChanges() was called. |
GetChildRelations() |
Returns the collection of child relations that belong to a specified table. |
GetParentRelations() |
Gets the collection of parent relations that belong to a specified table. |
HasChanges() |
Gets a value indicating whether the DataSet has changes, including new, |
|
deleted, or modified rows. |
Merge() |
Merges this DataSet with a specified DataSet. |
ReadXml() |
Allow you to read XML data from a valid stream (file based, memory |
ReadXmlSchema() |
based, or network based) into the DataSet. |
RejectChanges() |
Rolls back all the changes made to this DataSet since it was created or the |
|
last time AcceptChanges() was called. |
WriteXml() |
Allow you to write out the contents of a DataSet into a valid stream. |
WriteXmlSchema() |
|
|
|
Building a DataSet
Now that you have a better understanding of the role of the DataSet (and some idea of what you can do with one), create a new Console Application named SimpleDataSet. Within the Main() method, define a new DataSet object that contains three extended properties representing your company name, a unique identifier (represented as a System.Guid type), and a time stamp (don’t forget to import the System.Data namespace):
static void Main(string[] args)
{
Console.WriteLine("***** Fun with DataSets *****\n");
// Create the DataSet object and add a few properties.
DataSet carsInventoryDS = new DataSet("Car Inventory");
carsInventoryDS.ExtendedProperties["TimeStamp"] = DateTime.Now; carsInventoryDS.ExtendedProperties["DataSetID"] = Guid.NewGuid(); carsInventoryDS.ExtendedProperties["Company"] = "Intertech Training"; Console.ReadLine();
}
If you are unfamiliar with the concept of a globally unique identifier (GUID), simply understand that it is a statically unique 128-bit number. While GUIDs are used throughout the COM framework to identify numerous COM-atoms (classes, interfaces, applications, etc.), the System. Guid type is still very helpful under .NET when you need to quickly generate a unique identifier.
In any case, a DataSet object is not terribly interesting until you insert any number of DataTables. Therefore, the next task is to examine the internal composition of the DataTable, beginning with the DataColumn type.

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
787 |
Working with DataColumns
The DataColumn type represents a single column within a DataTable. Collectively speaking, the set of all DataColumn types bound to a given DataTable represents the foundation of a table’s schema information. For example, if you were to model the Inventory table of the AutoLot database (see Chapter 22), you would create four DataColumns, one for each column (CarID, Make, Color, and PetName). Once you have created your DataColumn objects, they are typically added into the columns collection of the DataTable type (via the Columns property).
Based on your background, you may know that a given column in a database table can be assigned a set of constraints (e.g., configured as a primary key, assigned a default value, configured to contain read-only information, etc.). Also, every column in a table must map to an underlying data type. For example, the Inventory table’s schema requires that the CarID column map to an integer, while Make, Color, and PetName map to an array of characters. The DataColumn class has numerous properties that allow you to configure these very things. Table 23-3 provides a rundown of some core properties.
Table 23-3. Properties of the DataColumn
Properties |
Meaning in Life |
AllowDBNull |
This property is used to indicate if a row can specify null values in this |
|
column. The default value is true. |
AutoIncrement |
These properties are used to configure the autoincrement behavior for a |
AutoIncrementSeed |
given column. This can be helpful when you wish to ensure unique values |
AutoIncrementStep |
in a given DataColumn (such as a primary key). By default, a DataColumn does |
|
not support autoincrement behavior. |
Caption |
This property gets or sets the caption to be displayed for this column. This |
|
allows you to define a user-friendly version of a literal database column |
|
name. |
ColumnMapping |
This property determines how a DataColumn is represented when a DataSet |
|
is saved as an XML document using the DataSet.WriteXml() method. |
ColumnName |
This property gets or sets the name of the column in the Columns collection |
|
(meaning how it is represented internally by the DataTable). If you do not |
|
set the ColumnName explicitly, the default values are Column with (n+1) |
|
numerical suffixes (i.e., Column1, Column2, Column3, etc.). |
DataType |
This property defines the data type (Boolean, string, float, etc.) stored in the |
|
column. |
DefaultValue |
This property gets or sets the default value assigned to this column when |
|
inserting new rows. This is used if not otherwise specified. |
Expression |
This property gets or sets the expression used to filter rows, calculate a |
|
column’s value, or create an aggregate column. |
Ordinal |
This property gets the numerical position of the column in the Columns |
|
collection maintained by the DataTable. |
ReadOnly |
This property determines if this column can be modified once a row has |
|
been added to the table. The default is false. |
Table |
This property gets the DataTable that contains this DataColumn. |
Unique |
This property gets or sets a value indicating whether the values in each row |
|
of the column must be unique or if repeating values are permissible. If a |
|
column is assigned a primary key constraint, the Unique property should be |
|
set to true. |
|
|

788 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Building a DataColumn
To continue with the SimpleDataSet project (and illustrate the use of the DataColumn), assume you wish to model the columns of the Inventory table. Given that the CarID column will be the table’s primary key, you will configure the DataColumn object as read-only, unique, and non-null (using the ReadOnly, Unique, and AllowDBNull properties). Update the Main() method to build four DataColumn objects:
static void Main(string[] args)
{
...
//Create data columns that map to the
//'real' columns in the Inventory table
//of the AutoLot database.
DataColumn carIDColumn = new DataColumn("CarID", typeof(int)); carIDColumn.Caption = "Car ID";
carIDColumn.ReadOnly = true; carIDColumn.AllowDBNull = false; carIDColumn.Unique = true;
DataColumn carMakeColumn = new DataColumn("Make", typeof(string)); DataColumn carColorColumn = new DataColumn("Color", typeof(string)); DataColumn carPetNameColumn = new DataColumn("PetName", typeof(string)); carPetNameColumn.Caption = "Pet Name";
Console.ReadLine();
}
Notice that when configuring the carIDColumn object, you have assigned a value to the Caption property. This property is very helpful in that it allows you to define a string value for display purposes, which can be distinct from the literal column name (column names in a literal database table are typically better suited for programming purposes [e.g., au_fname] than display purposes [e.g., Author First Name]).
Enabling Autoincrementing Fields
One aspect of the DataColumn you may choose to configure is its ability to autoincrement. Simply put, an autoincrementing column is used to ensure that when a new row is added to a given table, the value of this column is assigned automatically, based on the current step of the increase. This can be helpful when you wish to ensure that a column has no repeating values (such as a primary key).
This behavior is controlled using the AutoIncrement, AutoIncrementSeed, and AutoIncrementStep properties. The seed value is used to mark the starting value of the column, whereas the step value identifies the number to add to the seed when incrementing. Consider the following update to the construction of the carIDColumn DataColumn:
static void Main(string[] args)
{
...
DataColumn carIDColumn = new DataColumn("CarID", typeof(int)); carIDColumn.ReadOnly = true;
carIDColumn.Caption = "Car ID"; carIDColumn.AllowDBNull = false; carIDColumn.Unique = true; carIDColumn.AutoIncrement = true;


790 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Table 23-4. Continued
Members |
Meaning in Life |
Table |
This property is used to obtain a reference to the DataTable containing this |
|
DataRow. |
AcceptChanges() |
These methods commit or reject all changes made to this row since the |
RejectChanges() |
last time AcceptChanges() was called. |
BeginEdit() |
These methods begin, end, or cancel an edit operation on a DataRow |
EndEdit() |
object. |
CancelEdit() |
|
Delete() |
This method marks this row to be removed when the AcceptChanges() |
|
method is called. |
IsNull() |
This method gets a value indicating whether the specified column |
|
contains a null value. |
|
|
Working with a DataRow is a bit different from working with a DataColumn, because you cannot create a direct instance of this type, as there is no public constructor:
// Error! No public constructor!
DataRow r = new DataRow();
Rather, you obtain a DataRow reference from a given DataTable. For example, assume you wish to insert two rows in the Inventory table. The DataTable.NewRow() method allows you to obtain the next slot in the table, at which point you can fill each column with new data via the type indexer.
When doing so, you can specify either the string name assigned to the DataColumn or its ordinal position:
static void Main(string[] args)
{
...
// Now add some rows to the Inventory Table.
DataRow carRow = inventoryTable.NewRow(); carRow["Make"] = "BMW";
carRow["Color"] = "Black"; carRow["PetName"] = "Hamlet"; inventoryTable.Rows.Add(carRow);
carRow = inventoryTable.NewRow();
//Column 0 is the autoincremented ID field,
//so start at 1.
carRow[1] = "Saab"; carRow[2] = "Red"; carRow[3] = "Sea Breeze";
inventoryTable.Rows.Add(carRow);
Console.ReadLine();
}
■Note If you pass the DataRow’s indexer method an invalid column name or ordinal position, you will receive a runtime exception.

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
791 |
At this point, you have a single DataTable containing two rows. Of course, you can repeat this general process to create a number of DataTables to define the schema and data content. Before you insert the inventoryTable object into your DataSet object, let’s check out the all-important RowState property.
Understanding the RowState Property
The RowState property is useful when you need to programmatically identify the set of all rows in a table that have changed from their original value, have been newly inserted, and so forth. This property may be assigned any value from the DataRowState enumeration, as shown in Table 23-5.
Table 23-5. Values of the DataRowState Enumeration
Value |
Meaning in Life |
Added |
The row has been added to a DataRowCollection, and AcceptChanges() has not been |
|
called. |
Deleted |
The row has been marked for deletion via the Delete() method of the DataRow. |
Detached |
The row has been created but is not part of any DataRowCollection. A DataRow is in |
|
this state immediately after it has been created and before it is added to a collection, |
|
or if it has been removed from a collection. |
Modified |
The row has been modified, and AcceptChanges() has not been called. |
Unchanged |
The row has not changed since AcceptChanges() was last called. |
|
|
While you are programmatically manipulating the rows of a given DataTable, the RowState property is set automatically. By way of example, add a new method to your Program class, which operates on a local DataRow object, printing out its row state along the way:
private static void ManipulateDataRowState()
{
//Create a temp DataTable for testing.
DataTable temp = new DataTable("Temp"); temp.Columns.Add(new DataColumn("TempColumn", typeof(int)));
//RowState = Detached.
DataRow row = temp.NewRow();
Console.WriteLine("After calling NewRow(): {0}", row.RowState);
//RowState = Added. temp.Rows.Add(row);
Console.WriteLine("After calling Rows.Add(): {0}", row.RowState);
//RowState = Added.
row["TempColumn"] = 10;
Console.WriteLine("After first assignment: {0}", row.RowState);
//RowState = Unchanged. temp.AcceptChanges();
Console.WriteLine("After calling AcceptChanges: {0}", row.RowState);
//RowState = Modified.
row["TempColumn"] = 11;
Console.WriteLine("After first assignment: {0}", row.RowState);