Contents
1. The
ADO.NET Object Model
2. Database Updates from
Datasets
3. Example
More Information on installing the .Net Framework click here.
Download full Visual Studio
C# .NET Example from this Article.
Overview
The ADO.NET object model is rich, but at its heart it is a fairly
straightforward set of classes. The most important of these is the DataSet. The
DataSet represents a subset of the entire database, cached on your machine without a
continuous connection to the database.
Periodically, you'll reconnect the DataSet to its parent database,
update the database with changes you've made to the DataSet, and update the DataSet
with changes in the database made by other processes.
This is highly efficient, but to be effective the DataSet must be
a robust subset of the database, capturing not just a few rows from a single table,
but also a set of tables with all the metadata necessary to represent the
relationships and constraints of the original database. This is, not surprisingly,
what ADO.NET provides.
The DataSet is composed of DataTable objects as well as
DataRelation objects. These are accessed as properties of the DataSet object. The
Tables property returns a DataTableCollection, which in turn contains all the
DataTable objects.
DataTables and DataColumns
The DataTable can be created programmatically or as a result of a
query against the database. The DataTable has a number of public properties,
including the Columns collection, which returns the DataColumnCollection object,
which in turn consists of DataColumn objects. Each DataColumn object represents a
column in a table.
DataRelations
In addition to the Tables collection, the DataSet has a Relations
property, which returns a DataRelationCollection consisting of DataRelation objects.
Each DataRelation represents a relationship between two tables through DataColumn
objects. For example, in the SQL Server 2000 Northwind database the Customers table
is in a relationship with the Orders table through the CustomerID column.
The nature of the relationship is one-to-many, or parent-to-child.
For any given order, there will be exactly one customer, but any given customer might
be represented in any number of orders.
Rows
DataTable's Rows collection returns a set of rows for any given
table. Use this collection to examine the results of queries against the database,
iterating through the rows to examine each record in turn. Programmers experienced
with ADO are often confused by the absence of the RecordSet with its moveNext and
movePrevious commands. With ADO.NET, you do not iterate through the DataSet; instead,
access the table you need, and then you can iterate through the Rows collection,
typically with a foreach loop. You'll see this in the first example in this
chapter.
Data Adapter
The DataSet is an abstraction of a relational database. ADO.NET
uses a DataAdapter as a bridge between the DataSet and the data source, which is the
underlying database. DataAdapter provides the Fill( ) method to retrieve data from
the database and populate the DataSet.
The DBConnection object represents a connection to a data source.
This connection can be shared among different command objects. The DBCommand object
allows you to send a command (typically a SQL statement or a stored procedure) to the
database. Often these objects are implicitly created when you create your DataSet,
but you can explicitly access these objects.
Rather than tie the DataSet object too closely to your database
architecture, ADO.NET uses a DataAdapter object to mediate between the DataSet object
and the database. This decouples the DataSet from the database and allows a single
DataSet to represent more than one database or other data source.
Datasets Datasets are a fundamental part of the ADO.NET
architecture, providing both high-performance data access as well as scalability.
Datasets store data in a disconnected cache. The structure of a dataset is
similar to that of a relational database; it exposes a hierarchical object model of
tables, rows, and columns. In addition, it contains constraints and relationships
defined for the dataset.
Two Step Process
Updating a data source via a dataset is a two-step process.
The first step is to update the dataset with new information — new
records, changed records, or deleted records.
If you are updating a data source (such as a database), the
second step is to send the changes from the dataset to the original data
source. That is, the process of updating the dataset does not also write the changes
through to an underlying data source; you must explicitly perform this second
step.
After changes have been made in a dataset, you can transmit the
changes to a data source. Most commonly, you do this by calling the Update
method of a data adapter. The method loops through each record in a data table,
determines what type of update is required (update, insert, or delete), if any, and
then executes the appropriate command.
How an Update Is Transmitted to the Data Source
As an illustration of how updates are made, suppose your
application uses a dataset containing a single data table. The application fetches
two rows from the database. After the retrieval, the in-memory data table looks like
this:
(RowState)
FirstName LastName
(Unchanged)
Nancy
Buchanan
(Unchanged)
James
Wilson
Your application changes "James" to "Jim". As a result of this
change, the value of the DataRow.RowState property for that row changes from
Unchanged to Modified. The value of the RowState property for the other
row remains Unchanged. The data table now looks like this:
(RowState)
FirstName LastName
(Unchanged)
Nancy
Buchanan
(Modified)
Jim
Wilson
You application now calls the Update method to transmit the
dataset to the database. The method inspects each row in turn. For the first row, the
method transmits no SQL statement to the database, because that row has not changed
since it was originally fetched from the database.
In a typical implementation, you might work your way through the
following steps:
-
Fill the tables for your DataSet using a stored procedure or
SQL.
-
Display the data in various DataTable objects within your
DataSet by either binding to a control or looping through the rows in the
tables.
-
Change data in individual DataTable objects by adding,
modifying, or deleting DataRow objects.
-
Invoke the GetChanges( ) method to create a second DataSet that
features only the changes to the data.
-
Check for errors in the second newly created DataSet by
examining the HasErrors property. If there are errors, check the HasErrors property
of each DataTable in the DataSet. If the table has errors, invoke the GetErrors( )
method of the DataTable and get back an array of DataRow objects with errors. On
each row you can examine the RowError property for specific information about the
error, which you can then resolve.
-
Merge the second Data Set with the first.
-
Call the Update( ) method on the DataAdapter object and pass in
the second (changed) DataSet.
-
Invoke the AcceptChanges( ) method on the DataSet, or invoke
RejectChanges( ) to cancel the changes.
This process gives you very fine control over the update to your
data as well as an opportunity to fix any data that would otherwise cause an
error.
In the following example, we will create a dialog box that
displays the contents of the Customer table in Northwinds. The goal is to test
updating a record, adding a new record, and deleting a record. The code is as simple
as possible, which means eliminating many of the error-checking and
exception-handling routines you might expect in a production program.
This form consists of a list box, a button for Update, an
associated text box, and a Delete button. There is also a set of eight text fields
that are used in conjunction with the New button. These text fields represent eight
of the fields in the Customers table in the Northwind database. There is also a label
at the bottom that you can use for writing messages to the user.
Accessing the Data
First, create the DataAdapter object and the DataSet as private
member variables, along with the DataTable. A DataAdapter may have four SQL commands
associated with it. Right now, we have only one: dataAdapter.SelectCommand. The
InitializeCommands( ) method creates the remaining three: InsertCommand,
UpdateCommand, and DeleteCommand. InitializeCommands( ) uses the AddParms method to
associate a column in each SQL command with the columns in the modified rows:
// The DataSet, DataAdapter, and
DataTable are members
// so that we can access them from any member method.
private SqlDataAdapter _dataAdapter;
private DataSet _dataSet;
private DataTable _dataTable;
// **** Constructor
public CustTrans()
{
// Create the visual Controls
InitializeComponent();
// Setup DB Connection **** ADJUST IT FOR
YOUR USE ****
string connectionString = "server=xeon;" +
"uid=sa; pwd=xxxxxx;
database=northwind";
string commandString = "Select * from Customers";
_dataAdapter = new SqlDataAdapter(commandString,
connectionString);
// Create INSERT, UPDATE, DELETE "Template"
Commands
InitializeCommands();
// Populate DataSet
_dataSet = new DataSet();
_dataAdapter.Fill(_dataSet,"Customers");
// Fill the list box with columns from the
Dataset
this.PopulateListBox();
}
InitializeCommands( ) creates each SQL command in turn, using
placeholders that correspond to the column argument passed to AddParm( ). The
DataAdapter uses these three commands to modify the table when you invoke Update(
).
// *** Bind parameters to appropriate
columns.
// Note: The params keyword lets you specify a method
// parameter that takes an argument where the number
// of arguments is variable.
private void AddParms(SqlCommand cmd, params string[] cols)
{
// Add each parameter. Note that each colum
in
// table "Customers" is of type VARCHAR !
foreach (String column in cols)
{
cmd.Parameters.Add(
"@" + column,
SqlDbType.Char, 0, column);
}
}
// *** Setup DataAdapter with expicit
INSERT, UPDATE,
// DELETE "Template" Commands.
private void InitializeCommands()
{
// Reuse the SelectCommand's
Connection.
SqlConnection con =
(SqlConnection)
_dataAdapter.SelectCommand.Connection;
// Create an explicit, reusable INSERT
command
_dataAdapter.InsertCommand = con.CreateCommand();
_dataAdapter.InsertCommand.CommandText =
"INSERT INTO customers " +
"(CustomerId, CompanyName, ContactName,
ContactTitle, " +
"Address, City, PostalCode, Phone) "
+
"VALUES(@CustomerId, @CompanyName,
@ContactName, " +
"@ContactTitle, @Address, @City,
@PostalCode, @Phone)";
// Bind parameters to appropriate columns
for INSERT command
this.AddParms(_dataAdapter.InsertCommand,
"CustomerId", "CompanyName",
"ContactName", "ContactTitle",
"Address", "City", "PostalCode",
"Phone");
// Create an explicit UPDATE
command
_dataAdapter.UpdateCommand = con.CreateCommand();
_dataAdapter.UpdateCommand.CommandText =
"UPDATE customers " +
"SET CompanyName = @CompanyName " +
"WHERE CustomerID = @CustomerId";
// Bind parameters to appropriate columns
for UPDATE command
this.AddParms(_dataAdapter.UpdateCommand, "CompanyName",
"CustomerID");
// Create an explicit DELETE
command
_dataAdapter.DeleteCommand = con.CreateCommand();
_dataAdapter.DeleteCommand.CommandText =
"DELETE FROM customers " +
"WHERE customerID = @CustomerId";
// Bind parameters to appropriate columns
for DELETE command
AddParms(_dataAdapter.DeleteCommand, "CustomerID");
}
// *** Fill the list box with columns
from the Dataset
private void PopulateListBox()
{
// Get the only one Table from the
Dataset
_dataTable = _dataSet.Tables[0];
listBoxCustomers.Items.Clear();
// Loop through the Dataset and add each
Row
// to the ListBox
foreach (DataRow dataRow in _dataTable.Rows)
{
listBoxCustomers.Items.Add(
dataRow["CustomerId"] + ", " +
dataRow["CompanyName"] + ", " +
dataRow["ContactName"] + ", " +
dataRow["ContactTitle"] + ", " +
dataRow["Address"] + ", " +
dataRow["City"] +
", " +
dataRow["PostalCode"] + ", " +
dataRow["Phone"]
+ ")" );
}
}
// *** Set all the text fields to empty
strings
private void ClearFields()
{
txtCompanyID.Text = "";
txtCompanyName.Text = "";
txtContactName.Text = "";
txtContactTitle.Text = "";
txtAddress.Text = "";
txtCity.Text = "";
txtZip.Text = "";
txtPhone.Text = "";
}
The form is now displayed, and you're ready to update a record.
Highlight a record and fill in a new customer name in the topmost text field. When
you press Update, read the resulting name and put it into the chosen record.
Updating a Record
First you must get the specific row the user wants to
change.
Declare a new object of type DataRow and initialize it with a
reference to the specific row in the DataTable's Rows collection that corresponds to
the selected item in the list box. Remember that DataTable was declared as a member
variable and initialized in the PopulateListBox( ) method shown in the previous
section.
The call to the static method DoEvents( ) of the Application class
causes the application to process Windows messages and paint the screen with the
message. If you were to leave this line out, the current thread would dominate the
processor and the messages would not be printed until the button handler completes
its work.
Call BeginEdit( ) on the DataRow to put the row into editing mode.
This suspends events on the row so that you could, if you chose, edit a number of
rows at once without triggering validation rules (there are no validation rules in
this example). It is good form to bracket changes on DataRows with calls to
BeginEdit( ) and EndEdit( ).
The actual edit is to the column CompanyName within the targetRow
object, which is set to the text value of the text control txtCustomerName. The net
effect is that the CompanyName field in the row is set to whatever the user put into
that text box.
Notice that the column you want is indexed within the row by the
name of that column. In this case, the name will match the name that is used in the
database, but this is not required. When you created the DataSet, you could have used
the TableMappings( ) method to change the names of the columns.
Having edited the column, you are ready to check to make sure
there are no errors. First, extract all the changes made to the DataSet (in this
case, there will be only one change) using the GetChanges( ) method, passing in a
DataRowState enumeration to indicate that you want only those rows that have been
modified. GetChanges( ) returns a new DataSet object.
// **** Handle the UPDATE button
click
protected void btnUpdate_Click (object sender, System.EventArgs
e)
{
int index;
// Get the selected row from the
ListBox
index = listBoxCustomers.SelectedIndex;
if (index == (-1))
{
lblMessage.Text = "Please select a
Customer";
return;
}
else
{
lblMessage.Text = "";
}
DataRow targetRow = _dataTable.Rows[index];
// Inform the user
lblMessage.Text = "Updating: " +
targetRow["CompanyName"];
Application.DoEvents();
// Edit the row, no validation is fired
between
// BeginEdit() and EndEdit(). The underlying Dataset
// will no mark the Row as an UPDATE!
targetRow.BeginEdit();
targetRow["CompanyName"] = txtCustomerName.Text;
targetRow.EndEdit();
// Use the GetChanges method to create a
second DataSet
// object that is then used to update a data source.
DataSet dataSetChanged =
_dataSet.GetChanges(DataRowState.Modified);
// Test to make sure all the changed rows
are without errors
bool okayFlag = true;
if (dataSetChanged.HasErrors)
{
okayFlag = false;
string msg = "Error in row with customer
ID ";
// Examine each
table in the changed DataSet
foreach (DataTable theTable in
dataSetChanged.Tables)
{
// If any table has errors, find out which rows
if
(theTable.HasErrors)
{
// Get the rows with errors
DataRow[] errorRows = theTable.GetErrors();
// iterate through the errors and correct
// (in our case, just identify)
foreach (DataRow theRow in errorRows)
{
msg = msg + theRow["CustomerID"];
}
}
}
lblMessage.Text = msg;
}
// If we have no errors
if (okayFlag)
{
// Update the
database on second dataset
_dataAdapter.Update(dataSetChanged,"Customers");
// Inform the
user
lblMessage.Text = "Updated " +
targetRow["CompanyName"];
Application.DoEvents();
// Accept the
changes and repopulate the list box
_dataSet.AcceptChanges();
PopulateListBox();
}
else
{ // If we had errors, reject
the changes
_dataSet.RejectChanges();
}
}
Deleting a Record
The code for handling the Delete button is even simpler. First,
get the target row and form the delete message. You don't want to show the message
until the row is deleted, but you need to get it now because after you delete the row
it will be too late.
Calling AcceptChanges( ) on the DataSet causes AcceptChanges( ) to
be called on each table within the DataSet. This in turn causes AcceptChanges( ) to
be called on each row in those tables. Thus the one call to dataSet.AcceptChanges( )
cascades down through all the contained tables and rows.
Next, you need to call Update() and AcceptChanges( ), and then
refresh the list box. However, if this operation fails, the row will still be marked
for deletion. If you then try to issue a legitimate command, such as an insertion,
update, or another deletion, the DataAdapter will try to commit the erroneous
deletion again, and the whole batch will fail because of that delete. In order to
avert this situation, wrap the remaining operations in a try block and call
RejectChanges( ) if they fail.
Deleting records from the Customers database might cause an
exception if the record deleted is constrained by database integrity rules. For
example, if a customer has orders in the Orders table, you cannot delete the customer
until you delete the orders. To solve this, the following example will create new
Customer records that you can then delete at will.
// **** Handle the DELETE button click
protected void btnDelete_Click (object sender, System.EventArgs e)
{
// Get the selected row
DataRow targetRow = _dataTable.Rows[listBoxCustomers.SelectedIndex];
// Prepare message for user
string msg = targetRow["CompanyName"] + " deleted. ";
// Delete the selected row
targetRow.Delete();
// Update the database
try
{
_dataAdapter.Update(_dataSet,"Customers");
_dataSet.AcceptChanges();
// Repopulate the list box without the deleted record
PopulateListBox();
// Inform the user
lblMessage.Text = msg;
Application.DoEvents();
}
catch (SqlException ex)
{
_dataSet.RejectChanges();
MessageBox.Show(ex.Message);
}
}
Creating New Records
To create a new record, the user will fill in the fields and press
the New button. This will fire the btnNew_Click event, which is tied to the
btnNew_Click event handling method. In the event handler, call DataTable.NewRow( ),
which asks the table for a new DataRow object. This is very elegant because the new
row that the DataTable produces has all the necessary DataColumns for this table. You
can just fill in the columns you care about, taking the text from the user interface
(UI). Now that the row is fully populated, just add it back to the table. The table
resides within the DataSet, so all you have to do is tell the DataAdapter object to
update the database with the DataSet and accept the changes. Next, update the user
interface, you can now repopulate the list box with your new added row and clear the
text fields so that you're ready for another new record.
// **** Handle the NEW button
click
protected void btnNew_Click (object sender, System.EventArgs e)
{
// Create a new row, populate it with
entered
// Data from the ListBox
DataRow newRow = _dataTable.NewRow();
newRow["CustomerID"] = txtCompanyID.Text;
newRow["CompanyName"] = txtCompanyName.Text;
newRow["ContactName"] = txtContactName.Text;
newRow["ContactTitle"] = txtContactTitle.Text;
newRow["Address"] =
txtAddress.Text;
newRow["City"] =
txtCity.Text;
newRow["PostalCode"] = txtZip.Text;
newRow["Phone"] =
txtPhone.Text;
// Add the new row to the table in the
dataset
_dataTable.Rows.Add(newRow);
// Update the database, any Error is
catched in catch block
try
{
// Prepared UPDATE
Command is executed
_dataAdapter.Update(_dataSet,"Customers");
_dataSet.AcceptChanges();
// Inform the user
and repaint Form
lblMessage.Text = "Updated!";
Application.DoEvents();
// Repopulate the
list box
PopulateListBox();
// Clear all the
text fields
ClearFields();
}
catch (SqlException ex)
{
_dataSet.RejectChanges();
MessageBox.Show(ex.Message);
}
}
|