Managing Concurrent Data Updates
To finish off looking at relational data handling in .NET, we''ll examine some of the issues that arise when you have multiple users updating your data–a problem area normally referred to as concurrency. It''s easy enough to see how such a problem could arise:
Alice in accounts receives a fax from a customer indicating that their address has changed. She opens the customer record in her browser and starts to change the address column values.
Just at this moment, Dave in dispatch (who received a copy of the fax) decides to update the customer''s delivery route code. He also opens the customer record in his browser and changes the routing code column value.
While Dave is doing this, Alice finishes editing the address and saves the record back to the database.
Shortly afterwards, Dave saves his updated record back to the database.
What''s happened is that Dave''s record, which was opened before Alice saved her changes, contains the old address details. So when he saves it back to the database, the changes made by Alice are lost. And while concurrency issues aren''t solely confined to databases (they can be a problem in all kinds of multi-user environments) it is obviously something that you can''t just ignore when building data access applications.
Avoiding Concurrency Errors
Various database systems and applications use different approaches to control the concurrent updates problem. One solution is the use of pessimistic record locking. When a user wants to update a record, they open it with pessimistic locking; preventing any other user opening the same record in update mode. Other users can only open the record in ''read'' mode until the first user saves their copy and releases their lock on the record.
Note | For maximum runtime efficiency, many database systems actually lock a ''page'' containing several contiguous records rather than just a single one–but the principle is the same. |
However, in a disconnected environment (particularly one with occasionally unreliable network links such as the Internet) pessimistic locking is not really feasible. If a user opens a record and then goes away, or the network connection fails, it will not be released. It requires some other process to monitor record locks and take decisions about when and if the user will come back to update the record so that the lock can be released.
Instead, within .NET, all data access is through optimistic record locking, which allows multiple users to open the same record for updating–possibly leading to the scenario we described at the start of this section. It means that you have to use some kind of code that can prevent errors occurring when you need to support concurrent updates. There are a few options you can take, such as:
Write stored procedures that do lock records and manage the updates to prevent concurrency errors. For example, you could add a column called "
Locked " and set this when a user fetches a row for updating. While it''s set, no other user could open the row for updating, only for reading. This is not a favored approach in .NET as it takes away the advantages of the disconnected model.
Arrange for your code to only update the actual columns that it changes the value of, minimizing the risk of (but not guaranteeing to prevent) concurrency errors. For example, in the previous scenario, if Dave in dispatch had only updated the route code column that he changed the value of, Alice''s changes to the address columns would not have been lost.
Compare the existing values in the records with the values that were there when you created your disconnected copy of the record. This way you can see if another user has changed any values in the database while you were working on your disconnected copy. This is the preferred solution in .NET, and there are built-in features that help you to implement it.
A Concurrency Error Example
To illustrate how concurrency error can be detected, try the example page Catching Concurrency Errors When Updating the Source Data (
concurrency -
error .
aspx ) shown in Figure 10-15. This page extracts a row from the source data table and displays the values in it. Then it executes a SQL statement directly against the original table in the database to change the
Title column of the row while the disconnected
DataSet is holding the original version of the row. You can see this in the screenshot after the first
DataGrid control.

Figure 10-15:
Next the code changes a couple of columns in the disconnected
DataSet table row, then calls the
Update method of the
DataAdapter to push this change back into the original source table. A
CommandBuilder object is used to create the SQL statement that performs the update, and you can see this statement displayed in the preceding page the
SELECT statement that originally fetched the row. Notice that it uses a
WHERE clause that tests all the values of the row in the database against the values held in the
DataSet . This means, of course, that (because the concurrent process has changed the row) the update fails and an error is returned. What''s happened is that the
Update process expects a single row to be updated, and when this didn''t happen it reports an error. The error message is displayed at the bottom of the page.
At this point, the developer would usually indicate to the user that this error had occurred, and give them the chance to reconcile the changes. Exactly how this is done and what options the user has depends on the application requirements. The usual process is to provide the user with the values that currently exist in the row as well as the values they entered, and allow them to specify which should be persisted into the data store.
The Code for the ''Catching Concurrency Errors'' Example
The only section of the code for this example that you haven''t seen before is that which performs a concurrent update to the source table in the database while the
DataSet is holding a disconnected copy of the rows. It''s simply a matter of creating a suitable SQL statement, a new
Connection and
Command object, and executing the SQL statement. Collect the number of rows affected by the update and display this in the page, along with the SQL statement you executed against the original data in the database.
''change one of the rows concurrently – i.e. while the
''DataSet is holding a disconnected copy of the data
Dim strUpdate As String
Dim datNow As DateTime = Now()
Dim strNow As String = datNow.ToString("dd-M-yy \a\t hh:mm:ss")
strUpdate = "UPDATE BookList SET Title = ''Book Updated on " _
& strNow & "'' WHERE ISBN = ''1000000002''"
Dim intRowsAffected As Integer
Dim objNewConnect As New OleDbConnection(strConnect)
Dim objNewCommand As New OleDbCommand(strUpdate, objNewConnect)
objNewConnect.Open()
intRowsAffected = objNewCommand.ExecuteNonQuery()
objNewConnect.Close()
outUpdate.InnerHtml = "Command object concurrently updated " _
& CStr(intRowsAffected) & " record(s)<br />" & strUpdate
Next, the code changes a couple of column values in the disconnected copy of the row within the
DataSet table. Then all you have to do is execute the
Update method of the
DataAdapter object. The error is trapped by the
Try..Catch construct (like that we''ve used in all the examples) and details are displayed in the page.
''change the same row in the table in the DataSet
objTable.Rows(0)("Title") = "Amateur Theatricals for Windows 2000"
objTable.Rows(0)("PublicationDate") = Now()
Try
''create an auto-generated command builder and set UPDATE command
Dim objCommandBuilder As New OleDbCommandBuilder(objDataAdapter)
objDataAdapter.UpdateCommand = objCommandBuilder.GetUpdateCommand()
''display the auto-generated UPDATE command statement
outUpdate.InnerText = objDataAdapter.UpdateCommand.CommandText
Now do the update (in this case we know it will fail):
intRowsAffected = objDataAdapter.Update(objDataSet, "Books")
outResult.InnerHtml = "<b>* DataSet.Update</b> affected <b>" _
& CStr(intRowsAffected) & "</b> row."
Catch objError As Exception
''display error details
outError.innerHTML = "* Error updating original data.<br />" _
& objError.Message & "<br />" & objError.Source
End Try
Updating Just the Changed Columns
In general, it is the process of modifying existing rows in a data store that is most likely to create a concurrency error. The process of deleting rows is usually less error-prone, and less likely to cause data inconsistencies in your database. Likewise, providing data entry programs are reasonably clever about how they create the values for unique columns; the process of inserting new rows is generally less of a problem.
One of the ways that you can reduce the likelihood of a concurrency error during row modification, as we suggested right at the start of this section of the chapter, is to push only the modified column values (the ones that have been changed by this user or process) into the original data store, rather than blindly updating all of the columns. Of course, this means that you can''t use the
Update method–you have to build and execute each SQL statement yourselves.
An Example of Updating Individual Columns
The example page Managing Concurrent Updates to Individual Columns (
concurrency-columns.aspx ) demonstrates the process we''ve just been discussing. Rather than updating all the columns in every row that has been modified, it only attempts to update the column values that have actually changed. The code extracts a series of rows from the
BookList table and displays them, then changes some of the column values and displays the rows again.
In Figure 10-16, you can see the original values of the rows in the database, and the values of the rows in our disconnected
DataSet table after the code has changed some of the values. This code changed the title of the first book, the title and publication date of the third book, and (highlighted in the screenshot) just the publication date for the fourth book.

Figure 10-16:
Lower down the the page, as shown in Figure 10-17, you can see that the code then concurrently updates two of the rows in the source database table while the
DataSet is holding a disconnected copy of the data. It changes the titles of the third and fourth books. Below this is another
DataGrid that shows just the modified rows in the
DataSet .

Figure 10-17:
Finally, at the bottom of the page you can see that–after the concurrent updates have taken place–we attempt to push our changes in the
DataSet back into the data store. However, in this case, the
Update method of the
DataAdapter is not used. Instead, each of the modified rows is processed individually by executing a custom SQL statement for each one. As you can see from comparing these, the statements used only update the columns that have been changed within the table in the
DataSet , and only check the existing values of these columns (as part of the
WHERE clause) in the database.
As seen in Figure 10-17, the first update succeeds because the concurrent process hasn''t changed the original row. Following this, the second update fails because you are attempting to change the title while the concurrent process has already changed this column in the database (look at Figure 10-16 to see the original values of the rows).
However, the third update also succeeds–even though the same row in the original table in the database has been concurrently updated. The concurrent process changed only the
Title column while our disconnected copy contains an updated value for only the
PublicationDate column. Hence, because the update code is clever enough to only update the changed columns, both updates can occur concurrently without the risk of inconsistencies arising.
The Code for the ''Updating Individual Columns'' Example
There are several things going on in this example page that we need to look at in more depth. For example, you need to be able to get at just the modified rows in the table within your
DataSet so that you can iterate through these rows processing the update for each one. Secondly, you need to look in more detail at how to create the values that you use in the
WHERE clause of your SQL statements to compare to a
DateTime column in SQL server.
Marshalling the Changed Rows in a DataSet
As mentioned in the previous chapter, every row in a table within a
DataSet has a
RowState property that indicates whether that row has changed since the table was filled, or since the last time the
AcceptChanges or
RejectChanges method was called. So, to get a list of the changed rows you could iterate through the table looking at this property in each row, and extract just the ones we want into an array–or into another table.
The general process of collecting together data and transferring it to another location is often referred to as marshalling. In your case, you want to marshal the changed rows from one table into another table, and the .NET data access classes make it easy through the
GetChanges method of the
DataSet object. It returns a
DataSet object containing just the changed rows. You can use the
GetChanges method in two ways:
With no parameters, whereupon it returns a
DataSet object with the default table (at index zero) filled with all the changed rows–e.g. all the rows that have been modified, deleted, or inserted.
With a
DataRowState value as the single parameter, whereupon it returns a
DataSet object with the default table (at index zero) filled with just the changed rows having that value for their
RowState property–e.g. just the rows that have been modified, or just the rows that have been deleted, or just the rows that have been inserted.
This process would also allow you to take a table that contained updated, deleted, and inserted rows and extract these into separate arrays of rows–one each for changed rows, deleted rows, and updated rows. You could then use the
Update method of the
DataAdapter with each table or array of rows in turn (as discussed earlier in this section of the chapter)–in the correct order to avoid any errors due to parent/child relationships within the source data tables.
Getting the Modified Rows into a New DataSet
The code in your page creates a variable to hold a
DataSet object, and then executes the
GetChanges method with the value
DataRowState.Modified as the single parameter. The new
DataSet object is returned and assigned to your variable
objChangeDS , and you can display the contents in the usual way using a
DataGrid control defined within the HTML section of the page.
''declare a variable to hold another DataSet object
Dim objChangeDS As DataSet
''get *changed* records into the new DataSet
''copy only rows with a RowState property of "Modified"
objChangeDS = objDataSet.GetChanges(DataRowState.Modified)
''display the modified records from the table in the new DataSet
dgrResult3.DataSource = objChangeDS.Tables(0).DefaultView
dgrResult3.DataBind() ''and bind (display) the data
As an aside (we don''t actually do it in our example here) you can use the same technique to get the inserted, deleted, or unchanged rows as well. To get the inserted rows into a
DataSet just specify the value
DataRowState.Added in the parameter to the
GetChanges method:
objChangeDS = objDataSet.GetChanges(DataRowState.Added)
The same applies to the deleted rows; specify
DataRowState.Deleted in the parameter to the
GetChanges method:
objChangeDS = objDataSet.GetChanges(DataRowState.Deleted)
However, if you then bind this data to a
DataGrid object, nothing will be displayed because all of the rows have been deleted! To get round this you can create a
DataView object explicitly for the table and then set the
RowStateFilter property to
DataViewRowState.Deleted as well (this topic was covered in previous chapters). Then it shows the deleted rows:
Dim objDataView As DataView = objChangeDS.Tables(0).DefaultView
objDataView.RowStateFilter = DataViewRowState.Deleted
dgrResult.DataSource = objDataView
dgrResult.DataBind()
Finally, to get the unchanged rows specify
DataRowState.Unchanged in the parameter to the
GetChanges method:
objChangeDS = objDataSet.GetChanges(DataRowState.Unchanged)
Getting Back to Our Example
After that short aside, let''s get back to the code for your example page. While there is quite a lot of code in this page, most of it is stuff that you''ve seen several times before. Basically, you extract the rowset from the database into a
DataSet and display it, execute a couple of SQL
UPDATE statements to change the original data store contents, then change some values in the same rows in the disconnected copy held within the
DataSet object. All these steps can be seen in the page.
What we want to concentrate on here is how to create and execute the SQL statements that you''ll use to perform the updates to the original data from the rows in the disconnected
DataSet .
Building the SQL Statements
The plan is to create the two ''root'' parts of the SQL statement (the
SET clause and the
WHERE clause) separately as you iterate through each column in the row, then assemble the complete statement afterwards. You''ve already marshaled the modified rows into a new
DataSet named
objChangeDS , so you can iterate through the single table in that
DataSet processing each modified row using a
For Each construct.
As you process each row, create your two sections of SQL statement. For the
WHERE clause include the test for the
Original value of the ISBN (the primary key). Then you can start the nested
For Each construct that will iterate through each column in this row, and collect the column name in a string variable.
The next step is to see if the value of the column has been changed since you loaded your
DataSet , by comparing the
Original and the
Current values. Note that this is nothing to do with checking the original values in the source table in the database. You''re disconnected from the database, and so you can''t see any concurrent updates going on. What you''re checking for here is if the contents of the disconnected row have been changed within the
DataSet since it was originally extracted from the source database.
''iterate through all the modified rows in the table
For Each objRow in objChangeDS.Tables(0).Rows
''create the two root parts of the SQL statement
strSQL = "UPDATE BookList SET "
strWhere = " WHERE ISBN=''" & objRow("ISBN", DataRowVersion.Original) & "''"
''iterate through all the columns in this row
For Each objColumn In objChangeDS.Tables(0).Columns
''see if this column has been changed since the DataSet was
''originally created by comparing Original and Current values
strColName = objColumn.ColumnName
If objRow(strColName, DataRowVersion.Current) <> _
objRow(strColName, DataRowVersion.Original) Then
...
Matching a SQL Server DateTime Column
If the column has been changed, you need to add it to both sections of the SQL statement you''re constructing. However, if the value is a
DateTime , you have to format the
Original value (which will be used in the
WHERE clause) to match the column in the source table in the database. To perform a match against a SQL
DateTime column, you have to specify the value in your disconnected row in a suitable format so that it can be compared properly.
The next part of the code extracts the original value of the
PublicationDate column from the row and formats it if it is a date/time–if not it just extracts the value. Then add the column name and values to the two sections of the SQL statement. You can use the
Current value in the
SET clause and the
Original value in the
WHERE clause.
After this, you can go round and process the next column. And once all the changed columns have been processed, tidy up the SQL statement by stripping off the extra comma and space you added and assemble it into one string. Then you can display it in the page.
...
''have to get format of DateTime exactly right for a comparison
If objColumn.DataType.ToString() = "System.DateTime" Then
datRowDateValue = objRow(strColName, DataRowVersion.Original)
strRowValue = datRowDateValue.Format("yyyy-MM-dd\ HH:mm:ss", _
Nothing)
Else
strRowValue = objRow(strColName, DataRowVersion.Original)
End If
strSQL &= strColName & "=''" _
& objRow(strColName, DataRowVersion.Current) & "'', "
strWhere &= " AND " & strColName & "=''" & strRowValue & "''"
End If
Next ''go to next column
''strip off extra comma and space from end of string
strSQL = Left(strSQL, Len(strSQL) –2) & strWhere
''display the SQL statement
strResults &= "* " & strSQL & " ... "
objCommand.CommandText = strSQL
...
Executing the SQL Statement
Now you can execute this SQL statement, check the number of rows affected, and display a suitable message for this row. If the number of rows affected is less than
1 you know there was a concurrency error–the original row in the source table has changed while you were holding the disconnected copy. Then, after processing this row, go back and do the next one in your modified rows table. When all the rows have been processed, display the result in a
<div> element located in the HTML of the page.
...
Try
intRowsAffected = objCommand.ExecuteNonQuery()
If intRowsAffected > 0 Then
strResults &= "... updated <b>" & intRowsAffected & "</b> row(s)"
Else
strResults &= "<b>Error</b>: Row was changed by another user"
End If
Catch objError As Exception
''display error details
strResults &= "Error: " & objError.Message & " –" _
& objError.Source & "<br />"
End Try
Next ''repeat for next row if any
outUpdates.InnerHtml = strResults ''then display the results
Handling Concurrency Errors
Your example simply displays the errors that were encountered due to concurrent updates in the page. It doesn''t provide any way to reconcile these errors. In fact, the .NET data access objects don''t provide any features for this, as there is no fixed way to do it. It all depends on what your application is doing, how the updates are being carried out, and what business rules you want to apply. The next example demonstrates another approach for managing concurrency errors.
Capturing Errors with the RowUpdated Event
The previous example attempted to reduce the likelihood of concurrency errors occurring by taking over the update process and replacing it with a custom system of SQL statements. This allows updates to be monitored individually. However, this kind of process is going to produce a performance hit when compared to the
Update method exposed by the
DataAdapter .
You saw in an earlier section of this chapter that the
DataAdapter raises two events for each row as the
Update method is being executed. These events allow you to examine each row before it is pushed into the original table (the
RowUpdating event) and after the update has been processed for that row (the
RowUpdated event). By writing handlers for these events, you can deal with many concurrency issues.
Of course, you don''t usually detect a concurrency error until you actually perform the update to a row against the original data source. You could use the
RowUpdating event to fetch the data again from the database before you attempted to perform your update, and see if it had changed, but this is an inefficient approach unless you really need to actually prevent update attempts that might result in a concurrency error.
Generally, a better solution is to trap any errors that occur during the update process and report these back so that the user (or some other process) can reconcile them. The next example demonstrates how you can do this, and also introduces a couple more features of ADO.NET.
Concurrent Updates and the RowUpdated Event
The example page Managing Concurrent Updates with the RowUpdated Event (
concurrency- rowupdated.aspx ) demonstrates how we can capture information about concurrency errors while updating a data source. It handles the
RowUpdated event, and creates a
DataSet object containing a single table that details all the errors that occurred. This
DataSet could be returned to the user, or passed to another process that will decide what to do next. In your example, you simply display the contents in the page.
So, as shown in Figure 10-18, when you open the example page you see the rowset with its original values when you fetched it from the database, and then the contents after you''ve made some changes to this disconnected data. We''ve deleted the first row (ISBN
1100000001 ), changed the title and publication date in the second row (ISBN
1100000002 ), and just the publication date in the third row (ISBN
1100000003 ).

Figure 10-18:
Next, as shown in Figure 10-19, the code in the page executes three SQL
UPDATE statements directly against the database (as in the previous example) to change the values in the three rows that you have just edited in the disconnected
DataSet .

Figure 10-19:
At the bottom of the page you can see a third
DataGrid control. This displays the contents of the new errors table that you''ve dynamically created in response to errors that occurred during the update process.
In the errors table, you can see that you''ve got several errors, and for each one the table provides information about the type of operation that was being executed (the Action, equivalent to the statement type), the primary key of the row, the name of the column that was modified in the
DataSet , and three values for this column. These are:
OriginalValue : The value when the
DataSet was first filled (the value that was in the database at that time).
CurrentValue : The current value in the
DataSet after the updates we made.
DatabaseValue : The value of this column at the present moment within the database (the value set by the concurrently executed SQL
UPDATE statement).
If you look at the result, the first three error rows indicate that you deleted a row in the
DataSet that was concurrently changed within the database. You can see that the
Title column was changed while you had the
DataSet open. These rows show the
Original values, but of course they cannot show the
Current values in the
DataSet because the row was deleted.
The fourth and fifth rows in the errors table are for the second row that you modified in your
DataSet . You changed the
Title and
PublicationDate columns in your
DataSet , while the concurrent process changed the
Title column–so the value for this column in the database is different from the
Original value.
In the last of the error rows, the update failed because the concurrent process had changed a different column than was changed in the
DataSet within that row. The database value and the
Original value are the same for this column, so it must have been a different column that caused the concurrency error.
If you intend to use a page like this to extract data that will be presented to a user so that they can manually reconcile the data, you may prefer to include all the columns from rows where a concurrency error occurred in the errors table. We''ll discuss this at the appropriate point as we work through the code.
The Code for the ''RowUpdated Event'' Example
The majority of the code in this example is the same as in our earlier concurrency examples. One difference is that you declare some of the variables you use as being global to the page, rather than within the
Page_Load event handler as you''ve done before. This is because you want to be able to access these variables within your
RowUpdated event handler.
<script language="vb" runat="server">
Dim gstrResult As String ''to hold the result messages
Dim gstrConnect As String ''to hold connection string
Dim gobjDataSet As DataSet ''to hold rows from database
Dim gobjErrorTable As DataTable ''to hold a list of errors
Dim gobjErrorDS As DataSet ''to hold the Errors table
Sub Page_Load()
page load event handler is here
In the
Page_Load event, fill a table in the
DataSet with some rows from the
BookList table in your example database and display these rows in the first
DataGrid control. Then change three of the rows in the
DataSet in exactly the same way as you did in the previous examples, and display the rowset in the second
DataGrid control.
Next create a new connection to the source database, and through it execute three SQL
UPDATE statements that change three of the rows in the database. These statements are displayed in the preceding page the second
DataGrid control. At this point, you are ready to push the updates back to the database. But before you do so, add your
OnRowUpdated event handler to the
DataAdapter so that it will be executed each time a row is updated in the original data source:
AddHandler objDataAdapter.RowUpdated, _
New OleDbRowUpdatedEventHandler(AddressOf OnRowUpdated)
Creating and Displaying the ''Errors'' DataSet
Before you start the update process, you need to create the new
DataSet that will contain details of errors that occur during the process. Create a
DataTable object named
Errors , and define the columns for this table (you saw how this works in the previous chapter). Then you can create a new
DataSet object and add the table you''ve just defined to it (notice that all these objects are referenced by global variables that you declared outside the
Page_Load event handler so that you can access them from other event handlers).
Now you can carry on as in other examples by creating the auto-generated commands for the update and executing them by calling the
DataAdapter object''s
Update method (not shown in this code). Once the update is complete, display the contents of your errors
DataSet in the third
DataGrid control at the bottom of the page.
''create a new empty Table object to hold error rows
gobjErrorTable = New DataTable("Errors")
''define the columns for the Errors table
gobjErrorTable.Columns.Add("Action", System.Type.GetType("System.String"))
gobjErrorTable.Columns.Add("RowKey", System.Type.GetType("System.String"))
gobjErrorTable.Columns.Add("ColumnName", System.Type.GetType("System.String"))
gobjErrorTable.Columns.Add("OriginalValue", _
System.Type.GetType("System.String"))
gobjErrorTable.Columns.Add("CurrentValue", _
System.Type.GetType("System.String"))
gobjErrorTable.Columns.Add("DatabaseValue", _
System.Type.GetType("System.String"))
''create a new empty DataSet object to hold Errors table
gobjErrorDS = New DataSet()
gobjErrorDS.Tables.Add(gobjErrorTable)
... execute Update method here to push changes into database ...
''display the contents of the Errors table
dgrResult3.DataSource = gobjErrorDS
dgrResult3.DataMember = "Errors"
dgrResult3.DataBind() ''and bind (display) the data
In this case, you''ve bound the
DataGrid to the
DataSet object itself (using the
DataSource property) and then specified that it should display the contents of the
Errors table within that
DataSet by setting the
DataMember property.
Getting the Current Value from the Database Table
Of course, the code shown so far won''t actually put any rows into the errors
DataSet . These rows are created within the
RowUpdated event handler whenever a concurrency error is detected. We know that we want to include in each row the current value of the column in the original database table at the point that the update process was executed–it will be different from the
Original value of that column in the
DataSet if that column in the row was changed by a concurrent process.
So, we have written a short function within the page that – given a connection string, primary key (ISBN) value, and a column name–will return the value of that column for that row from the source database. The function is named
GetCurrentColumnValue :
Function GetCurrentColumnValue(strConnect As String, strISBN As String, _
strColumnName As String) As String
''select existing column value from underlying table in the database
Dim strSQL = "SELECT " & strColumnName _
& " FROM BookList WHERE ISBN=''" & strISBN & "''"
Dim objConnect As New OleDbConnection(strConnect)
Dim objCommand As New OleDbCommand(strSQL, objConnect)
Try
objConnect.Open()
''use ExecuteScalar for efficiency, it returns only one item
''get the value direct from it and convert to a String
GetCurrentColumnValue = objCommand.ExecuteScalar().ToString()
objConnect.Close()
Catch objError As Exception
GetCurrentColumnValue = "*Error*"
End Try
End Function
One interesting point is that we use the
ExecuteScalar method of the
Command object to get the value. The
ExecuteScalar method returns just a single value from a query (rather than a rowset, for which we''d have to use a
DataReader object).
So, it is extremely efficient when compared to a
DataReader , where we have to call the
Read method to load the first row of results, and then access the column by name or ordinal index.
Note | The ExecuteScalar method is especially appropriate for queries that calculate a value, such as summing values or working out the average value in a column for some or all of the rows. In our case, it''s useful because our SQL statement also only returns a single value (sometimes referred to as a singleton). |
So, this simple function will return the value of a specified column in a specified row, or the string value "
*Error* " if it can''t access it (for example if it has been deleted).
The OnRowUpdated Event Handler
Finally, the page contains the
OnRowUpdated event handler itself. Remember that this is called after each row has been updated in the source database, whether or not there was an error. So, the first thing we do is check the
RecordsAffected field of the
RowUpdatedEventArgs object to see if the update for this row failed. If it did, we need to add details of the error to our errors
DataSet .
We want to know what type of update this is, so we extract the
StatementType as a
String using the enumeration''s
GetName method, and store this in a local variable for use later. We also need to extract the value of the ISBN column from the row, as this is the primary key we''ll need for locating the row later. Being the primary key means that it should not be possible for the user to change the value in that row of the
DataSet , which would mean that we could just access the Original value.
However, if they were allowed to change the value, we would have to use the Current value of that column to extract the updated value. But if this row has been deleted in the
DataSet , there will be no Current value. In our example, to demonstrate the technique, we first check to see if it is a deleted row by examining the
RowState property, and then extract the value from the appropriate version of the column.
''event handler for the RowUpdated event
Sub OnRowUpdated(objSender As Object, objArgs As OleDbRowUpdatedEventArgs)
''see if the update failed
If objArgs.RecordsAffected < 1 Then
''get the text description of the StatementType
Dim strType = System.Enum.GetName(objArgs.StatementType.GetType(), _
objArgs.StatementType)
''get the primary key of the row (the ISBN). Must look at Original
''version in deleted rows because they have no Current version
Dim strRowKey As String
If objArgs.Row.RowState = DataRowState.Deleted Then
strRowKey = objArgs.Row("ISBN", DataRowVersion.Original)
Else
strRowKey = objArgs.Row("ISBN", DataRowVersion.Current)
End If
...
Handling the Columns for Deleted Rows
Now we can check which column(s) caused the concurrency error to occur, or simply collect all the column values if the concurrency error occurred for a row that was deleted in the
DataSet . We start by getting a reference to the table in our original
DataSet , the one we filled with rows from the database, and we declare a couple of other variables that we''ll need as well.
Then we iterate through the
Columns collection of this table, extracting the column name and again checking whether this is a deleted row. If it is, we want to add the values from this column to our error database. We create a new
DataRow based on the
Errors table, and then we can start filling in the values:
We saved the values for the first three columns of our
Errors table (the statement type, row key and column name) as strings earlier on in our event handler. The next value comes from the row referenced by the
RowUpdatedEventArgs object that is passed to our event handler. Because it is a deleted row, we can only access the
Original value, and we set the
CurrentValue column of our
Errors table to "
[NULL] ".
The final value comes from the custom
GetCurrentColumnValue function we described earlier, and contains the current value of this column in this row within the source database. And after we''ve filled in all the values for the new row we add it to the
Errors table.
...
''get a reference to the original table in the DataSet
Dim objTable As DataTable = gobjDataSet.Tables(0)
Dim objColumn As DataColumn ''to hold a DataColumn object
Dim strColumnName As String ''to hold the column name
''iterate through the columns in the current row
For Each objColumn In objTable.Columns
''get the column name as a string
strColumnName = objColumn.ColumnName
''if this is a deleted row, insert all the original columns values
If objArgs.Row.RowState = DataRowState.Deleted Then
''create a new DataRow object instance in this table
Dim objDataRow As DataRow = gobjErrorTable.NewRow()
''and fill in the values
objDataRow("Action") = strType
objDataRow("RowKey") = strRowKey
objDataRow("ColumnName") = strColumnName
objDataRow("OriginalValue") _
= objArgs.Row(strColumnName, DataRowVersion.Original)
objDataRow("CurrentValue") = "[NULL]"
objDataRow("DatabaseValue") _
= GetCurrentColumnValue(gstrConnect, strRowKey, strColumnName)
''add new row to the Errors table
gobjErrorTable.Rows.Add(objDataRow)
Else
...
Handling the Columns for Modified Rows
The preceding code handles the case of a deleted row, but for a modified row the
Else section of the
If..Then construct we started will be executed. This section of the event handler code is shown in the following code. The first step here is to compare the
Current and
Original values of the row. If they are different, we know that this column in the current row has been modified within the
DataSet since it was filled from the database table.
Notice that this is why we only get the modified columns in our errors table. If the concurrent process changes a different column to the one(s) that are modified in the
DataSet , a row will not appear in the
Errors table. We could simply remove this
If..Then construct, which will cause all the values from all the columns in a row that caused a concurrency error to be included in the table. However, in that case we would probably also want to change the way we extract the current values from the database, as using a separate function call for each column would certainly not be the most efficient technique.
Returning to the code in our example, since we know that this row caused a concurrency error and that this column has been changed since the
DataSet was filled, we add details about the values in this column to our table named
Errors within the new errors
DataSet we created earlier as we did for a deleted row, though this time using the
Current value of the column for the
CurrentValue column in the
Errors table. And, as before, after filling in the row values we add it to the
Errors table.
The
Next statement at the end means we can go back, look at the next column and repeat the process.
...
Else
''see if this column has been modified
If objArgs.Row(strColumnName, DataRowVersion.Current) _
<> objArgs.Row(strColumnName, DataRowVersion.Original) Then
''create a new DataRow object instance in this table
Dim objDataRow As DataRow = gobjErrorTable.NewRow()
''and fill in the values
objDataRow("Action") = strType
objDataRow("RowKey") = strRowKey
objDataRow("ColumnName") = strColumnName
objDataRow("OriginalValue") _
= objArgs.Row(strColumnName, DataRowVersion.Original)
objDataRow("CurrentValue") _
= objArgs.Row(strColumnName, DataRowVersion.Current)
objDataRow("DatabaseValue") _
= GetCurrentColumnValue(gstrConnect, strRowKey, strColumnName)
''add new row to the Errors table
gobjErrorTable.Rows.Add(objDataRow)
End If
End If
Next ''go to next column
So that you can see the result and better understand what the code does, we''ve repeated the relevant section of the screenshot of this example page again in Figure 10-20:

Figure 10-20:
Handling the Columns for Inserted Rows
Notice that we aren''t specifically handling errors for new (inserted) rows in our example. In general, a concurrency error can only occur in this situation if the page allocates an existing primary key to the new row, or perhaps because a parent row was deleted when we are trying to add a related row to a child table. However, it''s not hard to add the code to handle insert errors as well. The
StatementType property of the
RowUpdatedEventArgs that is passed to the event handler will be
StatementType.Insert in this case, and the rows will have
DataRowState.Added as their
RowState property.
The one point to be aware of is that there will be no
Original value in the row in the
DataSet , in the same way as there is no
Current value for a deleted row (hence our example code would fail when trying to access the
Current value). And, of course, in this case the
GetCurrentColumnValue function will return the value of the column inserted by another user, or an error value if the insert fails for some other reason.
Returning an UpdateStatus Value
The other important point when using the
RowUpdating and
RowUpdated event handlers that we haven''t mentioned so far is how we manage the status value that is exposed by the
Status field of the
RowUpdatingEventArgs and
RowUpdatedEventArgs objects. In our earlier example of using the
RowUpdating and
RowUpdated events (
rowupdated-event.aspx ) we just ignored these values, but that was really only acceptable because we didn''t get any concurrency errors during the update process.
When the
DataAdapter object''s
Update method is executing, each call to the
RowUpdating and
RowUpdated event handler includes a status flag value. We can set this to a specific value from the
UpdateStatus enumeration to tell the
Update method what to do next, as shown in the following table:
Continue | Default. The DataAdapter will continue to process rows (including the current one if this is a RowUpdating event) as part of the Update method call. |
ErrorsOccurred | The DataAdapter will stop processing rows and treat the RowUpdating or RowUpdated event as raising an error. |
SkipAllRemainingRows | The DataAdapter will stop processing rows and end the Update method, but it will not treat the RowUpdating or RowUpdated event as an error. |
SkipCurrentRow | The DataAdapter will not process this row (if this is a RowUpdating event), but will continue to process all remaining rows as part of the Update method call. |
Because the default is
Continue , the
Update process will actually stop executing and report a runtime error when the first concurrency error occurs if we just ignore this status flag. So, as we''re handling the concurrency errors ourselves, we must set the value to
UpdateStatus.SkipCurrentRow so that the concurrency error doesn''t cause the
Update process to be terminated. This is the last step in our event handler, as shown in the code:
...
''set the Status property of the row to skip current row update
''if there is an error. Default is Continue, which means an error
''will halt execution for this failed update and not process the
''remaining updated or deleted rows
objArgs.Status = UpdateStatus.SkipCurrentRow
End If
End Sub
In this example, you''ve seen how we can capture information on concurrency errors, allowing the user or another process to take a reasoned decision on how to reconcile the values. Because we''ve placed the information in a disconnected
DataSet object, it could easily be remoted to a client via HTTP or a Web Service, or passed directly to another tier of the application.
Locating Errors After an Update Is Complete
There is one final approach to managing concurrent updates that we can take advantage of in ADO.NET when using the
Update method of the
DataAdapter object. Instead of reacting to each
RowUpdated event, we can force the
DataAdapter to continue processing the updates for each row even if it encounters an error (rather than terminating the
Update process when the first concurrency or other error occurs).
All we need to do is set the
ContinueUpdateOnError property of the
DataAdapter object that is performing the
Update to
True . Then, whenever an error is encountered, the
DataAdapter will simply insert the error message that it receives into the
RowError property of the relevant row within the
DataSet , and continue with the next updated row.
The
RowError property is a
String value. You saw how to use this in the example from the previous chapter where we used the
RowUpdated event of the
DataTable object (rather than the event of the same name exposed by the
DataAdapter object that we''ve been using in this chapter).
So, if we can wait until after the
Update process has finished to review and fix errors, we have an easier option for managing concurrency errors. The process is:
Once the appropriate
DataAdapter is created and ready to perform the
Update , set the
ContinueUpdateOnError property of the
DataAdapter object to
True .
Call the
Update method of the
DataAdapter object to push the changes into the data source.
After the
Update process completes, check the
HasErrors property of the
DataSet object to see if any of the rows contain an error (e.g. have a non-empty value for their
RowError property).
If there are (one or more) errors, check the
HasErrors property of each
DataTable object in the
DataSet to see which ones contain errors
Iterate through the rows of each
DataTable that does contain errors, checking the
RowError property of each row–or use the
GetErrors method of the
DataTable to get an array of the rows with errors in them
Display or feed back to the user the error details and column values so that they can retry the updates as required.
Using the ContinueUpdateOnError Property
We''ve provided an example that carries out this series of steps while attempting to update a data source. The page Locating Concurrency Errors After Updating the Source Data
(concurrency-continue.aspx ) shown in Figure 10-21 displays the rows that it extracts from our sample database, edits two of the rows (the first and second ones), then displays the rowset again to show the changes.

Figure 10-21:
After that, the same process as we used in earlier examples changes the same two rows in the source database using a separate connection, while we are holding a disconnected copy of the data in our
DataSet . Then, as you can see at the bottom of the page, it displays the errors found in the
DataSet after the update process has completed. It shows the error message (the value of the
RowError property for that row), and the original, current, and underlying (database) values for the row.
The Code for the ContinueUpdateOnError Example
Most of the code we use in this example is identical to the previous example. The only real differences are in the preparation for the
Update process, and in the way that we extract and display the row values afterwards. We don''t set up any event handlers of course, because we''re not going to be reacting to the
RowUpdated event in this case. However, as shown in the code, at the point where we''re ready to call the
Update method of the
DataAdapter , we set the
DataAdapter object''s
ContinueUpdateOnError property to
True .
...
''prevent exceptions being thrown due to concurrency errors
objDataAdapter.ContinueUpdateOnError = True
''perform the update on the original data
objDataAdapter.Update(objDataSet, "Books")
...
Checking for Row Errors
After the
Update process has finished, we must check for row errors. The process is the same as we used in the previous chapter when we were looking at the
RowError property in general, and as we described in the introduction to the current example. The complete code for this part of the process is shown in the following code:
''see if there are any update errors anywhere in the DataSet
If objDataSet.HasErrors Then
Dim objThisRow As DataRow
Dim intIndex As Integer
''check each table for errors in that table
Dim objThisTable As DataTable
For Each objThisTable In objDataSet.Tables
If objThisTable.HasErrors Then
strResult &= "One or more errors found in table ''<b>" _
& objThisTable.TableName & "</b>:''<p />"
''get collection containing only rows with errors
''using the GetErrors method of the DataTable object
''check each row in this table for errors
For Each objThisRow In objThisTable.GetErrors()
''display the error details and column values
strResult &= "* Row with ISBN=<b>" _
& objThisRow("ISBN") _
& "</b> has error <b>" _
& objThisRow.RowError & "</b><br />" _
& "Original Values: "
''iterate through row collecting original and current values
For intIndex = 0 To objThisTable.Columns.Count - 1
strResult &= objThisRow(intIndex, DataRowVersion.Original) & ", "
Next
strResult = Left(strResult, Len(strResult) - 2)
strResult &= "<br />Current Values: "
For intIndex = 0 To objThisTable.Columns.Count - 1
strResult &= objThisRow(intIndex, DataRowVersion.Current) & ", "
Next
strResult = Left(strResult, Len(strResult) - 2)
''use function declared later in page to get underlying values
strResult &= "<br />Underlying (database) Values: " _
& GetUnderlyingValues(strConnect, objThisRow("ISBN")) _
& "<p />"
Next
End If
Next ''table
End If
''display the results of the Update in <div> elsewhere on page
outResult.InnerHtml = strResult
Extracting the Underlying Row Values
The only other new code in this page is the
GetUnderlyingValues function that extracts the underlying database values, so that they can be displayed along with the
Original and
Current values from the row in the
DataSet . As shown in the following code, It works in much the same way as the
GetCurrentColumnValue function we used in the previous example, but this time we want all the row values. So we use a
DataReader to read the row and then return a
String that we build up containing all the column values.
Function GetUnderlyingValues(strConnect As String, strRowKey As String) _
As String
''select existing column values from underlying table in database
Dim strSQL = "SELECT * FROM BookList WHERE ISBN=''" & strRowKey & "''"
''create connection and command to access database
Dim objConnect As New OleDbConnection(strConnect)
Dim objCommand As New OleDbCommand(strSQL, objConnect)
''declare the variables we''ll need
Dim objReader As OleDbDataReader
Dim strValues As String = "
Dim intIndex As Integer
Try
''get a DataReader containing the specified row data
objConnect.Open()
objReader = objCommand.ExecuteReader()
''put values from row into a string to return
If objReader.Read() Then
For intIndex = 0 To objReader.FieldCount - 1
strValues &= objReader.GetValue(intIndex) & ", "
Next
End If
''close connection and return result
objConnect.Close()
GetUnderlyingValues = Left(strValues, Len(strValues) - 2)
Catch objError As Exception
GetUnderlyingValues = "*Error*"
End Try
End Function