Data and the Desktop
ASP.NET VERSIONS: 1.0 | 1.1| 2.0
Create Maintainable Business Object Collections
By Brian Noyes
When you design a program for .NET, it's sometimes difficult to decide whether to implement business layer classes for all the data objects and collections of objects your program needs, or to simply use DataSet objects to contain the data. After all, the state of those objects usually gets populated from some form of data store, such as a database or an XML file, and DataSets are easy to populate from either. DataSets are also especially attractive for binding to data-aware controls such as the .NET DataGrid classes, because those controls have built-in support for display and manipulation of multiple tables in a DataSet (although only the WinForms control supports hierarchical binding).
However, there are two big downsides to using DataSet objects instead of business object classes. The first is that by coding directly against the tables and fields of the data contained in a DataSet, you're tightly coupling your code to the underlying schema of the data you're working with. If the schema of the query or file you populated that DataSet with changes, your consuming code breaks and you won't have any indication of that breakage until run time.
The second big problem is that when you're working with raw DataSets, you're working with weakly typed data. When you read or write data from or to a DataSet, you use the Item property on the DataRow class (usually implicitly using the indexer on an instance of a DataRow). The Item property just returns an object reference to the underlying contents of the field indicated by the index or name passed to Item. As far as the compiler is concerned, you can try to stuff any data type into a field, or pull 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 typed environment.
Enter Typed DataSets
Using typed DataSets can address both of these problems to a large degree. Typed DataSets are classes in .NET that you generate using the framework tools from an XSD schema. You can create them most easily using the Visual Studio.NET design environment, but you can also create them from a command line using the xsd.exe SDK tool.
When you use the tools to create a typed DataSet, a set of classes is generated that gives you a strongly-typed data model for coding against the underlying data. This generated code file contains a top-level class that represents the DataSet itself. 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, within the row class for each table, there are named properties exposing each field as a strongly-typed data member. As a result, when you code against the properties of the typed DataSet and its contained class definitions, you'll get compile-time type checking for accessing the tables, rows, and fields of the DataSet with which you're working.
I've given you a concrete example of how to read and write data from the Orders and Order Detail tables from the Northwind sample database, which comes with SQL Server, to give you a better idea of how you code against a typed DataSet. To create a typed DataSet to contain both the Orders and Order Details table with a parent/child relationship between them, you first need an XSD schema. You can either handcraft one with the appropriate elements and types, or you can let VS.NET do the work for you.
Next you want to create SqlConnection and SqlDataAdapter objects. Open Server Explorer and either create a Data Connection to the Northwind database, or navigate to it through the Servers node. Open the Northwind Tables node so that the Orders and Order Details tables are visible. You will then need a design surface to drag them onto. Create a new component class in your project (use the Add Component feature from the Project menu), and drag the Orders table from the Server Explorer tree onto the design surface of the component class.
Now that you've created the SqlConnection and SqlDataAdapter objects, right-click on the SqlDataAdapter created and select Generate Dataset from the context menu. This brings up a dialog box (see Figure 1). Enter a name for the new DataSet, and an XSD schema file and underlying typed DataSet code file will be added to your project (see Figure 2). You'll need to select Show All Files in Solution Explorer to see the DataSet code file nested under the XSD file created for the typed DataSet. You can then repeat the process to add the Order Details table to the same typed DataSet by selecting Generate Dataset on the Order Details SqlDataAdapter.
Figure 1: After selecting Generate Dataset with a SqlDataAdapter selected in the designer, you can choose whether to create a new typed DataSet or to add the data from the DataAdapter to an existing DataSet.
Figure 2: A typed DataSet is generated from an XSD Schema file that contains element definitions for the tables the typed DataSet will contain. When you generate a DataSet from a DataAdapter in VS.NET, the XSD file is generated for you, along with a linked code file that contains the .NET class definitions to code against the typed DataSet.
If you inspect the code generated for you in the DataSet code file (nested under the XSD file in Solution Explorer), you'll see classes that define an object model (see Figure 3). You have two choices for adding a relationship between the Orders and Order Details tables within the typed DataSet. The first is to simply create the relation in code when you populate the DataSet by adding a DataRelation to the Relations collection on the DataSet. The second is to edit the XSD schema to add an XSD relation between the Orders and Order Details elements in the schema, based on the OrderID element in each table. Doing the latter automatically adds the code to the typed DataSet class, adding a DataRelation and a foreign key constraint between the columns of the two tables. This is the approach I use in the sample code.
Figure 3: A typed DataSet contains type definitions for the DataSet itself, each of the tables it contains, a type for the rows of the table, and properties on the row type to encapsulate the fields in strongly typed members.
Fill a Typed DataSet
To fill a typed DataSet once it's defined, you just call Fill on the SqlDataAdapter that's set up to populate 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 because the typed DataSet is, in fact, a DataSet itself through inheritance from the base class. The SqlDataAdapter doesn't know anything about your typed DataSet class, but it doesn't have to. It will use the methods and properties of your DataSet's base class to do business as usual on a DataSet. Behold the polymorphic power of inheritance.
public static OrdersDataSet GetOrders()
// Create DBObjects component to talk to database.
DBObjects dbo = new DBObjects();
// Construct the data set
OrdersDataSet ds = new OrdersDataSet();
public static void UpdateOrders(OrdersDataSet dsOrders)
DBObjects dbo = new DBObjects();
Figure 4: To fill a typed DataSet, just call SqlDataAdapter.Fill, passing in the DataSet reference and the name of the table. The table name will have to correspond to the name of the table in the XSD schema so that it matches the bindings in the typed DataSet code. To update it, just pass the DataSet or DataTable into the Update method of the SqlDataAdapter.
As I've mentioned, the typed DataSet derives from the DataSet as a base class, so you can easily access the underlying capabilities of the DataSet. For example, you might want to save or load the contents of your DataSet to or from an XML file. However, you should avoid using DataSet base class properties such as the Rows collection, 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 from the DataSet, the untyped approach would do this:
DataRow row = m_dsOrders.Tables["Orders"].Rows[i];
int orderId = (int)row["OrderID"];
With a typed DataSet, you can simply do this (notice that no casts are involved, and no hard-coded schema name values exist):
int orderId = m_dsOrders.Orders[i].OrderID;
The download code contains a sample application using the typed DataSet I've just described to retrieve orders and order details from the Northwind database. It binds the Orders table to a grid, and allows you to add order items to an order. It uses the strongly-typed properties to populate the new rows of order details (see Figure 5). The sample application also demonstrates that you can generate DataSets from stored procedures just as easily as you can for raw tables.
private void btnAdd_Click(object sender,
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);
// Get the data set out of session.
OrdersDataSet dsOrders = Session["OrdersDataSet"]
// Now populate a new row for the DataSet.
OrdersDataSet.OrderDetailsRow newRow =
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.
Figure 5: You can access fields in a typed DataSet through the strongly-typed properties of the OrderDetailsRow class, which is derived from DataRow and defined as a nested class of the OrdersDataSet class.
The sample uses an enhanced version of the Order Details table that contains the ProductName as well as the ProductID for presentation purposes. To achieve this, the Order Details table within the OrdersDataSet was created based on a stored procedure instead of a raw table. The steps to add the results of the stored procedure as a table to the DataSet are quite similar to what I described before. The only difference is that the SqlDataAdapter that you use to add the table to the DataSet is one you create by dragging the stored procedure out to the designer surface from Server Explorer, rather than dragging a table out. I also had to handcraft a SqlCommand object to perform the inserts for new Order Detail items so that it would ignore the ProductName, and associated that command object with the InsertCommand property for the SqlDataAdapter.
When you add a table to a typed DataSet from a SqlDataAdapter that uses a stored procedure 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 to change this to something else (such as CustomersTable), just edit the top-level element name for the table in the XSD file to set it to whatever 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 the data's underlying schema. At this point, you might be thinking that with typed DataSets, your code is just as coupled to the underlying schema as before. And you would be right. However, typed DataSets have a distinct advantage in managing changes to the underlying schema, resulting directly from the fact that they are typed.
The advantage comes from the fact that if the schema changes, you can simply regenerate the typed DataSet code using 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 affected in the form of compiler errors. Because of changing schemas, you're able to make the required corrections faster, and with fewer errors, than if you had to try to root out all the corrections needed when programming against the untyped DataSet. So with a typed DataSet, you're still tightly coupled to the underlying schema, but it becomes 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 well to the logical constructs of your application, or are not within your development team's control, you might want to consider defining business object classes that are completely decoupled from the underlying schema, using a data mapping pattern to populate the business object state from data.
The other thing to keep in mind is that you should avoid directly modifying the machine-generated code for the DataSet. If you do modify the source directly, you'll need to keep close track of the changes you made so that you can integrate those changes when you regenerate the code. In some cases you might be able to derive classes from the typed DataSet and nested classes to modify their behavior. In most cases, you'll be better off wrapping the typed DataSet in a container class, and having the container class expose the modifications you need.
Unfortunately, there are still situations where you will have to deal with late bound access to 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 working with your data through a DataView for sorting or filtering, you'll have to step back into the late bound access by field name or index, as with untyped DataSets. The other place is when you're dealing with data binding with windows or Web form controls. Data binding in .NET is done in a late-bound fashion, and in many cases you have to pass a field name as a string to specify the column to bind to.
Typed DataSets provide a clean coding model for working with DataSets that can improve the maintainability of your code through strong typing and easy re-creation of the DataSet code when the underlying schema changes. They enable you to achieve the strongly typed and object-oriented benefits of custom business object collections, without sacrificing the ease and flexibility of the DataSet object. They are not a panacea for all scenarios, but I recommend you always try to use typed DataSets instead of raw DataSets, except for toy projects or localized uses of a DataSet.
The sample code in this article is available for download.
Brian Noyes is a software architect with IDesign, Inc. (http://www.idesign.net), a .NET-focused architecture and design consulting firm. Brian specializes in designing and building data-driven distributed Windows and Web applications with .NET. He has more than 12 years of experience in programming, engineering, and project management, and is a contributing editor for asp.netPRO, and other publications. Contact him at mailto:[email protected].