Managing Data and Relationships: The DataSet Class


The DataSet class has been designed as an offline container of data. It has no notion of database connections. In fact, the data held within a DataSet doesn’t necessarily need to have come from a database - it could just as easily be records from a CSV file, or points read from a measuring device.

A DataSet class consists of a set of data tables, each of which will have a set of data columns and data rows (see Figure 25-4). In addition to defining the data, you can also define links between tables within the DataSet class. One common scenario would be when defining a parent-child relationship (commonly known as master/detail). One record in a table (say Order) links to many records in another table (say Order_Details). This relationship can be defined and navigated within the DataSet.

image from book
Figure 25-4

The following sections describe the classes that are used with a DataSet class.

Data Tables

A data table is very similar to a physical database table - it consists of a set of columns with particular properties and might have zero or more rows of data. A data table might also define a primary key, which can be one or more columns, and might also contain constraints on columns. The generic term for this information used throughout the rest of the chapter is schema.

Several ways exist to define the schema for a particular data table (and indeed the DataSet class as a whole). These are discussed after introducing data columns and data rows. Figure 25-5 shows some of the objects that are accessible through the data table.

image from book
Figure 25-5

A DataTable object (and also a DataColumn) can have an arbitrary number of extended properties associated with it. This collection can be populated with any user-defined information pertaining to the object. For example, a given column might have an input mask used to validate the contents of that column - a typical example is the U.S. Social Security number. Extended properties are especially useful when the data is constructed within a middle tier and returned to the client for some processing. You could, for example, store validation criteria (such as min and max) for numeric columns in extended properties and use this in the UI tier when validating user input.

When a data table has been populated - by selecting data from a database, reading data from a file, or manually populating within code - the Rows collection will contain this retrieved data.

The Columns collection contains DataColumn instances that have been added to this table. These define the schema of the data, such as the data type, nullability, default values, and so on. The Constraints collection can be populated with either unique or primary key constraints.

One example of where the schema information for a data table is used is when displaying that data in a DataGrid (which is discussed in Chapter 29, “Viewing .NET Data”). The DataGrid control uses properties such as the data type of the column to decide what control to use for that column. A bit field within the database will be displayed as a check box within the DataGrid. If a column is defined within the database schema as NOT NULL, this fact will be stored within the DataColumn so that it can be tested when the user attempts to move off a row.

Data Columns

A DataColumn object defines properties of a column within the DataTable, such as the data type of that column, whether the column is read-only, and various other facts. A column can be created in code, or it can be automatically generated by the runtime.

When creating a column, it is also useful to give it a name; otherwise, the runtime will generate a name for you in the form Columnn where n is an incrementing number.

The data type of the column can be set either by supplying it in the constructor or by setting the DataType property. Once you have loaded data into a data table you cannot alter the type of a column - you’ll just receive an ArgumentException.

Data columns can be created to hold the following .NET Framework data types:

Boolean

Decimal

Int64

TimeSpan

Byte

Double

Sbyte

UInt16

Char

Int16

Single

UInt32

DateTime

Int32

String

UInt64

Open table as spreadsheet

Once created, the next thing to do with a DataColumn object is to set up other properties, such as the nullability of the column or the default value. The following code fragment shows a few of the more common options to set on a DataColumn object:

  DataColumn customerID = new DataColumn("CustomerID" , typeof(int)); customerID.AllowDBNull = false; customerID.ReadOnly = false; customerID.AutoIncrement = true; customerID.AutoIncrementSeed = 1000; DataColumn name = new DataColumn("Name" , typeof(string)); name.AllowDBNull = false; name.Unique = true; 

The following table shows the properties that can be set on a DataColumn object.

Open table as spreadsheet

Property

Description

AllowDBNull

If true, permits the column to be set to DBNull.

AutoIncrement

Defines that this column value is automatically generated as an incrementing number.

AutoIncrementSeed

Defines the initial seed value for an AutoIncrement column.

AutoIncrementStep

Defines the step between automatically generated column values, with a default of one.

Caption

Can be used for displaying the name of the column on screen.

ColumnMapping

Defines how a column is mapped into XML when a DataSet class is saved by calling DataSet.WriteXml.

ColumnName

The name of the column. This is auto-generated by the runtime if not set in the constructor.

DataType

Defines the System.Type value of the column.

DefaultValue

Can define a default value for a column.

Expression

Defines the expression to be used in a computed column.

Data Rows

This class makes up the other part of the DataTable class. The columns within a data table are defined in terms of the DataColumn class. The actual data within the table is accessed using the DataRow object. The following example shows how to access rows within a data table. First, the connection details:

 string source = "server=(local);" +                 " integrated security=SSPI;" +                 "database=northwind"; string select = "SELECT ContactName,CompanyName FROM Customers"; SqlConnection  conn = new SqlConnection(source);

The following code introduces the SqlDataAdapter class, which is used to place data into a DataSet class. SqlDataAdapter issues the SQL clause and fills a table in the DataSet class called Customers with the output of the following query. (For more details on the SqlDataAdapter class, see the section “Populating a DataSet” later in the chapter.)

  SqlDataAdapter da = new SqlDataAdapter(select, conn); DataSet ds = new DataSet(); da.Fill(ds , "Customers"); 

In the following code, you might notice the use of the DataRow indexer to access values from within that row. The value for a given column can be retrieved using one of the several overloaded indexers. These permit you to retrieve a value knowing the column number, name, or DataColumn:

  foreach(DataRow row in ds.Tables["Customers"].Rows)    Console.WriteLine("'{0}' from {1}" , row[0] ,row[1]); 

One of the most appealing aspects of DataRow is that it is versioned. This permits you to receive various values for a given column in a particular row. The versions are described in the following table.

Open table as spreadsheet

DataRow Version Value

Description

Current

The value existing at present within the column. If no edit has occurred, this will be the same as the original value. If an edit (or edits) has occurred, the value will be the last valid value entered.

Default

The default value (in other words, any default set up for the column).

Original

The value of the column when originally selected from the database. If the DataRow’s AcceptChanges method is called, this value will update to the Current value.

Proposed

When changes are in progress for a row, it is possible to retrieve this modified value. If you call BeginEdit() on the row and make changes, each column will have a proposed value until either EndEdit() or CancelEdit() is called.

The version of a given column could be used in many ways. One example is when updating rows within the database, in which instance it is common to issue a SQL statement such as the following:

  UPDATE Products SET    Name = Column.Current WHERE  ProductID = xxx AND    Name = Column.Original; 

Obviously, this code would never compile, but it shows one use for original and current values of a column within a row.

To retrieve a versioned value from the DataRow indexer, use one of the indexer methods that accepts a DataRowVersion value as a parameter. The following snippet shows how to obtain all values of each column in a DataTable object:

  foreach (DataRow row in ds.Tables["Customers"].Rows ) {   foreach ( DataColumn dc in ds.Tables["Customers"].Columns )   {     Console.WriteLine ("{0} Current  = {1}" , dc.ColumnName ,                                               row[dc,DataRowVersion.Current]);     Console.WriteLine ("    Default  = {0}" , row[dc,DataRowVersion.Default]);     Console.WriteLine ("    Original = {0}" , row[dc,DataRowVersion.Original]);   } } 

The whole row has a state flag called RowState, which can be used to determine what operation is needed on the row when it is persisted back to the database. The RowState property is set to keep track of all the changes made to the DataTable, such as adding new rows, deleting existing rows, and changing columns within the table. When the data is reconciled with the database, the row state flag is used to determine what SQL operations should occur. The following table provides an overview of the flags that are defined by the DataRowState enumeration.

Open table as spreadsheet

DataRowState

Value Description

Added

Indicates that the row has been newly added to a DataTable’s Rows collection. All rows created on the client are set to this value and will ultimately issue SQL INSERT statements when reconciled with the database.

Deleted

Indicates that the row has been marked as deleted from the DataTable by means of the DataRow.Delete() method. The row still exists within the DataTable but will not normally be viewable onscreen (unless a DataView has been explicitly set up). DataViews are discussed in the next chapter. Rows marked as deleted in the DataTable will be deleted from the database when reconciled.

Detached

Indicates that a row is in this state immediately after it is created, and can also be returned to this state by calling DataRow.Remove(). A detached row is not considered to be part of any data table, and as such no SQL for rows in this state will be issued.

Modified

Indicates that a row will be Modified if the value in any column has been changed.

Unchanged

Indicates that the row has not been changed since the last call to AcceptChanges().

The state of the row depends also on what methods have been called on the row. The AcceptChanges() method is generally called after successfully updating the data source (that is, after persisting changes to the database).

The most common way to alter data in a DataRow is to use the indexer; however, if you have a number of changes to make you also need to consider the BeginEdit() and EndEdit() methods.

When an alteration is made to a column within a DataRow, the ColumnChanging event is raised on the row’s DataTable. This permits you to override the ProposedValue property of the DataColumnChangeEventArgs class, and change it as required. This is one way of performing some data validation on column values. If you call BeginEdit() before making changes, the ColumnChanging event will not be raised. This permits you to make multiple changes and then call EndEdit() to persist these changes. If you want to revert to the original values, call CancelEdit().

A DataRow can be linked in some way to other rows of data. This permits the creation of navigable links between rows, which is common in master/detail scenarios. The DataRow contains a GetChildRows() method that will return an array of associated rows from another table in the same DataSet as the current row. These are discussed in the “Data Relationships” section later in this chapter.

Schema Generation

You can create the schema for a DataTable in three ways:

  • Let the runtime do it for you.

  • Write code to create the table(s).

  • Use the XML schema generator.

Runtime Schema Generation

The DataRow example shown earlier presented the following code for selecting data from a database and populating a DataSet class:

  SqlDataAdapter da = new SqlDataAdapter(select , conn); DataSet ds = new DataSet(); da.Fill(ds , "Customers"); 

This is obviously easy to use, but it has a few drawbacks as well. For example, you have to make do with the default column names, which might work for you, but in certain instances you might want to rename a physical database column (say PKID) to something more user-friendly.

You could naturally alias columns within your SQL clause, as in SELECT PID AS PersonID FROM PersonTable; I would always recommend not renaming columns within SQL, because a column only really needs to have a “pretty” name onscreen.

Another potential problem with automated DataTable/DataColumn generation is that you have no control over the column types that the runtime chooses for your data. It does a fairly good job of deciding the correct data type for you, but as usual there are instances where you need more control. For example, you might have defined an enumerated type for a given column, so as to simplify user code written against your class. If you accept the default column types that the runtime generates, the column will likely be an integer with a 32-bit range, as opposed to an enum with your predefined options.

Last, and probably most problematic, is that when using automated table generation, you have no typesafe access to the data within the DataTable - you are at the mercy of indexers, which return instances of object rather than derived data types. If you like sprinkling your code with typecast expressions, skip the following sections.

Hand-Coded Schema

Generating the code to create a DataTable, replete with associated DataColumns, is fairly easy. The examples within this section access the Products table from the Northwind database shown in Figure 25-6.

image from book
Figure 25-6

The following code manufactures a DataTable, which corresponds to the schema shown in Figure 25-6 (but doesn’t cover the nullability of columns):

  public static void ManufactureProductDataTable(DataSet ds) {    DataTable   products = new DataTable("Products");    products.Columns.Add(new DataColumn("ProductID", typeof(int)));    products.Columns.Add(new DataColumn("ProductName", typeof(string)));    products.Columns.Add(new DataColumn("SupplierID", typeof(int)));    products.Columns.Add(new DataColumn("CategoryID", typeof(int)));    products.Columns.Add(new DataColumn("QuantityPerUnit", typeof(string)));    products.Columns.Add(new DataColumn("UnitPrice", typeof(decimal)));    products.Columns.Add(new DataColumn("UnitsInStock", typeof(short)));    products.Columns.Add(new DataColumn("UnitsOnOrder", typeof(short)));    products.Columns.Add(new DataColumn("ReorderLevel", typeof(short)));    products.Columns.Add(new DataColumn("Discontinued", typeof(bool)));    ds.Tables.Add(products); } 

You can alter the code in the DataRow example to utilize this newly generated table definition as follows:

 string source = "server=(local);" +                 "integrated security=sspi;" +                 "database=Northwind"; string select = "SELECT * FROM Products"; SqlConnection conn = new SqlConnection(source); SqlDataAdapter cmd = new SqlDataAdapter(select, conn); DataSet ds = new DataSet(); ManufactureProductDataTable(ds); cmd.Fill(ds, "Products"); foreach(DataRow row in ds.Tables["Products"].Rows)    Console.WriteLine("'{0}' from {1}", row[0], row[1]);

The ManufactureProductDataTable() method creates a new DataTable, adds each column in turn, and finally appends this to the list of tables within the DataSet. The DataSet has an indexer that takes the name of the table and returns that DataTable to the caller.

The previous example is still not really type-safe, because indexers are being used on columns to retrieve the data. What would be better is a class (or set of classes) derived from DataSet, DataTable, and DataRow that define type-safe accessors for tables, rows, and columns. You can generate this code yourself; it’s not particularly tedious and you end up with truly type-safe data access classes.

If you don’t like generating these type-safe classes yourself, help is at hand. The .NET Framework includes support for using XML schemas to define a DataSet class, a DataTable class, and the other classes that have been described in this section. (For more details on this method, see the section “XML Schemas” later in this chapter.)

Data Relationships

When writing an application, it is often necessary to obtain and cache various tables of information. The DataSet class is the container for this information. With regular OLE DB, it was necessary to provide a strange SQL dialect to enforce hierarchical data relationships, and the provider itself was not without its own subtle quirks.

The DataSet class, on the other hand, has been designed from the start to establish relationships between data tables with ease. The code in this section shows how to generate manually and populate two tables with data. So, if you don’t have access to SQL Server or the Northwind database, you can run this example anyway:

  DataSet ds = new DataSet("Relationships"); ds.Tables.Add(CreateBuildingTable()); ds.Tables.Add(CreateRoomTable()); ds.Relations.Add("Rooms",                   ds.Tables["Building"].Columns["BuildingID"],                   ds.Tables["Room"].Columns["BuildingID"]); 

The tables used in this example are shown in Figure 25-7. They contain a primary key and name field, with the Room table having BuildingID as a foreign key.

image from book
Figure 25-7

These tables have been kept deliberately simple. The following code shows how to iterate through the rows in the Building table and traverse the relationship to list all of the child rows from the Room table:

  foreach(DataRow theBuilding in ds.Tables["Building"].Rows) {    DataRow[] children = theBuilding.GetChildRows("Rooms");    int roomCount = children.Length;    Console.WriteLine("Building {0} contains {1} room{2}",                      theBuilding["Name"],                      roomCount,                      roomCount > 1 ? "s" : "");    // Loop through the rooms    foreach(DataRow theRoom in children)       Console.WriteLine("Room: {0}", theRoom["Name"]); } 

The key difference between the DataSet class and the old-style hierarchical Recordset object is in the way the relationship is presented. In a hierarchical Recordset object, the relationship was presented as a pseudo-column within the row. This column itself was a Recordset object that could be iterated through. Under ADO.NET, however, a relationship is traversed simply by calling the GetChildRows() method:

 DataRow[] children = theBuilding.GetChildRows("Rooms");

This method has a number of forms, but the preceding simple example just uses the name of the relationship to traverse between parent and child rows. It returns an array of rows that can be updated as appropriate by using the indexers, as shown in earlier examples.

What’s more interesting with data relationships is that they can be traversed both ways. Not only can you go from a parent to the child rows, but you can also find a parent row (or rows) from a child record simply by using the ParentRelations property on the DataTable class. This property returns a DataRelationCollection, which can be indexed using the [] array syntax (for example, ParentRelations[“Rooms”]), or as an alternative the GetParentRows() method can be called, as shown here:

  foreach(DataRow theRoom in ds.Tables["Room"].Rows) {    DataRow[] parents = theRoom.GetParentRows("Rooms");    foreach(DataRow theBuilding in parents)       Console.WriteLine("Room {0} is contained in building {1}",                         theRoom["Name"],                         theBuilding["Name"]); } 

Two methods with various overrides are available for retrieving the parent row(s): GetParentRows() (which returns an array of zero or more rows) and GetParentRow() (which retrieves a single parent row given a relationship).

Data Constraints

Changing the data type of columns created on the client is not the only thing a DataTable is good for. ADO.NET permits you to create a set of constraints on a column (or columns), which are then used to enforce rules within the data.

The following table lists the constraint types that are currently supported by the runtime, embodied as classes in the System.Data namespace.

Open table as spreadsheet

Constraint

Description

ForeignKeyConstraint

Enforces a link between two DataTables within a DataSet.

UniqueConstraint

Ensures that entries in a given column are unique.

Setting a Primary Key

As is common with a table in a relational database, you can supply a primary key, which can be based on one or more columns from the DataTable.

The following code creates a primary key for the Products table, whose schema was constructed by hand earlier.

Note that a primary key on a table is just one form of constraint. When a primary key is added to a DataTable, the runtime also generates a unique constraint over the key column(s). This is because there isn’t actually a constraint type of PrimaryKey - a primary key is simply a unique constraint over one or more columns.

  public static void ManufacturePrimaryKey(DataTable dt) {    DataColumn[] pk = new DataColumn[1];    pk[0] = dt.Columns["ProductID"];    dt.PrimaryKey = pk; } 

Because a primary key can contain several columns, it is typed as an array of DataColumns. A table’s primary key can be set to those columns simply by assigning an array of columns to the property.

To check the constraints for a table, you can iterate through the ConstraintCollection. For the auto-generated constraint produced by the preceding code, the name of the constraint is Constraint1. That’s not a very useful name, so to avoid this problem it is always best to create the constraint in code first, then define which column(s) make up the primary key.

The following code names the constraint before creating the primary key:

 DataColumn[] pk = new DataColumn[1]; pk[0] = dt.Columns["ProductID"]; dt.Constraints.Add(new UniqueConstraint("PK_Products", pk[0])); dt.PrimaryKey = pk;

Unique constraints can be applied to as many columns as you want.

Setting a Foreign Key

In addition to unique constraints, a DataTable class can also contain foreign key constraints. These are primarily used to enforce master/detail relationships, but can also be used to replicate columns between tables if you set the constraint up correctly. A master/detail relationship is one where there is commonly one parent record (say an order) and many child records (order lines), linked by the primary key of the parent record.

A foreign key constraint can only operate over tables within the same DataSet, so the following example uses the Categories table from the Northwind database (shown in Figure 25-8), and assigns a constraint between it and the Products table.

image from book
Figure 25-8

The first step is to generate a new data table for the Categories table:

  DataTable categories = new DataTable("Categories"); categories.Columns.Add(new DataColumn("CategoryID", typeof(int))); categories.Columns.Add(new DataColumn("CategoryName", typeof(string))); categories.Columns.Add(new DataColumn("Description", typeof(string))); categories.Constraints.Add(new UniqueConstraint("PK_Categories",                                categories.Columns["CategoryID"])); categories.PrimaryKey = new DataColumn[1]                               {categories.Columns["CategoryID"]}; 

The last line of this code creates the primary key for the Categories table. The primary key in this instance is a single column; however, it is possible to generate a key over multiple columns using the array syntax shown.

Then the constraint can be created between the two tables:

  DataColumn parent = ds.Tables["Categories"].Columns["CategoryID"]; DataColumn child = ds.Tables["Products"].Columns["CategoryID"]; ForeignKeyConstraint fk =    new ForeignKeyConstraint("FK_Product_CategoryID", parent, child); fk.UpdateRule = Rule.Cascade; fk.DeleteRule = Rule.SetNull; ds.Tables["Products"].Constraints.Add(fk); 

This constraint applies to the link between Categories.CategoryID and Products.CategoryID. There are four different ForeignKeyConstraint - use those that permit you to name the constraint.

Setting Update and Delete Constraints

In addition to defining that there is some type of constraint between parent and child tables, you can define what should happen when a column in the constraint is updated.

The previous example sets the update rule and the delete rule. These rules are used when an action occurs to a column (or row) within the parent table, and the rule is used to decide what should happen to row(s) within the child table that could be affected. Four different rules can be applied through the Rule enumeration:

  • Cascade - If the parent key has been updated, copy the new key value to all child records. If the parent record has been deleted, delete the child records also. This is the default option.

  • None - No action whatsoever. This option leaves orphaned rows within the child data table.

  • SetDefault - Each child record affected has the foreign key column(s) set to its default value, if one has been defined.

  • SetNull - All child rows have the key column(s) set to DBNull. (Following the naming convention that Microsoft uses, this should really be SetDBNull).

Important 

Constraints are enforced only within a DataSet class if the EnforceConstraints property of the DataSet is true.

This covers the main classes that make up the constituent parts of the DataSet class, and has shown how to generate manually each of these classes in code. You can also define a DataTable, DataRow, DataColumn, DataRelation, and Constraint using the XML schema file(s) and the XSD tool that ships with .NET. The following section describes how to set up a simple schema and generate type-safe classes to access your data.

XML Schemas

XML is firmly entrenched in ADO.NET - indeed, the remoting format for passing data between objects is now XML. With the .NET runtime, it is possible to describe a DataTable class within an XML schema definition file (XSD). What’s more, you can define an entire DataSet class, with a number of DataTable classes, a set of relationships between these tables, and include various other details to fully describe the data.

When you have defined an XSD file, there is a new tool in the runtime that will convert this schema to the corresponding data access class(es), such as the type-safe product DataTable class shown earlier. Let’s start with a simple XSD file (Products.xsd) that describes the same information as the Products sample discussed earlier and then extend it to include some extra functionality:

  <?xml version="1.0" encoding="utf-8" ?> <xs:schema  targetNamespace="http://tempuri.org/XMLSchema1.xsd"  xmlns:mstns="http://tempuri.org/XMLSchema1.xsd"   xmlns:xs="http://www.w3.org/2001/XMLSchema"   xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">   <xs:element name="Product">     <xs:complexType>       <xs:sequence>         <xs:element name="ProductID" msdata:ReadOnly="true"           msdata:AutoIncrement="true" type="xs:int" />         <xs:element name="ProductName" type="xs:string" />         <xs:element name="SupplierID" type="xs:int" minOccurs="0" />         <xs:element name="CategoryID" type="xs:int" minOccurs="0" />         <xs:element name="QuantityPerUnit" type="xs:string" minOccurs="0" />         <xs:element name="UnitPrice" type="xs:decimal" minOccurs="0" />         <xs:element name="UnitsInStock" type="xs:short" minOccurs="0" />         <xs:element name="UnitsOnOrder" type="xs:short" minOccurs="0" />         <xs:element name="ReorderLevel" type="xs:short" minOccurs="0" />         <xs:element name="Discontinued" type="xs:boolean" />       </xs:sequence>     </xs:complexType>   </xs:element> </xs:schema> 

These options are covered in detail in Chapter 26, “Manipulating XML”; for now, this file basically defines a schema with the id attribute set to Products. A complex type called Product is defined, which contains a number of elements, one for each of the fields within the Products table.

These items map to data classes as follows. The Products schema maps to a class derived from DataSet. The Product complex type maps to a class derived from DataTable. Each subelement maps to a class derived from DataColumn. The collection of all columns maps to a class derived from DataRow.

Thankfully, there is a tool within the .NET Framework that produces the code for these classes with the help of the input XSD file. Because its sole job in life is to perform various functions on XSD files, the tool itself is called XSD.EXE.

Generating Code with XSD

Assuming that you saved the preceding file as Product.xsd, you would convert the file into code by issuing the following command in a command prompt:

 xsd Product.xsd /d 

This creates the file Product.cs.

Various switches can be used with XSD to alter the output generated. Some of the more commonly used are shown in the following table.

Open table as spreadsheet

Switch

Description

/dataset (/d)

Enables you to generate classes derived from DataSet, DataTable, and DataRow.

/language:<language>

Permits you to choose which language the output file will be written in. C# is the default, but you can choose VB for a Visual Basic .NET file.

/namespace:<namespace>

Enables you to define the namespace that the generated code should reside within. The default is no namespace.

The following is an abridged version of the output from XSD for the Products schema. The output has been altered slightly to fit into a format appropriate for the book. To see the complete output, run XSD.EXE on the Products schema (or one of your own making) and take a look at the .cs file generated. The example includes the entire source code plus the Product.xsd file. (Note that this output is part of the downloadable code file available at www.wrox.com):

  //------------------------------------------------------------------------------ // <autogenerated> //     This code was generated by a tool. //     Runtime Version:2.0.50727.312 // //     Changes to this file may cause incorrect behavior and will be lost if //     the code is regenerated. // </autogenerated> //------------------------------------------------------------------------------ using System; // // This source code was auto-generated by xsd, Version=2.0.40426.16. // [Serializable()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Diagnostics.DebuggerStepThrough()] [System.ComponentModel.ToolboxItem(true)] [System.Xml.Serialization.XmlSchemaProviderAttribute("GetTypedDataSetSchema")] [System.Xml.Serialization.XmlRootAttribute("Products")] public partial class Products : System.Data.DataSet { {    private ProductDataTable tableProduct;    public Products()    public ProductDataTable Product    public override DataSet Clone()    public delegate void ProductRowChangeEventHandler ( object sender,                                                        ProductRowChangeEvent e);    [System.Diagnostics.DebuggerStepThrough()]    public partial class ProductDataTable : DataTable, IEnumerable    [System.Diagnostics.DebuggerStepThrough()]    public class ProductRow : DataRow } 

All private and protected members have been removed to concentrate on the public interface. The bolded ProductDataTable and ProductRow definitions show the positions of two nested classes, which will be implemented next. You review the code for these classes after a brief explanation of the DataSet-derived class.

The Products() constructor calls a private method, InitClass(), which constructs an instance of the DataTable-derived class ProductDataTable, and adds the table to the Tables collection of the DataSet class. The Products data table can be accessed by the following code:

  DataSet ds = new Products(); DataTable products = ds.Tables["Products"]; 

Or, more simply by using the property Product, available on the derived DataSet object:

  DataTable products = ds.Product; 

Because the Product property is strongly typed, you could naturally use ProductDataTable rather than the DataTable reference shown in the previous code.

The ProductDataTable class includes far more code (note this is an abridged version of the code):

  [System.Serializable()] [System.Diagnostics.DebuggerStepThrough()] [System.Xml.Serialization.XmlSchemaProviderAttribute("GetTypedTableSchema")] public partial class ProductDataTable : DataTable, System.Collections.IEnumerable {    private DataColumn columnProductID;    private DataColumn columnProductName;    private DataColumn columnSupplierID;    private DataColumn columnCategoryID;    private DataColumn columnQuantityPerUnit;    private DataColumn columnUnitPrice;    private DataColumn columnUnitsInStock;    private DataColumn columnUnitsOnOrder;    private DataColumn columnReorderLevel;    private DataColumn columnDiscontinued;        public ProductDataTable()   {         this.TableName = "Product";         this.BeginInit();         this.InitClass();         this.EndInit();   } 

The ProductDataTable class, derived from DataTable and implementing the IEnumerable interface, defines a private DataColumn instance for each of the columns within the table. These are initialized again from the constructor by calling the private InitClass() member. Each column is given an internal accessor, which is used by the DataRow class (which is described shortly):

  [System.ComponentModel.Browsable(false)] public int Count {    get { return this.Rows.Count; } } internal DataColumn ProductIDColumn {    get { return this.columnProductID; } } // Other row accessors removed for clarity -- there is one for each of the columns 

Adding rows to the table is taken care of by the two overloaded (and significantly different) AddProductRow() methods. The first takes an already constructed DataRow and returns a void. The second takes a set of values, one for each of the columns in the DataTable, constructs a new row, sets the values within this new row, adds the row to the DataTable object, and returns the row to the caller. Such widely different functions shouldn’t really have the same name!

  public void AddProductRow(ProductRow row) {    this.Rows.Add(row); } public ProductRow AddProductRow ( string ProductName , int SupplierID ,                                   int CategoryID , string QuantityPerUnit ,                                   System.Decimal UnitPrice , short UnitsInStock ,                                   short UnitsOnOrder , short ReorderLevel ,                                   bool Discontinued ) {    ProductRow rowProductRow = ((ProductRow)(this.NewRow()));    rowProductRow.ItemArray = new object[]    {       null,       ProductName,       SupplierID,       CategoryID,       QuantityPerUnit,       UnitPrice,       UnitsInStock,       UnitsOnOrder,       ReorderLevel,       Discontinued    };    this.Rows.Add(rowProductRow);    return rowProductRow; } 

Just like the InitClass() member in the DataSet-derived class, which added the table into the DataSet class, the InitClass() member in ProductDataTable adds columns to the DataTable class. Each column’s properties are set as appropriate, and the column is then appended to the columns collection:

  private void InitClass() {    this.columnProductID = new DataColumn ( "ProductID",                                             typeof(int),                                             null,                                             System.Data.MappingType.Element);    this.columnProductID.ExtendedProperties.Add         ("Generator_ChangedEventName", "ProductIDChanged");    this.columnProductID.ExtendedProperties.Add         ("Generator_ChangingEventName", "ProductIDChanging");    this.columnProductID.ExtendedProperties.Add         ("Generator_ColumnPropNameInRow", "ProductID");    this.columnProductID.ExtendedProperties.Add         ("Generator_ColumnPropNameInTable", "ProductIDColumn");    this.columnProductID.ExtendedProperties.Add         ("Generator_ColumnVarNameInTable", "columnProductID");    this.columnProductID.ExtendedProperties.Add         ("Generator_DelegateName", "ProductIDChangeEventHandler");    this.columnProductID.ExtendedProperties.Add         ("Generator_EventArgName", "ProductIDChangeEventArg");   this.Columns.Add(this.columnProductID);   // Other columns removed for clarity      this.columnProductID.AutoIncrement = true;   this.columnProductID.AllowDBNull = false;   this.columnProductID.ReadOnly = true;   this.columnProductName.AllowDBNull = false;   this.columnDiscontinued.AllowDBNull = false; } public ProductRow NewProductRow() {     return ((ProductRow)(this.NewRow())); } 

NewRowFromBuilder() is called internally from the DataTable class’s NewRow() method. Here, it creates a new strongly typed row. The DataRowBuilder instance is created by the DataTable class, and its members are accessible only within the System.Data assembly:

  protected override DataRow NewRowFromBuilder(DataRowBuilder builder) {    return new ProductRow(builder); } 

The last class to discuss is the ProductRow class, derived from DataRow. This class is used to provide type-safe access to all fields in the data table. It wraps the storage for a particular row, and provides members to read (and write) each of the fields in the table.

In addition, for each nullable field, there are functions to set the field to null, and check if the field is null. The following example shows the functions for the SupplierID column:

  [System.Diagnostics.DebuggerStepThrough()] public class ProductRow : DataRow {    private ProductDataTable tableProduct;    internal ProductRow(DataRowBuilder rb) : base(rb)    {       this.tableProduct = ((ProductDataTable)(this.Table));    }    public int ProductID    {       get { return ((int)(this[this.tableProduct.ProductIDColumn])); }       set { this[this.tableProduct.ProductIDColumn] = value; }    }   // Other column accessors/mutators removed for clarity    public bool IsSupplierIDNull()    {       return this.IsNull(this.tableProduct.SupplierIDColumn);    }    public void SetSupplierIDNull()    {       this[this.tableProduct.SupplierIDColumn] = System.Convert.DBNull;    } } 

The following code utilizes the classes ouptut from the XSD tool to retrieve data from the Products table and display that data to the console:

 using System; using System.Data; using System.Data.SqlClient; public class XSD_DataSet {    public static void Main()    {       string source = "server=(local);" +                       " integrated security=SSPI;" +                       "database=northwind";       string select = "SELECT * FROM Products";       SqlConnection conn = new SqlConnection(source);       SqlDataAdapter da = new SqlDataAdapter(select , conn);       Products ds = new Products();       da.Fill(ds , "Product");       foreach(Products.ProductRow row in ds.Product )       Console.WriteLine("'{0}' from {1}" ,                          row.ProductID ,                          row.ProductName);    } }

The main areas of interest are highlighted. The output of the XSD file contains a class derived from DataSet, Products, which is created and then filled by the use of the data adapter. The foreach statement uses the strongly typed ProductRow and also the Product property, which returns the Product data table.

To compile this example, issue the following commands:

 xsd product.xsd /d 

and

 csc /recurse:*.cs 

The first generates the Products.cs file from the Products.XSD schema, and then the csc command utilizes the /recurse:*.cs parameter to go through all files with the extension .cs and add these to the resulting assembly.




Professional C# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net