Dataand the Desktop
LANGUAGES: C#
ASP.NET VERSIONS: 1.0 | 1.1| 2.0
Typed DataSets
Create Maintainable Business Object Collections
By Brian Noyes
When youdesign a program for .NET, it's sometimes difficult to decide whether toimplement business layer classes for all the data objects and collections ofobjects your program needs, or to simply use DataSet objects to contain the data. After all, the state of thoseobjects usually gets populated from some form of data store, such as a databaseor an XML file, and DataSets areeasy to populate from either. DataSetsare also especially attractive for binding to data-aware controls such as the.NET DataGrid classes, because thosecontrols have built-in support for display and manipulation of multiple tablesin a DataSet (although only the WinForms control supports hierarchical binding).
However,there are two big downsides to using DataSetobjects instead of business object classes. The first is that by codingdirectly against the tables and fields of the data contained in a DataSet, you're tightly coupling yourcode to the underlying schema of the data you're working with. If the schema ofthe query or file you populated that DataSetwith changes, your consuming code breaks and you won't have any indication ofthat breakage until run time.
Thesecond big problem is that when you're working with raw DataSets, you're working with weakly typed data. When you read orwrite data from or to a DataSet, youuse the Item property on the DataRow class (usually implicitly usingthe indexer on an instance of a DataRow).The Item property just returns anobject reference to the underlying contents of the field indicated by the indexor name passed to Item. As far asthe compiler is concerned, you can try to stuff any data type into a field, orpull any type out, because all data types in .NET derive from the Object base class. Using this approach,therefore, you won't be able to detect type incompatibilities until run time -a situation you want to avoid now that you're working in .NET's strongly typedenvironment.
Enter Typed DataSets
Usingtyped DataSets can address both ofthese problems to a large degree. Typed DataSetsare classes in .NET that you generate using the framework tools from an XSDschema. You can create them most easily using the Visual Studio.NET designenvironment, but you can also create them from a command line using the xsd.exeSDK tool.
When youuse the tools to create a typed DataSet,a set of classes is generated that gives you a strongly-typed data model forcoding against the underlying data. This generated code file contains atop-level class that represents the DataSetitself. Within that class there are nested class types for each table the DataSet contains. Within those classes,there are nested type definitions for the rows of the table. Finally, withinthe row class for each table, there are named properties exposing each field asa strongly-typed data member. As a result, when you code against the propertiesof the typed DataSet and its containedclass definitions, you'll get compile-time type checking for accessing thetables, rows, and fields of the DataSetwith which you're working.
I'vegiven you a concrete example of how to read and write data from the Orders andOrder Detail tables from the Northwind sample database, which comes with SQLServer, to give you a better idea of how you code against a typed DataSet. To create a typed DataSet to contain both the Orders andOrder Details table with a parent/child relationship between them, you firstneed an XSD schema. You can either handcraft one with the appropriate elementsand types, or you can let VS.NET do the work for you.
Next youwant to create SqlConnection and SqlDataAdapter objects. Open ServerExplorer and either create a Data Connection to the Northwind database, ornavigate to it through the Servers node. Open the NorthwindTables node so that the Orders and OrderDetails tables are visible. You will then need a design surface to drag themonto. Create a new component class in your project (use the Add Component feature from the Project menu), and drag theOrders table from the Server Explorer tree onto the design surface of thecomponent class.
Now thatyou've created the SqlConnection andSqlDataAdapter objects, right-clickon the SqlDataAdapter created andselect Generate Dataset from the context menu. This brings up a dialog box (seeFigure 1). Enter a name for the new DataSet,and an XSD schema file and underlying typed DataSet code file will be added to yourproject (see Figure 2). You'll need to select ShowAll Files in Solution Explorer to see the DataSet code file nested under the XSDfile created for the typed DataSet.You can then repeat the process to add the Order Details table to the sametyped DataSet by selecting Generate Dataset on the OrderDetails SqlDataAdapter.
Figure 1: After selecting Generate Dataset with a SqlDataAdapterselected in the designer, you can choose whether to create a new typed DataSet or to add the data from theDataAdapter to an existing DataSet.
Figure 2: A typed DataSetis generated from an XSD Schema file that contains element definitions for thetables the typed DataSet willcontain. When you generate a DataSetfrom a DataAdapter in VS.NET, the XSD file is generated for you, along with alinked code file that contains the .NET class definitions to code against thetyped DataSet.
If youinspect the code generated for you in the DataSetcode file (nested under the XSD file in Solution Explorer), you'll see classesthat define an object model (see Figure 3). You have two choices for adding arelationship between the Orders and Order Details tables within the typed DataSet. The first is to simply createthe relation in code when you populate the DataSetby adding a DataRelation to the Relations collection on the DataSet. The second is to edit the XSDschema to add an XSD relation between the Orders and Order Details elements inthe schema, based on the OrderID element in each table. Doing the latterautomatically adds the code to the typed DataSetclass, adding a DataRelation and aforeign key constraint between the columns of the two tables. This is theapproach I use in the sample code.
Figure 3: A typed DataSetcontains type definitions for the DataSetitself, each of the tables it contains, a type for the rows of the table, andproperties on the row type to encapsulate the fields in strongly typed members.
Fill a TypedDataSet
To fill atyped DataSet once it's defined, youjust call Fill on the SqlDataAdapter that's set up topopulate the table (see Figure 4). Likewise, updating the database from the DataSet simply uses the Update method of the SqlDataAdapter, passing in the DataSet reference. This works becausethe typed DataSet is, in fact, a DataSet itself through inheritance fromthe base class. The SqlDataAdapterdoesn't know anything about your typed DataSetclass, but it doesn't have to. It will use the methods and properties of your DataSet's baseclass to do business as usual on a DataSet.Behold the polymorphic power of inheritance.
public static OrdersDataSet GetOrders()
{
// Create DBObjects component to talk todatabase.
DBObjects dbo = new DBObjects();
// Construct the data set
OrdersDataSet ds = new OrdersDataSet();
dbo.OrdersDataAdapter.Fill(ds,"Orders");
dbo.OrderDetailsEnhancedDataAdapter.Fill(
ds, "OrderDetails");
return ds;
}
public static void UpdateOrders(OrdersDataSet dsOrders)
{
DBObjects dbo = new DBObjects();
dbo.OrderDetailsEnhancedDataAdapter.Update(
dsOrders.OrderDetails);
}
Figure 4: To fill a typed DataSet,just call SqlDataAdapter.Fill,passing in the DataSet reference andthe name of the table. The table name will have to correspond to the name ofthe table in the XSD schema so that it matches the bindings in the typed DataSet code. To update it, just passthe DataSet or DataTable into the Updatemethod of the SqlDataAdapter.
As I'vementioned, the typed DataSet derivesfrom the DataSet as a base class, soyou can easily access the underlying capabilities of the DataSet. For example, you might want to save or load the contentsof your DataSet to or from an XMLfile. However, you should avoid using DataSetbase class properties such as the Rowscollection, because they give you type-unsafe access to the underlying data,negating some of the benefits of the typed DataSet.So, for example, if you were trying to extract the OrderID value for a row fromthe DataSet, the untyped approachwould do this:
DataRow row =m_dsOrders.Tables["Orders"].Rows[i];
int orderId = (int)row["OrderID"];
With atyped DataSet, you can simply dothis (notice that no casts are involved, and no hard-coded schema name valuesexist):
int orderId = m_dsOrders.Orders[i].OrderID;
Thedownload code contains a sample application using the typed DataSet I've just described to retrieveorders and order details from the Northwind database. It binds the Orders tableto a grid, and allows you to add order items to an order. It uses thestrongly-typed properties to populate the new rows of order details (see Figure5). The sample application also demonstrates that you can generate DataSets from stored procedures just aseasily as you can for raw tables.
private void btnAdd_Click(object sender,
System.EventArgs e)
{
int productId;
decimal unitPrice;
short quantity;
float discount;
try // Extract the values from form controls.
{
productId = int.Parse(cmbProduct.SelectedItem.Value);
unitPrice = decimal.Parse(txtUnitPrice.Text);
quantity = short.Parse(txtQuantity.Text);
discount = float.Parse(txtDiscount.Text);
}
catch (Exception ex)
{
Response.Write("Invalid entry:"+ex.Message);
return;
}
// Get the data set out of session.
OrdersDataSet dsOrders =Session["OrdersDataSet"]
as OrdersDataSet;
// Now populate a new row for the DataSet.
OrdersDataSet.OrderDetailsRow newRow =
dsOrders.OrderDetails.NewOrderDetailsRow();
newRow.OrderID = m_parentOrderId;
newRow.ProductID = productId;
newRow.ProductName =cmbProduct.SelectedItem.Text;
newRow.UnitPrice = unitPrice;
newRow.Quantity = quantity;
newRow.Discount = discount;
// Add it to the DataSet.
dsOrders.OrderDetails.AddOrderDetailsRow(newRow);
Server.Transfer("OrderForm.aspx");
}
Figure 5: You can access fields in a typed DataSet through the strongly-typed properties of the OrderDetailsRow class, which is derivedfrom DataRow and defined as a nestedclass of the OrdersDataSet class.
Thesample uses an enhanced version of the Order Details table that contains theProductName as well as the ProductID for presentation purposes. To achievethis, the Order Details table within the OrdersDataSetwas created based on a stored procedure instead of a raw table. The steps toadd the results of the stored procedure as a table to the DataSet are quite similar to what I described before. The onlydifference is that the SqlDataAdapterthat you use to add the table to the DataSetis one you create by dragging the stored procedure out to the designer surfacefrom Server Explorer, rather than dragging a table out. I also had to handcrafta SqlCommand object to perform theinserts for new Order Detail items so that it would ignore the ProductName, andassociated that command object with the InsertCommandproperty for the SqlDataAdapter.
When youadd a table to a typed DataSet froma SqlDataAdapter that uses a storedprocedure to retrieve the data, the table in the DataSet will be named the same as the stored procedure with "Table"appended to it. For example, if the stored procedure is named GetCustomers, the resulting typed DataSet table is named GetCustomersTable. If you want tochange this to something else (such as CustomersTable),just edit the top-level element name for the table in the XSD file to set it towhatever you'd like. In this example, you would change the element name from GetCustomers to Customers.
Maintaining Typed DataSets
Earlier,I mentioned that one of the downsides to using basic DataSets was the fact that your consuming code is coupled to thedata's underlying schema. At this point, you might be thinking that with typed DataSets, your code is just as coupledto the underlying schema as before. And you would be right. However, typed DataSets have a distinct advantage inmanaging changes to the underlying schema, resulting directly from the factthat they are typed.
Theadvantage comes from the fact that if the schema changes, you can simplyregenerate the typed DataSet codeusing VS.NET or the xsd.exe tool. Once you do that and recompile your project,you'll get immediate, precise feedback about which lines of code are affectedin the form of compiler errors. Because of changing schemas, you're able tomake the required corrections faster, and with fewer errors, than if you had totry to root out all the corrections needed when programming against the untypedDataSet. So with a typed DataSet, you're still tightly coupled to the underlying schema, but itbecomes a little easier to tolerate that fact from a maintenance standpoint.However, if you code in a world where the schemas are volatile, do not map wellto the logical constructs of your application, or are not within yourdevelopment team's control, you might want to consider defining business objectclasses that are completely decoupled from the underlying schema, using a datamapping pattern to populate the business object state from data.
The otherthing to keep in mind is that you should avoid directly modifying themachine-generated code for the DataSet.If you do modify the source directly, you'll need to keep close track of thechanges you made so that you can integrate those changes when you regeneratethe code. In some cases you might be able to derive classes from the typed DataSet and nested classes to modifytheir behavior. In most cases, you'll be better off wrapping the typed DataSet in a container class, andhaving the container class expose the modifications you need.
Unfortunately,there are still situations where you will have to deal with late bound accessto the data in your typed DataSets.One is when you're working with DataViews. There is no corresponding typed DataView class, so if you're workingwith your data through a DataView for sorting or filtering, you'll have to stepback into the late bound access by field name or index, as with untyped DataSets. The other place is whenyou're dealing with data binding with windows or Web form controls. Databinding in .NET is done in a late-bound fashion, and in many cases you have topass a field name as a string to specify the column to bind to.
Conclusion
Typed DataSets provide a clean coding modelfor working with DataSets that canimprove the maintainability of your code through strong typing and easyre-creation of the DataSet code whenthe underlying schema changes. They enable you to achieve the strongly typedand object-oriented benefits of custom business object collections, withoutsacrificing the ease and flexibility of the DataSet object. They are not a panacea for all scenarios, but Irecommend you always try to use typed DataSetsinstead of raw DataSets, except fortoy projects or localized uses of a DataSet.
The sample code in thisarticle is available for download.
BrianNoyes is a software architect with IDesign, Inc. (http://www.idesign.net), a .NET-focusedarchitecture and design consulting firm. Brian specializes in designing andbuilding data-driven distributed Windows and Web applications with .NET. He hasmore than 12 years of experience in programming, engineering, and projectmanagement, and is a contributing editor for asp.netPRO,and other publications. Contact him at mailto:brian.noyes@idesign.net.