Professional ASP.NET 1.1 [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Professional ASP.NET 1.1 [Electronic resources] - نسخه متنی

Alex Homeret

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید








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



/ 244