Cutting Edge

Collections and Data Binding

Dino Esposito

Code download available at:CuttingEdge0505.exe(159 KB)

Contents

Collection Basics
Fill the Collection
A Word on Generics
Binding to ASP.NET Controls
Binding to Windows Forms
Binding to Web Services
Wrap Up

When it's time to design the Data Access Layer (DAL) of your distributed Microsoft® .NET Framework-based app, one of the key decisions you'll make is how you'll pass data to and from methods of DAL classes. There are quite a few options for passing business data to and from DAL classes, but in most real-world cases the choices boil down to just two—using DataSet objects or collections of custom business entity objects. The "Patterns and Practices" series of articles and books provide some guidance on the most commonly implemented practices. For instance, issues related to passing data through data tiers are fully dissected in the article at Designing Data Tier Components and Passing Data Through Tiers. For a more complete list, see Microsoft Patterns.

DataSets have built-in support for optimistic concurrency and the ability to define and handle complex relationships between tables. DataSets are also serializable and don't usually require changes to the DAL API should the relevant database schema change. Similarly, collections of custom objects support serialization and don't require any method signature change if the database schemas are modified. Collections also circumvent performance penalties that DataSets can incur. For example, DataSet marshaling can be problematic when thousands of data rows are involved (for details see Cutting Edge in the December 2002 and October 2004 issues of MSDN®Magazine). DataSets are particularly suited to moving small to medium sized sets of data; for methods mostly needing instance data and scalar values, an approach based on custom business entities is perhaps preferable.

Data that DAL classes retrieve will often be bound to Windows® and Web Forms controls and also will often be used to serve Web services requests. I've found out that the majority of books and articles out there (including mine) tend to cover mostly data binding from the DataSet perspective. Here I'll cover data binding with custom collections from all perspectives—Windows Forms, Web Forms, and Web services. But before I go any further, a quick refresher on collections in the .NET Framework 1.x, and a glimpse of generics in the .NET Framework 2.0, are in order.

Collection Basics

A collection is a set of homogeneous objects that are managed through the interface of a container class. A collection is a typed entity; subsequently, all contained objects agree to the same declared type. .NET defines a number of different kinds of collections; each maps to a different kind of data structure—queue, stack, list, hashtable, and more. Let's compare the various collection classes.

The .NET Framework comes with plenty of collection classes, and they may all look alike at first glance. So how can you be sure to get the right one for your task? The more specialized a collection is, the more limited it is, but also the better it is for particular situations. Figure 1 lists the most common collection classes used in the .NET Framework 1.x. For each class, you can see the main interface it implements and the source namespace. Figure 2 details the characteristics of the interfaces behind collections.

Figure 2 Collection Interfaces in the .NET Framework 1.x

Interface Description
IEnumerable Defines methods to support a simple iteration over a collection
ICollection Adds size, copy, and synchronization capabilities to enumerable collections
IList Defines a collection of objects that can be individually accessed by index
IDictionary Represents a collection of key/value pairs

Figure 1 Collection Classes in the .NET Framework 1.x

Class Interface Namespace Description
Queue ICollection System.Collections A first-in, first-out (FIFO) collection of objects
Stack ICollection System.Collections A last-in, first-out (LIFO) collection of objects
ArrayList IList System.Collections A weakly typed list of objects
StringCollection IList System.Collections.Specialized A strongly typed list of strings
Hashtable IDictionary System.Collections A collection of key/value pairs that are organized based on the hash code of the key
StringDictionary IDictionary System.Collections.Specialized A hashtable with the key strongly typed as String rather than as Object
SortedList IDictionary System.Collections A sorted collection of key/value pairs
ListDictionary IDictionary System.Collections.Specialized Singly linked list designed for collections that typically contain 10 items or fewer
NameValueCollection IDictionary System.Collections.Specialized A sorted collection of associated string keys and string values that can be accessed either with a key or with an index

A collection is an enumerable list of objects, meaning that it implements the IEnumerable interface. The interface basically exposes an enumerator object to make for-each constructs work, and more specifically to get the members of the enumerable in a generic way. In addition to IEnumerable, collections implement additional interfaces such as ICollection or IList to supply additional capabilities. For example, ICollection adds the Count property and CopyTo method for copying values to an external array. Note that collection classes are generally not thread-safe, though synchronization can optionally be provided through properties defined in the ICollection interface.

IList extends ICollection with Add and Remove methods. IList also adds the Item property, which allows indexed, array-like access to items. The IDictionary interface defines an API that represents a collection of key/value pairs. IDictionary exposes methods similar to those in IList, but with different signatures. All dictionaries feature properties such as Keys and Values.

Array, ArrayList, and StringCollection are examples of collection classes available in the .NET Framework. Typical dictionary classes are ListDictionary, Hashtable, and SortedList. Figure 3 provides some tips for choosing the .NET Framework 1.x collection that will meet your needs.

Figure 3 Choosing Your Collection Class

Class Description
Queue and Stack Ideal in a scenario where you receive data and need to process data one element at a time in a specific order.
Arrays The most efficient tools for fixed-size, indexed collections. Note that for one-dimensional arrays, compilers use built-in Microsoft intermediate language (MSIL) instructions to process them quickly and more effectively.
ArrayList An array-like growable alternative to fixed-size arrays that can store any kind of object. Note that many publicly available collection classes use an ArrayList internally and just create a more friendly programming interface around it.
StringCollection A better choice than ArrayList for a dynamic size array of strings, because it provides a strongly typed interface.
Hashtable and ListDictionary Good options when key-based access is required. You might want to choose ListDictionary to handle a small number of items (20 or fewer) and opt for Hashtable for greater numbers.
HybridDictionary A wrapper class that begins as a ListDictionary and automatically switches to Hashtable when the size grows beyond the critical threshold. (Used by some internal ASP.NET classes.)
StringDictionary A better choice than Hashtables as long as your collections contains only strings.
NameValueCollection The slowest collection. You should use it only if you want to provide both indexed and key-based access to data.
SortedList A hybrid of a Hashtable and an array. It behaves like a Hashtable when you access an element by key and mimics an array if you use an index. You should use this class only if you need to maintain the sorted order.

How do you build your own collection class for data binding purposes? A custom collection is a strongly typed collection that exposes objects of a particular type. In ASP.NET server controls, there are lots of custom collections. For example, the type of the Items property that many list controls feature is ListItemCollection, which implements ICollection. To create a custom collection, you could write a class that implements ICollection, but a better option is to inherit from CollectionBase, as shown in Figure 4 for the sample OrderCollection class.

Figure 4 The OrderCollection Class

using System; using System.Collections; namespace Msdn.CuttingEdge.Samples { public class OrderCollection : CollectionBase { // Class constructor public OrderCollection() {} // Add method public void Add(OrderInfo o) { InnerList.Add(o); } // Indexer property public OrderInfo this[int index] { get { return (OrderInfo) InnerList[index]; } set { InnerList[index] = value; } } } }

The OrderCollection class groups instances of the OrderInfo class, which in turn collects a bunch of public properties describing an order, as in the SQL Server™ Northwind database. Figure 5 shows a simple implementation of the OrderInfo class. It's simple because it only exposes some of the fields in the Orders table. In a realistic scenario, you probably want to map foreign keys to actual names (employee and customer) as well as optionally drill down in the query to get a child collection of order details.

Figure 5 The OrderInfo Class

using System; using System.ComponentModel; namespace Msdn.CuttingEdge.Samples { [Serializable] public class OrderInfo { private int m_id; private DateTime m_date; private int m_employeeID; private string m_customerID; public OrderInfo() {} public OrderInfo(int id) { m_id = id; } public OrderInfo(int id, DateTime date) : this(id) { m_date = date; } public int ID { get {return m_id;} } public DateTime Date { get {return m_date;} set {m_date = value;} } public string CustomerID { get {return m_customerID;} set {m_customerID = value;} } public int EmployeeID { get {return m_employeeID;} set {m_employeeID = value;} } } }

Fill the Collection

In Figure 5, the collection supplies no fill method. You can use (optionally static) methods on an accessor class to physically load data into a new instance of the collection or extend the collection's constructor to accept any parameter required to perform data access, as shown in the following line of code:

OrderCollection orders = new OrdersCollection(year);

By incorporating some data awareness into the collection, you get tighter coupling between data containers and data accessors; on the bright side, though, you probably get better code abstraction. Put another way, it would be like having the DataSet class expose a method to accept a query or stored procedure name. As you know, this is not the case in the .NET Framework, where adapters act as decoupled data accessors and provide the fill capabilities.

While building data accessors, it's usually a good idea to employ a set of data access helper components. These components centralize common data access tasks such as the management of database connections, the handling of parameters, the execution of SQL commands, and stored procedures. An excellent example of a similar layer is the Microsoft Data Access Application Block.

A Word on Generics

Collection troubles disappear in the .NET Framework 2.0, because that version introduces generics. Generics offer classes that implement ICollection and other interfaces in a strongly typed way with a type dynamically provided by the programmer. Generic classes share some characteristics with collections; for example, collections behave the same way regardless of the type of contained objects. To build a collection in the .NET Framework 2.0, you need one line of code indicating one of a variety of collection types—enumerable, collection, list, dictionary, or binding list:

public class OrderCollection : Collection<OrderInfo> {}

Collections built on generics provide a unique combination of reusability, type safety, and efficiency in a way that classic .NET Framework 1.x collections could not. The .NET Framework 2.0 supplies a new namespace—System.Collections.Generic—which contains several new generics-based collection classes (see Figure 6). The list doesn't include the most interesting class—BindingList<T>; that's located in the System.ComponentModel namespace. In addition to the basic functionalities of a collection, BindingList<T> supplies a framework for you to code typical binding functions such as sorting, searching, and CRUD (create-read-update-delete) operations. Thus it is the favorite class for binding tasks in Windows and ASP.NET 2.0 applications when ADO.NET containers, like DataSet and DataTable, are not going to be used.

Figure 6 Generics-Powered Collection-Related Classes in .NET

Generic Class Description Non-Generic Counterpart
BindingList<T> List that supports complex data-binding scenarios None
Collection<T> Provides the base class for a generic collection CollectionBase
Comparer<T> Compares two objects of the same generic type for sorting Comparer
Dictionary<K, V> A collection of key/value pairs Hashtable
IEnumerable<T> A collection that can be iterated using a for-each statement IEnumerable
KeyedCollection<T, U> A keyed collection KeyedCollection
LinkedList<T> A doubly linked list None
List<T> The base class for a list ArrayList
Queue<T> A FIFO collection of objects Queue
ReadOnlyCollection<T> The base class for a generic read-only collection ReadOnlyCollectionBase
SortedDictionary<K, V> A collection of key/value pairs sorted by key SortedList
Stack<T> A simple LIFO collection of objects Stack

Binding to ASP.NET Controls

Imagine you a have a set of DAL components that can return a collection of orders issued in a given year. You get this information through the following code and bind it to a data-bound control:

OrderCollection orders = Helpers.LoadOrders(1997); DataGrid1.DataSource = orders; DataGrid1.DataBind();

The grid defines its columns as follows:

<Columns> <asp:BoundColumn DataField="ID" HeaderText="ID" /> <asp:BoundColumn DataField="Date" HeaderText="Date" DataFormatString="{0:d}" /> </Columns>

If you're familiar with the Northwind database, you know that the Orders table doesn't have columns labeled "ID" or "Date". The DataField attribute of the grid's columns reference public properties on the OrderInfo class rather than columns on an Orders table resultset. The type of the data members in the bound collection is OrderInfo.

For binding purposes, it's essential that collection members expose public properties and not public fields. More precisely, if you redefine the OrderInfo class you saw in Figure 5 to look like the following code, you would get an HTTP exception:

public class OrderInfo { public int ID; public DateTime Date; public int EmployeeID; public string CustomerID; }

The error message explains that no field or property named "ID" has been found on the selected data source. This is due to the fact that fields are not supported in .NET data binding, even though fields may be marked as public in the class definition. So you have to resort to properties. The difference between fields and properties is that a field is a public property that provides callers with direct, read/write access to the underlying data, while a property is a public property where access to the underlying data is filtered by get/set accessors. If you're still interested in binding to fields, see the .NET Matters column in this issue and in the April 2005 MSDN Magazine.

Once you build your custom business entities to expose properties, ASP.NET data binding is automatic, and even paging won't pose any significant problems. But what about sorting?

Generally speaking, ASP.NET grids sort their data in one of two ways: either by using a new database query with an ORDER BY clause or by using a DataView object. The former approach doesn't require Web server resources and is probably the fastest technique for sorting. After all, the SQL Server sorting algorithms are very well optimized. However, making a request back to SQL Server for sorted data is not necessarily a smart move because network latency, query execution, and data marshaling time affect the overall performance. On the other hand, if you sort on the Web server using an ADO.NET DataView object, or a sortable array or collection, you may tax the Web server's memory unless you cache sorted views for further use. (See the sidebar "Sorting in the .NET Framework.")

This goes to show that in spite of the effectiveness of the employed algorithm, your sorting decisions shouldn't be taken lightly. The approach to sorting should consider binding and paging. In general terms, caching is usually a good answer to paging and sorting issues. The bottom line is that, for high performance and highly scalable applications, you might want to consider storing presorted copies of the same data, either from SQL Server or sorted once through a DataView or an array sort method. Of course, there can be drawbacks to caching, such as stale data and filling up cache pages with infrequently used data.

To sort through a DataView, you must have data stored in a DataTable object. To sort a custom collection that internally stores its data in an array or in an ArrayList, you'll normally use the Array.Sort static method or the ArrayList.Sort instance method, making use of a custom comparer implementation or relying on the contained objects implementation of the IComparable interface. Figure 7 shows an enhanced version of the OrderCollection class that supports sorting. The new version of this class features a new Sort method which takes a sort expression (typically, a property name) and a value indicating the sort direction. These arguments are used to initialize a comparer class:

public class OrderComparer : IComparer { public OrderComparer(string sortExpr, OrderSortDirection direction) { ... } ... }

Figure 7 Sort-Enabled Version of OrderCollection

public class OrderCollection : OrderCollectionBase { // Class constructor public OrderCollection() {} // Add sort capabilities public void Sort(string sortExpression, OrderSortDirection direction) { SortInternal(sortExpression, direction); } protected virtual void SortInternal( string sortExpression, OrderSortDirection direction) { OrderInfo[] rgOrders = new OrderInfo[this.Count]; this.InnerList.CopyTo(rgOrders); Array.Sort(rgOrders, new OrderComparer(sortExpression, direction)); Reload(rgOrders); } protected void Reload(OrderInfo[] rgOrders) { this.InnerList.Clear(); InnerList.AddRange(rgOrders); } }

The IComparer interface features a single method—Compare—which is used to order a pair of elements in the array. In this case, the elements are two instances of OrderInfo. The value of the specified property is retrieved via the PropertyDescriptor class and compared to return an integer value. For more details, see the code download available at the MSDN Magazine Web site.

As a result, you can now handle the SortCommand event on the DataGrid and sort the bound collection. Here's a code snippet:

void SortCommand(object source, DataGridSortCommandEventArgs e) { BindSortedData(e.SortExpression); }

Sorting in the .NET Framework

Sorting is not a quick operation. Computationally speaking, O(N*LogN) comparison operations is typically the best performance you can get from a general-purpose sort algorithm. Let's delve deeper in the internal implementation of sorting in the ADO.NET DataView object and arrays.

The DataView class works on top of a DataTable and maintains an index of row views optionally filtered by an expression and sorted by a combination of columns. The index is updated each and every time a new filter or sort expression is specified. References to the selected table rows are cached in an array of DataRowView objects. The array is sorted using an implementation of the QuickSort algorithm.

Arrays also sort using the QuickSort algorithm. The implementation of QuickSort is a bit different in the two cases. Arrays attempt to optimize the algorithm on the type of contained objects and do a lot more of exception handling and static checks that the DataView structure and internal design makes unnecessary.

As mentioned, the QuickSort algorithm makes O(N*LogN) comparisons on average to sort N items. In the worst case, though, it makes O(N*N) comparisons. The QuickSort's worst case is when all items are already sorted in reverse order. QuickSort is significantly faster in practice than other O(N*LogN) algorithms because its inner loop can be efficiently implemented on a variety of architectures.

The helper method retrieves the collection from the ASP.NET cache and sorts it on the specified column and direction. Next, it caches the sorted version and binds to the grid, as you can see in the code in Figure 8.

Figure 8 Sorting and Binding

void BindSortedData(string sortExpression) { OrderCollection orders = (OrderCollection) Cache["MyData"]; if (orders == null) { LoadData(); orders = (OrderCollection) Cache["MyData"]; } orders.Sort(sortExpression, OrderSortDirection.Descending); Cache["MyData"] = orders; DataGrid1.DataSource = orders; DataGrid1.DataBind(); }

Figure 9 shows a fully functional ASP.NET grid bound to an instance of the OrderCollection class.

Figure 2 Sortable Grid

Figure 2** Sortable Grid **

To finish the ASP.NET binding discussion, let's review what happens with update and delete statements. Much like core binding, in-place editing is not really affected by the type of the enumerable object you use as the data source. Button columns that you would use for Edit and Delete operations work as expected and there's no other issue to face with ADO.NET objects as far as presentation is concerned.

You have two options for updating the back-end data store: performing an update to the Web server cache or performing a direct update on the database. The first option is appropriate if you're using cached data; it requires that you also supply a Submit button (or a timed mechanism) to let users persist changes periodically. Any Web server failures that occur in the meantime between two successive submissions cause a loss of data. When the user clicks to edit and then update a grid's row, the changes are persisted only in the ASP.NET cache (see Figure 10).

Figure 10 Editing and Updating a Bound Collection

void DataGrid1_UpdateCommand(object source, DataGridCommandEventArgs e) { // Get order being edited OrderCollection orders = RetrieveData(); OrderInfo currentOrder = orders[e.Item.DataSetIndex]; // Get textboxes TextBox txtOrderDate = e.Item.Cells[2].Controls[0] as TextBox; TextBox txtEmployeeID = e.Item.Cells[3].Controls[0] as TextBox; if (txtOrderDate != null) currentOrder.Date = DateTime.Parse(txtOrderDate.Text); if (txtEmployeeID != null) currentOrder.EmployeeID = Int32.Parse(txtEmployeeID.Text); DataGrid1.EditItemIndex = -1; BindData(); }

To fire a direct update to the database whenever the user exits edit mode, you might want to call a method on the DAL that takes an OrderInfo filled with fresh values and uses its contents to prepare and execute a stored procedure or a SQL command.

In the end, ASP.NET data binding doesn't change that much if you use custom collections in lieu of classic ADO.NET objects. Binding is nearly identical, even if from a template perspective. Data-bound expressions look the same regardless of the type of data source; the field name parameter of DataBinder.Eval can be either the name of a column or a public property. Sorting requires a bit more effort because you are required to implement a sortable collection. In terms of performance, DataView and arrays do nearly the same, as both use the QuickSort algorithm. Moreover, code based on arrays could even be slightly faster because of the thick layer of code and helper objects that surround and are used by ADO.NET DataViews.

Binding to Windows Forms

When you design a DAL, you normally make no assumptions on the calling application—be it an ASP.NET page, a Windows-based application, or a Web service. On the other hand, a DAL is merely a family of .NET classes with a given programming interface. A DAL, though, could reside on a remote machine. If this is the case, you need to ensure that your collection items (that is, OrderInfo) are serializable for the data transfer to take place successfully. Also, in Windows Forms applications, collections pose no significant problems for binding and paging operations.

What about sorting? Interestingly, if you use the following code snippet to populate a Windows DataGrid everything works fine, except that sorting is disabled:

OrderCollection orders = Helpers.LoadOrders(1997); dataGrid1.DataSource = orders;

Try clicking on any column header and verify that nothing happens, regardless of the fact that the AllowSorting property is set to True. Now try binding another grid in the same form to any ADO.NET objects. Amazingly, the grid now sorts like a champ. So, what's up?

By design, the Windows Forms DataGrid doesn't expose a public sorting API like the Web DataGrid, and like the DataGridView control in ASP.NET 2.0. (For a sneak preview, see last month's Cutting Edge column.) To customize sorting, you can write some code around the grid, catch user clicks on the header bar, figure out the underlying column, and proceed manually with sorting. Not impossible, but not an easy task either. But the hot question is: why won't the DataGrid sort on a custom collection?

By design, the Windows DataGrid relies on methods on the IBindingList interface for sorting. If the bound collection doesn't implement this interface, the grid will not sort your data. In the .NET Framework 1.x, only two classes implement IBindingList: DataView and DataViewManager. When you bind a DataTable or a DataSet, the control automatically extracts the default view and uses that DataView object for binding.

More often than not, Windows Forms applications use batch update for persisting in-memory changes to a remote database. Along with data adapters, DataSets provide functionality to handle batch updates and implement optimistic forms of concurrency. A similar, fairly complex mechanism isn't available for custom collections and should be built from the ground up when needed. However, with BindingList<T> it's fairly straightforward; see TableAdapters and Object Binding - Bridging the gap for an example.

Binding to Web Services

The DataSet is often used for returning data from a Web service method, although this is not recommended as it leads to tightly coupled designs and provides few interop capabilities.

The DataSet is XML-serializable, meaning that it can be used as an input argument or return value for WebMethods. .NET provides two main mechanisms for data serialization—through formatters and through the XML serializer—and each serves different application scenarios. Formatters can serialize any .NET object that is marked with the [Serializable] attribute; the XML serializer is a specialized component that takes care of marshaling parameters and return values in and out of Web service methods.

Formatters are designed for type fidelity and to preserve the full state of an object. The XML serializer limits you to persisting the public interface of the object being serialized and creating an XML representation of only public properties and fields with no support for circular references. The XML serializer also works for types that implement the IXmlSerializable interface, including the illustrious DataSet type.

Note that the .NET Framework adds two ways to enhance DataSet serialization. First, binary serialization of DataSets provides for a more compact representation, but limits you to .NET-to-.NET communications. Additionally, the new SerializationMode option allows developers to skip schema serialization on typed DataSets. This has a huge impact for developers who are sending small payloads. For small payload DataSets, 90 percent of the data would be the schema, which is already known on both sides.

The DataSet represents a collection of data tables and offers the opportunity to group together multiple resultsets in a single instance. In the context of Web services, the DataSet type is helpful when you need to grab some data, present it to the user, and post back to apply changes. Note that in the .NET Framework 1.x, the DataSet is the only ADO.NET object that can be used with Web service methods, because it's the only one that implements IXmlSerializable. The DataTable has been extended in ADO.NET 2.0 to implement the same interface.

The DataSet is a polymorphic type whose layout is determined only at run time once the object has been filled with data. Schema information can be embedded, but this doesn't normally help Web service toolkits to map it to a platform-specific class. So DataSets are fine as long as the Web service is consumed from within a .NET client. In non-.NET environments, handling DataSet return values is problematic and requires the platform's low-level XML API.

To work around these issues, you often write WebMethods to make them receive and return arrays or collections for better compatibility with other platforms. Arrays and collections of custom classes also significantly reduce the workload for the network as they require much less room to serialize to XML. Here's an example of a bunch of Web methods that wrap the same core functionality in different signatures.

[WebMethod] public DataSet GetOrdersAsDataSet(...) [WebMethod] public OrderInfo[] GetOrdersAsArray(...) [WebMethod] public OrderCollection GetOrdersAsCollection(...) [WebMethod] public string GetOrdersAsXml(...)

In Windows and Web Forms apps, you can add a reference to a Web service in order to generate a client-side proxy, call a method on that proxy, and bind the resulting value. When the return value is a DataSet, binding poses no problems at all. What if the Web method returns an array or a collection of custom structures?

When you import a Web service reference into your project, Visual Studio® .NET creates a proxy class to manage outbound calls. This class also imports any class declaration that serves for completing the call. There's the rub.

No matter how a custom class like OrderInfo is defined by the Web service source code, it gets rebuilt by the .NET Framework command-line tool—wsdl.exe—using fields, not properties (in the .NET Framework 2.0, however, properties are generated by default). As mentioned, arrays or collections of classes devoid of properties can't be bound to Windows and Web controls.

To fix the problem, you ensure that Windows and Web clients include a correct definition for any custom business entity used by the Web service. A correct definition is a definition that uses public properties in lieu of public fields. You can achieve this in one of two ways. You can edit the autogenerated proxy file to replace fields with properties, or you can remove all the class definitions from the reference file and add it to the project through an additional class file. Note that the proxy implementation file is normally hidden from view in Visual Studio .NET and shows warnings about the potential risks you take by editing it. However, feel free to edit it if you know what you're doing. Any change to the Web method's signature, or the class structure, may require you to edit the proxy implementation file or any other helper class you used.

Wrap Up

Custom collections are a valid alternative to ADO.NET container objects in most data-binding scenarios. The use of custom business entities in lieu of DataSets presents both advantages and disadvantages, but this is definitely an option that's worth exploring. Binding collections to Windows and Web Forms doesn't raise major issues except those involving sorting. For Web services, collections require some coding because of the limitations of the Visual Studio .NET importer tool that uses public fields instead of public properties. In general, DataSets are easy to use, but are not necessarily an optimal solution. Collections are lightweight objects that are also preferable from an interop standpoint.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and his newest book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.