Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
278C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T
such as data type conversion problems and errors with duplicated data or violated relationships. Where the DataSet support for XML really shines is if you need to exchange the information in the DataSet with other applications and business processes.
You’ll learn more about the DataSet support for XML in Chapter 12.
The DataSet Classes
The DataSet is the heart of disconnected data access. The DataSet contains two important ingredients: a collection of zero or more tables (exposed through the Tables property) and a collection of zero or more relationships that you can use to link tables together (exposed through the Relationships property). Figure 8-3 shows the basic structure of the DataSet.
Figure 8-3. Dissecting the DataSet
■Note Occasionally, novice ADO.NET developers make the mistake of assuming that the DataSet should contain all the information from a given table in the data source. This is not the case. For performance reasons, you will probably use the DataSet to work with a small subset of the total information in the data source. Also, the tables in the DataSet do not need to map directly to tables in the data source. A single table can hold the results of a query on one table, or it can hold the results of a JOIN query that combines data from more than one linked table.
As you can see in Figure 8-3, each record is represented as a DataRow object. To manage disconnected changes, the DataSet tracks versioning information for every DataRow. When you edit the value in a row, the original value is kept in memory, and the row is marked as changed. When you add or delete a row, the row is marked as added or deleted.
C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T |
279 |
Always remember that the data in the data source is not touched at all when you work with the DataSet objects. Instead, all the changes are made locally to the DataSet in memory. The DataSet never retains any type of connection to a data source. If you want to extract records from a database and use them to fill a table in a DataSet, you need to use another ADO.NET object: a DataAdapter. The DataAdapter also allows you to update the data source according to the changes you make in the DataSet (although direct commands are a preferred approach to updating in ASP.NET).
The DataSet also has methods that can write and read XML data and schemas and has methods you can use to quickly clear and duplicate data. Table 8-1 outlines these methods. You’ll learn more about XML in Chapter 12.
Table 8-1. DataSet XML and Miscellaneous Methods
Method |
Description |
GetXml() and GetXmlSchema() |
Returns a string with the data (in XML markup) or schema |
|
information for the DataSet. The schema information is |
|
the structural information such as the number of tables, |
|
their names, their columns, their data types, and their |
|
relationships. |
WriteXml() and WriteXmlSchema() |
Persists the data and schema represented by the DataSet to |
|
a file or a stream in XML format. |
ReadXml() and ReadXmlSchema() |
Creates the tables in a DataSet based on an existing XML |
|
document or XML schema document. The XML source can |
|
be a file or any other stream. |
Clear() |
Empties all the data from the tables. However, this method |
|
leaves the schema and relationship information intact. |
Copy() |
Returns an exact duplicate of the DataSet, with the same |
|
set of tables, relationships, and data. |
Clone() |
Returns a DataSet with the same structure (tables and |
|
relationships) but no data. |
Merge() |
Takes another DataSet as input and merges it into the |
|
current DataSet, adding any new tables and merging any |
|
existing tables. |
|
|
The DataTable Class
As you can see in Figure 8-3, each item in the DataSet.Tables collection is a DataTable. The DataTable contains its own collections—the Columns collection of DataColumn objects (which describe the name and data type of each field) and the Rows collection of DataRow objects (which contain the actual data in each record).
■Tip ASP.NET adds a new CreateDataReader() method to the DataSet and DataTable classes. You can call
this to return a DataReader-style object that lets you iterate over your disconnected data. This feature is particularly useful if you have existing code that expects a DataReader. The CreateDataReader() method returns a DataTableReader object, which derives from DbDataReader and implements the IDataReader interface, like
all the DataReader objects.
280 C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T
The DataRow Class
Each DataRow object represents a single record in a table that has been retrieved from the data source. The DataRow is the container for the actual field values. You can access them by field name, as in myRow["FieldNameHere"].
The DataAdapter Class
The DataAdapter serves as a bridge between a single DataTable in the DataSet and the data source. It contains all the available commands for querying and updating the data source.
The DataAdapter provides three key methods, as listed in Table 8-2.
Table 8-2. DataAdapter Methods
Method |
Description |
Fill() |
Adds a DataTable to a DataSet by executing the query in the SelectCommand. |
|
If your query returns multiple result sets, this method will add multiple |
|
DataTable objects at once. You can also use this method to add data to an |
|
existing DataTable. |
FillSchema() |
Adds a DataTable to a DataSet by executing the query in the SelectCommand |
|
and retrieving schema information only. This method doesn’t add any data to |
|
the DataTable. Instead, it simply preconfigures the DataTable with detailed |
|
information about column names, data types, primary keys, and unique |
|
constraints. |
Update() |
Examines all the changes in a single DataTable and applies this batch of |
|
changes to the data source by executing the appropriate InsertCommand, |
|
UpdateCommand, and DeleteCommand operations. |
|
|
To enable the DataAdapter to edit, delete, and add rows, you need to specify Command objects for the UpdateCommand, DeleteCommand, and InsertCommand properties of the DataAdapter. To use the DataAdapter to fill a DataSet, you must set the SelectCommand.
Figure 8-4 shows how a DataAdapter and its Command objects work together with the data source and the DataSet.
Filling a DataSet
In the following example, you’ll see how to retrieve data from a SQL Server table and use it to fill a DataTable object in the DataSet. You’ll also see how to display the data by using a Repeater control or by programmatically cycling through the records and displaying them one by one. All the logic takes place in the event handler for the Page.Load event.
First, the code creates the connection and defines the text of the SQL query:
string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection con = new SqlConnection(connectionString); string sql = "SELECT * FROM Employees";
C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T |
281 |
Figure 8-4. How the DataAdapter interacts with the data source
The next step is to create a new instance of the SqlDataAdapter class that will retrieve the employee list. Although every DataAdapter supports four Command objects, only one of these (the SelectCommand) is required to fill a DataSet. To make life even easier, you can create the Command object you need and assign it to the DataAdapter.SelectCommand property in one step. You just need to supply a Connection object and query string in the DataAdapter constructor, as shown here:
SqlDataAdapter da = new SqlDataAdapter(sql, con);
Now you need to create a new, empty DataSet and use the DataAdapter. Fill() method to execute the query and place the results in a new DataTable in the DataSet. At this point, you can also specify the name for the table. If you don’t, a default name (like Table) will be used automatically. In the following example, the table name corresponds to the name of the source table in the database, although this is not a requirement:
DataSet ds = new DataSet(); da.Fill(ds, "Employees");
282 C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T
Note that this code doesn’t open the connection by calling Connection.Open(). Instead, the DataAdapter opens and closes the linked connection automatically when you call the Fill() method. As a result, the only line of code you should consider placing in an exception-handling block is the call to DataAdapter.Fill(). Alternatively, you can also open and close the connection manually. If the connection is open when you call Fill(), the DataAdapter will use that connection and won’t close
it automatically. This approach is useful if you want to perform multiple operations with the data source in quick succession and you don’t want to incur the additional overhead of repeatedly opening and closing the connection each time.
The last step is to display the contents of the DataSet. A quick approach is to use the same technique that was shown in the previous chapter and build an HTML string by examining each record. The following code cycles through all the DataRow objects in the DataTable and displays the field values of each record in a bulleted list:
StringBuilder htmlStr = new StringBuilder(""); foreach (DataRow dr in ds.Tables["Employees"].Rows)
{
htmlStr.Append("<li>");
htmlStr.Append(dr["TitleOfCourtesy"].ToString()); htmlStr.Append(" <b>"); htmlStr.Append(dr["LastName"].ToString()); htmlStr.Append("</b>, "); htmlStr.Append(dr["FirstName"].ToString()); htmlStr.Append("</li>");
}
HtmlContent.Text = htmlStr.ToString();
Of course, the ASP.NET model is designed to save you from coding raw HTML. A much better approach is to bind the data in the DataSet to a data-bound control, which automatically generates the HTML you need based on a single template. Chapter 9 describes the data-bound controls in detail.
■Note When you bind a DataSet to a control, no data objects are stored in view state. The data control stores enough information to show only the data that’s currently displayed. If you need to interact with a DataSet over multiple postbacks, you’ll need to store it in the ViewState collection manually (which will greatly increase the size of the page) or the Session or Cache objects.
Working with Multiple Tables and Relationships
The next example shows a more advanced use of the DataSet that, in addition to providing disconnected data, uses table relationships. This example demonstrates how to retrieve some records from the Categories and Products tables of the Northwind database and how to create a relationship between them so that it’s easy to navigate from a category record to all of its child products and create a simple report.
The first step is to initialize the ADO.NET objects and declare the two SQL queries (for retrieving categories and products), as shown here:
string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T |
283 |
string sqlCat = "SELECT CategoryID, CategoryName FROM Categories"; string sqlProd = "SELECT ProductName, CategoryID FROM Products";
SqlDataAdapter da = new SqlDataAdapter(sqlCat, con);
DataSet ds = new DataSet();
Next, the code executes both queries, adding two tables to the DataSet. Note that the connection is explicitly opened at the beginning and closed after the two operations, ensuring the best possible performance.
try
{
con.Open();
//Fill the DataSet with the Categories table. da.Fill(ds, "Categories");
//Change the command text and retrieve the Products table.
//You could also use another DataAdapter object for this task. da.SelectCommand.CommandText = sqlProd;
da.Fill(ds, "Products");
}
finally
{
con.Close();
}
In this example, the same DataAdapter is used to fill both tables. This technique is perfectly legitimate, and it makes sense in this scenario because you don’t need to reuse the DataAdapter to update the data source. However, if you were using the DataAdapter both to query data and to commit changes, you probably wouldn’t use this approach. Instead, you would use a separate DataAdapter for each table so that you could make sure each DataAdapter has the appropriate insert, update, and delete commands for the corresponding table.
At this point you have a DataSet with two tables. These two tables are linked in the Northwind database by a relationship against the CategoryID field. This field is the primary key for the Categories table and the foreign key in the Products table. Unfortunately, ADO.NET does not provide any way to read a relationship from the data source and apply it to your DataSet automatically. Instead, you need to manually create a DataRelation that represents the relationship.
A relationship is created by defining a DataRelation object and adding it to the DataSet. Relations collection. When you create the DataRelation, you specify three constructor arguments: the name of the relationship, the DataColumn for the primary key in the parent table, and the DataColumn for the foreign key in the child table.
Here’s the code you need for this example:
//Define the relationship between Categories and Products. DataRelation relat = new DataRelation("CatProds",
ds.Tables["Categories"].Columns["CategoryID"],
ds.Tables["Products"].Columns["CategoryID"]);
//Add the relationship to the DataSet. ds.Relations.Add(relat);
284 C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T
Once you’ve retrieved all the data, you can loop through the records of the Categories table and add the name of each category to the HTML string:
StringBuilder htmlStr = new StringBuilder("");
// Loop through the category records and build the HTML string. foreach (DataRow row in ds.Tables["Categories"].Rows)
{
htmlStr.Append("<b>");
htmlStr.Append(row["CategoryName"].ToString());
htmlStr.Append("</b><ul>");
...
Here’s the interesting part. Inside this block, you can access the related product records for the current category by calling the DataRow.GetChildRows() method. Once you have this array of product records, you can loop through it using a nested foreach loop. This is far simpler than the code you’d need in order to look up this information in a separate object or to execute multiple queries with traditional connection-based access.
The following piece of code demonstrates this approach, retrieving the child records and completing the outer foreach loop:
...
//Get the children (products) for this parent (category). DataRow[] childRows = row.GetChildRows(relat);
//Loop through all the products in this category. foreach (DataRow childRow in childRows)
{
htmlStr.Append("<li>");
htmlStr.Append(childRow["ProductName"].ToString());
htmlStr.Append("</li>");
}
htmlStr.Append("</ul>");
}
The last step is to display the HTML string on the page:
HtmlContent.Text = htmlStr.ToString();
The code for this example is now complete. If you run the page, you’ll see the output shown in Figure 8-5.
■Tip A common question new ADO.NET programmers have is, when do you use JOIN queries and when do you use DataRelation objects? The most important consideration is whether you plan to update the retrieved data. If you do, using separate tables and a DataRelation object always offers the most flexibility. If not, you could use either approach, although the JOIN query may be more efficient because it involves only a single round-trip across the network, while the DataRelation approach often requires two to fill the separate tables.
C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T |
285 |
Figure 8-5. A list of products in each category
REFERENTIAL INTEGRITY
When you add a relationship to a DataSet, you are bound by the rules of referential integrity. For example, you can’t delete a parent record if there are linked child rows, and you can’t create a child record that references a nonexistent parent. This can cause a problem if your DataSet contains only partial data. For example, if you have a full list of customer orders, but only a partial list of customers, it could appear that an order refers to a customer who doesn’t exist just because that customer record isn’t in your DataSet. One way to get around this problem is to create a DataRelation without creating the corresponding constraints. To do so, use the DataRelation constructor that accepts the Boolean createConstraints parameter and set it to false, as shown here:
DataRelation relat = new DataRelation("CatProds", ds.Tables["Categories"].Columns["CategoryID"], ds.Tables["Products"].Columns["CategoryID"], false);
Another approach is to disable all types of constraint checking (including unique value checking) by setting the DataSet.EnableConstraints property to false before you add the relationship.
286 C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T
Searching for Specific Rows
The DataTable provides a useful Select() method that allows you to retrieve an array of DataRow objects based on an SQL expression. The expression you use with the Select() method plays the same role as the WHERE clause in a SELECT statement.
For example, the following code retrieves all the products that are marked as discontinued:
// Get the children (products) for this parent (category).
DataRow[] matchRows = DataSet.Tables["Products"].Select("Discontinued = 0")
// Loop through all the discontinued products and generate a bulleted list. htmlStr.Append("</b><ul>");
foreach (DataRow row in childRows)
{
htmlStr.Append("<li>");
htmlStr.Append(row["ProductName"].ToString());
htmlStr.Append("</li>");
}
htmlStr.Append("</ul>");
In this example, the Select() statement uses a fairly simple filter string. However, you’re free to use more complex operators and a combination of different criteria. For more information, refer to the MSDN class library reference description for the DataColumn.Expression property, or refer to Table 8-3 and the discussion about filter strings in the “Filtering with a DataView” section.
■Note The Select() method has one potential caveat—it doesn’t support a parameterized condition. As a result, it’s open to SQL injection attacks. Clearly, the SQL injection attacks that a malicious user could perform in this situation are fairly limited, because there’s no way to get access to the actual data source or execute additional commands. However, a carefully written value could still trick your application into returning extra information from the table. If you create a filter expression with a user-supplied value, you might want to iterate over the DataTable manually to find the rows you want, instead of using the Select() method.
Using the DataSet in a Custom Data Class
There’s no reason you can’t use the DataSet or DataTable as the return value from a method in your custom data access class. For example, you could rewrite the GetAllEmployees() method shown earlier with the following DataSet code:
public DataTable GetAllEmployees()
{
SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetEmployee", con); cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4)); cmd.Parameters["@EmployeeID"].Value = employeeID;
SqlDataAdapter da = new SqlDataAdapter(sql, con);
DataSet ds = new DataSet();
// Fill the DataSet. try
{
da.Fill(ds, "Employees"); return ds.Tables["Employees"];
C H A P T E R 8 ■ D ATA C O M P O N E N T S A N D T H E D ATA S E T |
287 |
}
catch
{
throw new ApplicationException("Data error.");
}
}
Interestingly, when you use this approach, you have exactly the same functionality at your fingertips. For example, in the next chapter you’ll learn to use the ObjectDataSource to bind to custom classes. The ObjectDataSource understands custom classes and the DataSet object equally well (and they have essentially the same performance).
The DataSet approach has a couple of limitations. Although the DataSet makes the ideal container for disconnected data, you may find it easier to create methods that return individual DataTable objects and even distinct DataRow objects (for example, as a return value from a GetEmployee() method). However, these objects don’t have the same level of data binding support as the DataSet, so you’ll need to decide between a clearer coding model (using the various disconnected data objects) and more flexibility (always using the full DataSet, even when returning only a single record). Another limitation is that the DataSet is weakly typed. That means there’s no compile-time syntax checking or IntelliSense to make sure you use the right field names (unlike with a custom data access class such as EmployeeDetails). You can get around this limitation by building a typed DataSet, but it takes more work. For more information about creating a typed DataSet, refer to Pro ADO.NET 2.0 (Apress, 2005).
Data Binding
Although there’s nothing stopping you from generating HTML by hand as you loop through disconnected data, in most cases ASP.NET data binding can simplify your life quite a bit. Chapter 9 discusses data binding in detail, but before continuing to the DataView examples in this chapter you need to know the basics.
The key idea behind data binding is that you associate a link between a data object and a control, and then the ASP.NET data binding infrastructure takes care of building the appropriate output.
One of the data-bound controls that’s easiest to use is the GridVew. The GridView has the builtin smarts to create an HTML table with one row per record and with one column per field.
To bind data to a data-bound control such as the GridView, you first need to set the DataSource property. This property points to the object that contains the information you want to display. In this case, it’s the DataSet:
GridView1.DataSource = ds;
Because data-bound controls can bind to only a single table (not the entire DataSet), you also need to explicitly specify what table you want to use. You can do that by setting the DataMember property to the appropriate table name, as shown here:
GridView1.DataMember = "Employees";
Finally, once you’ve defined where the data is, you need to call the control’s DataBind() method to copy the information from the DataSet into the control. If you forget this step, the control will remain empty, and the information will not appear on the page.
GridView1.DataBind();
As a shortcut, you can call the DataBind() method of the current page, which walks over every control that supports data binding and calls the DataBind() method.
