Tải bản đầy đủ (.pdf) (105 trang)

Mastering Microsoft Visual Basic 2010 phần 8 pptx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (1.06 MB, 105 trang )

STORING DATA IN DATASETS 703
untyped DataSets. In the following chapter, I’ll discuss in detail typed DataSets and how to use
them in building data-bound applications.
The DataAdapter Class
To use DataSets in your application, you must first create a DataAdapter object, which is the
preferred technique for populating the DataSet. The DataAdapter is nothing more than a col-
lection of Command objects that are needed to execute the various SQL statements against the
database. As you recall from our previous discussion, we interact with the database by using
four different command types: one to select the data and load them to the client computer with
the help of a DataReader object (a Command object with the SELECT statement) and three more
to submit to the database the new rows (a Command object with the INSERT statement), update
existing rows (a Command object with the UPDATE statement), and delete existing rows (a Com-
mand object with the DELETE statement). A DataAdapter is a container for Connection and
Command objects. If you declare a SqlDataAdapter object with a statement like the following:
Dim DA As New SqlDataAdapter
you’ll see that it exposes the properties described in Table 16.1.
Table 16.1: SqlDataAdapter object properties
Property Description
InsertCommand A Command object that’s executed to insert a new row
UpdateCommand A Command object that’s executed to update a row
DeleteCommand A Command object that’s executed to delete a row
SelectCommand A Command object that’s executed to retrieve selected rows
Each of these properties is an object and has itsownConnectionproperty,becauseeachmay
not act on the same database (as unlikely as it may be). These properties also expose their own
Parameters collection, which you must populate accordingly before executing a command.
The DataAdapter class performs the two basic tasks of a data-driven application: It retrieves
data from the database to populate a DataSet and submits the changes to the database.
To populate a DataSet, use the Fill method, which fills a specific DataTable object. There’s
one DataAdapter per DataTable object in the DataSet, and you must call the corresponding
Fill method to populate each DataTable. To submit the changes to the database, use the
Update method of the appropriate DataAdapter object. The Update method is overloaded, and


you can use it to submit a single row to the database or all edited rows in a DataTable. The
Update method uses the appropriate Command object to interact with the database.
Passing Parameters Through the DataAdapter
Let’s build a DataSet in our code to demonstrate the use of the DataAdapter objects. As with
all the data objects mentioned in this chapter, you must add a reference to the System.Data
namespace with the Imports statement.
704 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
Start by declaring a DataSet variable:
Dim DS As New DataSet
To access the classes discussed in this section, you must import the System.Data namespace
in your module. Then create the various commands that will interact with the database:
Dim cmdSelectCustomers As String = "SELECT * FROM Customers " &
"WHERE Customers.Country=@country"
Dim cmdDeleteCustomer As String = "DELETE Customers WHERE CustomerID=@CustomerID"
Dim cmdEditCustomer As String = "UPDATE Customers " &
"SET CustomerID = @CustomerID, CompanyName = @CompanyName, " &
"ContactName = @ContactName, ContactTitle = @ContactTitle " &
"WHERE CustomerID = @CustID"
Dim cmdInsertCustomer As String = "INSERT Customers " &
" (CustomerID, CompanyName, ContactName, ContactTitle) " &
"VALUES(@CustomerID, @CompanyName, @ContactName, @ContactTitle) "
You can also create stored procedures for the four basic operations and use their names in
the place of the SQL statements. It’s actually a bit faster, and safer, to use stored procedures.
I’ve included only a few columns in the examples to keep the statements reasonably short.
The various commands use parameterized queries to interact with the database, and you must
add the appropriate parameters to each Command object. After the SQL statements are in
place, we can build the four Command properties of the DataAdapter object. Start by declaring a
DataAdapter object:
Dim DACustomers As New SqlDataAdapter()
Because all Command properties of the DataAdapter object will act on the same database, you

can create a Connection object and reuse it as needed:
Dim CN As New SqlConnection(ConnString)
The ConnString variable is a string with the proper connection string. Now we can create the
four Command properties of the DACustomers DataAdapter object.
Let’s start with the SelectCommand property of the DataAdapter object. The following state-
ments create a new Command object based on the preceding SELECT statement and then set up
a Parameter object for the @country parameter of the SELECT statement:
DACustomers.SelectCommand = New SqlClient.SqlCommand(cmdSelectCustomers)
DACustomers.SelectCommand.Connection = CN
Dim param As New SqlParameter
param.ParameterName = "@Country"
param.SqlDbType = SqlDbType.VarChar
param.Size = 15
param.Direction = ParameterDirection.Input
param.IsNullable = False
param.Value = "Germany"
DACustomers.SelectCommand.Parameters.Add(param)
STORING DATA IN DATASETS 705
This is the easier, if rather verbose, method of specifying a Parameter object. You are familiar
with the Parameter object properties and already know how to configure and add parameters
to a Command object via a single statement. As a reminder, an overloaded form of the Add
method allows you to configure and attach a Parameter object to a Command object Parameters
collection with a single, if lengthy, statement:
DA.SelectCommand.Parameters.Add(
New System.Data.SqlClient.qlParameter(
paramName, paramType, paramSize, paramDirection,
paramNullable, paramPrecision, paramScale,
columnName, rowVersion, paramValue)
The paramPrecsion and paramScale arguments apply to numeric parameters, and you
can set them to 0 for string parameters. The paramNullable argument determines whether

the parameter can assume a Null value. The columnName argument is the name of the table
column to which the parameter will be matched. (You need this information for the INSERT
and UPDATE commands.) The rowVersion argument determines which version of the field in
the DataSet will be used — in other words, whether the DataAdapter will pass the current
version (DataRowVersion.Current) or the original version (DataRowVersion.Original)
of the field to the parameter object. The last argument, paramValue, is the parameter’s
value. You can specify a value as we did in the SelectCommand example, or you can set
this argument to Nothing and let the DataAdapter object assign the proper value to each
parameter. (You’ll see in a moment how this argument is used with the INSERT and UPDATE
commands.)
Finally, you can open the connection to the database and then call the DataAdapter’s Fill
method to populate a DataTable in the DataSet:
CN.Open
DACustomers.Fill(DS, "Customers")
CN.Close
The Fill method accepts as arguments a DataSet object and the name of the DataTable
it will populate. The DACustomers DataAdapter is associated with a single DataTable and
knows how to populate it, as well as how to submit the changes to the database. The
DataTable name is arbitrary and need not match the name of the database table where the
data originates. The four basic operations of the DataAdapter (which are none other than
the four basic data-access operations of a client application) are also known as CRUD
operations: Create/Retrieve/Update/Delete.
The CommandBuilder Class
Each DataAdapter object that you set up in your code is associated with a single SELECT query,
which may select data from one or multiple joined tables. The INSERT/UPDATE/DELETE queries
of the DataAdapter can submit data to a single table. So far, you’ve seen how to manually set
up each Command object in a DataAdapter object. There’s a simpler method to specify the
queries: You start with the SELECT statement, which selects data from a single table, and then
let a CommandBuilder object infer the other three statements from the SELECT statement. Let’s
see this technique in action.

706 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
Declare a new SqlCommandBuilder object by passing the name of the adapter for which you
want to generate the statements:
Dim CustomersCB As SqlCommandBuilder =
New SqlCommandBuilder(DA)
This statement is all it takes to generate the InsertCommand, UpdateCommand, and
DeleteCommand objects of the DACustomers SqlDataAdapter object. When the compiler
runs into the previous statement, it will generate the appropriate Command objects and attach
them to the DACustomers SqlDataAdapter. Here are the SQL statements generated by the
CommandBuilder object for the Products table of the Northwind database:
UPDATE Command
UPDATE [Products] SET [ProductName] = @p1,
[CategoryID] = @p2, [UnitPrice] = @p3,
[UnitsInStock] = @p4, [UnitsOnOrder] = @p5
WHERE (([ProductID] = @p6))
INSERT Command
INSERT INTO [Products]
([ProductName], [CategoryID],
[UnitPrice], [UnitsInStock],
[UnitsOnOrder])
VALUES (@p1, @p2, @p3, @p4, @p5)
DELETE Command
DELETE FROM [Products] WHERE (([ProductID] = @p1))
These statements are based on the SELECT statement and are quite simple. You may notice
that the UPDATE statement simply overrides the current values in the Products table. The
CommandBuilder can generate a more elaborate statement that takes into consideration
concurrency. It can generate a statement that compares the values read into the DataSet to
the values stored in the database. If these values are different, which means that another
user has edited the same row since the row was read into the DataSet, it doesn’t perform the
update. To specify the type of UPDATE statement you want to create with the CommandBuilder

object, set its ConflictOption property, whose value is a member of the ConflictOption
enumeration: CompareAllSearchValues (compares the values of all columns specified in
the SELECT statement), CompareRowVersion (compares the original and current versions
of the row), and OverwriteChanges (simply overwrites the fields of the current row in the
database).
The OverwriteChanges option generates a simple statement that locates the row to be
updated with its ID and overwrites the current field values unconditionally. If you set the
ConflictOption property to CompareAllSearchValues, the CommandBuilder will generate
the following UPDATE statement:
UPDATE [Products]
SET [ProductName] = @p1, [CategoryID] = @p2,
[UnitPrice] = @p3, [UnitsInStock] = @p4,
STORING DATA IN DATASETS 707
[UnitsOnOrder] = @p5
WHERE (([ProductID] = @p6) AND ([ProductName] = @p7)
AND ((@p8 = 1 AND [CategoryID] IS NULL) OR
([CategoryID] = @p9)) AND
((@p10 = 1 AND [UnitPrice] IS NULL) OR
([UnitPrice] = @p11)) AND
((@p12 = 1 AND [UnitsInStock] IS NULL) OR
([UnitsInStock] = @p13)) AND
((@p14 = 1 AND [UnitsOnOrder] IS NULL) OR
([UnitsOnOrder] = @p15)))
This is a lengthy statement indeed. The row to be updated is identified by its ID, but the oper-
ation doesn’t take place if any of the other fields don’t match the value read into the DataSet.
This statement will fail to update the corresponding row in the Products table if it has already
been edited by another user.
The last member of the ConflictOption enumeration, the CompareRowVersion option,
works with tables that have a TimeStamp column, which is automatically set to the time of
the update. If the row has a time stamp that’s later than the value read when the DataSet was

populated, it means that the row has been updated already by another user and the UPDATE
statement will fail.
The SimpleDataSet sample project, which is discussed later in this chapter and demon-
strates the basic DataSet operations, generates the UPDATE/INSERT/DELETE statements for
the Categories and Products tables with the help of the CommandBuilder class and displays
them on the form when the application starts. Open the project to examine the code, and
change the setting of the ConflictOption property to see how it affects the autogenerated SQL
statements.
Accessing the DataSet’s Tables
The DataSet consists of one or more tables, which are represented by the DataTable class. Each
DataTable in the DataSet may correspond to a table in the database or a view. When you exe-
cute a query that retrieves fields from multiple tables, all selected columns will end up in a
single DataTable of the DataSet. You can select any DataTable in the DataSet by its index or
its name:
DS.Tables(0)
DS.Tables("Customers")
Each table contains columns, which you can access through the Columns collection. The
Columns collection consists of DataColumn objects, with one DataColumn object for each
column in the corresponding table. The Columns collection is the schema of the DataTable
object, and the DataColumn class exposes properties that describe a column. ColumnName is the
column’s name, DataType is the column’s type, MaxLength is the maximum size of text
columns, and so on. The AutoIncrement property is True for Identity columns, and the
AllowDBNull property determines whether the column allows Null values. In short, all the
properties you can set visually as you design a table are also available to your code through
the Columns collection of the DataTable object. You can use the DataColumn class’s properties
to find out the structure of the table or to create a new table. To add a table to a DataSet, you
can create a new DataTable object. Then create a DataColumn object for each column, set its
708 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
properties, and add the DataColumn objects to the DataTable Columns collection. Finally, add
the DataTable to the DataSet. The process is described in detail in the online documentation, so

I won’t repeat it here.
Working with Rows
As far as data are concerned, each DataTable consists of DataRow objects. All DataRow objects
of a DataTable have the same structure and can be accessed through an index, which is the
row’s order in the table. To access the rows of the Customers table, use an expression like
the following:
DS.Customers.Rows(iRow)
where iRow is an integer value from zero (the first row in the table) up to DS.Customers.Rows
.Count – 1 (the last row in the table). To access the individual fields of a DataRow object, use
the Item property. This property returns the value of a column in the current row by either its
index,
DS.Customers.Rows(0).Item(0)
or its name:
DS.Customers.Rows(0).Item("CustomerID")
To iterate through the rows of a DataSet, you can set up a For…Next loop like the following:
Dim iRow As Integer
For iRow = 0 To DSProducts1.Products.Rows.Count - 1
‘ process row: DSProducts.Products.Rows(iRow)
Next
Alternatively, you can use a For Each…Next loop to iterate through the rows of
the DataTable:
Dim product As DataRow
For Each product In DSProducts1.Products.Rows
‘ process prodRow row:
‘ product.Item("ProductName"),
‘ product.Item("UnitPrice"), and so on
Next
To edit a specific row, simply assign new values to its columns. To change the value of the
ContactName column of a specific row in a DataTable that holds the customers of the North-
wind database, use a statement like the following:

DS.Customers(3).Item("ContactName") = "new contact name"
STORING DATA IN DATASETS 709
The new values are usually entered by a user on the appropriate interface, and in your
code you’ll most likely assign a control’s property to a row’s column with statements like
the following:
If txtName.Text.Trim <> "" Then
DS.Customers(3).Item("ContactName") = txtName.Text
Else
DS.Customers(3).Item("ContactName") = DBNull.Value
End If
The code segment assumes that when the user doesn’t supply a value for a column, this col-
umn is set to null (if the column is nullable, of course, and no default value has been specified).
If the control contains a value, this value is assigned to the ContactName column of the fourth
row in the Customers DataTable of the DS DataSet.
Handling Null Values
An important (and quite often tricky) issue in coding data-driven applications is the handling
of Null values. Null values are special, in the sense that you can’t assign them to control prop-
erties or use them in other expressions. Every expression that involves Null values will throw a
runtime exception. The DataRow object provides the IsNull method, which returns True if the
column specified by its argument is a Null value:
If customerRow.IsNull("ContactName") Then
‘ handle Null value
Else
‘ process value
End If
In a typed DataSet, DataRow objects provide a separate method to determine whether a spe-
cific column has a Null value. If the customerRow DataRow belongs to a typed DataSet, you
can use the IsContactNameNull method instead:
If customerRow.IsContactNameNull Then
‘ handle Null value for the ContactName

Else
‘ process value: customerRow.ContactName
End If
If you need to map Null columns to specific values, you can do so with the ISNULL() func-
tion of T-SQL, as you retrieve the data from the database. In many applications, you want to
display an empty string or a zero value in place of a Null field. You can avoid all the compar-
isons in your code by retrieving the corresponding field with the ISNULL() function in your
SQL statement. Where the column name would appear in the SELECT statement, use an expres-
sion like the following:
ISNULL(customerBalance, 0.00)
710 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
If the customerBalance column is Null for a specific row, SQL Server will return the numeric
value zero. This value can be used in reports or other calculations in your code. Notice that the
customer’s balance shouldn’t be Null. A customer always has a balance, even if it’s zero. When
a product’s price is Null, it means that we don’t know the price of the product (and there-
fore can’t sell it). In this case, a Null value can’t be substituted with a zero value. You must
always carefully handle Null columns in your code, and how you’ll handle them depends on
the nature of the data they represent.
Adding and Deleting Rows
To add a new row to a DataTable, you must first create a DataRow object, set its column val-
ues, and then call the Add method of the Rows collection of the DataTable to which the new
row belongs, passing the new row as an argument. If the DS DataSet contains the Customers
DataTable, the following statements will add a new row for the Customers table:
Dim newRow As New DataRow = dataTable.NewRow
newRow.Item("CompanyName") = "new company name"
newRow.Item("CustomerName") = "new customer name"
newRow.Item("ContactName") = "new contact name"
DS.Customers.Rows.Add(newRow)
Notice that you need not set the CustomerID column. This column is defined as an Identity
column and is assigned a new value automatically by the DataSet. Of course, when the row

is submitted to the database, the ID assigned to the new customer by the DataSet may already
be taken. SQL Server will assign a new unique value to this column when it inserts it into
the table. It’s recommended that you set the AutoIncrementSeed property of an Identity
column to 0 and the AutoIncrement property to –1 so that new rows are assigned consecutive
negative IDs in the DataSet. Presumably, the corresponding columns in the database have a
positive Identity setting, so when these rows are submitted to the database, they’re assigned
the next Identity value automatically. If you’re designing a new database, use globally unique
identifiers (GUIDs) instead of Identity values. A GUID can be created at the client and is
unique: It can be generated at the client and will also be inserted in the table when the row is
committed. To create GUIDs, call the NewGuid method of the Guid class:
newRow.Item("CustomerID") = Guid.NewGuid
To delete a row, you can remove it from the Rows collection with the Remove or RemoveAt
method of the Rows collection, or you can call the Delete method of the DataRow object that
represents the row. The Remove method accepts a DataRow object as an argument and removes
it from the collection:
Dim customerRow As DS.CustomerRow
customerRow = DS.Customers.Rows(2)
DS.Customers.Remove(customerRow)
The RemoveAt method accepts as an argument the index of the row you want to delete in the
Rows collection. Finally, the Delete method is a method of the DataRow class, and you must
apply it to a DataRow object that represents the row to be deleted:
customerRow.Delete
STORING DATA IN DATASETS 711
Deleting versus Removing Rows
The Remove method removes a row from the DataSet as if it were never read when the
DataSet was filled. Deleted rows are not always removed from the DataSet, because
the DataSet maintains its state. If the row you’ve deleted exists in the underlying table
(in other words, if it’s a row that was read into the DataSet when you filled it), the row will
be marked as deleted but will not be removed from the DataSet. If it’s a row that was added
to the DataSet after it was read from the database, the deleted row is actually removed from

the Rows collection.
You can physically remove deleted rows from the DataSet by calling the DataSet’s
AcceptChanges method. However, after you’ve accepted the changes in the DataSet, you
can no longer submit any updates to the database. If you call the DataSet RejectChanges
method, the deleted rows will be restored in the DataSet.
Navigating Through a DataSet
The DataTables making up a DataSet may be related — they usually are. There are methods
that allow you to navigate from table to table following the relations between their rows. For
example, you can start with a row in the Customers DataTable, retrieve its child rows in the
Orders DataTable (the orders placed by the selected customer), and then drill down to the
details of each of the selected orders.
The relations of a DataSet are DataRelation objects and are stored in the Relations property
of the DataSet. Each relation is identified by a name, the two tables it relates to, and the
fields of the tables on which the relation is based. It’s possible to create relations in your
code, and the process is really quite simple. Let’s consider a DataSet that contains the Cate-
gories and Products tables. To establish a relation between the two tables, create two instances
of the DataTable object to reference the two tables:
Dim tblCategories As DataTable = DS.Categories
Dim tblProducts As DataTable = DS.Products
Then create two DataColumn objects to reference the columns on which the relation is based.
They’re the CategoryID columns of both tables:
Dim colCatCategoryID As DataColumn =
tblCategories.Columns("CategoryID")
Dim colProdCategoryID As DataColumn =
tblProducts.Columns("CategoryID")
And finally, create a new DataRelation object, and add it to the DataSet:
Dim DR As DataRelation
DR = New DataRelation("Categories2Products",
colCatCategoryID, colProdCategoryID)
Notice that you need to specify only the columns involved in the relation, and not the tables

to be related. The information about the tables is derived from the DataColumn objects. The
first argument of the DataRelation constructor is the relation’s name. If the relation involves
712 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
multiple columns, the second and third arguments of the constructor become arrays of Data-
Column objects.
To navigate through related tables, the DataRow object provides the GetChildRows
method, which returns the current row’s child rows as an array of DataRow objects, and
the GetParentRow/GetParentRows methods, which return the current row’s parent row(s).
GetParentRow returns a single DataRow object, and GetParentRows returns an array of
DataRow objects. Because a DataTable may be related to multiple DataTables, you must
also specify the name of the relation. Consider a DataSet with the Products, Categories, and
Suppliers tables. Each row of the Products table can have two parent rows, depending on
which relation you want to follow. To retrieve the product category, use a statement like
the following:
DS.Products(iRow).GetParentRow("CategoriesProducts")
The product supplier is given by the following expression:
DS.Products(iRow).GetParentRow("SuppliersProducts")
If you start with a category, you can find out the related products with the GetChildRows
method, which accepts as an argument the name of a Relation object:
DS.Categories(iRow).GetChildRows("CategoriesProducts")
To iterate through the products of a specific category (in other words, the rows of the Prod-
ucts table that belong to a category), set up a loop like the following:
Dim product As DataRow
For Each product In DS.Categories(iRow).
GetChildRows("CategoriesProducts")
’ process product
Next
Row States and Versions
Each row in the DataSet has a State property. This property indicates the row’s state, and
its value is a member of the DataRowState enumeration, whose members are described in

Table 16.2.
You can use the GetChanges method to find the rows that must be added to the under-
lying table in the database, the rows to be updated, and the rows to be removed from the
underlying table.
If you want to update all rows of a DataTable, call an overloaded form of the
DataAdapter Update method, which accepts as an argument a DataTable and submits
its rows to the database. The edited rows are submitted through the UpdateCommand
object of the appropriate DataAdapter, the new rows are submitted through the Insert-
Command object, and the deleted rows are submitted through the DeleteCommand object.
STORING DATA IN DATASETS 713
Instead of submitting the entire table, however, you can create a subset of a DataTable that con-
tains only the rows that have been edited, inserted, or deleted. The GetChanges method of the
DataTable object retrieves a subset of rows, depending on the argument you pass to it, and this
argument is a member of the DataRowState enumeration:
Dim DT As New DataTable =
Products1.Products.GetChanges(DataRowState.Deleted)
Table 16.2: DataSet state property members
Property Member Description
Added The row has been added to the DataTable, and the AcceptChanges method
has not been called.
Deleted The row was deleted from the DataTable, and the AcceptChanges method
has not been called.
Detached The row has been created with its constructor but has not yet been added to
a DataTable.
Modified The row has been edited, and the AcceptChanges method has not been
called.
Unchanged The row has not been edited or deleted since it was read from the database or
the AcceptChanges was last called. (In other words, the row’s fields are
identical to the values read from the database.)
This statement retrieves the rows of the Customers table that were deleted and stores them

in a new DataTable. The new DataTable has the same structure as the one from which the
rows were copied, and you can access its rows and their columns as you would access any
DataTable of a DataSet. You can even pass this DataTable as an argument to the appropriate
DataAdapter’s Update method. This form of the Update method allows you to submit selected
changes to the database.
In addition to a state, rows have a version. What makes the DataSet such a powerful tool
for disconnected applications is that it maintains not only data but also the changes in its
data. The Rows property of the DataTable object is usually called with the index of the desired
row, but it accepts a second argument, which determines the version of the row you want
to read:
DS.Tables(0).Rows(i, version)
This argument is a member of the DataRowVersion enumeration, whose values are described
in Table 16.3.
714 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
Table 16.3: DataRowVersion enumeration members
Enumeration Member Description
Current Returns the row’s current values (the fields as they were edited
in the DataSet).
Default Returns the default values for the row. For added, edited, and
current rows, the default version is the same as the current version.
For deleted rows, the default versions are the same as the original
versions. If the row doesn’t belong to a DataTable, the default version
is the same as the proposed version.
Original Returns the row’s original values (the values read from the database).
Proposed Returns the row’s proposed value (the values assigned to a row that
doesn’t yet belong to a DataTable).
If you attempt to submit an edited row to the database and the operation fails, you can give
the user the option to edit the row’s current version or to restore the row’s original values. To
retrieve the original version of a row, use an expression like the following:
DS.Tables(0).Row(i, DataRowVersion.Original)

Although you can’t manipulate the version of a row directly, you can use the
AcceptChanges and RejectChanges methods to either accept the changes or reject them.
These two methods are exposed by the DataSet, DataTable, and DataRow classes. The dif-
ference is the scope: Applying RejectChanges to the DataSet restores all changes made to the
DataSet (not a very practical operation), whereas applying RejectChanges to a DataTable
object restores the changes made to the specific table rows; applying the same method to the
DataRow object restores the changes made to a single row.
The AcceptChanges method sets the original value of the affected row(s) to the proposed
value. Deleted rows are physically removed. The RejectChanges method removes the pro-
posed version of the affected row(s). You can call the RejectChanges method when the user
wants to get rid of all changes in the DataSet. Notice that after you call the AcceptChanges
method, you can no longer update the underlying tables in the database, because the DataSet
no longer knows which rows were edited, inserted, or deleted. Call the AcceptChanges method
only for DataSets you plan to persist on disk and not submit to the database.
Performing Update Operations
One of the most important topics in database programming is how to submit changes to the
database. There are basically two modes of operation: single updates and multiple updates.
A client application running on a local-area network along with the database server can (and
should) submit changes as soon as they occur. If the client application is not connected to the
database server at all times, changes may accumulate at the client and can be submitted in
batch mode when a connection to the server is available.
From a developer’s point of view, the difference between the two modes is how you handle
update errors. If you submit individual rows to the database and the update operation fails,
PERFORMING UPDATE OPERATIONS 715
you can display a warning and let the user edit the data again. You can write code to restore
the row to its original state, or not. In any case, it’s fairly easy to handle isolated errors. If the
application submits a few dozen rows to the database, several of these rows may fail to update
the underlying table, and you’ll have to handle the update errors from within your code. At the
very least, you must validate the data as best as you can at the client before submitting it to the
database. No matter how thoroughly you validate your data, however, you can’t be sure that

they will be inserted into the database successfully.
Another factor you should consider is the nature of the data you work with. Let’s consider
an application that maintains a database of books and an application that takes orders. The
book maintenance application handles publishers, authors, translators, and related data. If two
dozen users are entering and editing titles, they will all work with the same authors. If you
allow them to work in disconnected mode, the same author name may be entered several times,
because no user can see the changes made by any other user. This application should be con-
nected: Every time a user adds a new author, the table with the author names in the database
must be updated so that other users can see the new author. The same goes for publishers,
translators, topics, and so on. A disconnected application of this type should also include utili-
ties to consolidate multiple author and publisher names.
An order-taking application can safely work in a disconnected mode, because orders entered
by one user are not aware of and don’t interfere with the orders entered by another user. You
can install the client application on several salespersons’ notebooks so they can take orders on
the go and upload them after establishing a connection between the notebook and the database
server (which may even happen when the salespeople return to the company’s offices).
Updating the Database with the DataAdapter
The simplest method of submitting changes to the database is to use each DataAdapter’s
Update method. The DataTable object provides the members you need to retrieve the rows
that failed to update the database, as well as the messages returned by the database server,
and you’ll see how these members are used in this section. The Update method may not have
updated all the rows in the underlying tables. If a product was removed from the Products
table in the database in the meantime, the DataAdapter’s UpdateCommand will not be able
to submit the changes made to that specific product. A product with a negative value may
very well exist at the client, but the database will reject this row, because it violates one of
the constraints of the Products table. It’s also important to validate the data at the client to
minimize errors when you submit changes to the database.
If the database returned any errors during the update process, the HasErrors property
of the DataSet object will be set to True. You can retrieve the rows in error from each table
with the GetErrors method of the DataTable class. This method returns an array of DataRow

objects, and you can process them in any way you see fit. The code shown in Listing 16.8
iterates through the rows of the Categories table that are in error and prints the description of
the error in the Output window.
Listing 16.8: Retrieving and displaying the update errors
If Products1.HasErrors Then
If Products1.Categories.GetErrors.Length = 0 Then
Console.WriteLine("Errors in the Categories DataTable")
Else
716 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
Dim RowsInError() As Products.CategoriesRow
RowsInError = Products1.Categories.GetErrors
Dim row As Products.CategoriesRow
Console.WriteLine("Errors in the Categories table")
For Each row In RowsInError
Console.WriteLine(vbTab & row.CategoryID & vbTab &
row.RowError)
Next
End If
Endif
The DataRow object exposes the RowError property, which is a description of the error
that prevented the update for the specific row. It’s possible that the same row has more than
a single error. To retrieve all columns in error, call the DataRow object’s GetColumnsInError
method, which returns an array of DataColumn objects that are the columns in error.
Handling Identity Columns
An issue that deserves special attention while coding data-driven applications is the handling
of Identity columns. Identity columns are used as primary keys, and each row is guaranteed
to have a unique Identity value because this value is assigned by the database the moment the
row is inserted into its table. The client application can’t generate unique values. When new
rows are added to a DataSet, they’re assigned Identity values, but these values are unique in
the context of the local DataSet. When a row is submitted to the database, any Identity column

will be assigned its final value by the database. The temporary Identity value assigned by the
DataSet is also used as a foreign key value by the related rows, and we must make sure that
every time an Identity value is changed, the change will propagate to the related tables.
Handling Identity values is an important topic, and here’s why: Consider an application
for entering orders or invoices. Each order has a header and a number of detail lines, which
are related to a header row with the OrderID column.Thiscolumnistheprimarykeyinthe
Orders table and is the foreign key in the Order Details table. If the primary key of a header is
changed, the foreign keys of the related rows must change also.
The trick in handling Identity columns is to make sure that the values generated by the
DataSet will be replaced by the database. You do so by specifying that the Identity column’s
starting value is –1 and its autoincrement is –1. The first ID generated by the DataSet will
be –1, the second one will be –2, and so on. Negative Identity values will be rejected by
the database, because the AutoIncrement properties in the database schema are positive. By
submitting negative Identity values to SQL Server, you ensure that new, positive values will be
generated and used by SQL Server.
You must also make sure that the new values will replace the old ones in the related rows.
In other words, we want these values to propagate to all related rows. The DataSet allows you
to specify that changes in the primary key will propagate through the related rows with the
UpdateRule property of the Relation.ChildKeyConstraint property. Each relation exposes
the ChildKeyConstraint property, which determines how changes in the primary key of a
relation affect the child rows. This property is an object that exposes a few properties of its
own. The two properties we’re interested in are UpdateRule and DeleteRule (what happens
to the child rows when the parent row’s primary key is changed or when the primary key is
deleted). You can use one of the rules described in Table 16.4.
VB 2010 AT WORK: THE SIMPLEDATASET PROJECT 717
Table 16.4: ChildKeyConstraint property rules
Rule Description
Cascade Foreign keys in related rows change every time the primary key changes
value so that they’ll always remain related to their parent row.
None The foreign key in the related row(s) is not affected.

SetDefault The foreign key in the related row(s) is set to the DefaultValue property
for the same column.
SetNull The foreign key in the related rows is set to Null.
As you can see, setting the UpdateRule property to anything other than Cascade will break
the relation. If the database doesn’t enforce the relation, you may be able to break it. If the rela-
tion is enforced, however, UpdateRule must be set to Rule.Cascade, or the database will not
accept changes that violate its referential integrity.
If you set UpdateRule to None, you may be able to submit the order to the database. How-
ever, the detail rows may refer to a different order. This will happen when the ID of the header
is changed because the temporary value is already taken. The detail rows will be inserted with
the temporary key and added to the details of another order. Notice that no runtime exception
will be thrown, and the only way to catch this type of error is by examining the data inserted
into the database by your application. By using negative values at the DataSet, you make sure
that the ID of both the header and all detail rows will be rejected by the database and replaced
with valid values. It goes without saying that it’s always a good idea to read back the rows
you submit to the database and ‘‘refresh’’ the data at the client. In the case of the ordering
application, for example, you could read back the order before printing it so that any errors
will be caught as soon as they occur, instead of discovering later orders that do not match
their printouts.
VB 2010 at Work: The SimpleDataSet Project
Let’s put together the topics discussed so far to build an application that uses a DataSet to store
and edit data at the client. The sample application is called SimpleDataSet, and its interface is
shown in Figure 16.4.
Click the large Read Products and Related Tables button at the top to populate a DataSet
with the rows of the Products and Categories tables of the Northwind database. The application
displays the categories and the products in each category in a RichTextBox control. Instead of
displaying all the columns in a ListView control, I’ve chosen to display only a few columns
of the Products table to make sure that the application connects to the database and populates
the DataSet.
The Edit DataSet button edits a few rows of both tables. The code behind this button

changes the name and price of a couple of products in random, deletes a row, and adds a new
row. It actually sets the price of the edited products to a random value in the range from –10
to 40 (negative prices are invalid, and they will be rejected by the database). The DataSet keeps
track of the changes, and you can review them at any time by clicking the Show Edits button,
which displays the changes in the DataSet in a message box, like the one shown in Figure 16.5.
718 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
Figure 16.4
The SimpleDataSet
project populates a
DataSetattheclient
with categories and
products.
Figure 16.5
Viewing the changes i n
the client DataSet
You can undo the changes and reset the DataSet to its original state by clicking the Reject
Changes button, which calls the RejectChanges method of the DataSet class to reject the edits
in all tables. It removes the new rows, restores the deleted ones, and undoes the edits in the
modified rows.
VB 2010 AT WORK: THE SIMPLEDATASET PROJECT 719
The Save DataSet and Load DataSet buttons persist the DataSet at the client so that you can
reload it later without having to access the database. The code shown in Listing 16.9 calls the
WriteXml and ReadXml methods and uses a hard-coded filename. WriteXml and ReadXml save
the data only, and you can’t create a DataSet by calling the ReadXml method; this method will
populate an existing DataSet.
To actually create and load a DataSet, you must first specify its structure. Fortunately, the
DataSet exposes the WriteXmlSchema and ReadXmlSchema methods, which store and read
the schema of the DataSet. WriteXmlSchema saves the schema of the DataSet, so you can
regenerate an identical DataSet with the ReadXmlSchema method, which reads an existing
schema and structures the DataSet accordingly. The code behind the Save DataSet and Load

DataSet buttons first calls these two methods to take care of the DataSet’s schema and then
calls the WriteXml and ReadXml methods to save/load the data.
Listing 16.9: Saving and loading the DataSet
Private Sub bttnSave_Click(…) Handles bttnSave.Click
Try
DS.WriteXmlSchema("DataSetSchema.xml")
DS.WriteXml("DataSetData.xml", XmlWriteMode.DiffGram)
Catch ex As Exception
MsgBox("Failed to save DataSet" & vbCrLf & ex.Message)
Exit Sub
End Try
MsgBox("DataSet saved successfully")
End Sub
Private Sub bttnLoad_Click(…) Handles bttnLoad.Click
Try
DS.ReadXmlSchema("DataSetSchema.xml")
DS.ReadXml("DataSetData.xml", XmlReadMode.DiffGram)
Catch ex As Exception
MsgBox("Failed to load DataSet" & vbCrLf & ex.Message)
Exit Sub
End Try
ShowDataSet()
End Sub
The Submit Edits button, finally, submits the changes to the database. The code attempts to
submit all edited rows, but some of them may fail to update the database. The local DataSet
doesn’t enforce any check constraints, so when the application attempts to submit a product
row with a negative price to the database, the database will reject the update operation.
The DataSet rows that failed to update the underlying tables are shown in a message box
like the one shown in Figure 16.6. You can review the values of the rows that failed to
update the database and the description of the error returned by the database and edit them

further. The rows that failed to update the underlying table(s) in the database remain in
the DataSet. Of course, you can always call the RejectChanges method for each row that
failed to update the database to undo the changes of the invalid rows. As is, the application
doesn’t reject any changes on its own. If you click the Show Edits button after an update
720 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
operation, you will see the rows that failed to update the database, because they’re marked as
inserted/modified/deleted in the DataSet.
Figure 16.6
Viewing the rows that
failed to update the
database and the error
message returned by the
DBMS
Let’s start with the code that loads the DataSet. When the form is loaded, the code initializes
two DataAdapter objects, which load the rows of the Categories and Products tables. The
names of the two DataAdapters are DACategories and DAProducts. They’re initialized to
the CN connection object and a simple SELECT statement, as shown in Listing 16.10.
Listing 16.10: Setting up the DataAdapters for the Categories and Products tables
Private Sub Form1_Load(…) Handles MyBase.Load
Dim CN As New SqlClient.SqlConnection(
"data source=localhost;initial catalog=northwind; " &
"Integrated Security=True")
DACategories.SelectCommand = New SqlClient.SqlCommand(
"SELECT CategoryID, CategoryName, Description FROM Categories")
DACategories.SelectCommand.Connection = CN
Dim CategoriesCB As SqlCommandBuilder = New SqlCommandBuilder(DACategories)
CategoriesCB.ConflictOption = ConflictOption.OverwriteChanges
DAProducts.SelectCommand = New SqlClient.SqlCommand(
"SELECT ProductID, ProductName, " &
"CategoryID, UnitPrice, UnitsInStock, " &

"UnitsOnOrder FROM Products ")
DAProducts.SelectCommand.Connection = CN
DAProducts.ContinueUpdateOnError = True
Dim ProductsCB As SqlCommandBuilder = New SqlCommandBuilder(DAProducts)
ProductsCB.ConflictOption = ConflictOption.CompareAllSearchableValues
End Sub
VB 2010 AT WORK: THE SIMPLEDATASET PROJECT 721
I’ve specified the SELECT statements in the constructors of the two DataAdapter objects and
let the CommandBuilder objects generate the update statement. You can change the value of
the ConflictOption property to experiment with the different styles of update statements that
the CommandBuilder will generate. When the form is loaded, all the SQL statements generated
for the DataAdapters are shown in the RichTextBox control. (The corresponding statements are
not shown in the listing, but you can open the project in Visual Studio to examine the code.)
The Read Products and Related Tables button populates the DataSet and then displays the
categories and products in the RichTextBox control by calling the ShowDataSet() subroutine,
as shown in Listing 16.11.
Listing 16.11: Populating and displaying the DataSet
Private Sub bttnCreateDataSet_Click(…) Handles bttnCreateDataSet.Click
DS.Clear()
DACategories.Fill(DS, "Categories")
DAProducts.Fill(DS, "Products")
DS.Relations.Clear()
DS.Relations.Add(New Data.DataRelation("CategoriesProducts",
DS.Tables("Categories").Columns("CategoryID"),
DS.Tables("Products").Columns("CategoryID")))
ShowDataSet()
End Sub
Private Sub ShowDataSet()
RichTextBox1.Clear()
Dim category As DataRow

For Each category In DS.Tables("Categories").Rows
RichTextBox1.AppendText(
category.Item("CategoryName") & vbCrLf)
Dim product As DataRow
For Each product In category.GetChildRows("CategoriesProducts")
RichTextBox1.AppendText(
product.Item("ProductID") & vbTab &
product.Item("ProductName" & vbTab)
If product.IsNull("UnitPrice") Then
RichTextBox1.AppendText(" " & vbCrLf)
Else
RichTextBox1.AppendText(
Convert.ToDecimal(product.Item("UnitPrice"))
.ToString("#.00") & vbCrLf)
End If
Next
Next
End Sub
After calling the Fill method to populate the two DataTables, the code sets up a Data-
Relation object to link the products to their categories through the CategoryID column and
then displays the categories and the corresponding products under each category. Notice the
722 CHAPTER 16 DEVELOPING DATA-DRIVEN APPLICATIONS
statement that prints the products. Because the UnitPrice column may be Null, the code calls
the IsNull method of the product variable to find out whether the current product’s price is
Null. If so, it doesn’t attempt to call the product.Item("UnitPrice") expression, which would
result in a runtime exception, and prints three asterisks in its place.
The Edit DataSet button modifies a few rows in the DataSet. Here’s the statement that
changes the name of a product selected at random (it appends the string NEW to the product’s
name):
DS.Tables("Products").Rows(

RND.Next(1, 78)).Item("ProductName") &= " - NEW"
The same button randomly deletes a product, sets the price of another row to a random
value in the range from –10 to 40, and inserts a new row with a price in the same range.
If you click the Edit DataSet button a few times, you’ll very likely get a few invalid rows.
The Show Edits button retrieves the edited rows of both tables and displays them. It uses
the DataRowState property to discover the state of the row (whether it’s new, modified,
or deleted) and displays the row’s ID and a couple of additional columns. Notice that you
can retrieve the proposed and original versions of the edited rows (except for the deleted
rows, which have no proposed version) and display the row’s fields before and after
the editing on a more elaborate interface. Listing 16.12 shows the code behind the Show
Edits button.
Listing 16.12: Viewing the edited rows
Private Sub bttnShow_Click(…)Handles bttnShow.Click
Dim product As DataRow
Dim msg As String = ""
For Each product In DS.Tables("Products").Rows
If product.RowState = DataRowState.Added Then
msg &= "ADDED PRODUCT: " &
product.Item("ProductName") & vbTab &
product.Item("UnitPrice").ToString & vbCrLf
End If
If product.RowState = DataRowState.Modified Then
msg &= "MODIFIED PRODUCT: " &
product.Item("ProductName") & vbTab &
product.Item("UnitPrice").ToString & vbCrLf
End If
If product.RowState = DataRowState.Deleted Then
msg &= "DELETED PRODUCT: " &
product.Item("ProductName",
DataRowVersion.Original) & vbTab &

product.Item("UnitPrice",
DataRowVersion.Original).ToString & vbCrLf
End If
Next
If msg.Length > 0 Then
THE BOTTOM LINE 723
MsgBox(msg)
Else
MsgBox("There are no changes in the dataset")
End If
End Sub
I only show the statements that print the edited rows of the Products DataTable in the listing.
Notice that the code retrieves the proposed versions of the modified and added rows but the
original version of the deleted rows.
The Submit Edits button submits the changes to the two DataTables to the database by
calling the Update method of the DAProducts DataAdapter and then the Update method of
the DACategories DataAdapter. After that, it retrieves the rows in error with the GetErrors
method and displays the error message returned by the DBMS with statements similar to the
ones shown in Listing 16.12.
The Bottom Line
Create and populate DataSets. DataSets are data containers that reside at the client and are
populated with database data. The DataSet is made up of DataTables, which correspond to
database tables, and you can establish relationships between DataTables, just like relating
tables in the database. DataTables, in turn, consist of DataRow objects.
Master It How do you populate DataSets and then submit the changes made at the client
to the database?
Establish relations between tables in the DataSet. You can think of the DataSet as a small
database that resides at the client, because it consists of tables and the relationships between
them. The relations in a DataSet are DataRelation objects, which are stored in the Relations
property of the DataSet. Each relation is identified by a name, the two tables it relates, and the

fields of the tables on which the relation is based.
Master It How do you navigate through the related rows of two tables?
Submit changes in the DataSet to the database. The DataSet maintains not only data at the
client but their states and versions too. It knows which rows were added, deleted, or modified
(the DataRowState property), and it also knows the version of each row read from the database
and the current version (the DataRowVersion property).
Master It How will you submit the changes made to a disconnected DataSet
to the database?

Chapter 17
Using the Entity Data Model
In Chapter 16, ‘‘Developing Data-Driven Applications,’’ you learned how to use DataSets, how
to perform data binding, and how to use LINQ to SQL. As it happens, LINQ to SQL is not the
only object-relational technology Microsoft has to offer.
In this chapter, you’ll discover Microsoft’s latest data technology, the Entity Framework.
Released initially with Service Pack 1 of Microsoft .NET Framework 3.5, it is the 2010 version
of Visual Studio that ships with this data technology out of the box for the first time. While
LINQ to SQL is a somewhat lightweight, code-oriented data technology, the Entity Framework
is a comprehensive, model-driven data solution.
The Entity Framework represents a central piece of Microsoft’s long-term data access
strategy. With its emphasis on modeling and on the isolation of data and application layers, it
promises to deliver a powerful data platform capable of supporting the applications through a
complete application life cycle.
For a Visual Basic programmer, it brings working with the data under the familiar
object-oriented mantle and provides numerous productivity enhancements. You will be able
to construct ‘‘zero SQL code’’ data applications, leverage LINQ when working with data,
and change the underlying data store without any impact on your application. In a few short
words, using the Entity Framework to work with data is a whole different ballgame.
In this chapter, you’ll learn how to do the following:
◆ Employ deferred loading when querying the Entity Data Model

◆ Use entity inheritance features in the Entity Framework
◆ Create and query related entities
The Entity Framework: Raising the Data Abstraction Bar
In Chapter 15, ‘‘Programming with ADO.NET,’’ you saw traditional .NET Framework tech-
niques for working with data. In stream-based data access, you use a DataReader to read from
the data store (typically a relational database) and can use the Command object to modify the
data in the data store. Set-based data access encapsulates data operations through the DataSet
object, whose collection of DataTable objects closely mimics the structure of tables or views in
the database. A DataSet lets you work with data in disconnected mode, so you can load the
726 CHAPTER 17 USING THE ENTITY DATA MODEL
data from the database into the application, disconnect, work on the data, and finally connect
and submit modifications to the database in a single operation.
Both techniques provide a well-known way to work with data in your Visual Basic
application. The DataSet goes one step further than stream-based data access in providing
the programming abstraction for data access that hides many of the complexities of low-level
data access. As a result, you will have to write a lot less SQL code. Neither method, however,
provides a higher-level abstraction of the underlying database structure. This interdependence
between your application and the data layer is problematic for several reasons, as you will see
in the next section.
The Entity Framework brings another level of abstraction to the data layer. It lets you work
with a conceptual representation of data, also known as a conceptual schema, instead of work-
ing with the data directly. This schema is then projected to your application layer, where code
generation is used to create a .NET representation of your conceptual schema. Next, the Entity
Framework generates a relational (or logical) schema used to describe the data model in rela-
tional terms. Finally, mapping between the relational schema and .NET classes is generated.
Based on this data, the Entity Framework is capable of creating and populating .NET objects
with the data from a data store and persisting modifications made on the object data back to
the data store.
How Will You Benefit from the Entity Framework?
One famous programming aphorism states that ‘‘all problems in computing can be solved by

another level of indirection.’’ Although the Entity Framework introduces new level of indirec-
tion (and abstraction), you will see that this additional level is actually put to a good use. I’ll
show you the problems that the folks at Microsoft tried to tackle with the Entity Framework
and how they managed to resolve them.
Preserving the Expressiveness of the Data Model
If you have a lot of experience working with relational databases, especially with databases
that have been around for some time and have been through numerous modifications, you
must have been puzzled by the actual meaning of some elements in a database. Questions like
the following might ring a bell: What is this column used for? Why is this set of data dupli-
cated between tables? Why is this set of columns in a table empty in certain rows?’’
A good understanding of your customer’s needs and business is crucial for the success of
the application you will be developing. This understanding can be written down in the form
of requirements and together with the description of the business (or problem domain) will be
indispensable for the design of your application.
An important part of the design of many applications is the data structure that the system
will use. One of the most popular methods for designing the data is the entity-relationship
model (ERM). The ERM is a conceptual representation of data where the problem domain is
described in the form of entities and their relationships. In Visual Studio, this model is called
the Entity Data Model (EDM), and you will learn how to create the EDM in the next section.
Figure 17.1 shows the sample Entity Data Model diagram inside Visual Studio 2010.
Entities generally can be identified by the primary key and have some important character-
istics known as attributes. For example, a person entity might have a primary key in the form
of their Social Security number (SSN) and attributes First and Last Name. (Although an SSN
conceptually fits well in the role of a primary key and therefore I chose it for the primary key
in the Person table in the example Books and Authors project later in this chapter, in practice
its use is discouraged. See ‘‘Using a Social Security Number as a Primary Key’’ in the following
sidebar for more information.)
THE ENTITY FRAMEWORK: RAISING THE DATA ABSTRACTION BAR 727
Figure 17.1
Entity Data Model dia-

gram in Visual Studio
2010’s EDM Designer
Using a Social Security Number as a Primary Key
Although a Social Security number might seem like the natural first choice for the primary
key in a table representing some kind of a ‘‘person’’ entity — a Customer, Client, Employee,
or User table, for example — its use in practice is actually strongly discouraged. Because of
privacy concerns, security threats like identity theft, and recent regulatory guidelines, SSNs
should be kept in a database only in encrypted form — or not kept at all. You should also
be aware that people can change their SSNs during their lifetime. As such, an SSN is not a
particularly good choice for a primary key.
If there is no good natural key, you can always resort to a surr ogate, database-generated
key. An artificial key, however, does not resolve the issue of duplicate entries that a number
like an SSN seems to resolve. Namely, there is nothing to prevent one person’s data from
being inserted into t he database twice. Inserting duplicate entities can present a serious data
consistency flaw, and as such it is best controlled on the database level.
If there is no natural key that can be used to control a duplicate entry, you can resort to
placing a UNIQUE constraint on a combination of person attributes. For example, it is highly
unlikely that you will find two persons with the same full name, date and place of birth, and
telephone number. If you do not have all of these attributes at your disposal, you might need
to relinquish the control of duplication to some other application layer or even to the user.

×