The ADO.NET Framework supports two models of data access. In the first part of this chapter, you saw how you can use the SqlConnection, SqlCommand, and SqlDataReader objects to connect to a database and retrieve data. When you read data from a database by using a SqlDataReader object, an open connection must be maintained between your application and the database. In this section, we examine the second model of data access supported by ADO.NET: the disconnected model. When you use the objects discussed in this section, you do not need to keep a connection to the database open. This section discusses four new ADO.NET objects: DataAdapter Enables you to transfer data from the physical database to the in-memory database and back again. DataTable Represents an in-memory database table. DataView Represents an in-memory database view. DataSet Represents an in-memory database. The ADO.NET objects discussed in this section are built on top of the ADO.NET objects discussed in the previous section. For example, behind the scenes, the DataAdapter uses a DataReader to retrieve data from a database. The advantage of using the objects discussed in this section is that they provide you with more functionality. For example, you can filter and sort the rows represented by a DataView. Furthermore, you can use the DataTable object to track changes made to records and accept or reject the changes. The big disadvantage of using the objects discussed in this section is that they tend to be slower and more resource intensive. Retrieving 500 records with a DataReader is much faster than retrieving 500 records with a DataAdapter. Note For detailed performance comparisons between the DataReader and DataAdapter, see Priya Dhawan's article at the Microsoft MSDN website (msdn.Microsoft.com), entitled "Performance Comparison: Data Access Techniques." Therefore, unless you need to use any of the specialized functionality supported by these objects, my recommendation is that you stick with the objects discussed in the first part of this chapter when accessing a database. In other words, DataReaders are good and DataAdapters are bad. Using the DataAdapter Object The DataAdapter acts as the bridge between an in-memory database table and a physical database table. You use the DataAdapter to retrieve data from a database and populate a DataTable. You also use a DataAdapter to push changes that you have made to a DataTable back to the physical database. The component in Listing 16.25 illustrates how you can use a SqlDataAdapter to populate a DataTable. Listing 16.25. App_Code\Movie8.vb [View full width] Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Web.Configuration Imports System.Collections.Generic Public Class Movie8 Private Shared ReadOnly _connectionString As String Public Function GetAll() As DataTable ' Initialize the DataAdapter Dim dad As New SqlDataAdapter("SELECT Title,Director FROM Movies", _connectionString) ' Create a DataTable Dim dtblMovies As New DataTable() ' Populate the DataTable dad.Fill(dtblMovies) ' Return results Return dtblMovies End Function Shared Sub New() _connectionString = WebConfigurationManager.ConnectionStrings("Movies") .ConnectionString End Sub End Class | The page in Listing 16.26 contains a GridView that is bound to an ObjectDataSource that represents the component in Listing 16.25 (see Figure 16.12). Figure 16.12. Displaying data with a DataAdapter. Listing 16.26. ShowMovie8.aspx <%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show Movie8</title> </head> <body> <form runat="server"> <div> <asp:GridView DataSource Runat="server" /> <asp:ObjectDataSource TypeName="Movie8" SelectMethod="GetAll" Runat="server" /> </div> </form> </body> </html> | Notice that a SqlConnection is never explicitly created in the component in Listing 16.25. When you call the SqlDataAdapter object's Fill() method, the SqlDataAdapter automatically creates and opens a connection. After the data is fetched from the database, the Fill() method automatically closes the connection. You don't need to wrap the call to the Fill() method within a Using or try...Catch statement. Internally, the SqlDataAdapter uses a try...Catch statement to ensure that its connection gets closed. Opening and closing a database connection is a slow operation. If you know that you will need to perform another database operation after using the SqlDataAdapter, then you should explicitly create a SqlConnection and open it like this: Dim con As New SqlConnection(...connection string...) Dim dad As New SqlDataAdapter("SELECT Title,Director FROM Movies", con) Using con con.Open() dad.Fill(dtblMovies) ... Perform other database operations with connection ... End Using If a SqlConnection is already open when you call the Fill() method, the Fill() method doesn't close it. In other words, the Fill() method maintains the state of the connection. Performing Batch Updates You can think of a SqlDataAdapter as a collection of four SqlCommand objects: SelectCommand Represents a SqlCommand used for selecting data from a database. UpdateCommand Represents a SqlCommand used for updating data in a database. InsertCommand Represents a SqlCommand used for inserting data into a database. DeleteCommand Represents a SqlCommand used for deleting data from a database. You can use a DataAdapter not only when retrieving data from a database. You can also use a DataAdapter when updating, inserting, and deleting data from a database. If you call a SqlDataAdapter object's Update() method, and pass the method a DataTable, then the SqlDataAdapter calls its UpdateCommand, InsertCommand, and DeleteCommand to make changes to the database. You can assign a SqlCommand object to each of the four properties of the SqlDataAdapter. Alternatively, you can use the SqlCommandBuilder object to create the UpdateCommand, InsertCommand, and DeleteCommand for you. The SqlCommandBuilder class takes a SqlDataAdapter that has a SELECT command and generates the other three commands automatically. For example, the page in Listing 16.27 displays all the records from the Movies database table in a spreadsheet created with a Repeater control (see Figure 16.13). If you make changes to the data and click the Update button, then the Movies database table is updated with the changes. Figure 16.13. Batch updating database records. Listing 16.27. ShowDataAdapterUpdate.aspx [View full width] <%@ Page Language="VB" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Private dad As SqlDataAdapter Private dtblMovies As DataTable Sub Page_Load() ' Create connection Dim connectionString As String = WebConfigurationManager.ConnectionStrings ("Movies").ConnectionString Dim con As New SqlConnection(connectionString) ' Create Select command dad = New SqlDataAdapter("SELECT Id,Title,Director FROM Movies", con) ' Create Update, Insert, and Delete commands with SqlCommandBuilder Dim builder As New SqlCommandBuilder(dad) ' Add data to DataTable dtblMovies = New DataTable() dad.Fill(dtblMovies) ' Bind data to Repeater rptMovies.DataSource = dtblMovies rptMovies.DataBind() End Sub Protected Sub lnkUpdate_Click(ByVal sender As Object, ByVal e As EventArgs) ' Update DataTable with changes For i as integer = 0 To rptMovies.Items.Count- 1 Dim item As RepeaterItem = rptMovies.Items(i) Dim txtTitle As TextBox = CType(item.FindControl("txtTitle"), TextBox) Dim txtDirector As TextBox = CType(item.FindControl("txtDirector"), TextBox) If dtblMovies.Rows(i)("Title") <> txtTitle.Text Then dtblMovies.Rows(i)("Title") = txtTitle.Text End If If dtblMovies.Rows(i)("Director") <> txtDirector.Text Then dtblMovies.Rows(i)("Director") = txtDirector.Text End If Next ' Set batch size to maximum size dad.UpdateBatchSize = 0 ' Perform update Dim numUpdated As Integer = dad.Update(dtblMovies) lblResults.Text = String.Format("Updated {0} rows", numUpdated) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show DataAdapter Update</title> </head> <body> <form runat="server"> <div> <asp:Repeater EnableViewState="false" Runat="server"> <HeaderTemplate> <table> <tr> <th>Title</th><th>Director</th> </tr> </HeaderTemplate> <ItemTemplate> <tr> <td> <asp:TextBox Text='<%#Eval("Title")%>' Runat="server" /> </td> <td> <asp:TextBox Text='<%#Eval("Director")%>' Runat="server" /> </td> </tr> </ItemTemplate> <FooterTemplate> </table> </FooterTemplate> </asp:Repeater> <br /> <asp:LinkButton Text="Update Movies" Runat="server" OnClick="lnkUpdate_Click" /> <br /><br /> <asp:Label EnableViewState="false" Runat="server" /> </div> </form> </body> </html> | The SqlDataAdapter in Listing 16.27 performs a batch update. When a SqlDataAdapter object's UpdateBatchSize property is set to the value 0, the SqlDataAdapter performs all its updates in a single batch. If you want to perform updates in smaller batches, then you can set the UpdateBatchSize to a particular size. Note Performing batch updates is a new feature of ADO.NET 2.0. Using the DataTable Object The DataTable object represents an in-memory database table. You can add rows to a DataTable with a SqlDataAdapter, with a SqlDataReader, with an XML file, or programmatically. For example, the page in Listing 16.28 builds a new DataTable programmatically. The contents of the DataTable are then displayed in a GridView control (see Figure 16.14). Figure 16.14. Displaying a DataTable that was built programmatically. Listing 16.28. ShowDataTableProgram.aspx <%@ Page Language="VB" %> <%@ Import Namespace="System.Data" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() ' Create the DataTable columns Dim NewDataTable As New DataTable() NewDataTable.Columns.Add("Id", GetType(Integer)) NewDataTable.Columns.Add("ProductName", GetType(String)) NewDataTable.Columns.Add("ProductPrice", GetType(Decimal)) ' Mark the Id column as an autoincrement column NewDataTable.Columns("Id").AutoIncrement = True ' Add some data rows For i As Integer = 1 To 10 Dim NewRow As DataRow = NewDataTable.NewRow() NewRow("ProductName") = "Product " & i.ToString() NewRow("ProductPrice") = 12.34 NewDataTable.Rows.Add(NewRow) Next ' Bind DataTable to GridView grdProducts.DataSource = NewDataTable grdProducts.DataBind() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show DataTable Programmatically</title> </head> <body> <form runat="server"> <div> <h1>Products</h1> <asp:GridView Runat="server" /> </div> </form> </body> </html> | In Listing 16.28, a DataTable with the following three columns is created: Id, ProductName, and ProductPrice. The data type of each column is specified with a .NET Framework type. For example, the ProductPrice column is created as a decimal column. Alternatively, you could create each column with a SqlType. For example, you could use System.Data.SqlTypes.SqlDecimal for the type of the ProductPrice column. Notice that the Id column is created as an autoincrement column. When you add new rows to the DataTable, the column increments its value automatically. Selecting DataRows You can retrieve particular rows from a DataTable by using the DataTable object's Select() method. The Select() method accepts a filter parameter. You can use just about anything that you would use in a SQL WHERE clause with the filter parameter. When you retrieve an array of rows with the Select() method, you can also specify a sort order for the rows. When specifying a sort order, you can use any expression that you would use with a SQL ORDER BY clause. For example, the page in Listing 16.29 caches a DataTable in memory with the ASP.NET Cache object. The page contains a TextBox control. When you enter a partial movie title into the TextBox control, a list of matching movies is displayed in a GridView control. The rows are sorted in order of the movie title (see Figure 16.15). Figure 16.15. Selecting matching rows from a cached DataTable. Listing 16.29. ShowDataTableSelect.aspx [View full width] <%@ Page Language="VB" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Protected Sub btnSearch_Click(ByVal sender As Object, ByVal e As EventArgs) ' Get movies DataTable from Cache Dim dtblMovies As DataTable = CType(Cache("MoviesToFilter"), DataTable) If IsNothing(dtblMovies) Then dtblMovies = GetMoviesFromDB() Cache("MoviesToFilter") = dtblMovies End If ' Select matching rows Dim filter As String = String.Format("Title LIKE '{0}*'", txtTitle.Text) Dim rows() As DataRow = dtblMovies.Select(filter, "Title") ' Bind to GridView grdMovies.DataSource = rows grdMovies.DataBind() End Sub Private Function GetMoviesFromDB() As DataTable Dim connectionString As String = WebConfigurationManager.ConnectionStrings ("Movies").ConnectionString Dim dad As New SqlDataAdapter("SELECT Title, Director FROM Movies", connectionString) Dim dtblMovies As New DataTable() dad.Fill(dtblMovies) Return dtblMovies End Function </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <style type="text/css"> th, td { padding:5px; } </style> <title>Show DataTable Select</title> </head> <body> <form runat="server"> <div> <asp:TextBox Tooltip="Search" Runat="server" /> <asp:Button Text="Search" Runat="server" OnClick="btnSearch_Click" /> <hr /> <asp:GridView AutoGenerateColumns="false" Runat="server"> <Columns> <asp:TemplateField HeaderText="Title"> <ItemTemplate> <%# CType(Container.DataItem,DataRow)("Title") %> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Director"> <ItemTemplate> <%#CType(Container.DataItem, DataRow)("Director")%> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> </div> </form> </body> </html> | The DataTable Select() method returns an array of DataRow objects. Notice that there is nothing wrong with binding an array of DataRow objects to a GridView control. However, you must explicitly cast each data item to a DataRow and read within a GridView TemplateField. DataRow States and DataRow Versions When you modify the rows in a DataTable, the DataTable keeps track of the changes that you make. A DataTable maintains both the original and modified version of each row. Each row in a DataTable has a particular RowState that has one of the following values: Unchanged The row has not been changed. Added The row has been added. Modified The row has been modified. Deleted The row has been deleted. Detached The row has been created but not added to the DataTable. Each row in a DataTable can have more than one version. Each version is represented by one of the following values of the DataRowVersion enumeration: Current The current version of the row. Default The default version of the row. Original The original version of the row. Proposed The version of a row that exists during editing. You can use the DataTable.AcceptChanges() method to copy the current versions of all the rows to the original versions of all the rows. And you can use the DataTable.RejectChanges() method to copy the original versions of all the rows to the current versions of all the rows. For example, the component in Listing 16.30 includes an AcceptChanges() and RejectChanges() method. The component maintains a DataTable in Session state. If you update a row in the DataTable, the row is updated in memory. If the RejectChanges() method is called, any changes made to the DataTable are rejected. If the AcceptChanges() method is called, the database is updated and all changes are accepted. Listing 16.30. App_Code\Movie9.vb [View full width] Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Web Imports System.Web.Configuration Public Class Movie9 Private dad As New SqlDataAdapter() Public Function GetAll() As DataTable Return CType(HttpContext.Current.Session("MoviesToEdit"), DataTable) End Function Public Sub Update(ByVal id As Integer, ByVal title As String, ByVal director As String) Dim movies As DataTable = CType(HttpContext.Current.Session("MoviestoEdit"), DataTable) Dim rowToEdit As DataRow = movies.Rows.Find(id) rowToEdit("title") = title rowToEdit("director") = director End Sub Public Sub RejectChanges() Dim movies As DataTable = CType(HttpContext.Current.Session("MoviestoEdit"), DataTable) movies.RejectChanges() End Sub Public Sub AcceptChanges() Dim movies As DataTable = CType(HttpContext.Current.Session("MoviestoEdit"), DataTable) dad.Update(movies) movies.AcceptChanges() End Sub Public Sub New() ' Create Data Adapter Dim connectionString As String = WebConfigurationManager.ConnectionStrings ("Movies").ConnectionString dad = New SqlDataAdapter("SELECT Id,Title,Director FROM Movies", connectionString) Dim builder As New SqlCommandBuilder(dad) dad.UpdateBatchSize = 0 Dim context As HttpContext = HttpContext.Current If IsNothing(context.Session("MoviesToEdit")) Then ' Add data to DataTable Dim dtblMovies As New DataTable() dad.Fill(dtblMovies) dtblMovies.PrimaryKey = New DataColumn() {dtblMovies.Columns("Id")} context.Session("MoviesToEdit") = dtblMovies End If End Sub End Class | The page in Listing 16.31 contains a GridView that is bound to the component in Listing 16.30. The GridView includes a column that indicates whether each row has been changed. The column displays the value of the corresponding DataRow object's RowState property (see Figure 16.16). Listing 16.31. ShowMovie9.aspx <%@ Page Language="VB" %> <%@ Import Namespace="System.Data" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Protected Sub btnReject_Click(ByVal sender As Object, ByVal e As EventArgs) Dim movie As New Movie9() movie.RejectChanges() grdMovies.DataBind() End Sub Protected Sub btnAccept_Click(ByVal sender As Object, ByVal e As EventArgs) Dim movie As New Movie9() movie.AcceptChanges() grdMovies.DataBind() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show Movie9</title> </head> <body> <form runat="server"> <div> <h1>Edit Movies</h1> <asp:GridView DataSource DataKeyNames="Id" AutoGenerateEditButton="true" Runat="server"> <Columns> <asp:TemplateField> <ItemTemplate> <%#CType(Container.DataItem, DataRowView).Row.RowState.ToString()%> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> <br /> <asp:Button Text="Reject Changes" OnClick="btnReject_Click" Runat="server" /> <asp:Button Text="Accept Changes" OnClick="btnAccept_Click" Runat="server" /> <asp:ObjectDataSource TypeName="Movie9" SelectMethod="GetAll" UpdateMethod="Update" Runat="server" /> </div> </form> </body> </html> | Figure 16.16. Tracking data row changes. If you click the Accept Changes button, all the changes made to the rows in the GridView are sent to the database. If you click the Reject Changes button, all the rows revert to their original values. Using the DataView Object The DataView object represents an in-memory database view. You can use a DataView object to create a sortable, filterable view of a DataTable. The DataView object supports three important properties: Sort Enables you to sort the rows represented by the DataView. RowFilter Enables you to filter the rows represented by the DataView. RowStateFilter Enables you to filter the rows represented by the DataView according to the row state (for example, OriginalRows, CurrentRows, Unchanged). The easiest way to create a new DataView is to use the DefaultView property exposed by the DataTable class like this: Dim dataView1 As DataView = dataTable1.DefaultView; The DefaultView property returns an unsorted, unfiltered view of the data contained in a DataTable. You also can directly instantiate a new DataView object by passing a DataTable, filter, sort order, and DataViewRowState filter to the DataView object's constructor, like this: Dim dataView1 As New DataView(dataTable1, _ "BoxOfficeTotals > 100000", _ "Title ASC", _ DataViewRowState.CurrentRows) This statement creates a new DataView from a DataTable that represents the Movies database table. The rows are filtered to include only the movies that have a box office total greater than 100,000 dollars. Also, the rows are sorted by the movie title in ascending order. Finally, all the current rows are represented from the DataTable (as opposed, for instance, to rows that have been deleted). The page in Listing 16.30 illustrates one way that you can use a DataView. In Listing 16.32, a DataView is cached in Session state. You can sort the cached DataView by clicking on the header links rendered by the GridView control (see Figure 16.17). Listing 16.32. ShowDataView.aspx [View full width] <%@ Page Language="VB" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() If IsNothing(Session("MoviesToSort")) Then Dim connectionString As String = WebConfigurationManager.ConnectionStrings( "Movies").ConnectionString Dim dad As New SqlDataAdapter("SELECT Id,Title,Director FROM Movies", connectionString) Dim dtblMovies As New DataTable() dad.Fill(dtblMovies) Session("MoviesToSort") = dtblMovies.DefaultView End If If Not Page.IsPostBack Then BindMovies() End If End Sub Private Sub BindMovies() grdMovies.DataSource = Session("MoviesToSort") grdMovies.DataBind() End Sub Protected Sub grdMovies_Sorting(ByVal sender As Object, ByVal e As GridViewSortEventArgs) Dim dvwMovies As DataView = CType(Session("MoviesToSort"), DataView) dvwMovies.Sort = e.SortExpression BindMovies() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show DataView</title> </head> <body> <form runat="server"> <div> <asp:GridView AllowSorting="true" OnSorting="grdMovies_Sorting" Runat="server" /> </div> </form> </body> </html> | Figure 16.17. Sorting a cached DataView. Using the DataSet Object The DataSet object represents an in-memory database. A single DataSet can contain one or many DataTable objects. You can define parent/child relationships between the DataTable objects contained in a DataSet. For example, the page in Listing 16.33 contains a treeView control. The TReeView displays a list of movie categories and, beneath each movie category, a list of matching movies (see Figure 16.18). Listing 16.33. ShowDataSet.aspx [View full width] <%@ Page Language="VB" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Private Sub Page_Load() If Not Page.IsPostBack Then BuildTree() End If End Sub Sub BuildTree() ' Create Connection Dim connectionString As String = WebConfigurationManager.ConnectionStrings ("Movies").ConnectionString Dim con As New SqlConnection(connectionString) ' Create Movie Categories DataAdapter Dim dadCategories As New SqlDataAdapter("SELECT Id,Name FROM MovieCategories", con) ' Create Movies DataAdapter Dim dadMovies As New SqlDataAdapter("SELECT Title,CategoryId FROM Movies", con) ' Add the DataTables to the DataSet Dim dstMovies As New DataSet() Using con con.Open() dadCategories.Fill(dstMovies, "Categories") dadMovies.Fill(dstMovies, "Movies") End Using ' Add a DataRelation dstMovies.Relations.Add("Children",dstMovies.Tables("Categories").Columns("Id") ,dstMovies.Tables("Movies").Columns("CategoryId")) ' Add the Movie Category nodes For Each categoryRow As DataRow In dstMovies.Tables("Categories").Rows Dim name As String = CType(categoryRow("Name"), String) Dim catNode As New TreeNode(name) TreeView1.Nodes.Add(catNode) ' Get matching movies Dim movieRows() As DataRow = categoryRow.GetChildRows("Children") For Each movieRow As DataRow In movieRows Dim title As String = CType(movieRow("Title"), String) Dim movieNode As New TreeNode(title) catNode.ChildNodes.Add(movieNode) Next Next End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show DataSet</title> </head> <body> <form runat="server"> <div> <asp:TreeView Runat="server" /> </div> </form> </body> </html> | Figure 16.18. Building a treeView from a DataSet. The treeView is built programmatically. In the BuildTree() method, a DataSet is created that contains two DataTable objects. The first DataTable represents the MovieCategories database table and the second DataTable represents the Movies database table. A parent/child relationship is created between the two DataTable objects with the help of a DataRelation. The DataRelation is used to get the movies that match each movie category. The DataRow.GetChildRows() method is called to retrieve the movies that match a particular movie category. |