
Pro CSharp 2008 And The .NET 3.5 Platform [eng]
.pdf
792 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
// RowState = Deleted. temp.Rows[0].Delete();
Console.WriteLine("After calling Delete: {0}", row.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.
Understanding the DataRowVersion Property
Beyond maintaining the current state of a row via the RowState property, a DataRow object maintains three possible versions of the data it contains via the DataRowVersion property. When a DataRow object is first constructed, it contains only a single copy of data, represented as the “current version.” However, as you programmatically manipulate a DataRow object (via various method calls), additional versions of the data spring to life. Specifically, the DataRowVersion can be set to any value of the related DataRowVersion enumeration (see Table 23-6).
Table 23-6. Values of the DataRowVersion Enumeration
Value |
Meaning in Life |
Current Represents the current value of a row, even after changes have been made.
Default The default version of DataRowState. For a DataRowState value of Added, Modified, or
Deleted, the default version is Current. For a DataRowState value of Detached, the version is Proposed.
Original Represents the value first inserted into a DataRow, or the value the last time
AcceptChanges() was called.
Proposed The value of a row currently being edited due to a call to BeginEdit().
As suggested in Table 23-6, the value of the DataRowVersion property is dependent on the value of the DataRowState property in a good number of cases. As mentioned, the DataRowVersion property will be changed behind the scenes when you invoke various methods on the DataRow (or, in some cases, the DataTable) object. Here is a breakdown of the methods that can affect the value of a row’s DataRowVersion property:
•If you call the DataRow.BeginEdit() method and change the row’s value, the Current and Proposed values become available.
•If you call the DataRow.CancelEdit() method, the Proposed value is deleted.
•After you call DataRow.EndEdit(), the Proposed value becomes the Current value.
•After you call the DataRow.AcceptChanges() method, the Original value becomes identical to the Current value. The same transformation occurs when you call DataTable. AcceptChanges().
•After you call DataRow.RejectChanges(), the Proposed value is discarded, and the version becomes Current.
Yes, this is a bit convoluted—especially due to the fact that a DataRow may or may not have all versions at any given time (you’ll receive runtime exceptions if you attempt to obtain a row version that is not currently tracked). Regardless of the complexity, given that the DataRow maintains three copies of data, it becomes very simple to build a front end that allows an end user to alter values,

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
793 |
change his or her mind and “roll back” values, or commit values permanently. You’ll see various examples of manipulating these methods over the remainder of this chapter.
Working with DataTables
The DataTable type defines a good number of members, many of which are identical in name and functionality to those of the DataSet. Table 23-7 describes some core members of the DataTable type beyond Rows and Columns.
Table 23-7. Key Members of the DataTable Type
Member |
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. |
Copy() |
A method that copies the schema and data of a given DataTable into a new |
|
instance. |
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. |
TableName |
Gets or sets the name of the table. This same property may also be specified as |
|
a constructor parameter. |
|
|
To continue with our current example, let’s set the PrimaryKey property of the DataTable to the carIDColumn DataColumn object. Be aware that the PrimaryKey property is assigned a collection of DataColumn objects, to account for a multicolumned key. In our case, however, we need to specify only the CarID column (being the first ordinal position in the table):
static void Main(string[] args)
{
...
// Mark the primary key of this table.
inventoryTable.PrimaryKey = new DataColumn[] { inventoryTable.Columns[0] };
...
}
Inserting DataTables into DataSets
At this point, our DataTable object is complete. The final step is to insert the DataTable into the carsInventoryDS DataSet object using the Tables collection. Assume that you have updated Main() to do so, and pass the DataSet object into a new (yet to be written) helper method named
PrintDataSet():

794CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
static void Main(string[] args)
{
...
//Finally, add our table to the DataSet. carsInventoryDS.Tables.Add(inventoryTable);
//Now print the DataSet.
PrintDataSet(carsInventoryDS);
Console.ReadLine();
}
The PrintDataSet() method simply iterates over the DataSet metadata (via the ExtendedProperties collection) and each DataTable in the DataSet, printing out the column names and row values using the type indexers:
static void PrintDataSet(DataSet ds)
{
// Print out any name and extended properties.
Console.WriteLine("DataSet is named: {0}", ds.DataSetName);
foreach (System.Collections.DictionaryEntry de in ds.ExtendedProperties)
{
Console.WriteLine("Key = {0}, Value = {1}", de.Key, de.Value);
}
Console.WriteLine();
foreach (DataTable dt in ds.Tables)
{
Console.WriteLine("=> {0} Table:", dt.TableName);
// Print out the column names.
for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
{
Console.Write(dt.Columns[curCol].ColumnName + "\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 23-3 shows the program’s output.

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
795 |
Figure 23-3. Contents of the example’s DataSet object
Processing DataTable Data Using DataTableReader Objects
Given your work in the previous chapter, you are sure to notice that the manner in which you process data using the connected layer (e.g., data reader objects) and the disconnected layer (e.g., DataSet objects) is quite different. Working with a data reader typically involves establishing a while loop, calling the Read() method, and using an indexer to pluck out the name/value pairs. On the other hand, DataSet processing typically involves a series of iteration constructs to drill into the data within the tables, rows, and columns.
Since the release of .NET 2.0, DataTables were provided with 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). The major benefit of this approach is that you now use a single model to process data, regardless of which “layer” of ADO.NET you use to obtain it. Assume you have authored the following helper function named PrintTable(), implemented as so:
static void PrintTable(DataTable dt)
{
// Get the 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}\t", 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. Now, assume you have updated the previous PrintDataSet() method to invoke PrintTable(), rather than drilling into the Rows and Columns collections:

796CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
static void PrintDataSet(DataSet ds)
{
//Print out any name and extended properties. Console.WriteLine("DataSet is named: {0}", ds.DataSetName);
foreach (System.Collections.DictionaryEntry de in ds.ExtendedProperties)
{
Console.WriteLine("Key = {0}, Value = {1}", de.Key, de.Value);
}
Console.WriteLine();
foreach (DataTable dt in ds.Tables)
{
Console.WriteLine("=> {0} Table:", 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---------------------------------- |
"); |
// Call our new helper method. |
|
PrintTable(dt); |
|
}
}
When you run the application, the output is identical to that of Figure 23-3. The only difference is how you are internally accessing the DataTable’s contents.
Serializing DataTable/DataSet Objects As XML
DataSets and DataTables both support the 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 to call the following final helper function (notice you will pass in a DataSet as the sole parameter):
static void DataSetAsXml(DataSet carsInventoryDS)
{
//Save this DataSet as XML. carsInventoryDS.WriteXml("carsDataSet.xml"); carsInventoryDS.WriteXmlSchema("carsDataSet.xsd");
//Clear out DataSet.
carsInventoryDS.Clear();
// Load DataSet from XML file. carsInventoryDS.ReadXml("carsDataSet.xml");
}
If you open the carsDataSet.xml file (which will be located under the \bin\Debug folder of your project), you will find that each column in the table has been encoded as an XML element:


798 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Figure 23-4. Serializing a DataSet in a binary format
Binding DataTable Objects to User Interfaces
At this point in the chapter, you have examined how to manually create, hydrate, and iterate over the contents of a DataSet object using the inherit object model of ADO.NET. While understanding how to do so is quite important, the .NET platform ships with numerous APIs that have the ability to “bind” data to user interface elements automatically.
For example, the original GUI toolkit of .NET, Windows Forms, supplies a control named DataGridView that has the built-in ability to display the contents of a DataSet or DataTable object using just a few lines of code. ASP.NET (.NET’s web development API) and the Windows Presentation Foundation API (the new, supercharged GUI API introduced with .NET 3.0) also support the notion of data binding in one form or another.
To continue our investigation of the disconnected layer of ADO.NET, our next task is to build a Windows Forms application that will display the contents of a DataTable object within its user interface. Along the way, we will also examine how to filter and change table data, and we’ll come to know the role of the DataView object. To begin, create a brand-new Windows Forms project workspace named WindowsFormsDataTableViewer, as shown in Figure 23-5.
■Note The current example assumes you have some experience using Windows Forms to build graphical user interfaces. If this is not the case, you may wish to simply open the solution and follow along, or return to this section once you have read Chapter 27, where you will formally investigate the Windows Forms API.
Rename your initial Form1.cs file to the more fitting MainForm.cs. Next, using the Visual Studio 2008 Toolbox drag a DataGridView control (renamed to carInventoryGridView via the Name property of the Properties window) onto the designer surface. Notice that when you do, you activate a context menu that allows you to connect to a physical data source. For the time being, completely ignore this aspect of the designer, as you will be binding your DataTable object programmatically. Finally, add a descriptive Label to your designer for information purposes. Figure 23-6 shows one possible look and feel.

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
799 |
Figure 23-5. Creating a Windows Forms application
Figure 23-6. The initial UI
Hydrating a DataTable from a Generic List<T>
Similar to the previous SimpleDataSet example, the WindowsFormsDataTableViewer application will construct a DataTable that contains a set of DataColumns representing various columns and rows of data. This time, however, you will fill the rows using your generic List<T> member variable. First, insert a new C# class into your project (named Car), defined as follows:

800 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
class Car
{
// Use C# automatic properties. public string carPetName { get; set; } public string carMake { get; set; } public string carColor { get; set; }
public Car(string petName, string make, string color)
{
carPetName = petName; carColor = color; carMake = make;
}
}
Now, within the default constructor, populate a List<T> member variable (named listCars) with a set of new Car objects:
public partial class MainForm : Form
{
// A collection of Car objects.
List<Car> listCars = new List<Car>();
public MainForm()
{
InitializeComponent();
// Fill the list with some cars. listCars.Add(new Car("Chucky", "BMW", "Green")); listCars.Add(new Car("Tiny", "Yugo", "White")); listCars.Add(new Car("Ami", "Jeep", "Tan"));
listCars.Add(new Car("Pain Inducer", "Caravan", "Pink")); listCars.Add(new Car("Fred", "BMW", "Pea Soup Green")); listCars.Add(new Car("Sidd", "BMW", "Black")); listCars.Add(new Car("Mel", "Firebird", "Red")); listCars.Add(new Car("Sarah", "Colt", "Black"));
}
}
Next, add a new member variable named inventoryTable of type DataTable to your MainForm class type. Add a new helper function to your class named CreateDataTable(), and call this method within the default constructor of the MainForm class:
void CreateDataTable()
{
// Create table schema.
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"; inventoryTable.Columns.AddRange(new DataColumn[] { carMakeColumn,
carColorColumn, carPetNameColumn });
// Iterate over the array list to make rows. foreach (Car c in listCars)
{
DataRow newRow = inventoryTable.NewRow(); newRow["Make"] = c.carMake;

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
801 |
newRow["Color"] = c.carColor; newRow["PetName"] = c.carPetName; inventoryTable.Rows.Add(newRow);
}
// Bind the DataTable to the carInventoryGridView. carInventoryGridView.DataSource = inventoryTable;
}
The method implementation begins by creating the schema of the DataTable by creating three DataColumn objects (for simplicity, I did not bother to add the autoincrementing CarID field), after which point they are added to the DataTable member variable. The row data is mapped from the List<T> field into the DataTable using a foreach iteration construct and the native ADO.NET object model.
However, notice that the final code statement within the CreateDataTable() method assigns the inventoryTable to the DataSource property. This single property is all you need to set to bind a DataTable to a DataGridView object. Under the hood, this GUI control is reading the row and column collections internally, much like you did with the PrintDataSet() method of the SimpleDataSet example. At this point, you should be able to run your application and see the DataTable within the DataGridView control, as shown in Figure 23-7.
Figure 23-7. Binding a DataTable to a Windows Forms DataGridView
Programmatically Deleting Rows
Now, assume you wish to update your graphical interface to allow the user to delete a row from the DataTable that is bound to the DataGridView. One approach is to call the Delete() method of the DataRow object that represents the row to terminate. Simply specify the index (or DataRow object) representing the row to remove. To allow the user to specify which row to delete, add a TextBox (named txtRowToRemove) and a Button control (named btnRemoveRow) to the current designer, as shown in Figure 23-8.