Professional Documents
Culture Documents
ADO.NET Tutorial
Silan Liu
2.1. DataReader
2.4. DataRow.IsNull
2.6. DataView
3. DATA BINDING
3.1. Binding a Property of a Control to a DataTable Column
3.2. CurrencyManager
3.5. Customizing Data Flow between the Control and its Data Source
4. DATASET SCHEMA
5. UPDATING DATABASE
5.8. DataSet.Merge
5.9. When there is a Separate Data Access Tier
6.2. XmlReader
6.4. XmlDataDocument
6.5. XPath
7. WEB APPLICATIONS
7.1. Paging
1. Connection
A connection knows how to physically connect to the database. It's ConnectionString property stores
all needed info. A connection is nothing more than a ConnectionString.
2. DataSet
A DataSet is a disconnected sub-copy of the original database, containing multiple tables and
restraints between them. The database is maintained by the DBMS, while the sub-copy of data is
maintained by the DataSet itself. When data in DataSet is changed, it caches the changes, until it is
updated back to the source database.
3. DataAdapter
A data adapter represents a set of methods used to perform a two-way data updating mechanism
between a disconnected DataTable and the database. It aggregates four commands: select, update,
insert and delete command. One adapter can only generate and fill one table in a DataSet. Therefore
to deal with multiple tables in a DataSet you need multiple DataAdapters.
4. Command
A command represents a particular method to get data from or set data into the database, usually in
the form of a SQL query or stored procedure. It has to be conducted through a connection, so a
command has a Connection property pointing to the connection.
OleDbConnection uses OLE DB data providers to communicate with different kinds of databases. You
can make use of Data Links dialog to construct the connection string for an OleDbConnection. You
can not use this dialog to construct connection string for other data sources. Therefore, for a
SqlConnection, e.g., you have to type in the connection string yourself.
Here is a connection string for an OleDbConnection using OLE DB Provider for SQL Database:
As you can see, the two connection strings are identical except that the connection string for an
OleDbConnection has an extra parameter “Provider=SQLOLEDB.1”.
As said before, a connection is all about a connection string. It can be manually keyed in, but Visual
Studio.NET provides a “Data Link” dialog to help you simplify the job.
To acquire the connection string with the help of “Data Link” dialog, create an empty .udl file, then
double-click it. The“Data Link” dialog will be brought up and the connection string will be finally
saved as text in this file. You can use the .udl file directly in the connection string:
Right-click the “Data Connections” item and choose “Add connection”. The “Data Link” dialog will be
brought up. The created connection will be listed under the “Data Connections” item in Server
Explorer, and can be dragged on to the component tray of a form or chosen by the “Data Adapter
Configuration Wizard”.
You can choose to use existing connection or create new one. It you choose to create a new
connection, the “Data Link” dialog will be brought up.
A connection is implicitly openned if it is not yet openned when DataAdapter’s Fill or Update is
called, and implicitly closed if it had been implicitly openned. This approach will produce
unnecessary overhead if we need to call a batch of Fill and/or Update methods. In this case we can
explicitly open the connection before the batch of calls, and explicitly close it after them.
In fact, a used connection is not destroyed. Instead it is by default pooled. When you open a
connection with the same connection string as the used one before it times out, you are actually
using the same pooled connection. To turn off the default connection pooling, add “OLE DB
Services=-4” into the connection string of a OLE DB .NET data provider, or “Pooling=False” to the
connection string of a SQL Client .NET data provider.
Because an existing connection in the pool is only reused if the new one has the same connection
string, if you have a data access middle tier to talk to database, to enable that the same connection
is reused when different users acquire the same data, you can not include the user’s credentials in
the connection string. you have to use the same connection string for different users. This means
that instead of letting the database to validate the user, the middle tier should do it itself using
network security measures such as SSL.
Data type length of a column can be confusing if overlooked: if the cell data (number or string)
entered by user exceeds the column length, the data will be simply truncated without any warning
message. This may lead to all sorts of tedious problem such as violation of constraints.
Also note that the length of number is the number of bytes, not the number of digits. For example, if
the data type of a column is smallint and the length is 2, then the maximum decimal number the
column can hold is 215 – 1 = 32767, because smallint is 8 bits long, length of 2 is 16 bits, and we
need one bit for sign.
Note that there are two types of transactions: transaction within a database and transaction across
databases i.e. distributed transaction. COM+ and .NET Enterprise Services handles the Distributed
Transactions. Here we only discuss about transactions within a database, which is a lot easier,
because the DBMS provides this functionality.
A transaction object is created from a connection by calling its BeginTransaction method. It starts a
transaction in the database. When a transaction’s Commit or Rollback method is called, it notices the
database to commit or roll back.
Once a transaction has been created for a connection, no command whose Transaction property is
not pointing to the transaction can work through this connection, including a SELECT command.
Once a transaction is committed/rolled back/closed, the transaction is finished. If you call Commit or
Rollback or Close again an exception will be thrown.
mcn.Open()
mda.UpdateCommand.Transaction = txn
mda.InsertCommand.Transaction = txn
mda.DeleteCommand.Transaction = txn
mda.Update(mds)
txn.Rollback()
1. Create a cryptographic key pair file using the .NET tool with any name:
sn –k myfilename.snk
2. Set the key pair file name in the AssemblyInfo.cs of your project:
<Assembly: AssemblyKeyFile("myfilename.snk")>
The key pair file is only needed for compilation, and not needed when the assembly is run. When
you compile, it should be in the “obj\debug” directory if it is a debug build or “obj\release” if it is a
release build.
Binary data are stored in SQL Server database as byte arrays. There are two SQL Server data types
that can be used to store byte arrays:
1) binary
For fixed-length byte array of 50 bytes long. Even if you put in a 20 byte long array, when you get it
out, it will still be 50 byte long, with the rest bytes being 0.
2) image
Variable-length byte array. If you put in an array of 23 bytes, when you get it out, it is 23 bytes.
This image data type only accept byte [], but it can be used to store any type of objects. The
following code shows how to serialize an float array into a byte array and store into the “BinaryData”
column of type image, and later retrieve this float array back:
mda.Fill(ds);
floats[i] = i * 1.1f;
formatter.Serialize(stream, floats);
stream.Close();
row["ID"] = 0;
row["BinaryData"] = bytes;
ds.Tables["Test"].Rows.Add(row);
mda.Update(ds);
Then, later...
mda.Fill(ds);
// Retrieve the stored byte array
stream.Close();
2.1. DataReader
DataReader is designed for speed. It supports very limited functionality: it is read-only, and once
you've read one row, you can not go back and read it again. It can only hold the result of one query,
so unless the database supports batch queries, a DataReader can only hold one data table.
In comparison, DataSet has more powerful functionality. It can hold the result of multiple queries i.e.
multiple tables.
If the DataReader contains results of a batch query, you can call its NextResult to move to the result
of next query.
da.Fill(ds)
MessageBox.Show(row("CustomerID"))
MessageBox.Show(row(0))
MessageBox.Show(row.Item("CustomerID"))
MessageBox.Show(row.Item(0))
Item is a parameterized property of DataRow. It’s its default property, so the first two ways are in
fact the same as the last two.
2.3. Different Versions of Cell Data
Each cell's value has two versions: original, proposed and current. After you have modified the value
of a cell, its current version will be the modified value, and its original version remains unchanged,
until you call AcceptChanges of the row. Then the original version will become the new value.
If you wrap the modification code with BeginEdit and EndEdit, after you have modified a cell and
before you call EndEdit, the original and current version are both unchanged, while the proposed
version is the new value. After you call EndEdit, current version becomes the new value, and
proposed version becomes invalid (will throw exception if you try to access it).
ds.Orders(0).BeginEdit()
ds.Orders(0).CustomerID = "ABCDE"
msg = "Original value = " & strOriginal & ", Current Value = " & strCurrent & ", Proposed Value = " &
strProposed
MessageBox.Show(msg)
ds.Orders(0).EndEdit()
2.4. DataRow.IsNull
DataRow has a method called IsNull which takes a column name or index and checks whether that
item is null.
When your table need an expression column, you have two ways. First, you can put the expression
in the SQL query such as
“Select OrderID, ProductID, UnitPrice, Quantity, UnitPrice * Quantity As Total From [Order Details]”
Then the dataset which is filled with the result will contain column called “Total”. This column has no
difference from other columns such as “OrderID”, “ProductID”, etc. When “UnitPrice” or “Quantity”
is changed, the corresponding “Total” will not change, because the evaluation of the expression is
done by the database, not the dataset.
If you want “Total” to change when “UnitPrice” or “Quantity” is changed, you should not query for
the expression. Instead you add an expression column into the DataTable and let it do the
calculation:
table.Columns.Add("Quantity", GetType(Decimal))
table.Columns.Add("UnitPrice", GetType(Decimal))
da.Fill(ds)
mdg.DataSource = ds.Tables("OrderDetails")
Note: the parameter columns from which the expression column is calculated should have already
been added into the table before the expression column is added, because DataTable evaluates the
expression when the expression column is added and checks whether the parameter columns
already exists in the table. If not, it will throw an exception.
The expression can be an aggregate result (sum, count or average) of a column of a child table, or a
column of a parent table, if a proper DataRelation has been set up. You can only use aggregate result
of a child table column but not the column itself, because for each row there are multiple rows in
the child table.
daOrders.SelectCommand.Connection = mcn
daOrderDetails.SelectCommand.Connection = mcn
daOrders.FillSchema(orders, SchemaType.Source)
daOrderDetails.FillSchema(orderDetails, SchemaType.Source)
ds.Relations.Add("Orders_OrderDetails", orders.Columns("OrderID"),
orderDetails.Columns("OrderID"), False)
orderDetails.Columns.Add("ShipCountry", GetType(String),
"Parent(Orders_OrderDetails).ShipCountry")
daOrders.Fill(orders)
daOrderDetails.Fill(orderDetails)
mdg1.DataSource = ds
mdg1.DataMember = "Orders"
mdg2.DataSource = ds
mdg2.DataMember = "OrderDetails"
If the table has multiple child tables, you can give the relation name as parameter to the “Child”
qualifier.
2.6. DataView
DataView represents a view on the data of a specific version on some selected rows and all columns
of a single DataTable. It is defined by the following properties:
2. RowFilter - a string representing the filtering criteria based on the content of the rows, such as
"OrderID > 10254".
4. Sort - a string representing the sorting column and order, such as "OrderID DESC".
DataViewRowState.ModifiedOriginal Or DataViewRowState.Deleted)
mDataGrid.DataSource = view
2. Specifying the data version to be viewed through a DataView based on the SourceVersion
property of the rows (DataRowVersion.Current or Original).
A DataRow's HasErrors property will return True if its RowError string property is not empty, or one
of its columns has an error:
mds2.Customers(0).RowError = "Something"
A DataRow has to belong to a certain DataTable. Its Table property is read-only. Therefore you can
not create a separate DataRow and add it into a table later. You have to call a table’s NewRow
method to create a new row, whose Table property already points to the table, then add it into the
table by calling its Rows property’s Add method. You can not ask one table for a new row, then add
it into another table.
Suppose you have a table with a primary key and you want to locate one record with a primary key,
instead of using DataTable.Select method to get back a one-element array, you can use the
DataRowCollection.Find method to get back one single row:
If the table has more than one primary key, you can pass in an array of objects representing the
keys.
3. DATA BINDING
If we want text box tbOrderID to display the OrderID column of of a row in table Orders in dataset
mds, we should bind the textbox’s Text property to that column:
If we want the BackColor property of the text box to be bound to the BColor column of table Colors:
3.2. CurrencyManager
All data-awareness controls including Form inherit from class Control, which has a parameterized
property BindingContext of type BindingContext, which manages all data-awareness controls that
the control contains. This property takes a data source (and a path if the data source has more than
one path, e.g. a dataset has multiple tables) as a parameter and returns a BindingManagerBase-
derived object – a PropertyManager if the control is single-pathed or a CurrencyManager if multi-
pathed, which is used to keep all controls that are bound to the same data source synchronized. The
BindingManagerBase can be created before any control has been bound to the data source.
The CurrencyManager lets you set or get the current record (Current property), its integer index
(Position property), and count of records (Count property). If the controls can display multiple
records, such as a DataGrid, the Position property reflects the current record (the row you click). If
the controls can only display one record, you then need nevigation buttons, which simply
increment/decrement the Position property.
When Position is changed, a PositionChanged event happens. When the current record is changed,
an ItemChanged event happens.
CurrencyManager.AddNew and RemoveAt adds a new record to or removes a record from the data
source.
¨ EndCurrentEdit
When you make some change on the control, CurrencyManager does not submit the change to the
data source until you change Position or call EndCurrentEdit.
Inherits System.Windows.Forms.Form
daOrders.Fill(ds.Orders)
End Sub
textbox1.Text = "Order " & (cManager.Position + 1) & " of " & cManager.Count
End Sub
DisplayOrdersPosition()
End Sub
cManager.Position -= 1
End Sub
cManager.Position += 1
End Sub
cManager.AddNew()
DisplayOrdersPosition()
End Sub
Private Sub btnDelete_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles
btnDelete.Click
cManager.RemoveAt(cManager.Position)
DisplayOrdersPosition()
End Sub
End Class
To bind a data source to controls in a child form, pass a reference of the CurrencyManager of this
data source to the child form. Then the child form can get a DataView from the CurrencyManager
and bind its controls to that DataView.
mDataView = mDataRowView.DataView
Me.BindingContext(vueDetail).Position = cm.Position
End Sub
Data binding for a ListControl such as a ComboBox or ListBox is complicated. Four properties needs
to be set:
2. ValueMember: String, the primary key of the source table. Used to look up rows in the table,
such as “Employees.EmployeeID”.
3. DisplayMember: String, the column in the source table that you want to display, can be the
same as the ValueMember, or a different column, such as “Employees.EmployeeName”;
4. SelectedValue: Object, the value to be provided to the ValueMember i.e. the primary key, to
look up the row, such as an Orders.EmployeeID of 1447.
These properties are need in the following example. Suppose the controls on the form are bound to
a record in table Orders, and the combo box is used to display column Orders.EmployeeID, which is a
foreign key to table Employees. If we want to provide more convenience to user, so that the combo
box displays the name of the employee instead of its ID, then we need to perform a SELECT query on
table Employees, to acquire a set of EmployeeName with EmployeeID and put them into a dataset.
Then we point the DataSource property of the combo box to this dataset, provide a search criteria
such as EmployeeID, and ask the combo box to display the corresponding EmployeeName.
3.5. Customizing Data Flow between the Control and its Data Source
The DataBindings property of a control is of type ControlBindingsCollection. Its method Add is called
to setup a binding relationship between the control and a data source, as shown in section Binding a
Property of a Control to a DataTable Column. Add returns a Binding object, which has two events:
Format and Parse. Format events fires when Binding loads data from data source into control, and
the Value property of the event returns an Object which is the data being loaded. You can change
the format of the data here. Parse event fires when Binding assigns the data in the control back to
data source. You can parse the data here.
cevent.Value = strNull
Else
cevent.Value = CDate(cevent.Value).ToShortDateString
End If
End Sub
cevent.Value = CDate(cevent.Value)
Else
cevent.Value = DBNull.Value
End If
End Sub
4. DATASET SCHEMA
If you create a brand-new dataset and immediately call DataAdapter.Fill, only the minimum set of
schema – the names and types of the table’s columns are filled into the dataset. This is because
retrieving schema from database takes extra time.
This fact is proved by the fact that the following code runs OK. Table “test1” has its first column
being primary key, and already has a row with primary key “a”. Now we do a fill and add a new row
with the same primary key “a”, the dataset accepts it without a complaint. Moreover, the last line of
code obviously shows that the dataset contains a table whose name is “Table”, not the expected
“test1”. If you change this to “test1”, there will be no data shown in the datagrid “dg” because there
isn’t such a table in the dataset. This may really confuse novices. These facts proves that those
schema information are not present in the dataset.
da.SelectCommand = selectCmd;
da.Fill(mds);
DataRow r = mds.Tables[0].NewRow();
r[0] = "a";
r[1] = "b";
mds.Tables[0].Rows.Add(r);
dg.DataSource = mds;
dg.DataMember = mds.Tables["Table"].TableName;
Normally you would want your dataset have all the constraints in it before you start to use it.
Compared with having no constraint in the dataset and submitting whatever data to database and
getting rejected at the last step, enforcing data integrity at the dataset can reduce network traffic
and improve performance.
There are different ways to create a dataset with schema. You can create a typed dataset class at
design time with schema built in, which is called a typed dataset (class), or you can create an empty
dataset at run time and add or fill schema information into it.
Generating a strongly typed dataset class at design time is the most efficient option, because typed
dataset offers compile-time type checking and at run time it already knows its schema.There are two
ways to generate a typed dataset:
1. Drag a data adapter onto the design view of the form, then right-click the DataAdapter and
choose “Generate DataSet”. A strongly typed dataset class will be generated, using the schema
retrieved from database through the data adapter. Using this method, the dataset can only contain
one table, because the data adapter is designed to be a pipeline between the dataset and ONE
database table.
2. Second, you can use dataset schema designer to create the XML Schema Definition (XSD) file of a
dataset, by dragging database tables from the server explorer onto the schema designer, or even by
creating elements yourself. Note this is the only way to create a dataset containg more than one
table, and the dataset can contain the full set of schema, including ForeignKeyContaints and
DataRelations.
This is the most transperent way because you do everything by writing your own code. You basically
create DataTables, add columns and their constraints such as primary key into it, then add the tables
into the dataset. Then you add foreign key constraints and data relations into the dataset. From
performance point of view it is slower then having a typed dataset class, but still generally
acceptable for a production-scale product.
This approach is the slowest and not recommended for production-scale products, because
retrieving schema from database takes a lot of time.
You can either retrieve the schema from database separately or together with data. To retrieve
schema separately, call DataAdapter.FillSchema. It can be done either before or after Fill. When you
call FillSchema, you have a choice to use the original table and column names in the database as the
table and column names of the dataset, or to use different names at your choice by providing a
mapping of the table and column names. The first parameter of FillSchema is the dataset, and the
second is an enumeration indicating whether you want to use original namesin the database
(SchemaType.Source), or the TableMappings and ColumnMappings that you have added into the
dataset (SchemaType.Map). See section Mapping Table Names in DataAdapter.Fill and Mapping
column Names in DataAdapter.Fill for details.
To fill schema together with data, set DataAdapter's MissingSchemaAction property to enumeration
MissingSchemaAction.AddWithKey before calling DataAdapter.Fill.
In the following example, the code to retrieve schema from database into dataset has been
commented out, so the result will contain no schema.
'da.MissingSchemaAction = MissingSchemaAction.AddWithKey
'da.FillSchema(ds, SchemaType.Source)
da.Fill(ds)
'da.FillSchema(ds, SchemaType.Source)
constraints = "Col Name: " & col.ColumnName & ". AutoIncrement: " & col.AutoIncrement &
" MaxLength: " & col.MaxLength & " AllowDBNull: " & col.AllowDBNull
MessageBox.Show(constraints)
Next
A foreign key relationship between two tables in a dataset can be represented by two entities:
The constructor of a ForeignKeyConstraint takes two columns as parameters: a parent column (the
column that acts as a primary key) and a child column (the one which acts as foreign key). The
constructor of a DataRelation takes a relation name and the same two columns.
We normally do not need schema/constraints when retrieving data from database into dataset,
because the database is has all the needed constraints already in place, and the data in the database
already conform to these constraints. On the other hand, we normally need those constraints in the
dataset when we change data in it.
We can solve this dilemma by setting the DataSet.EnforceConstraints property to false right before
calling DataAdapter.Fill, and set it back to true right after the call. These way when data is filled into
the dataset all the constraints does not function but when we update data they do function.
With a dataset with full schema, we can use DataRelations to navigate between rows in child and
parent tables. To navigate from one row to another, call DataRow’s GetChildRows and
GetParentRow method passing a DataRelation as a parameter.
In the above example, to get all records in the association table “Order Details” with OrderID 10249,
you say
DataGrid knows to invoke these methods and relations of the dataset that is bound to it, so that user
can get a row’s child rows conveniently by clicking the “unfold” buttons at the left of a parent row.
When using a strongly typed dataset, the generated code provides you with a lot more convenience
– you do not need to know the DataRelation to be able to navigate from a parent table to a child
table or the other way around. For example, in the above example, suppose the typed dataset is
“MyTypedDS”, all you need to say is
When we want to join tables in the database, the easiest way is to use SQL join queries to get the
results from database directly, and store the returned result in a table in a dataset. For example, the
following SQL query joins “Orders” table and “Products” table through an association table “Order
Details”:
WHERE O.OrderID = OD.OrderID AND OD.ProductID = P.ProductID AND O.OrderID <= 10250
2. If you change anything in the joined table, the DataAdapter can not guarantee to update the
database correctly, because it is designed to work with tables that parallel the tables in the
database.
Therefore, the better way to do a join in ADO.NET is to split the joining select query into multiple
queries, each of which only returns the columns of one database table, so that they all parallel the
database tables, and use DataRelations to navigate between tables. Especially when you use strongly
typed dataset, navigating between tables requires only one method call.
'Create DataAdapters
"WHERE O.OrderID = OD.OrderID AND OD.ProductID = P.ProductID AND O.OrderID <= 10250"
'Fill tables
daOrders.Fill(orders)
daProducts.Fill(products)
daOrderDetails.Fill(orderDetails)
'Add DataRelations
ds.Relations.Add("Orders_OrderDetails", orders.Columns("OrderID"),
orderDetails.Columns("OrderID"))
ds.Relations.Add("Products_OrderDetails", products.Columns("ProductID"),
orderDetails.Columns("ProductID"))
'Display tables
mdg1.DataSource = orders
mdg2.DataSource = orderDetails
mdg3.DataSource = products
Keyword DISTINCT above is used because otherwise the result will contain redundant rows that
violate unique constraint.
After you create a DataAdapter with a select command, all schema information such as base catalog
(Northwind), base table name (Order Details), column names, column size, data type, allow null,
primary key etc. can be retrieved through a schema table:
"Initial Catalog=Northwind;Trusted_Connection=Yes;"
"ORDER BY ProductID"
cn.Open()
reader.Close()
cn.Close()
mdg.DataSource = schemaTable
daOrderDetails.Fill(ds)
mdg.DataSource = ds.Tables("Table")
To change this default behaviour , you have to tell DataAdapter how to name the table in the
dataset. There are two ways to do this.
First, you can create an empty table in the dataset yourself with the desired name, then instruct the
DataAdapter.Fill method to fill data into this existing table:
ds.Tables.Add("OrderDetails")
daOrderDetails.Fill(ds.Tables("OrderDetails"))
mdg.DataSource = ds.Tables("OrderDetails")
Or, if you want Fill to create the new table with your desired name, you should create a
DataTableMapping object, store it in the TableMappings collection of the data adpater, and pass to
Fill as the second parameter the name of the table mapping.
daOrderDetails.TableMappings.Add("OrderDetails", "MyTable")
mdg.DataSource = ds.Tables("MyTable")
However, in fact, the first string is MERELY used as the mapping name. As said before, the result set
retrieved from the database does not contain the table name. Therefore, instead of saying “create a
tabled named MyTable, and fill it with data retrieved from table OrderDetails in the database”, the
table mapping is actually saying “create a tabled named MyTable, and fill it with data retrieved from
one or several tables in the database that I don’t know”. To prove this claim, the following code also
retrieves the data from the database table OrderDetails as before:
daOrderDetails.TableMappings.Add("Anything", "MyTable")
mdg.DataSource = ds.Tables("MyTable")
You can call Fill of the same data adapter with two DataTableMappings, to fill data into two tables in
the same dataset:
daOrderDetails.TableMappings.Add("Mapping1", "OrderDetails1")
daOrderDetails.TableMappings.Add("Mapping2", "OrderDetails2")
daOrderDetails.Fill(ds, "Mapping1")
mdg2.DataSource = ds.Tables("OrderDetails1")
daOrderDetails.Fill(ds, "Mapping2")
mdg2.DataSource = ds.Tables("OrderDetails2")
Suppose you only want to change the name of one column and leave others unchanged. You do not
need to provide column mappings for all the columns. DataAdapter has a MissingMappingAction
property, which has three enumeration values: PassThrough (default), Ignore and Error:
1. When it is PassThrough and the Fill result contains a column that is not in the ColumnMappings
collection, it will let the new column name pass through to the dataset.
3. If it is Error, an exception will be generated. Therefore, if you want to use the same column
name as the database for all columns, you don't need to populate the ColumnMappings collection at
all.
In the following example, the DataSet will only contain the first column "CustomerID", although the
query actually returns three columns:
tableMapping.ColumnMappings.Add("CustomerID", "CustomerCode")
da.MissingMappingAction = MissingMappingAction.PassThrough
da.Fill(ds)
mdg.DataSource = ds.Tables("CustomerNameAndCompany")
A strongly typed dataset is represented by a normal class, which inherits from dataset and also have
methods and properties that represent the schema of a set of specific tables. It can be generated
from a XML Schema Definition file (.xsd). It can also be generated by Visual Studio .NET development
environment directly from a DataAdapter. The development environment actually does the
following things under the hood:
2. Use DataSet.WriteXmlSchema to write the schema into a file and add this file into the project;
3. Use XML Schema Definition tool XSD.exe to generate a class file, and add this file into the
project;
daOrders.SelectCommand.Connection = mcn
daOrderDetails.SelectCommand.Connection = mcn
daOrders.FillSchema(orders, SchemaType.Source)
daOrderDetails.FillSchema(orderDetails, SchemaType.Source)
ds.Relations.Add("Orders_OrderDetails", orders.Columns("OrderID"),
orderDetails.Columns("OrderID"))
ds.WriteXmlSchema("DSOrder_OrderDetails.XSD")
To generate a XSD file at design time, right-click the project icon, choose “Add new item”, then
choose “DataSet”. In the design pane create a DataSet or table by “Add new group”, then choose
“Add new element” to add new columns. Or you can simply drag a table or a stored procedure from
the server explorer into the schema design pane.
If the corresponding tables and the constraints and relations already exist in the database, which is
true in most of the cases, you only need to drap the table in Server Explorer into the design pane.
To generate a class file from a XSD file on command line, enter the following command:
To generate a class file from a XSD file in Visual Studio .NET, add this XSD file into project, double-
click it to open its designer pane, and right-click and choose “Generate DataSet”. Note: the
namespace of the generated dataset is determined by the “RootNamespace” setting in the project
file (csproj).
Then, you can add this class file into the project, and use it as a normal class.
daOrders.Fill(ds.Orders)
daOrderDetails.Fill(ds.OrderDetails)
mdg1.DataSource = ds
mdg1.DataMember = "Orders"
mdg2.DataSource = ds
mdg2.DataMember = "OrderDetails"
The following code shows how strongly typed database can make coding easier and enables
compile-time checking:
row1("OrderID") = 10246
row1("CustomerID") = "VINET"
row1("EmployeeID") = 5
row1("ShipCountry") = "P.R.China"
ds.Tables("Orders").Rows.Add(row1)
row2.OrderID = 10247
row2.CustomerID = "VINET"
row2.EmployeeID = 5
row2.ShipCountry = "P.R.China"
ds.Orders.AddOrdersRow(row2)
row1 = ds.Tables("Orders").Rows.Find(10246)
row2 = ds.Orders.FindByOrderID(10247)
To see how typed dataset greatly simplifies navigating through data tables, see section "Navigating
Between Tables with DataRelations”.
I once ran into a bug with caused me quite some effort to find. I generated a strongly typed dataset.
I wrote my own data adapter code for it using my own SQL queries. Initially all worked fine.
Then I decided to add an extra table into the dataset and an extra column in an existing table, which
is a foreign key to the new table. I re-generated the typed dataset, and added code for the new data
adapter for the new table. But I forgot to add the extra column in the SELECT command of the
existing table – the command text used to be
FROM Orders
FROM Orders
When I ran the code, exception was thrown saying “rows violating non-null, unique or foreign key
constraints”.
This was because of the typed nature of the typed dataset. If we are using a untyped dataset, the
dataset becomes whatever the SELECT command got from the database. If we are using a typed
dataset, however, the SELECT command must conform to the dataset schema.
7) Unique is not stored, because only the database can check this;
5. UPDATING DATABASE
DataSet, DataTable and DataRow all have properties called HasError and RowState, and methods
called AcceptChanges and RejectChanges.
RowState property is used to submit changes (modifies, inserts or deletes) back to the database. The
code which does the submitting job (can be your own code or DataAdapter.Update) will go through
all rows and check its RowState property. If the property is Added, Modified or Deleted, the code will
go get the corresponding columns of this row for the parameter collection of the corresponding
command, and call its ExecuteNonQuery to submit the change.
If the number returned by ExecuteNonQuery (indicating how many rows in database has been
affected) is 1, it means the operation is successful. The AcceptChanges method of the row in the
dataset will be called. Then, the RowState property of the row (or all changed rows that a table or
dataset contains) will be set to Unchanged if it was Added or Modified. If the RowState used to be
Deleted, the row will be removed.
When the RejectChanges method of DataSet, DataTable or DataRow is called, if its RowState
property is Modified or Deleted, it will be reset to Unchanged, and the row will go back to its
previous state. If the property is Added, the row will be removed from the dataset.
¨ Parameters
If you want to use the same command text to submit the changes in multiple rows, you need to use
parameters in the command text, and provide a mapping between the parameters and the
corresponding columns. You may also need to specify which version of the column to use, e.g. the
current version or the original version.
¨ Submitting changes using ad hoc queries
Property SourceVersion's default value is DataRowVersion.Current, so you only need to set it if you
want it to be DataRowVersion.Original.
Note: the SQL queries wrapped by the commands are a simplified version, which does not
accommadate NULL scenarios. See section Accommadating NULL Values for details.
' SelectCommand
mda.SelectCommand.Connection = mcn
' UpdateCommand
cmd1.CommandText = "UPDATE [Order Details] SET OrderID = ?, ProductID = ?, Quantity = ? " & _
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
mda.UpdateCommand = cmd1
' Here we need to specify the SourceVersion to be Original, because the current version is the
' modified version, and we need the original version to look up in the database.
' DeleteCommand
mda.DeleteCommand = cmd2
' InsertCommand
cmd3.CommandText = "INSERT INTO [Order Details] (OrderID, ProductID, Quantity) VALUES(?, ?, ?)"
mda.InsertCommand = cmd3
mda.Fill(mds)
mdg.DataSource = mds
mdg.DataMember = "Table"
Note: because here we do not use named parameters like with stored procedures, we can not reuse
parameters even if they are the same. Suppose a query needs 6 parameters and two of them are the
same, you still have to provide 6 parameters instead of 5.
The use of parameters in a stored procedure is very similar to the commands. The difference is that
parameters are named in stored procedures. The four stored procedures are:
AS
@OrderID_New int,
@ProductID_New int,
@Quantity_New smallint,
@OrderID_Orig int,
@ProductID_Orig int
AS
@OrderID int,
@ProductID int,
@Quantity smallint
AS
@OrderID int,
@ProductID int
AS
There is little change to the code to use stored procedure: simply replace the query string with the
name of the stored procedure, and change the CommandType property of the command from
default value CommandType.Text to CommandType.StoredProcedure:
' SelectCommand
cmd1.CommandText = "SelectOrderDetails"
cmd1.CommandType = CommandType.StoredProcedure
mda.SelectCommand = cmd1
' UpdateCommand
cmd2.CommandText = "UpdateOrderDetails"
cmd2.CommandType = CommandType.StoredProcedure
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
mda.UpdateCommand = cmd2
' DeleteCommand
cmd3.CommandText = "DeleteOrderDetails"
cmd3.CommandType = CommandType.StoredProcedure
mda.DeleteCommand = cmd3
' InsertCommand
cmd4.CommandText = "InsertOrderDetails"
cmd4.CommandType = CommandType.StoredProcedure
mda.InsertCommand = cmd4
mda.Fill(mds)
mdg.DataSource = mds
mdg.DataMember = "Table"
Note: because the parameters in the stored procedures are named, you can reuse parameters if
they are the same – suppose a query needs 6 parameters and two of them are the same, you only
need to provide 5 parameters.
The SQL queries in section Submitting changes using your own commands are a simplified version,
which does not accommadate NULL scenarios. For example, the updating command uses the
following SQL query:
UPDATE [Order Details] SET ... WHERE ... AND ProductName = NULL
because database can not compare NULL values with "=" operator, it does not think it is a match.
Database can only check a variable’s nullness with "IS NULL". Therefore, to accommadate NULL
value for column ProductName, the comparison of ProductName should become
UPDATE [Order Details] SET ... WHERE ... AND (ProductName = ? OR ((ProductName IS NULL) AND (?
IS NULL))
When you are submitting a change using the SQL UPDATE command, if the WHERE clause only
includes the primary key column(s), then the change submitted by you will simply overwrite all
changes submitted after you retrieved the original data from database.
To prevent this, you can check in the WHERE clause of your SQL query whether the original version
of all the columns are equal to the current values in the database. Then, if some one updated any
column after your retrieval, your update attempt will fail and DataAdapter will throw a
DBConcurrencyException. But this approach has drawbacks. If there are a lot of columns in the table,
or especially if one column is binary large object like pictures, comparing all columns will be
awkward or even unacceptable.
In such a case, a time stamp column can be used. The value of the time stamp column is changed
automatically by the database every time the row is modified. Therefore, if you check both the
original version of the primary key column(s) and the time stamp column in the WHERE clause, you
can guarantee that your update will not overwrite others.
Adding a time stamp column into a SQL Server database is the same as adding any other column:
simply specify the type to be "timestamp", and the length and AllowNull property of the column will
be automatically set by the database. However, note that the OleDbType of the corresponding
parameter in the update command's parameter collection should be Binary.
Because the value of the time stamp column is automatically generated by the database, after you
submit a change or insert a new row, the time stamp column in the database has a new value, while
that in your dataset is still the old value (if it is an update) or null (if it is an insert). If you modify that
row again and try to submit, you will fail, because the time stamp value in your row is no longer the
same as the database. Therefore, you have to retrieve the new time stamp value from the database
every time you do an update or insert.
Refer to section Refreshing DataSet After Submitting Changes for code example of using time
stamping.
The command text generated by the “Data Adapter Configuration Wizard” always refreshes all
columns after submitting:
1. Using batch queries – as you have just seen above. You must have a second SELECT query that
returns a record containing the column(s) that you want to retrieve in your UPDATE and INSERT
command. The problem is: not all databases support batch queries. SQL Server supports it, but
Oracle and Access doesn't.
2. Using output parameters - you must use stored procedures for your UPDATE and INSERT
command, and the stored procedure must have output parameters returning the value of the
column that you want to refresh.
After the UPDATE or INSERT command returns, both the first row in the returned result set and the
stored procedure’s output parameter may contain the refreshed values. The command can be
configured to use just one, or use one and if fails use another. This behaviour is controlled by the
command's UpdatedRowSource property, which can be UpdateRowSource.Both (default value),
FirstReturnedRecord, OutputParameters or None. When it is None, DataAdapter will simply not
refresh the dataset. It can save a very little bit of time.
' The following example is based on Northwind table [Order Details], with an added
mda.SelectCommand.Connection = mcn
' UpdateCommand
"SELECT TStamp FROM [Order Details] WHERE OrderID = ? AND ProductID = ?"
updateCmd.UpdatedRowSource = UpdateRowSource.FirstReturnedRecord
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
mda.UpdateCommand = updateCmd
' DeleteCommand
deleteCmd.CommandText = "DELETE FROM [Order Details] WHERE OrderID = ? AND ProductID = ?"
mda.DeleteCommand = deleteCmd
' InsertCommand
"SELECT TStamp FROM [Order Details] WHERE OrderID = ? AND ProductID = ?"
insertCmd.UpdatedRowSource = UpdateRowSource.FirstReturnedRecord
mda.InsertCommand = insertCmd
mda.Fill(mds)
mdg.DataSource = mds
mdg.DataMember = "Table"
Database Northwind has the following stored procedures for table [Order Details]:
(1)–––––––––––––––––––––––––––––––––––––––––––––––
AS
SELECT OrderID, ProductID, Quantity, TStamp FROM [Order Details] WHERE OrderID <
10254
(2) –––––––––––––––––––––––––––––––––––––––––––––––
@OrderID_New int,
@ProductID_New int,
@Quantity_New smallint,
@OrderID_Orig int,
@ProductID_Orig int,
@TStamp timestamp
AS
IF @@ROWCOUNT = 1
SELECT TStamp FROM [Order Details] WHERE OrderID = @OrderID_New and ProductID =
@ProductID_New
(3) –––––––––––––––––––––––––––––––––––––––––––––––
@OrderID int,
@ProductID int,
@Quantity smallint
AS
(4) –––––––––––––––––––––––––––––––––––––––––––––––
@OrderID int,
@ProductID int
AS
DELETE FROM [Order Details] WHERE OrderID = @OrderID AND ProductID = @ProductID
' SelectCommand
selectCmd.CommandText = "SelectOrderDetails"
selectCmd.CommandType = CommandType.StoredProcedure
mda.SelectCommand = selectCmd
' UpdateCommand
updateCmd.UpdatedRowSource = UpdateRowSource.FirstReturnedRecord
updateCmd.CommandText = "UpdateOrderDetails"
updateCmd.CommandType = CommandType.StoredProcedure
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
mda.UpdateCommand = updateCmd
' DeleteCommand
deleteCmd.CommandText = "DeleteOrderDetails"
deleteCmd.CommandType = CommandType.StoredProcedure
mda.DeleteCommand = deleteCmd
' InsertCommand
insertCmd.UpdatedRowSource = UpdateRowSource.FirstReturnedRecord
insertCmd.CommandText = "InsertOrderDetails"
insertCmd.CommandType = CommandType.StoredProcedure
mda.InsertCommand = insertCmd
mda.Fill(mds)
mdg.DataSource = mds
mdg.DataMember = "Table"
Note that in the UPDATE command the time stamp parameter is used for input only, and in the
INSERT command there is no time stamp parameter.
Suppose database Northwind has the following stored procedures for table [Order Details]:
(1) –––––––––––––––––––––––––––––––––––––––––––––––
AS
SELECT OrderID, ProductID, Quantity, TStamp FROM [Order Details] WHERE OrderID < 10254
(2) –––––––––––––––––––––––––––––––––––––––––––––––
@OrderID_New int,
@ProductID_New int,
@Quantity_New smallint,
@OrderID_Orig int,
@ProductID_Orig int,
AS
IF @@ROWCOUNT = 1
SELECT @TStamp = TStamp FROM [Order Details] WHERE OrderID = @OrderID_New and
ProductID = @ProductID_New
(3) –––––––––––––––––––––––––––––––––––––––––––––––
@OrderID int,
@ProductID int,
@Quantity smallint,
AS
SELECT @TStamp = TStamp FROM [Order Details] WHERE OrderID = @OrderID and
ProductID = @ProductID
(4) –––––––––––––––––––––––––––––––––––––––––––––––
@OrderID int,
@ProductID int
)
AS
DELETE FROM [Order Details] WHERE OrderID = @OrderID AND ProductID = @ProductID
' SelectCommand
selectCmd.CommandText = "SelectOrderDetails"
selectCmd.CommandType = CommandType.StoredProcedure
mda.SelectCommand = selectCmd
' UpdateCommand
updateCmd.UpdatedRowSource = UpdateRowSource.OutputParameters
updateCmd.CommandText = "UpdateOrderDetails"
updateCmd.CommandType = CommandType.StoredProcedure
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param.Direction = ParameterDirection.InputOutput
mda.UpdateCommand = updateCmd
' DeleteCommand
deleteCmd.CommandText = "DeleteOrderDetails"
deleteCmd.CommandType = CommandType.StoredProcedure
mda.DeleteCommand = deleteCmd
' InsertCommand
insertCmd.UpdatedRowSource = UpdateRowSource.OutputParameters
insertCmd.CommandText = "InsertOrderDetails"
insertCmd.CommandType = CommandType.StoredProcedure
param.Direction = ParameterDirection.Output
mda.InsertCommand = insertCmd
mda.Fill(mds)
mdg.DataSource = mds
mdg.DataMember = "Table"
Note that in the UPDATE command the time stamp parameter is used for both input and output, and
in the INSERT command the time stamp parameter is used for output only.
1. When there are multiple rows updated, the event will be fired multiple times after each row is
updated;
2. The event handler gets a event argument with a pointer to the updated row in the DataSet,
therefore you don't need to find this row yourself.
Try
' SelectCommand
mda.SelectCommand.Connection = mcn
' UpdateCommand
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param = updateCmd.Parameters.Add("TimeStamp_Orig", OleDbType.Binary, 8, "TStamp")
param.SourceVersion = DataRowVersion.Original
mda.UpdateCommand = updateCmd
' DeleteCommand
deleteCmd.CommandText = "DELETE FROM [Order Details] WHERE OrderID = ? AND ProductID = ?"
mda.DeleteCommand = deleteCmd
' InsertCommand
mda.InsertCommand = insertCmd
mda.Fill(mds)
mdg.DataSource = mds
mdg.DataMember = "Table"
mCmdGetNewTs.Connection = mcn
mCmdGetNewTs.Parameters.Add("OrderID", OleDbType.Integer)
mCmdGetNewTs.Parameters.Add("ProductID", OleDbType.Integer)
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
Try
mda.Update(mds)
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
mCmdGetNewTs.Parameters("OrderID").Value = e.Row("OrderID")
mCmdGetNewTs.Parameters("ProductID").Value = e.Row("ProductID")
e.Row.AcceptChanges()
End If
End Sub
To solve this problem, SQL server introduces a special SELECT command: SELECT @@IDENTITY. This
command will return the lastest primary key which is generated by the database, like those auto-
increment columns. Note that this command is not scoped – it will return the lastest identity
generated anywhere – even if it is in another table. So if there is a trigger in the stored procedure
which writes a logging record into a log table after you update your table, and that log table also
have a auto-increment identity column, then the returned identity will be that of the log table.
To solve this problem, SQL server introduces a scoped SELECT command: SELECT SCOPE_IDENTY(). It
will only return the auto-generated identify of the table that you have submitted. When you are
using stored procedure, you should use output parameter to pass out the result of the SELECT
SCOPE_IDENTY() command. After the stored procedure is executed, this command will return null.
Only the INSERT query needs to get back the newly generated identity. UPDATE query does not
cause the identity to be regenerated.
When you update or insert, do not try to submit the auto-generated column, otherwise an exception
will happen.
You can set the AutoIncrement property of the column in the dataset which corresponds to the
auto-generated identity column in the database to true, and the AutoIncrementSeed and
AutoIncrementStep to –1, so that user will know that this is a auto-generated column.
' SelectCommand
mda.SelectCommand.Connection = mcn
' UpdateCommand
param.SourceVersion = DataRowVersion.Original
mda.UpdateCommand = updateCmd
' DeleteCommand
mda.DeleteCommand = deleteCmd
' InsertCommand
insertCmd.CommandText = "INSERT INTO Orders (CustomerID, ShipCountry) VALUES(?, ?); " & _
mda.InsertCommand = insertCmd
mda.Fill(mds)
' The following use of -1 is so that when you add new records into the dataset, before updating,
' they all have ids like “-1”, “-2”, “-3”, so that user can easily distinguish these
colId.AutoIncrement = True
colId.AutoIncrementSeed = -1
colId.AutoIncrementStep = -1
mdg.DataSource = mds
mdg.DataMember = "Table"
@CustomerID nchar(5),
@ShipCountry nvarchar(15),
AS
The code for the INSERT command is (all the rest can be the same as retrieving using batch query):
' InsertCommand
insertCmd.CommandText = "InsertOrders"
insertCmd.CommandType = CommandType.StoredProcedure
param.Direction = ParameterDirection.Output
mda.InsertCommand = insertCmd
In the above code, the insertion was done through a data adapter and a dataset, and the retrieved
identity was set back to the row in the dataset. Now if you do the insertion directly through a
command, you can retrieve the identity from the command’s parameter. In the following example,
table test4 has two columns: ID of bigint which is the identity column, and Name of varchar. The
stored procedure is:
@Name varchar,
as
The code to insert a new row and retrieve the generated identity is:
cmd.CommandText = "testsp_insert_test4";
cmd.CommandType = CommandType.StoredProcedure;
param.Direction = ParameterDirection.Output;
cn.Open();
cmd.ExecuteNonQuery();
Each time a table is changed by the UPDATE query, the database will send a “n row(s) affected.”
message. The DataAdapter uses the total number of rows to judge whether the update is successful
– if it is 0, the update is deemed to have failed. If it is more than 0, it is successful.
If a stored procedure writes into a log table about whether your update succeeded, then even if it
failed, the DataAdapter will still get one successful row affected because of the log record. To
prevent this from happening, use SET ONCOUNT ON to turn off the sending of the message for all
updates except for your update.
5.8. DataSet.Merge
DataSet.Merge merges data rows together on primary keys. When you call the target dataset’s
Merge method to merge the source DataSet in, if the they both contain a row with the same primary
key, the row in the source will by default overwrite the row in the target – the original version of the
source row will overwrite the original version of the target row, and and the current version of the
source row will overwrite the current version of the target row.
If you do not set up the primary key for at least the target DataSet, the two rows in both DataSets
will exist in the target DataSet after merging. This is usually not what we want.
Note that the overwrite is done on a whole-row basis – one row is either totally overwritten by
another, or not at all.
If you pass True to Merge’s second parameter bool preserveChanges, then it will only allow the
original version of the target row to be overwritten, while still keeping the current version of the
target row unchanged. This feature is very useful when there is a concurrency conflict – the
database record has been changed by another user. After refreshing the original version of the
record in your dataset, you can successfully submit it next time –to avoid conflict UPDATE command
is normally set up to require the original version of your record to be the same as the database
record.
mdg2.DataSource = view2
mdg3.DataSource = mds2
mdg3.DataMember = mds2.Customers.TableName
End Sub
mds1.Customers(0).CustomerName = "Frank1"
mds2.Customers(0).Title = "Engineer1"
mds2.AcceptChanges()
mds2.Customers(0).Title = "Engineer2"
End Sub
Try
mds1.Clear()
mds2.Clear()
mda.Fill(mds1)
mda.Fill(mds2)
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
Private Sub btnMerge_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles
btnMerge.Click
mds1.Merge(mds2)
End Sub
mds1.Merge(mds2, True)
End Sub
Original
Frank
Engineer
Current
Frank1
Engineer
Original
Frank
Engineer1
Current
1
Frank
Engineer2
After merging without passing True as the second parameter to Merge, the target row is totally the
same as the source row:
Original
Frank
Engineer1
Current
Frank
Engineer2
If we pass True as the second parameter to Merge, the target row is:
Original
Frank
Engineer1
Current
Frank1
Engineer
When the presentation tier which has the user interface and data access tier which contains the data
adapter are in the same tier, there is only one dataset in the system – controls on forms bind to it,
data adapter uses it to submit changes to database, and the changes in the database such as time
stamp or auto-increment IDs are retrieved back into it.
When presentation and data access tier are separated by network, submitting process becomes a bit
more complicated. We could still simply pass the whole dataset in presentation tier to the data
access tier, then free/destroy the presentation-tier dataset, and let the data access tier return the
updated dataset and assign it back to the presentation-tier dataset handle. But in this approach a lot
of unnecessary nework traffic may incur.
The better approach is to call the presentation-tier dataset’s GetChanges method to acquire a new
dataset, which has the same schema but only contains the changed records. Then we pass this
dataset to the data access tier, and get the updated version back. Then we must merge it back into
the presentation-tier dataset by calling its Merge method, so that the new values created by the
database such as the auto-increment or time stamp column will overwrite the old ones.
After successfully updating the database, the data adapter in data access tier will call AcceptChanges
of the dataset in hand to reset to Unmodified the DataRowState property of all changed rows.
However, the presentation-tier dataset is not touched. So the changed rows in it will remain
“changed”. If you do not explicitly call AcceptChanges for it, next time you update, the same rows
will be sent to data access tier again. Therefore, remember to call AcceptChanges after the returned
dataset has been merged back.
The following code uses the same stored procedures as section Refreshing Dataset After Submitting
Changes | Using stored procedure output parameters.
' SelectCommand
selectCmd.CommandText = "SelectOrderDetails"
selectCmd.CommandType = CommandType.StoredProcedure
mda.SelectCommand = selectCmd
' UpdateCommand
updateCmd.UpdatedRowSource = UpdateRowSource.OutputParameters
updateCmd.CommandText = "UpdateOrderDetails"
updateCmd.CommandType = CommandType.StoredProcedure
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param.Direction = ParameterDirection.InputOutput
mda.UpdateCommand = updateCmd
' DeleteCommand
deleteCmd.CommandText = "DeleteOrderDetails"
deleteCmd.CommandType = CommandType.StoredProcedure
deleteCmd.Parameters.Add("OrderID", OleDbType.Integer, 0, "OrderID")
mda.DeleteCommand = deleteCmd
' InsertCommand
insertCmd.UpdatedRowSource = UpdateRowSource.OutputParameters
insertCmd.CommandText = "InsertOrderDetails"
insertCmd.CommandType = CommandType.StoredProcedure
param.Direction = ParameterDirection.Output
mda.InsertCommand = insertCmd
End Sub
mda.Fill(ds)
End Function
mda.Update(ds)
UpdateDataBase = ds
End Function
End Class
Inherits System.Windows.Forms.Form
mds = mDataAccess.GetDataSet()
mdg.DataSource = mds
mdg.DataMember = "Table"
End Sub
mds.Merge(dsBack)
mds.AcceptChanges()
End Sub
End Class
If a table’s primary key is auto-generated, when merging the dataset returned by the data access tier
to the presentation-tier dataset, an inserted row in the main dataset has a dummy primary key,
while the same row in the returned dataset has an auto-generated key. Because they have different
primary keys, the returned row will NOT overwrite the one in the main DataSet, but will coexist. This
is not what we want.
1. After you have submitted the dataset containging changes to the data access tier, if no
exception happens, it means that all the new rows has been successfully inserted. Therefore, before
merging, you can select all pending inserted rows in the presentation-tier dataset and purge them.
2. You can create an extra auto-increment column for the main DataSet, that doesn’t match to any
column in the database. Because this column is unique in the scope of the main DataSet, you can
temporarily change the primary key to this column before merging. This way the pending inserted
rows in the presentation-tier dataset will have the same primary keys as those returned. After
merging, you change the primary key back. This is not an elegant solution. When the table in
question has child tables the solution may become complex.
Neither of these two solutions are elegant enough. The best solution is not to use database-
generated primary keys, i.e. to know the identities of rows before they are inserted. For example,
you can use GUIDs as primary keys.
The following code illustrates the first approach. It works on Northwind database’s Orders table, and
uses the same InsertOrders stored procedure as section Retrieving Auto-generated Identity (SQL
Server) | Retrieving using stored procedure output parameters.
' SelectCommand
mda.SelectCommand.Connection = mcn
' UpdateCommand
param.SourceVersion = DataRowVersion.Original
mda.UpdateCommand = updateCmd
' DeleteCommand
mda.DeleteCommand = deleteCmd
' InsertCommand
insertCmd.CommandText = "InsertOrders"
insertCmd.CommandType = CommandType.StoredProcedure
param.Direction = ParameterDirection.Output
mda.InsertCommand = insertCmd
mda.ContinueUpdateOnError = True
End Sub
mda.Fill(ds)
With table.Columns("OrderID")
.AutoIncrement = True
.AutoIncrementSeed = -1
.AutoIncrementStep = -1
End With
GetDataSet = ds
End Function
mda.Update(ds)
UpdateDataBase = ds
End Function
End Class
Code for presentation tier:
Inherits System.Windows.Forms.Form
mds = mDataAccess.GetDataSet()
mdg.DataSource = mds
mdg.DataMember = "Table"
End Sub
Try
tbl.Rows.Remove(row)
Next
mds.Merge(dsBack)
mds.AcceptChanges()
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
mds.Clear()
mds = mDataAccess.GetDataSet()
mdg.DataSource = mds
mdg.DataMember = "Table"
End Sub
End Class
Oracle offers a sequence object, which can be called by different users and always return unique
numbers. You can use the following stored procedure to take advantage of it:
BEGIN
END;
By including all columns in the WHERE clause or using time stamp column, data adapter can detect
concurrency conflicts. Data adapter’s response to a detected concurrency conflict is controled by its
ContinueUpdateOnError property:
1. If ContinueUpdateOnError is false, which is the default value, when a concurrency conflict is
detected, data adapter will stop updating the rest of pending rows and throw an
DBConcurrencyException.
2. If ContinueUpdateOnError is true, data adapter will set the conflicting row’s HasErrors property
to true, and RowError to an error message such as “Concurrency violation: UpdateCommand affects
0 rows”, then it will continue processing the rest of rows.
Therefore, if you do not want to just stop on concurrency conflict, but want to handle each
conflicting row and move on to next, then you should set this property to true. Then the only place
to handle the conflicts on a record-by-record basis is in the DataAdapter.RowUpdated event handler.
This event is fired after each record is updated, with the event argument e pointing to the submitted
row. If e.Status is UpdateStatus.ErrorsOccurred and the type of e.Errors is DBConcurrencyException,
then we know there is a concurrency conflict.
If the dataset is bound to a DataGrid, for each row that the grid will show a red warning sign at the
left of the conflicting rows, and when you hover the mouse over it, the error message will be shown.
Then, as the simplest solution, user can refresh the dataset and try to modify again.
There are two kinds of concurrency violations: the submitted row has been changed or deleted by
another user.
To find out which type it is, catch the DataAdapter.RowUpdated event for each row updated. If there
is an concurrency error, then we make a separate query for the original row in the database with the
submitted primary key. If one row is returned, we know the error is caused by a row changed by
another user. If there is 0 row returned, the error is caused by a row deleted by another user.
1. Keep the current version of the dataset row, and overwrite its original version with the
database row. This way your own change is still retained and you have the ability to submit your
change successfully in next update – UPDATE command requires that the original version of the
record is the same as the database.
2. Overwrite both the current and original version of the dataset row with the database row. This
way your own change is lost.
In both cases because the original version of the dataset row is synchronized with the database, next
update attempt will success. As discussed in section DataSet.Merge, the above options are selected
by the second boolean parameter to DataSet.Merge.
The first two options synchronize your dataset with the database, while with the third option the
submitted row will remain unchanged in your dataset but not in the database.
There are so many options that we have discussed before, which may produce many custom
solutions to handle conflicts in an application. Here we discuss two approaches:
1. Create a separate dataset to store all database-version of the conflicting rows. Each time a
conflicting row is despatched to the RowUpdated event handler, query the corresponding original
row in the database, and add it into the dataset. Then, when the update returns, you present the
conflicting rows all at once to the user in, for example, a data grid. For each of them, let the user
know its type of conflict (changed or deleted), and offer user the corresponding options to resolve it.
2. Let user select one option for each type of conflict prior to submitting, or the options may be
decided by the business rules. Then, in the event handler of RowUpdated, instead of storing the
conflicting database rows for later processing, use the choice already made by the user to resolve
the conflict on the fly.
The following example code uses the second approach. Note that to handle the conflicts, we do not
need to do anything to the submitting logic i.e. the four commands of the data adapter. We only
need to implement the RowUpdated event handler.
Column Name
Data Type
Len
Descrip
CustomerID
smallint
Identity
CustomerName
varchar
50
Allow Null
Title
varchar
50
Allow Null
City
varchar
50
Allow Null
TStamp
timestamp
Allow Null
The UPDATE and INSERT command uses stored procedures, while SELECT and DELETE command uses
query strings:
@CustomerName_New varchar(50),
@Title_New varchar(50),
@City_New varchar(50),
@CustomerID_Orig smallint,
AS
IF @@ROWCOUNT = 1
RETURN
ALTER PROCEDURE dbo.Customers_Insert
@CustomerName varchar(50),
@Title varchar(50),
@City varchar(50),
AS
RETURN
The code of the application is as follow. Note that this application is a one-tier application, with
presentation and data access within one application. If they are in different tiers, the conflict
resolving rules should be sent from the presentation to the data access tier together with the
dataset containing data to submit to database, and the RowUpdated event will be handled in data
access tier.
updateCmd.CommandType = CommandType.StoredProcedure
param.SourceVersion = DataRowVersion.Original
param.SourceVersion = DataRowVersion.Original
param.Direction = ParameterDirection.InputOutput
mda.UpdateCommand = updateCmd
insertCmd.CommandType = CommandType.StoredProcedure
param.Direction = ParameterDirection.Output
param.Direction = ParameterDirection.Output
mda.InsertCommand = insertCmd
mda.DeleteCommand = deleteCmd
mda.ContinueUpdateOnError = True
Try
mda.Fill(mds)
With mds.Tables(0).Columns("CustomerID")
.AutoIncrement = True
.AutoIncrementSeed = -1
.AutoIncrementStep = -1
End With
mdg.DataSource = mds
mdg.DataMember = mds.Tables(0).TableName
Dim selectCmd As New OleDbCommand("SELECT * FROM Customers WHERE CustomerID
= ?", mcn)
mdaConflict.SelectCommand = selectCmd
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
Handles mda.RowUpdated
' mdaConflict is a data adapter used to query the database for the conflicting row
mdaConflict.SelectCommand.Parameters("CustomerID").Value = e.Row("CustomerID")
' Resolving errors. rdbtnLose, rdbtnReject, rdbtnReinsert etc. are radio buttons.
mds.Merge(rowsDb)
End If
rowDs.RejectChanges()
ElseIf rdbtnReinsert.Checked Then ' User chooses to reinsert this row into database
mds.Tables(0).Rows.Remove(e.Row)
newRow.ItemArray = items
mds.Tables(0).Rows.Add(newRow)
Else ' User chooses to remove this row from his dataset
mds.Tables(0).Rows.Remove(e.Row)
End If
End If
e.Status = UpdateStatus.Continue
End If
End Sub
SubmitDataSet()
End Sub
Try
mdsDb.Clear()
mda.Update(mds)
If mds.HasChanges Then
mda.Update(mds)
End If
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
mds.Clear()
mda.Fill(mds)
End Sub
Note: If you catch the RowUpdated event and there is a concurrency violation on one row, the
DataAdapter will append an error message such as "Concurrency violation: UpdateCommand affects
0 rows" to the end of its RowError property AFTER the RowUpdated event handler returns.
Therefore, if you have resolved the conflict and want no error or warning sign shown on your
datagrid, what you should do is NOT to call the row's ClearErrors method because there is no error
message set yet, but to set the OleDbRowUpdatedEventArgs's Status property to
UpdateStatus.Continue. When the handler returns, DataAdapter sees this Continue status and will
not append any error message to the row's RowError property. As long as RowError is empty, this
row appears to have no error (see section Row and Column Errors).
ds.WriteXml(“test.xml”, XmlWriteMode.WriteSchema)
If we choose WriteSchema as the second parameter to method WriteXml, the XML stream will
contain both schema and data:
- <MyDataSetName>
- <xs:complexType>
- <xs:choice maxOccurs="unbounded">
- <xs:element name="Order_x0020_Details">
- <xs:complexType>
- <xs:sequence>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
- <Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>11</ProductID>
<Quantity>12222</Quantity>
<TStamp>AAAAAAAAG1w=</TStamp>
</Order_x0020_Details>
- <Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>42</ProductID>
<Quantity>10</Quantity>
<TStamp>AAAAAAAAFrY=</TStamp>
</Order_x0020_Details>
- <Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>72</ProductID>
<Quantity>5</Quantity>
<TStamp>AAAAAAAAFrc=</TStamp>
</Order_x0020_Details>
</MyDataSetName>
- <MyDataSetName>
- <Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>11</ProductID>
<Quantity>12222</Quantity>
<TStamp>AAAAAAAAG1w=</TStamp>
</Order_x0020_Details>
- <Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>42</ProductID>
<Quantity>10</Quantity>
<TStamp>AAAAAAAAFrY=</TStamp>
</Order_x0020_Details>
- <Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>72</ProductID>
<Quantity>5</Quantity>
<TStamp>AAAAAAAAFrc=</TStamp>
</Order_x0020_Details>
</MyDataSetName>
If we choose DiffGram, the current version of the dataset plus the original version of those changed
will be written:
- <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
- <MyDataSetName>
<OrderID>10248</OrderID>
<ProductID>11</ProductID>
<Quantity>12</Quantity>
<TStamp>AAAAAAAAG1w=</TStamp>
</Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>72</ProductID>
<Quantity>5</Quantity>
<TStamp>AAAAAAAAFrc=</TStamp>
</Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>51</ProductID>
<Quantity>17</Quantity>
</Order_x0020_Details>
</MyDataSetName>
- <diffgr:before>
<OrderID>10248</OrderID>
<ProductID>11</ProductID>
<Quantity>12222</Quantity>
<TStamp>AAAAAAAAG1w=</TStamp>
</Order_x0020_Details>
<OrderID>10248</OrderID>
<ProductID>42</ProductID>
<Quantity>10</Quantity>
<TStamp>AAAAAAAAFrY=</TStamp>
</Order_x0020_Details>
</diffgr:before>
</diffgr:diffgram>
A diffgram enables us to submit the updated dataset in XML format to the database.
6.2. XmlReader
XmlReader handles the content of a XML document as a stream. Each time method Read is called, it
reads in one node, which is a string enclosed in a pair of angle brackets “<>”. The content of the
node is parsed and stored in memory. When you access the Name, Value or NodeType of the
XmlReader, you are accessing those of this “current” node. Once XmlReader has read in one node,
the node has been “consumed”. It can not go back to nodes already read. It can only read forward.
while(xtr.Read())
if (xtr.HasValue)
if (xtr.HasAttributes)
while (xtr.MoveToNextAttribute())
if (xtr.HasValue)
{
Unlike XmlReader, XmlDocument does not treat the content of a XML document as a stream. Instead
it loads all the content of the document into memory.
All the nodes in the XML hierarchy are represented by XmlNodes, which are linked together using
parent-child, sibling-sibling pointers. The DocumentElement property of XmlDocument represents
the root node of the document, through which you can navigate to any other node. When you have
one XmlNode at hand, to navigate horizontally, use its property NextSibling or PreviousSibling; to
navigate vertically, use ParentNode or FirstChild.
These nodes does not contain data themselves. They refer to the single copy of the XML content in
memory. You can insert new node, delete node, change the content of a node, etc. These changes
are done directly to that single copy of data. When you call XmlDocument’s Save method, the
changed data will be saved into a XML file.
See the following sample code. Class XmlToString.DocToString takes a XmlDocument and iterates all
of its nodes and puts all the content into the returned string. Method NodeToString does similar job
but only to child nodes of the passed node. Form1.btnLoadXml_Click creates a XmlDocument, loads
its content from a XML file, and invokes XmlToString to display the content of the XmlDocument.
if (node.Value != null)
astrContent += ", Value = " + node.Value;
if (i > 0)
astrContent += "]";
astrContent += "\r\n";
if (node.HasChildNodes)
{
XmlNode childNode = node.FirstChild;
childNode = childNode.NextSibling;
// Element <Author>Silan Liu</Author> will still return true of HaschildNodes, and its child
node
// is of type Text, which is the inner text. So in this case we shouldn't get the child node.
return strContent;
}
return strContent;
...
try
mdoc.Load(tbXmlDocName.Text);
tbDisplayDoc.Text = mXmlToString.DocToString(mdoc);
MessageBox.Show(ex.Message);
}
<Books>
<Title>Inside ADO.NET</Title>
<CD Language="Chinese">
</CD>
</Book>
<Title>Inside PowerBuilder</Title>
<CD Language="English">
</CD>
</Book>
<Title>Business in China</Title>
<CD>None</CD>
</Book>
<CD>
</CD>
</Books>
Node = Books
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Yang Xie, Attributes = [Gender = Female, Country = Australia]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
Node = CD
doc.Load("Books.xml");
if (root.HasChildNodes)
if (child.Attributes != null)
child.Attributes[0].Value = "10888";
doc.Save("Books.xml");
after run, you will see the value of the “Pages” attribute of the first “Book” element in “Books.xml” is
changed to “10888”. This proves that XmlDocument can also write back the changes.
6.4. XmlDataDocument
Even with the help of XmlDocument, manipulating XML is still a very cumbersome job, compared
with working with our old friend – the cheerful and intelligent DataSet. Besides, how can we bind a
XML document with a control such as data grid?
That is the purpose of XmlDataDocument, which inherits from XmlDocument, and acts as a bridging
between a XML document and a DataSet. You can create a XmlDataDocument then get from it a
DataSet, or vice versa. After you’ve changed any one of them, the other one will be automatically
synchronized.
doc.Load("Books.xml");
DisplayXmlDocument(doc);
mdg.DataSource = mds;
mdg.DataMember = mds.Tables[0].TableName;
After run, the data grid will show the content of the dataset, while the text box will show the
content of the XmlDataDocument. If you then change anything in the XmlDataDocument or the
DataSet, the other one will be automatically updated.
Note:
1. After the XmlDataDocument loads in the XML data, it is automatically loaded into the dataset
using its ReadXml method. Therefore, the content of the XML data must comply to the XML schema
that has been read into the dataset. If they don’t match the data won’t be loaded into the dataset.
2. The dataset does not need to contain all the columns that are contained in the XML document,
just like when used on a database table.
With the help of XmlDataDocument, you do not need to directly manipulate the XML document. You
can deal with DataSet all the time, and let XmlDataDocument do the translating.
6.5. XPath
In many aspects, a XML document is a database. The DOM and its Visual Studio .NET
implementations, such as XmlReader and XmlDocument, are the DBMS that allows you to retrieve
and change data in the database. XmlDocument enables us to move node by node in both ways, but
it is not enough. We need the ability to select a subset of elements using criteria, such as SQL query
from Books
XPath is a XML language that does this job. In Visual Studio .NET, you can simply pass a XPath
expression as a string to XmlNode.SelectNodes, which will return a list of nodes in the form of
XmlNodeList, which conforms to the query.
1. / when used in the beginning of the expression means the root element, while when used after
an element name means one level down that element;
The following sample code and XML document is based on those in section XmlDocument &
XmlNode:
try
XmlNodeList nl = mdoc.DocumentElement.SelectNodes(tbXPath.Text);
strContent += mXmlToString.NodeToString(node);
tbDisplayXPath.Text = strContent;
MessageBox.Show(ex.Message);
The following table shows the XPath query which is run on “Books.xml”:
/Books/Book/Author
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Yang Xie, Attributes = [Gender = Female, Country = Australia]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
/Books/*/Author
Get all Author elements that are two levels below /Books.
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Yang Xie, Attributes = [Gender = Female, Country = Australia]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
Node = Author, InnerText = Norah Jones, Attributes = [Gender = Female, Country = USA]
//Author
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Yang Xie, Attributes = [Gender = Female, Country = Australia]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
Node = Author, InnerText = Norah Jones, Attributes = [Gender = Female, Country = USA]
//CD/Author
Get all Author elements which are under a CD element, no matter which level the CD element is on.
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
Node = Author, InnerText = Norah Jones, Attributes = [Gender = Female, Country = USA]
/Books/Book/*
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Yang Xie, Attributes = [Gender = Female, Country = Australia]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
/Books/Book/@Pages
Result:
Result:
/Books/Book/Author[.="Silan Liu"]
Get all Author elements under /Books/Book whose value is “Silan Liu”
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
/Books/Book[./Author="Silan Liu"]/Author
Ditto - Get all Author elements under /Books/Book whose value is “Silan Liu”
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
/Books/Book/Author[@Country="China"]
Result:
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
/Books/Book[@Pages>1000]
Get all Book elements that are under /Books and whose Pages attribute is geater than 1000.
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
/Books/Book[starts-with(Title, "Inside")]
Get all Book elements that are under /Books and whose Title starts with “Inside”.
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
Node = Author, InnerText = Yang Xie, Attributes = [Gender = Female, Country = Australia]
Node = Author, InnerText = Mao Mao, Attributes = [Gender = Female, Country = China]
/Books/Book[@Language="English"]/Author
Get all Author elements that are under /Books/Book whose Language attribute is “English”.
Result:
Node = Author, InnerText = Silan Liu, Attributes = [Gender = Male, Country = Australia]
If you append “FOR XML AUTO, ELEMENTS” to the end of a SQL query (AUTO is to name the element
of each row after the table name, ELEMENTS is to store the column values as XML elements – by
default they are stored as attributes), SQL server 2000 will return the result of the query in XML
format. Such a query can only be executed by the ExecuteXmlReader method of SqlCommand and
SqlXmlCommand, which returns a XmlReader.
A XmlReader can be passed to DataSet.ReadXml to load its data into the dataset, or to
XmlDocument.Load to load its data into the XmlDocument.
The difference between SqlCommand and SqlDataCommand on this aspect is: the XmlReader
returned by a SqlCommand does not have a root node. If you want to read it into a dataset using its
ReadXml method, you have to pass XmlReadMode.Fragment as the second parameter. In
comparison, a XmlReader returned by a SqlXmlCommand is a complete XML document, as long as
you specify the root node using the command’s RootTag property:
Dim cmd As New SqlXmlCommand(“SELECT * FROM Customers FOR XML AUTO, ELEMENTS”,
strConn)
cmd.RootTag = “ROOT”
xmlDoc.Load(rdr)
When you provide a XPATH to the command, it converts it to a “FOR XML” SQL query. When you
submit a diffgram, the command generates a batch of SQL queries wrapped in a transaction. In both
cases the command needs to know the schema of the table to be able to generate those SQL
queries, so you need to provide the command with a XSD file:
cmd.SchemaPath = “C:\MySchema.xsd”
cmd.CommandType = SqlXmlCommandType.XPath
mds.ReadXml(rdr)
mds.WriteXml(“C:\MyDiffGram.xml”, XmlWriteMode.DiffGram)
cmd.SchemaPath = “C:\MySchema.xsd”
cmd.CommandType = SqlXmlCommandType.DiffGram
cmd.ExecuteNonQuery
7. WEB APPLICATIONS
7.1. Paging
If the data source contains all rows that will ever be displayed, a DataGrid which is bound to the data
source knows how to page through its rows. You only need to set the following properties of the
data grid:
· AllowPaging: true
· CurrentPageIndex: indicates the page of data in the data source that is to be displayed by the
data grid.
The first four properties can be set once for all, in Property Builder at design time, or in sub
Page_Load at run time. The CurrentPageIndex property can be set in PageIndexChanged event,
which is fired when user clicks a navigating button. Once CurrentPageIndex’s value changed, the
data grid will acquire the corresponding page of data in data source through the data binding:
Handles gridCustomers.PageIndexChanged
daCustomers.Fill(tblCustomers)
gridCustomers.DataSource = vueCustomers
gridCustomers.CurrentPageIndex = e.NewPageIndex
gridCustomers.DataBind()
End Sub
Note that because of the stateless nature of web application, each time an event handler is called, it
is called from a recreated new page. That’s why we have to fill the dataset again.
As you can see from the example, although each time the data grid only displays one page of data,
all data must be retrieved and stored in the data source. Thus it is not efficient.
The efficient way is to only fetch the current page of data from the database into the data source. If
you do this, and you still want to make use of the navigation buttons and the PageIndexChanged
event provided by the data grid, you must set the its AllowCustomPaging property to true and
VirtualItemCount property to the total amount of rows available for display in the database.
Otherwise the data grid generates the navigation buttons according to the total amount of rows in
the bound data source, which in this case is always the amount of one page.
There is one overloaded DataAdapter.Fill, which takes the number of rows to skip ahead and the
number of rows to fill as the second and third parameter. Unlike the Paging with DataGrid approach,
the data source is only filled with one page of data. However, the data adapter still fetches all rows
specified in the SQL query. So this approach is the same inefficient, and it is less convenient because
you have to take care of the navigation issues yourself.
Only paging through SQL query can avoid fetching excessive data and reduce network traffic. For
Access and SQL Server database, we can use the “TOP” clause to achieve this goal. The following SQL
query fetches the 41~50 rows from the database:
SELECT TOP 10 CustomerID, CompanyName, ContactName, Country
Following is a whole process for editing and submitting changes in a web application:
1. When user connects to the server for the first time, the web page queries the database and
acquires a result set such as a dataset, and uses it to generate a HTML page and sends it to the user’s
browser. If the server does not want to query the database for every post-back, it should also store
the result set somehow:
...
da.Fill(tableOrders);
Session.Add("tableOrders", tableOrders);
dg.DataSource = tableOrders;
dg.DataBind();
2. When user clicks a button indicating that he wants to edit a field, if that field is initially not shown
editable, the web page should generate a new HTML page with that field now showing editable.
Before all, for a DataGrid, if you want a column to be editable, you should add it as a bound column
in the datagrid’s Property Builder, and add “Edit” and/or “Delete” button columns to it.
When the user clicks the “Edit” button, a EditCommand event is fired, and the event’s Item property
contains the DataGrid row to be edited. Server can use this Item property’s ItemIndex property to
set the DataGrid’s EditItemIndex property. Then, when the datagrid’s DataBind method is called, it
will generate a new HTML page, with the row indexed by EditItemIndex containing editable
TextBoxes for the editable columns:
dg.DataSource = (DataTable)Session["tableOrders"];
dg.EditItemIndex = e.Item.ItemIndex;
dg.DataBind();
3. After user has made the change, he clicks a button, the change is post back to the server. The
event handler of the button should either retrieve the result set stored somehow by last page or
make a new query to database to acquire a new result set. Then it should get the changed row
contained in the event, set it into the result set, and update the database with the result set.
ds.Tables[TABLE_NAME].Rows[iRow][i - 1] = ((TextBox)e.Item.Cells[i].Controls[0]).Text;
da.Update(tableOrders);
Session["tableOrders"] = tableOrders;
dg.DataSource = tableOrders;
dg.DataBind();
dg.DataSource = (DataSet)Session["tableOrders"];
dg.EditItemIndex = -1;
dg.DataBind();