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 DataTable 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 = "Row has been changed in the database."; else e.Row.RowError = "Row has been deleted from the database."; } } private void daB_RowUpdated(object sender, SqlRowUpdatedEventArgs e) { // Bheck 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. conflictDaB.SelectCommand.Parameters["@Id"].Value = e.Row["ID"]; // Get the row from the data source for the conflicts table. if(conflictDaB.Fill(conflictDsB, TABLENAME) == 1) e.Row.RowError = "Row has been changed in the database."; else e.Row.RowError = "Row has been deleted from the database."; } }
Discussion
The RowUpdated event of the DataAdapter occurs during the Update( ) method after the command to update a row is executed against the data source. The event fires with each row update attempt.
The RowUpdated event handler receives an argument of type RowUpdatedEventArgs that provides information specifically related to the event as described in Table 6-14.
Table 6-14. RowUpdatedEventArgs properties
Property |
Description |
---|---|
Command |
Gets or sets the Command executed when the Update( ) method is called. |
Errors |
Gets errors generated by the .NET data provider when the Command was executed. |
RecordsAffected |
Gets the number of rows changed, inserted, or deleted by the execution of the Command . |
Row |
Gets the DataRow sent through the Update( ) method. |
StatementType |
Gets the type of SQL statement executed. This is one of the following values from the StatementType enumeration: Select , Insert , Update , or Delete . |
Status |
Gets or sets the action to take with the current and remaining rows during the Update( ) method. This is a value from the UpdateStatus enumeration described in Table 6-15. |
TableMapping |
Gets the DataTableMapping sent through the Update( ) method. |
Table 6-15 describes the values in the UpdateStatus enumeration used by the Status property of the RowUpdatedEventArgs object.
Table 6-15. UpdateStatus enumeration
Value |
Description |
---|---|
Continue |
Continue processing the rows. This is the default value. |
ErrorsOccurred |
The event handler reports that the update should be treated as an error. |
SkipAllRemainingRows |
Do not update the current row and skip updating the remaining rows. |
SkipCurrentRow |
Do not update the current row and continue updating with the subsequent row. |
The Update( ) method of the DataAdapter raises two events for every row in the data source that is updated. The order of the events is:
- The values in the DataRow are moved to parameter values.
- The OnRowUpdating event is raised.
- The update command executes against the row in the data source.
- If the UpdatedRowSource property of the Command is set to FirstReturnedRecord or Both , the first returned result is placed in the DataRow .
- If the UpdateRowSource property of the Command is set to OutputParameters or Both , the output parameters are placed in the DataRow .
- The OnDataRowUpdated event is raised.
- AcceptChanges( ) is called.
The DBConcurrencyException is raised during the update operation on a row if zero rows are affected. This usually indicates a concurrency violation.