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.

click to expand
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.

click to expand
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 .

click to expand
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 ).

click to expand
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 .

click to expand
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:

click to expand
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.

click to expand
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  



Professional ASP. NET 1.1
Professional ASP.NET MVC 1.0 (Wrox Programmer to Programmer)
ISBN: 0470384611
EAN: 2147483647
Year: 2006
Pages: 243

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net