DekGenius.com
[ Team LiB ] Previous Section Next Section

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 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:

  1. The values in the DataRow are moved to parameter values.

  2. The OnRowUpdating event is raised.

  3. The update command executes against the row in the data source.

  4. If the UpdatedRowSource property of the Command is set to FirstReturnedRecord or Both, the first returned result is placed in the DataRow.

  5. If the UpdateRowSource property of the Command is set to OutputParameters or Both, the output parameters are placed in the DataRow.

  6. The OnDataRowUpdated event is raised.

  7. 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.

    [ Team LiB ] Previous Section Next Section