[ Team LiB ]
Recipe 6.11 Resolving Data Conflicts
Problem
You need to effectively resolve data conflicts and prevent overwriting of existing data
when attempting to update changes in a DataSet to a database where the underlying data
has changed.
Solution
Handle the DBConcurrencyException within the RowUpdated event of the DataAdapter.
The schema of table TBL0611 used in this solution is shown in Table 6-13.
Table 6-13. TBL0611 schema
Column name Data type Length Allow nulls?
Id int 4 No
Field1 nvarchar 50 Yes
The sample code contains seven event handlers:
Form.Load
Creates a DataSet A containing a single DataTable A, filling the schema and data
for the table from TBL0611 from the database using a DataAdapter. The
ContinueUpdateOnError property of the DataAdapter is set to true. A
CommandBuilder object is created to generate the updating logic for the
DataAdapter. The default view of DataTable A is bound to a data grid on the form.
A conflict table is created to store the original row data for a row when a
concurrency error is encountered while updating a row from DataTable A back to
TBL0611 in the database. A DataAdapter that uses a parameterized SQL SELECT
statement to retrieve the original row data is created. The schema for the conflict
table is loaded from TBL0611 using the DataAdapter. The default view of the
conflict table is bound to a data grid on the form.
A DataSet B and a conflict table for DataTable B row update concurrency errors
are created in the same way as described for DataSet A.
Refresh A Button.Click
Clears the conflict table, clears the data in table A, and uses a DataAdapter to fill
table A from TBL0611 in the database.
Refresh B Button.Click
Clears the conflict table, clears the data in table B, and uses a DataAdapter to fill
table B from TBL0611 in the database.
Update A Button.Click
Clears the conflict table and uses a DataAdapter to update changes made to table A
back to table TBL0611 in the database.
Update B Button.Click
Clears the conflict table and uses a DataAdapter to update changes made to table B
back to table TBL0611 in the database.
A DataAdapter.RowUpdated
Checks to see if a concurrency error occurred when updating the row in DataTable
A to table TBL0611 in the database. If an error occurred during the deletion of a
row, the RejectChanges( ) method is used to cancel the delete.
For all rows with a concurrency error, the Id for the row is retrieved from the row
and used with the DataAdapter for the conflict table to try to get the original data
for the row from the TBL0611 in the database. An error is set on the row in the
conflict table indicating whether it was changed (the row in error was retrieved
from the database) or deleted (a row in error could not be retrieved from the
database).
B DataAdapter.RowUpdated
Checks to see if a concurrency error occurred when updating the row in DataTa
b
le
B to table TBL0611 in the database. If an error occurred during the deletion of a
row, the RejectChanges( ) method is used to cancel the delete.
For all rows with a concurrency error, the Id for the row is retrieved from the row
and used with the DataAdapter for the conflict table to try to get the original data
for the row from the TBL0611 in the database. An error is set on the row in the
conflict table indicating whether it was changed (the row in error was retrieved
from the database) or deleted (a row in error could not be retrieved from the
database).
The C# code is shown in Example 6-29.
Example 6-29. File: ResolveDataConflictsForm.cs
// Namespaces, variables, and constants
using System;
using System.Configuration;
using System.Text;
using System.Data;
using System.Data.SqlClient;
private const String TABLENAME = "TBL0611";
private SqlDataAdapter daA, daB;
private DataSet dsA, dsB;
// . . .
private void ResolveDataConflictsForm_Load(object sender,
System.EventArgs e)
{
// Tables A and B are filled from the same data source table.
String sqlText = "SELECT * FROM " + TABLENAME;
// Create the DataAdapter for table A.
daA = new SqlDataAdapter(sqlText,
ConfigurationSettings.AppSettings["Sql_SqlAuth_ConnectString"]);
daA.ContinueUpdateOnError = true;
// Handle the RowUpdated event.
daA.RowUpdated += new SqlRowUpdatedEventHandler(daA_RowUpdated);
// Get the schema and data for table A in the DataSet.
dsA = new DataSet("A");
daA.FillSchema(dsA, SchemaType.Source, TABLENAME);
daA.Fill(dsA, TABLENAME);
// Create the command builder.
SqlCommandBuilder cbA = new SqlCommandBuilder(daA);
// Bind the default view for table A to the grid.
dataGridA.DataSource = dsA.Tables[TABLENAME].DefaultView;
// Create a DataAdapter to retrieve original rows
// for conflicts when updating table A.
conflictDaA = new SqlDataAdapter(sqlText + " WHERE Id = @Id",
ConfigurationSettings.AppSettings["Sql_ConnectString"]);
conflictDaA.SelectCommand.Parameters.Add("@Id", SqlDbType.Int, 0);
// Create a DataSet with the conflict table schema.
conflictDsA = new DataSet( );
daA.FillSchema(conflictDsA, SchemaType.Source, TABLENAME);
// Bind the default view for conflict table B to the grid.
conflictDataGridA.DataSource =
conflictDsA.Tables[TABLENAME].DefaultView;
// Create the DataAdapter for table B.
daB = new SqlDataAdapter(sqlText,
ConfigurationSettings.AppSettings["Sql_SqlAuth_ConnectString"]);
daB.ContinueUpdateOnError = true;
// Handle the RowUpdated event.
daB.RowUpdated += new SqlRowUpdatedEventHandler(daB_RowUpdated);
// Get the schema and data for table A in the DataSet.
dsB = new DataSet("B");
daB.FillSchema(dsB,SchemaType.Source,TABLENAME);
daB.Fill(dsB, TABLENAME);
// Create the command builder.
SqlCommandBuilder cbB = new SqlCommandBuilder(daB);
// Bind the default view for table A to the grid.
dataGridB.DataSource = dsB.Tables[TABLENAME].DefaultView;
// Create a DataAdapter to retrieve original rows
// for conflicts when updating table B.
conflictDaB = new SqlDataAdapter(sqlText + " WHERE Id = @Id",
ConfigurationSettings.AppSettings["Sql_ConnectString"]);
conflictDaB.SelectCommand.Parameters.Add("@Id", SqlDbType.Int, 0);
// Create a DataSet with the conflict table schema.
conflictDsB = new DataSet( );
daB.FillSchema(conflictDsB,SchemaType.Source,TABLENAME);
// Bind the default view for conflict table B to the grid.
conflictDataGridB.DataSource =
conflictDsB.Tables[TABLENAME].DefaultView;
}
private void refreshAButton_Click(object sender, System.EventArgs e)
{
// Clear the conflict table and reload the data.
conflictDsA.Clear( );
dsA.Clear( );
daA.Fill(dsA, TABLENAME);
}
private void refreshBButton_Click(object sender, System.EventArgs e)
{
// Clear the conflict table and reload the data.
conflictDsB.Clear( );
dsB.Clear( );
daB.Fill(dsB, TABLENAME);
}
private void updateAButton_Click(object sender, System.EventArgs e)
{
// Clear the conflict table and update table A to data source.
conflictDsA.Clear( );
daA.Update(dsA, TABLENAME);
}
private void updateBButton_Click(object sender, System.EventArgs e)
{
// Clear the conflict table and update table B to data source.
conflictDsB.Clear( );
daB.Update(dsB, TABLENAME);
}
private void daA_RowUpdated(object sender, SqlRowUpdatedEventArgs e)
{
// Check if a concurrency exception occurred.
if (e.Status == UpdateStatus.ErrorsOccurred &&
e.Errors.GetType( ) == typeof(DBConcurrencyException))
{
// If the row was deleted, reject the delete.
if(e.Row.RowState == DataRowState.Deleted)
e.Row.RejectChanges( );
// Get the row ID.
conflictDaA.SelectCommand.Parameters["@Id"].Value =
e.Row["ID"];
// Get the row from the data source for the conflicts table.
if(conflictDaA.Fill(conflictDsA, TABLENAME) == 1)
e.Row.RowError =