Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

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

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

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

CHAPTER 23 ADO.NET PART II: THE DISCONNECTED LAYER

785

Relations property. At this point, you can navigate between the connected tables as you search for data. You will see how this is done a bit later in the chapter.

The ExtendedProperties property provides access to the PropertyCollection object, which allows you to associate any extra information to the DataSet as name/value pairs. This information can literally be anything at all, even if it has no bearing on the data itself. For example, you can associate your company’s name to a DataSet, which can then function as in-memory metadata. Other examples of extended properties might include time stamps, an encrypted password that must be supplied to access the contents of the DataSet, a number representing a data refresh rate, and so forth.

Note The DataTable class also supports extended properties via the ExtendedProperties property.

Key Properties of the DataSet

Before exploring too many other programmatic details, let’s take a look at some core members of the DataSet. Beyond the Tables, Relations, and ExtendedProperties properties, Table 23-1 describes some additional properties of interest.

Table 23-1. Properties of the Mighty DataSet

Property

Meaning in Life

CaseSensitive

Indicates whether string comparisons in DataTable objects are case

 

sensitive (or not).

DataSetName

Represents the friendly name of this DataSet. Typically this value is

 

established as a constructor parameter.

EnforceConstraints

Gets or sets a value indicating whether constraint rules are followed when

 

attempting any update operation.

HasErrors

Gets a value indicating whether there are errors in any of the rows in any of

 

the DataTables of the DataSet.

RemotingFormat

Allows you to define how the DataSet should serialize its content (binary

 

or XML).

 

 

Key Methods of the DataSet

The methods of the DataSet work in conjunction with some of the functionality provided by the aforementioned properties. In addition to interacting with XML streams, the DataSet provides methods that allow you to copy the contents of your DataSet, navigate between the internal tables, and establish the beginning and ending points of a batch of updates. Table 23-2 describes some core methods.

Table 23-2. Methods of the Mighty DataSet

Methods

Meaning in Life

AcceptChanges()

Commits all the changes made to this DataSet since it was loaded or the

 

last time AcceptChanges() was called.

Clear()

Completely clears the DataSet data by removing every row in each

 

DataTable.

Continued

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;

CHAPTER 23 ADO.NET PART II: THE DISCONNECTED LAYER

789

carIDColumn.AutoIncrementSeed = 0; carIDColumn.AutoIncrementStep = 1;

...

}

Here, the carIDColumn object has been configured to ensure that as rows are added to the respective table, the value for this column is incremented by 1. Because the seed has been set at 0, this column would be numbered 0, 1, 2, 3, and so forth.

Adding DataColumn Objects to a DataTable

The DataColumn type does not typically exist as a stand-alone entity, but is instead inserted into a related DataTable. To illustrate, create a new DataTable type (fully detailed in just a moment) and insert each DataColumn object in the columns collection using the Columns property:

static void Main(string[] args)

{

...

// Now add DataColumns to a DataTable.

DataTable inventoryTable = new DataTable("Inventory"); inventoryTable.Columns.AddRange(new DataColumn[]

{ carIDColumn, carMakeColumn, carColorColumn, carPetNameColumn }); Console.ReadLine();

}

At this point, the DataTable object contains four DataColumn objects that represent the schema of the in-memory Inventory table. However, the table is currently devoid of data, and the table is currently outside of the table collection maintained by the DataSet. We will deal with both of these shortcomings, beginning with populating the table with data via DataRow objects.

Working with DataRows

As you have seen, a collection of DataColumn objects represents the schema of a DataTable. In contrast, a collection of DataRow types represents the actual data in the table. Thus, if you have 20 listings in the Inventory table of the AutoLot database, you can represent these records using 20 DataRow types. Using the members of the DataRow class, you are able to insert, remove, evaluate, and manipulate the values in the table. Table 23-4 documents some (but not all) of the members of the

DataRow type.

Table 23-4. Key Members of the DataRow Type

Members

Meaning in Life

HasErrors

The HasErrors property returns a Boolean value indicating if there are

GetColumnsInError()

errors. If so, the GetColumnsInError() method can be used to obtain the

GetColumnError()

offending members, and GetColumnError() can be used to obtain the

ClearErrors()

error description, while the ClearErrors() method removes each error

RowError

listing for the row. The RowError property allows you to configure a textual

 

description of the error for a given row.

ItemArray

This property gets or sets all of the values for this row using an array of

 

objects.

RowState

This property is used to pinpoint the current “state” of the DataRow using

 

values of the RowState enumeration.

Continued

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);