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

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

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

794 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

Understanding the Role of the DataSet

Simply put, a DataSet is an in-memory representation of external data. More specifically, a DataSet is a class type that maintains three internal strongly typed collections (see Figure 22-11).

Figure 22-11. 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 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 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 timestamps, 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.

Members of the DataSet

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

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

795

Table 22-9. 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

This new .NET 2.0 property allows you to define how the DataSet

 

should serialize its content (binary or XML) for the .NET remoting layer.

 

 

The methods of the DataSet mimic 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/clone the contents of your DataSet, as well as establish the beginning and ending points of a batch of updates. Table 22-10 describes some core methods.

Table 22-10. 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.

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

Overloaded. Gets a value indicating whether the DataSet has changes,

 

including new, deleted, or modified rows.

Merge()

Overloaded. 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 DataSet.AcceptChanges was called.

WriteXml()

Allow you to write out the contents of a DataSet into a valid stream.

WriteXmlSchema()

 

 

 

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 two extended properties representing your company name and timestamp (don’t forget to “use” System.Data):

796CHAPTER 22 DATABASE ACCESS WITH ADO.NET

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with DataSets *****\n");

// Create the DataSet object.

DataSet carsInventoryDS = new DataSet("Car Inventory"); carsInventoryDS.ExtendedProperties["TimeStamp"] = DateTime.Now; carsInventoryDS.ExtendedProperties["Company"] = "Intertech Training";

}

}

A DataSet without DataTables is a bit like a workweek without a weekend. Therefore, the next task is to examine the internal composition of the DataTable, beginning with the DataColumn type.

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 Cars database, 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).

If you have a background in relational database theory, you know that a given column in a data 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 22-11 provides a rundown of some core properties.

Table 22-11. 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

AutoIncrementSeed

a given column. This can be helpful when you wish to ensure unique

AutoIncrementStep

values 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 (e.g.,

 

what the end user sees in a DataGridView).

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.

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

797

Properties

Meaning in Life

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.

 

 

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 Cars 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";

}

Enabling Autoincrementing Fields

One aspect of the DataColumn you may choose to configure is its ability to autoincrement. Simply put, autoincrementing columns are 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 incrementation. 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));

798 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

carIDColumn.ReadOnly = true; carIDColumn.Caption = "Car ID"; carIDColumn.AllowDBNull = false; carIDColumn.Unique = true; carIDColumn.AutoIncrement = true; 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 a DataColumn 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 });

}

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 Cars 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 22-12 documents some (but not all) of the members of the DataRow type.

Table 22-12. Key Members of the DataRow Type

Members

Meaning in Life

HasErrors

The HasErrors property returns a Boolean value indicating if there are

GetColumnsInError()

errors.

GetColumnError()

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

ClearErrors()

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

RowError

error description, while the ClearErrors() method removes each

 

error 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.

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.

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

799

Members

Meaning in Life

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; rather, you obtain a 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, as shown here:

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(); carRow["Make"] = "Saab"; carRow["Color"] = "Red"; carRow["PetName"] = "Sea Breeze"; inventoryTable.Rows.Add(carRow);

}

Notice how the DataRow class defines an indexer that can be used to gain access to a given DataColumn by numerical position as well as column name. At this point, you have a single DataTable containing two rows.

Understanding the DataRow.RowState Property

The RowState property is useful when you need to programmatically identify the set of all rows in a table that have changed, have been newly inserted, and so forth. This property may be assigned any value from the DataRowState enumeration, as shown in Table 22-13.

Table 22-13. 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 deleted 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.

 

 

800 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

While you are programmatically manipulating the rows of a given DataTable, the RowState property is set automatically:

static void Main(string[] args)

{

...

DataRow carRow = inventoryTable.NewRow();

//Prints out: Row State is: Detatched.

Console.WriteLine("Row State is: {0}.", carRow.RowState); carRow["Make"] = "BMW";

carRow["Color"] = "Black"; carRow["PetName"] = "Hamlet"; inventoryTable.Rows.Add(carRow);

//Prints out: Row State is: Added.

Console.WriteLine("Row State is: {0}.", inventoryTable.Rows[0].RowState);

...

}

As you can see, the ADO.NET DataRow is smart enough to remember its current state of affairs. Given this, the owning DataTable is able to identify which rows have been modified. This is a key feature of the DataSet, as when it comes time to send updated information to the data store, only the modified data is submitted.

Working with DataTables

The DataTable defines a good number of members, many of which are identical in name and functionality to those of the DataSet. Table 22-14 describes some core properties of the DataTable type beyond Rows and Columns.

Table 22-14. Key Members of the DataTable Type

Property

Meaning in Life

CaseSensitive

Indicates whether string comparisons within the table are case sensitive (or

 

not). The default value is false.

ChildRelations

Returns the collection of child relations for this DataTable (if any).

Constraints

Gets the collection of constraints maintained by the table.

DataSet

Gets the DataSet that contains this table (if any).

DefaultView

Gets a customized view of the table that may include a filtered view or

 

a cursor position.

MinimumCapacity

Gets or sets the initial number of rows in this table (the default is 25).

ParentRelations

Gets the collection of parent relations for this DataTable.

PrimaryKey

Gets or sets an array of columns that function as primary keys for the data

 

table.

RemotingFormat

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

 

XML) for the .NET remoting layer. This property is new in .NET 2.0.

TableName

Gets or sets the name of the table. This same property may also be specified

 

as a constructor parameter.

 

 

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

801

For the current example, let’s set the PrimaryKey property of the DataTable to the carIDColumn DataColumn object:

static void Main(string[] args)

{

...

// Mark the primary key of this table.

inventoryTable.PrimaryKey = new DataColumn[] { inventoryTable.Columns[0] };

}

Once you do this, the DataTable example is complete. The final step is to insert your DataTable into the carsInventoryDS DataSet object. Then you’ll pass your DataSet to a (yet to be written) helper method named PrintDataSet():

static void Main(string[] args)

{

...

//Finally, add our table to the DataSet. carsInventoryDS.Tables.Add(inventoryTable);

//Now print the DataSet.

PrintDataSet(carsInventoryDS);

}

The PrintDataSet() method simply iterates over each DataTable in the DataSet, printing out the column names and row values using the type indexers:

static void PrintDataSet(DataSet ds)

{

Console.WriteLine("Tables in '{0}' DataSet.\n", ds.DataSetName); foreach (DataTable dt in ds.Tables)

{

Console.WriteLine("{0} Table.\n", dt.TableName);

// Print out the column names.

for (int curCol = 0; curCol < dt.Columns.Count; curCol++)

{

Console.Write(dt.Columns[curCol].ColumnName.Trim() + "\t");

}

 

Console.WriteLine("\n----------------------------------

");

// Print the DataTable.

for (int curRow = 0; curRow < dt.Rows.Count; curRow++)

{

for (int curCol = 0; curCol < dt.Columns.Count; curCol++)

{

Console.Write(dt.Rows[curRow][curCol].ToString() + "\t");

}

Console.WriteLine();

}

}

}

Figure 22-12 shows the program’s output.

802 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

Figure 22-12. Contents of the example’s DataSet object

Working with .NET 2.0 DataTableReaders

DataTables provide a number of methods beyond what we’ve examined thus far. For example, like

DataSets, DataTables support AcceptChanges(), GetChanges(), Copy(), and ReadXml()/WriteXml() methods. As of .NET 2.0, DataTables also now support a method named CreateDataReader(). This method allows you to obtain the data within a DataTable using a data reader–like navigation scheme (forward-only, read-only). To illustrate, create a new helper function named PrintTable(), implemented as so:

private static void PrintTable(DataTable dt)

{

Console.WriteLine("\n***** Rows in DataTable *****");

//Get the new .NET 2.0 DataTableReader type.

DataTableReader dtReader = dt.CreateDataReader();

//The DataTableReader works just like the DataReader. while (dtReader.Read())

{

for (int i = 0; i < dtReader.FieldCount; i++)

{

Console.Write("{0} = {1} ", dtReader.GetName(i), dtReader.GetValue(i).ToString().Trim());

}

Console.WriteLine();

}

dtReader.Close();

}

Notice that the DataTableReader works identically to the data reader object of your data provider. Using a DataTableReader can be an ideal choice when you wish to quickly pump out the data within a DataTable without needing to traverse the internal row and column collections. To call this method, simply pass in the correct table:

static void Main(string[] args)

{

...

// Print out the DataTable via 'table reader'.

PrintTable(carsInventoryDS.Tables["Inventory"]);

}

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

803

Persisting DataSets (and DataTables) As XML

To wrap up the current example, recall that DataSets and DataTables both support WriteXml() and ReadXml() methods. WriteXml() allows you to persist the object’s content to a local file (as well as into any System.IO.Stream-derived type) as an XML document. ReadXml() allows you to hydrate the state of a DataSet (or DataTable) from a given XML document. In addition, DataSets and DataTables both support WriteXmlSchema() and ReadXmlSchema() to save or load an *.xsd file. To test this out for yourself, update your Main() method with the final set of code statements:

static void Main(string[] args)

{

...

//Save this DataSet as XML. carsInventoryDS.WriteXml("carsDataSet.xml"); carsInventoryDS.WriteXmlSchema("carsDataSet.xsd");

//Clear out DataSet and print contents (which are empty). carsInventoryDS.Clear();

PrintDataSet(carsInventoryDS);

//Load and print the DataSet. carsInventoryDS.ReadXml("carsDataSet.xml"); PrintDataSet(carsInventoryDS);

}

If you open the carsDataSet.xml file, you will find that each column in the table has been encoded as an XML element:

<?xml version="1.0" standalone="yes"?> <Car_x0020_Inventory>

<Inventory>

<CarID>0</CarID>

<Make>BMW</Make>

<Color>Black</Color>

<PetName>Hamlet</PetName>

</Inventory>

<Inventory>

<CarID>1</CarID>

<Make>Saab</Make>

<Color>Red</Color> <PetName>Sea Breeze</PetName>

</Inventory> </Car_x0020_Inventory>

Finally, recall that the DataColumn type supports a property named ColumnMapping, which can be used to control how a column should be represented in XML. The default setting is MappingType.Element. However, if you establish the CarID column as an XML attribute as follows by updating your existing carIDColumn DataColumn object

static void Main(string[] args)

{

...

DataColumn carIDColumn = new DataColumn("CarID", typeof(int));

...

carIDColumn.ColumnMapping = MappingType.Attribute;

}

you will find the following XML: