- A View to a DataSet
- Parsing XML with the DataSet
- The Strongly Typed DataSet
- Conclusion
The Strongly Typed DataSet
The strongly typed DataSet fills a special role in the world of ADO.NET. It makes the DataSet look more like an object from the data domain it models, by using typed properties instead of polymorphic, generic collections. It does this magic through implementation inheritance of the DataSet, DataTable, and DataRow objects along with augmentation code to wire the structure together when the root DataSet-based class is instantiated. The end result is easier navigation and better data retrieval and storage, without pesky runtime type or column errors.
The first step in creating a strongly typed DataSet is adding an XML schema file that will represent its structure to a project. The best way to do this is use the Data Set item in the Add New Item menu option of the IDE as it creates the XML schema file and adds DataSet-specific namespaces and attributes to the schema document. In Figure 6, notice that the file type extension is xsd for the CustOrders DataSet being created.
Figure 6 Using Add New Item to add a typed DataSet.
The next step in the process is to add the data structures. We continue with our incessant demo loop of the Customers and Orders tables from Northwind to do them in a strongly typed manner. The easiest way to add tables is to select them in the Server Explorer Database node and drag them onto the design surface of the typed DataSet, as shown in Figure 7. The XML schema document representing the DataSet gets the metadata from the database tables to represent them faithfully after the UI manipulation. This brings us to the picture depicted in Figure 8. The IDE is not equipped to link the two tables; we're responsible for creating the relationship between them. Fortunately, we have the XML toolbox at our disposal. Select the Relation control and drag it onto the Customers table sitting in the designer surface. This will pop up a dialog like that in Figure 9, which has all the information necessary to link the two tables.
Figure 7 Dragging Customers and Orders onto typed DataSet.
Figure 8 XML toolbox options.
Figure 9 Relationship-building dialog.
Listing 5 shows the resulting XML schema document, generated and viewable from the XML tab of the XSD document. Microsoft extensions to the XML schema document are scoped using the msdata namespace to help identify the schema as belonging to a typed DataSet, as well as helping with the auto-increment OrderID field and the CustomerID primary and foreign keys. These attributes don't get in the way of interoperability with other platforms or environments; XML parsers and validators ignore the attributes that are not scoped by the XML Schema namespace.
Listing 5Final Typed DataSet Xml Schema
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?> <xs:schema id="CustOrders" targetNamespace="http://tempuri.org/CustOrders.xsd" elementFormDefault="qualified" attributeFormDefault="qualified" xmlns="http://tempuri.org/CustOrders.xsd" xmlns:mstns="http://tempuri.org/CustOrders.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xs:element name="CustOrders" msdata:IsDataSet="true"> <xs:complexType> <xs:choice maxOccurs="unbounded"> <xs:element name="Customers"> <xs:complexType> <xs:sequence> <xs:element name="CustomerID" type="xs:string" /> <xs:element name="CompanyName" type="xs:string" /> <xs:element name="ContactName" type="xs:string" minOccurs="0" /> <xs:element name="ContactTitle" type="xs:string" minOccurs="0" /> <xs:element name="Address" type="xs:string" minOccurs="0" /> <xs:element name="City" type="xs:string" minOccurs="0" /> <xs:element name="Region" type="xs:string" minOccurs="0" /> <xs:element name="PostalCode" type="xs:string" minOccurs="0" /> <xs:element name="Country" type="xs:string" minOccurs="0" /> <xs:element name="Phone" type="xs:string" minOccurs="0" /> <xs:element name="Fax" type="xs:string" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="Orders"> <xs:complexType> <xs:sequence> <xs:element name="OrderID" msdata:ReadOnly="true" msdata:AutoIncrement="true" type="xs:int" /> <xs:element name="CustomerID" type="xs:string" minOccurs="0" /> <xs:element name="EmployeeID" type="xs:int" minOccurs="0" /> <xs:element name="OrderDate" type="xs:dateTime" minOccurs="0" /> <xs:element name="RequiredDate" type="xs:dateTime" minOccurs="0" /> <xs:element name="ShippedDate" type="xs:dateTime" minOccurs="0" /> <xs:element name="ShipVia" type="xs:int" minOccurs="0" /> <xs:element name="Freight" type="xs:decimal" minOccurs="0" /> <xs:element name="ShipName" type="xs:string" minOccurs="0" /> <xs:element name="ShipAddress" type="xs:string" minOccurs="0" /> <xs:element name="ShipCity" type="xs:string" minOccurs="0" /> <xs:element name="ShipRegion" type="xs:string" minOccurs="0" /> <xs:element name="ShipPostalCode" type="xs:string" minOccurs="0" /> <xs:element name="ShipCountry" type="xs:string" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> <xs:unique name="CustOrdersKey1" msdata:PrimaryKey="true"> <xs:selector xpath=".//mstns:Customers" /> <xs:field xpath="mstns:CustomerID" /> </xs:unique> <xs:unique name="CustOrdersKey2" msdata:PrimaryKey="true"> <xs:selector xpath=".//mstns:Orders" /> <xs:field xpath="mstns:OrderID" /> </xs:unique> <xs:keyref name="CustOrders" refer="CustOrdersKey1"> <xs:selector xpath=".//mstns:Orders" /> <xs:field xpath="mstns:CustomerID" /> </xs:keyref> </xs:element> </xs:schema>
Once the schema document is added to the solution, the IDE defaults to automatically generating a typed DataSet class during project builds. The menu option for the XML schema document in Figure 10 and the Solution Explorer picture with the generated CustOrders.cs file in Figure 11 confirm this. The generated class file is visible in the Solution Explorer if you select the Show All Files icon, which is highlighted in blue in Figure 11.
Figure 10 Generate DataSet.
Figure 11 Strongly typed DataSet in Solution Explorer.
The strongly typed DataSet works in a similar manner to the regular DataSet. In the TypedDataSet form class in Listing 6, we use the predefined helper classes to load the DataSet using a DataAdapter in a manner identical to previous demos in this article. The main difference is the lack of linking; a typed DataSet comes pre-built with relationships. The code also demonstrates how to add a row, do navigation between tables, and pull property values from the rows manually (as contrasted with data binding). This demonstrates the ease of use and built-in error prevention. A screenshot of the demo form is depicted in Figure 12.
Listing 6TypedDataSet Form to Manipulate a Strongly Typed DataSet
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; namespace DisconnectedOpsIIDemo { /// <summary> /// Summary description for TypedDataSet. /// </summary> public class TypedDataSet : System.Windows.Forms.Form { [STAThread] static void Main(string[] args) { Application.Run(new TypedDataSet()); } private CustOrders TCustDS; private System.Windows.Forms.ListBox listBox1; private System.Windows.Forms.DataGrid dataGrid1; /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.Container components = null; public TypedDataSet() { // // Required for Windows Form Designer support // InitializeComponent(); // // TODO: Add any constructor code after // InitializeComponent call // } /// <summary> /// Clean up any resources being used. /// </summary> protected override void Dispose( bool disposing ) { if( disposing ) { if(components != null) { components.Dispose(); } } base.Dispose( disposing ); } #region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.listBox1 = new System.Windows.Forms.ListBox(); this.dataGrid1 = new System.Windows.Forms.DataGrid(); ((System.ComponentModel.ISupportInitialize) (this.dataGrid1)).BeginInit(); this.SuspendLayout(); // // listBox1 // this.listBox1.Location = new System.Drawing.Point(8, 176); this.listBox1.Name = "listBox1"; this.listBox1.Size = new System.Drawing.Size(496, 160); this.listBox1.TabIndex = 0; // // dataGrid1 // this.dataGrid1.DataMember = ""; this.dataGrid1.HeaderForeColor = System.Drawing.SystemColors.ControlText; this.dataGrid1.Location = new System.Drawing.Point(8, 8); this.dataGrid1.Name = "dataGrid1"; this.dataGrid1.Size = new System.Drawing.Size(496, 160); this.dataGrid1.TabIndex = 1; // // TypedDataSet // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(512, 349); this.Controls.AddRange( new System.Windows.Forms.Control[] { this.dataGrid1, this.listBox1}); this.Name = "TypedDataSet"; this.Text = "TypedDataSet"; this.Load += new System.EventHandler(this.TypedDataSet_Load); ((System.ComponentModel.ISupportInitialize) (this.dataGrid1)).EndInit(); this.ResumeLayout(false); } #endregion private void TypedDataSet_Load(object sender, System.EventArgs e) { // load the typed DataSet and bind it to // a DataGrid control TCustDS = new CustOrders(); DataAdapterUtility.LoadCustomers(TCustDS); DataAdapterUtility.LoadOrders(TCustDS); dataGrid1.DataSource = TCustDS.Customers; // add a customer CustOrders.CustomersRow newCustRow = TCustDS.Customers.AddCustomersRow( "ACME", "Acme Corp", "Dale Michalk", "No Title", "100 Main Street", "Utopia", "NY", "12345", "US", "555 555 5555","666 666 6666"); // add an order TCustDS.Orders.AddOrdersRow(newCustRow, 1, DateTime.Now, DateTime.Now, DateTime.Now, 1, 100.00M, "Airborne", "100 Main Street", "Utopia", "NY", "12345", "US"); // navigate through the typed DataSet foreach (CustOrders.CustomersRow custRow in TCustDS.Customers.Rows) { listBox1.Items.Add( "Customer: " + custRow.ContactName); CustOrders.OrdersRow[] orderRows = custRow.GetOrdersRows(); int orderCount = orderRows.Length; for (int index = 0; index < orderCount; index++) { listBox1.Items.Add( "\tOrder:" + orderRows[index].OrderID + " Date:" + orderRows[index].OrderDate); } } } } }
Figure 12 TypedDataSet form display.
You can see the man behind the curtain by cracking open the auto-generated CustOrders.cs class file. The InitClass of the CustOrders in Listing 7 is the most interesting because it orchestrates most of the initialization by creating the custom CustomersDataTable and OrdersDataTable members, as well as pre-wiring the constraints and relationships between them. Each table does its own work as well. Listing 8 shows how the CustomersDataTable InitClass method adds its column definitions. The rest of the details of the CustOrder class are left to the reader to spelunk and explore.
Listing 7Initializing the Strongly Typed DataSet
private void InitClass() { this.DataSetName = "CustOrders"; this.Prefix = ""; this.Namespace = "http://tempuri.org/CustOrders.xsd"; this.Locale = new System.Globalization.CultureInfo("en-US"); this.CaseSensitive = false; this.EnforceConstraints = true; this.tableCustomers = new CustomersDataTable(); this.Tables.Add(this.tableCustomers); this.tableOrders = new OrdersDataTable(); this.Tables.Add(this.tableOrders); ForeignKeyConstraint fkc; fkc = new ForeignKeyConstraint("CustOrders", new DataColumn[] { this.tableCustomers.CustomerIDColumn}, new DataColumn[] { this.tableOrders.CustomerIDColumn}); this.tableOrders.Constraints.Add(fkc); fkc.AcceptRejectRule = AcceptRejectRule.None; fkc.DeleteRule = Rule.Cascade; fkc.UpdateRule = Rule.Cascade; this.relationCustOrders = new DataRelation("CustOrders", new DataColumn[] { this.tableCustomers.CustomerIDColumn}, new DataColumn[] { this.tableOrders.CustomerIDColumn}, false); this.Relations.Add(this.relationCustOrders); }
Listing 8Initializing the Strongly Typed Customers Table
private void InitClass() { this.columnCustomerID = new DataColumn("CustomerID", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnCustomerID); this.columnCompanyName = new DataColumn("CompanyName", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnCompanyName); this.columnContactName = new DataColumn("ContactName", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnContactName); this.columnContactTitle = new DataColumn("ContactTitle", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnContactTitle); this.columnAddress = new DataColumn("Address", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnAddress); this.columnCity = new DataColumn("City", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnCity); this.columnRegion = new DataColumn("Region", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnRegion); this.columnPostalCode = new DataColumn("PostalCode", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnPostalCode); this.columnCountry = new DataColumn("Country", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnCountry); this.columnPhone = new DataColumn("Phone", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnPhone); this.columnFax = new DataColumn("Fax", typeof(string), null, System.Data.MappingType.Element); this.Columns.Add(this.columnFax); this.Constraints.Add( new UniqueConstraint("CustOrdersKey1", new DataColumn[] { this.columnCustomerID}, true)); this.columnCustomerID.AllowDBNull = false; this.columnCustomerID.Unique = true; this.columnCompanyName.AllowDBNull = false; }