Implementing Optimistic Concurrency (VB)
Download Sample App or Download PDF
For a web application that allows multiple users to edit data, there is the risk that two users may be editing the same data at the same time. In this tutorial we’ll implement optimistic concurrency control to handle this risk.
Introduction
For web applications that only allow users to view data, or for those that include only a single user who can modify data, there’s no threat of two concurrent users accidentally overwriting one another’s changes. For web applications that allow multiple users to update or delete data, however, there’s the potential for one user’s modifications to clash with another concurrent user’s. Without any concurrency policy in place, when two users are simultaneously editing a single record, the user who commits her changes last will override the changes made by the first.
For example, imagine that two users, Jisun and Sam, were both visiting a page in our application that allowed visitors to update and delete the products through a GridView control. Both click the Edit button in the GridView around the same time. Jisun changes the product name to “Chai Tea” and clicks the Update button. The net result is an UPDATE
statement that is sent to the database, which sets all of the product’s updateable fields (even though Jisun only updated one field, ProductName
). At this point in time, the database has the values “Chai Tea,” the category Beverages, the supplier Exotic Liquids, and so on for this particular product. However, the GridView on Sam’s screen still shows the product name in the editable GridView row as “Chai”. A few seconds after Jisun’s changes have been committed, Sam updates the category to Condiments and clicks Update. This results in an UPDATE
statement sent to the database that sets the product name to “Chai,” the CategoryID
to the corresponding Beverages category ID, and so on. Jisun’s changes to the product name have been overwritten. Figure 1 graphically depicts this series of events.
Figure 1: When Two Users Simultaneously Update a Record There s Potential for One User ’s Changes to Overwrite the Other ’s (Click to view full-size image)
Similarly, when two users are visiting a page, one user might be in the midst of updating a record when it is deleted by another user. Or, between when a user loads a page and when they click the Delete button, another user may have modified the contents of that record.
There are three concurrency control strategies available:
- Do Nothing -if concurrent users are modifying the same record, let the last commit win (the default behavior)
- Optimistic Concurrency - assume that while there may be concurrency conflicts every now and then, the vast majority of the time such conflicts won’t arise; therefore, if a conflict does arise, simply inform the user that their changes can’t be saved because another user has modified the same data
- Pessimistic Concurrency - assume that concurrency conflicts are commonplace and that users won’t tolerate being told their changes weren’t saved due to another user’s concurrent activity; therefore, when one user starts updating a record, lock it, thereby preventing any other users from editing or deleting that record until the user commits their modifications
All of our tutorials thus far have used the default concurrency resolution strategy - namely, we’ve let the last write win. In this tutorial we’ll examine how to implement optimistic concurrency control.
[!NOTE] We won’t look at pessimistic concurrency examples in this tutorial series. Pessimistic concurrency is rarely used because such locks, if not properly relinquished, can prevent other users from updating data. For example, if a user locks a record for editing and then leaves for the day before unlocking it, no other user will be able to update that record until the original user returns and completes his update. Therefore, in situations where pessimistic concurrency is used, there’s typically a timeout that, if reached, cancels the lock. Ticket sales websites, which lock a particular seating location for short period while the user completes the order process, is an example of pessimistic concurrency control.
Step 1: Looking at How Optimistic Concurrency is Implemented
Optimistic concurrency control works by ensuring that the record being updated or deleted has the same values as it did when the updating or deleting process started. For example, when clicking the Edit button in an editable GridView, the record’s values are read from the database and displayed in TextBoxes and other Web controls. These original values are saved by the GridView. Later, after the user makes her changes and clicks the Update button, the original values plus the new values are sent to the Business Logic Layer, and then down to the Data Access Layer. The Data Access Layer must issue a SQL statement that will only update the record if the original values that the user started editing are identical to the values still in the database. Figure 2 depicts this sequence of events.
Figure 2: For the Update or Delete to Succeed, the Original Values Must Be Equal to the Current Database Values (Click to view full-size image)
There are various approaches to implementing optimistic concurrency (see Peter A. Bromberg’s Optmistic Concurrency Updating Logic for a brief look at a number of options). The ADO.NET Typed DataSet provides one implementation that can be configured with just the tick of a checkbox. Enabling optimistic concurrency for a TableAdapter in the Typed DataSet augments the TableAdapter’s UPDATE
and DELETE
statements to include a comparison of all of the original values in the WHERE
clause. The following UPDATE
statement, for example, updates the name and price of a product only if the current database values are equal to the values that were originally retrieved when updating the record in the GridView. The @ProductName
and @UnitPrice
parameters contain the new values entered by the user, whereas @original_ProductName
and @original_UnitPrice
contain the values that were originally loaded into the GridView when the Edit button was clicked:
[!code-sqlMain]
1: UPDATE Products SET
2: ProductName = @ProductName,
3: UnitPrice = @UnitPrice
4: WHERE
5: ProductID = @original_ProductID AND
6: ProductName = @original_ProductName AND
7: UnitPrice = @original_UnitPrice
[!NOTE] This
UPDATE
statement has been simplified for readability. In practice, theUnitPrice
check in theWHERE
clause would be more involved sinceUnitPrice
can containNULL
s and checking ifNULL = NULL
always returns False (instead you must useIS NULL
).
In addition to using a different underlying UPDATE
statement, configuring a TableAdapter to use optimistic concurrency also modifies the signature of its DB direct methods. Recall from our first tutorial, Creating a Data Access Layer, that DB direct methods were those that accepts a list of scalar values as input parameters (rather than a strongly-typed DataRow or DataTable instance). When using optimistic concurrency, the DB direct Update()
and Delete()
methods include input parameters for the original values as well. Moreover, the code in the BLL for using the batch update pattern (the Update()
method overloads that accept DataRows and DataTables rather than scalar values) must be changed as well.
Rather than extend our existing DAL’s TableAdapters to use optimistic concurrency (which would necessitate changing the BLL to accommodate), let’s instead create a new Typed DataSet named NorthwindOptimisticConcurrency
, to which we’ll add a Products
TableAdapter that uses optimistic concurrency. Following that, we’ll create a ProductsOptimisticConcurrencyBLL
Business Logic Layer class that has the appropriate modifications to support the optimistic concurrency DAL. Once this groundwork has been laid, we’ll be ready to create the ASP.NET page.
Step 2: Creating a Data Access Layer That Supports Optimistic Concurrency
To create a new Typed DataSet, right-click on the DAL
folder within the App_Code
folder and add a new DataSet named NorthwindOptimisticConcurrency
. As we saw in the first tutorial, doing so will add a new TableAdapter to the Typed DataSet, automatically launching the TableAdapter Configuration Wizard. In the first screen, we’re prompted to specify the database to connect to - connect to the same Northwind database using the NORTHWNDConnectionString
setting from Web.config
.
Figure 3: Connect to the Same Northwind Database (Click to view full-size image)
Next, we are prompted as to how to query the data: through an ad-hoc SQL statement, a new stored procedure, or an existing stored procedure. Since we used ad-hoc SQL queries in our original DAL, use this option here as well.
Figure 4: Specify the Data to Retrieve Using an Ad-Hoc SQL Statement (Click to view full-size image)
On the following screen, enter the SQL query to use to retrieve the product information. Let’s use the exact same SQL query used for the Products
TableAdapter from our original DAL, which returns all of the Product
columns along with the product’s supplier and category names:
[!code-sqlMain]
1: SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
2: UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
3: (SELECT CategoryName FROM Categories
4: WHERE Categories.CategoryID = Products.CategoryID)
5: as CategoryName,
6: (SELECT CompanyName FROM Suppliers
7: WHERE Suppliers.SupplierID = Products.SupplierID)
8: as SupplierName
9: FROM Products
Figure 5: Use the Same SQL Query from the Products
TableAdapter in the Original DAL (Click to view full-size image)
Before moving onto the next screen, click the Advanced Options button. To have this TableAdapter employ optimistic concurrency control, simply check the “Use optimistic concurrency” checkbox.
Figure 6: Enable Optimistic Concurrency Control by Checking the “Use optimistic concurrency” CheckBox (Click to view full-size image)
Lastly, indicate that the TableAdapter should use the data access patterns that both fill a DataTable and return a DataTable; also indicate that the DB direct methods should be created. Change the method name for the Return a DataTable pattern from GetData to GetProducts, so as to mirror the naming conventions we used in our original DAL.
Figure 7: Have the TableAdapter Utilize All Data Access Patterns (Click to view full-size image)
After completing the wizard, the DataSet Designer will include a strongly-typed Products
DataTable and TableAdapter. Take a moment to rename the DataTable from Products
to ProductsOptimisticConcurrency
, which you can do by right-clicking on the DataTable’s title bar and choosing Rename from the context menu.
Figure 8: A DataTable and TableAdapter Have Been Added to the Typed DataSet (Click to view full-size image)
To see the differences between the UPDATE
and DELETE
queries between the ProductsOptimisticConcurrency
TableAdapter (which uses optimistic concurrency) and the Products TableAdapter (which doesn’t), click on the TableAdapter and go to the Properties window. In the DeleteCommand
and UpdateCommand
properties’ CommandText
subproperties you can see the actual SQL syntax that is sent to the database when the DAL’s update or delete-related methods are invoked. For the ProductsOptimisticConcurrency
TableAdapter the DELETE
statement used is:
[!code-sqlMain]
1: DELETE FROM [Products]
2: WHERE (([ProductID] = @Original_ProductID)
3: AND ([ProductName] = @Original_ProductName)
4: AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
5: OR ([SupplierID] = @Original_SupplierID))
6: AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
7: OR ([CategoryID] = @Original_CategoryID))
8: AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
9: OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
10: AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
11: OR ([UnitPrice] = @Original_UnitPrice))
12: AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
13: OR ([UnitsInStock] = @Original_UnitsInStock))
14: AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
15: OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
16: AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
17: OR ([ReorderLevel] = @Original_ReorderLevel))
18: AND ([Discontinued] = @Original_Discontinued))
Whereas the DELETE
statement for the Product TableAdapter in our original DAL is the much simpler:
[!code-sqlMain]
1: DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
As you can see, the WHERE
clause in the DELETE
statement for the TableAdapter that uses optimistic concurrency includes a comparison between each of the Product
table’s existing column values and the original values at the time the GridView (or DetailsView or FormView) was last populated. Since all fields other than ProductID
, ProductName
, and Discontinued
can have NULL
values, additional parameters and checks are included to correctly compare NULL
values in the WHERE
clause.
We won’t be adding any additional DataTables to the optimistic concurrency-enabled DataSet for this tutorial, as our ASP.NET page will only provide updating and deleting product information. However, we do still need to add the GetProductByProductID(productID)
method to the ProductsOptimisticConcurrency
TableAdapter.
To accomplish this, right-click on the TableAdapter’s title bar (the area right above the Fill
and GetProducts
method names) and choose Add Query from the context menu. This will launch the TableAdapter Query Configuration Wizard. As with our TableAdapter’s initial configuration, opt to create the GetProductByProductID(productID)
method using an ad-hoc SQL statement (see Figure 4). Since the GetProductByProductID(productID)
method returns information about a particular product, indicate that this query is a SELECT
query type that returns rows.
Figure 9: Mark the Query Type as a “SELECT
which returns rows” (Click to view full-size image)
On the next screen we’re prompted for the SQL query to use, with the TableAdapter’s default query pre-loaded. Augment the existing query to include the clause WHERE ProductID = @ProductID
, as shown in Figure 10.
Figure 10: Add a WHERE
Clause to the Pre-Loaded Query to Return a Specific Product Record (Click to view full-size image)
Finally, change the generated method names to FillByProductID
and GetProductByProductID
.
Figure 11: Rename the Methods to FillByProductID
and GetProductByProductID
(Click to view full-size image)
With this wizard complete, the TableAdapter now contains two methods for retrieving data: GetProducts()
, which returns all products; and GetProductByProductID(productID)
, which returns the specified product.
Step 3: Creating a Business Logic Layer for the Optimistic Concurrency-Enabled DAL
Our existing ProductsBLL
class has examples of using both the batch update and DB direct patterns. The AddProduct
method and UpdateProduct
overloads both use the batch update pattern, passing in a ProductRow
instance to the TableAdapter’s Update method. The DeleteProduct
method, on the other hand, uses the DB direct pattern, calling the TableAdapter’s Delete(productID)
method.
With the new ProductsOptimisticConcurrency
TableAdapter, the DB direct methods now require that the original values also be passed in. For example, the Delete
method now expects ten input parameters: the original ProductID
, ProductName
, SupplierID
, CategoryID
, QuantityPerUnit
, UnitPrice
, UnitsInStock
, UnitsOnOrder
, ReorderLevel
, and Discontinued
. It uses these additional input parameters’ values in WHERE
clause of the DELETE
statement sent to the database, only deleting the specified record if the database’s current values map up to the original ones.
While the method signature for the TableAdapter’s Update
method used in the batch update pattern hasn’t changed, the code needed to record the original and new values has. Therefore, rather than attempt to use the optimistic concurrency-enabled DAL with our existing ProductsBLL
class, let’s create a new Business Logic Layer class for working with our new DAL.
Add a class named ProductsOptimisticConcurrencyBLL
to the BLL
folder within the App_Code
folder.
Figure 12: Add the ProductsOptimisticConcurrencyBLL
Class to the BLL Folder
Next, add the following code to the ProductsOptimisticConcurrencyBLL
class:
[!code-vbMain]
1: Imports NorthwindOptimisticConcurrencyTableAdapters
2: <System.ComponentModel.DataObject()> _
3: Public Class ProductsOptimisticConcurrencyBLL
4: Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
5: Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
6: Get
7: If _productsAdapter Is Nothing Then
8: _productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
9: End If
10: Return _productsAdapter
11: End Get
12: End Property
13: <System.ComponentModel.DataObjectMethodAttribute _
14: (System.ComponentModel.DataObjectMethodType.Select, True)> _
15: Public Function GetProducts() As _
16: NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
17: Return Adapter.GetProducts()
18: End Function
19: End Class
Note the using NorthwindOptimisticConcurrencyTableAdapters
statement above the start of the class declaration. The NorthwindOptimisticConcurrencyTableAdapters
namespace contains the ProductsOptimisticConcurrencyTableAdapter
class, which provides the DAL’s methods. Also before the class declaration you’ll find the System.ComponentModel.DataObject
attribute, which instructs Visual Studio to include this class in the ObjectDataSource wizard’s drop-down list.
The ProductsOptimisticConcurrencyBLL
’s Adapter
property provides quick access to an instance of the ProductsOptimisticConcurrencyTableAdapter
class, and follows the pattern used in our original BLL classes (ProductsBLL
, CategoriesBLL
, and so on). Finally, the GetProducts()
method simply calls down into the DAL’s GetProducts()
method and returns a ProductsOptimisticConcurrencyDataTable
object populated with a ProductsOptimisticConcurrencyRow
instance for each product record in the database.
Deleting a Product Using the DB Direct Pattern with Optimistic Concurrency
When using the DB direct pattern against a DAL that uses optimistic concurrency, the methods must be passed the new and original values. For deleting, there are no new values, so only the original values need be passed in. In our BLL, then, we must accept all of the original parameters as input parameters. Let’s have the DeleteProduct
method in the ProductsOptimisticConcurrencyBLL
class use the DB direct method. This means that this method needs to take in all ten product data fields as input parameters, and pass these to the DAL, as shown in the following code:
[!code-vbMain]
1: <System.ComponentModel.DataObjectMethodAttribute _
2: (System.ComponentModel.DataObjectMethodType.Delete, True)> _
3: Public Function DeleteProduct( _
4: ByVal original_productID As Integer, ByVal original_productName As String, _
5: ByVal original_supplierID As Nullable(Of Integer), _
6: ByVal original_categoryID As Nullable(Of Integer), _
7: ByVal original_quantityPerUnit As String, _
8: ByVal original_unitPrice As Nullable(Of Decimal), _
9: ByVal original_unitsInStock As Nullable(Of Short), _
10: ByVal original_unitsOnOrder As Nullable(Of Short), _
11: ByVal original_reorderLevel As Nullable(Of Short), _
12: ByVal original_discontinued As Boolean) _
13: As Boolean
14: Dim rowsAffected As Integer = Adapter.Delete(
15: original_productID, _
16: original_productName, _
17: original_supplierID, _
18: original_categoryID, _
19: original_quantityPerUnit, _
20: original_unitPrice, _
21: original_unitsInStock, _
22: original_unitsOnOrder, _
23: original_reorderLevel, _
24: original_discontinued)
25: ' Return true if precisely one row was deleted, otherwise false
26: Return rowsAffected = 1
27: End Function
If the original values - those values that were last loaded into the GridView (or DetailsView or FormView) - differ from the values in the database when the user clicks the Delete button the WHERE
clause won’t match up with any database record and no records will be affected. Hence, the TableAdapter’s Delete
method will return 0
and the BLL’s DeleteProduct
method will return false
.
Updating a Product Using the Batch Update Pattern with Optimistic Concurrency
As noted earlier, the TableAdapter’s Update
method for the batch update pattern has the same method signature regardless of whether or not optimistic concurrency is employed. Namely, the Update
method expects a DataRow, an array of DataRows, a DataTable, or a Typed DataSet. There are no additional input parameters for specifying the original values. This is possible because the DataTable keeps track of the original and modified values for its DataRow(s). When the DAL issues its UPDATE
statement, the @original_ColumnName
parameters are populated with the DataRow’s original values, whereas the @ColumnName
parameters are populated with the DataRow’s modified values.
In the ProductsBLL
class (which uses our original, non-optimistic concurrency DAL), when using the batch update pattern to update product information our code performs the following sequence of events:
- Read the current database product information into a
ProductRow
instance using the TableAdapter’sGetProductByProductID(productID)
method - Assign the new values to the
ProductRow
instance from Step 1 - Call the TableAdapter’s
Update
method, passing in theProductRow
instance
This sequence of steps, however, won’t correctly support optimistic concurrency because the ProductRow
populated in Step 1 is populated directly from the database, meaning that the original values used by the DataRow are those that currently exist in the database, and not those that were bound to the GridView at the start of the editing process. Instead, when using an optimistic concurrency-enabled DAL, we need to alter the UpdateProduct
method overloads to use the following steps:
- Read the current database product information into a
ProductsOptimisticConcurrencyRow
instance using the TableAdapter’sGetProductByProductID(productID)
method - Assign the original values to the
ProductsOptimisticConcurrencyRow
instance from Step 1 - Call the
ProductsOptimisticConcurrencyRow
instance’sAcceptChanges()
method, which instructs the DataRow that its current values are the “original” ones - Assign the new values to the
ProductsOptimisticConcurrencyRow
instance - Call the TableAdapter’s
Update
method, passing in theProductsOptimisticConcurrencyRow
instance
Step 1 reads in all of the current database values for the specified product record. This step is superfluous in the UpdateProduct
overload that updates all of the product columns (as these values are overwritten in Step 2), but is essential for those overloads where only a subset of the column values are passed in as input parameters. Once the original values have been assigned to the ProductsOptimisticConcurrencyRow
instance, the AcceptChanges()
method is called, which marks the current DataRow values as the original values to be used in the @original_ColumnName
parameters in the UPDATE
statement. Next, the new parameter values are assigned to the ProductsOptimisticConcurrencyRow
and, finally, the Update
method is invoked, passing in the DataRow.
The following code shows the UpdateProduct
overload that accepts all product data fields as input parameters. While not shown here, the ProductsOptimisticConcurrencyBLL
class included in the download for this tutorial also contains an UpdateProduct
overload that accepts just the product’s name and price as input parameters.
[!code-vbMain]
1: Protected Sub AssignAllProductValues( _
2: ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
3: ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
4: ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
5: ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
6: ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
7: ByVal discontinued As Boolean)
8: product.ProductName = productName
9: If Not supplierID.HasValue Then
10: product.SetSupplierIDNull()
11: Else
12: product.SupplierID = supplierID.Value
13: End If
14: If Not categoryID.HasValue Then
15: product.SetCategoryIDNull()
16: Else
17: product.CategoryID = categoryID.Value
18: End If
19: If quantityPerUnit Is Nothing Then
20: product.SetQuantityPerUnitNull()
21: Else
22: product.QuantityPerUnit = quantityPerUnit
23: End If
24: If Not unitPrice.HasValue Then
25: product.SetUnitPriceNull()
26: Else
27: product.UnitPrice = unitPrice.Value
28: End If
29: If Not unitsInStock.HasValue Then
30: product.SetUnitsInStockNull()
31: Else
32: product.UnitsInStock = unitsInStock.Value
33: End If
34: If Not unitsOnOrder.HasValue Then
35: product.SetUnitsOnOrderNull()
36: Else
37: product.UnitsOnOrder = unitsOnOrder.Value
38: End If
39: If Not reorderLevel.HasValue Then
40: product.SetReorderLevelNull()
41: Else
42: product.ReorderLevel = reorderLevel.Value
43: End If
44: product.Discontinued = discontinued
45: End Sub
46: <System.ComponentModel.DataObjectMethodAttribute( _
47: System.ComponentModel.DataObjectMethodType.Update, True)> _
48: Public Function UpdateProduct(
49: ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
50: ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
51: ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
52: ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
53: ByVal discontinued As Boolean, ByVal productID As Integer, _
54: _
55: ByVal original_productName As String, _
56: ByVal original_supplierID As Nullable(Of Integer), _
57: ByVal original_categoryID As Nullable(Of Integer), _
58: ByVal original_quantityPerUnit As String, _
59: ByVal original_unitPrice As Nullable(Of Decimal), _
60: ByVal original_unitsInStock As Nullable(Of Short), _
61: ByVal original_unitsOnOrder As Nullable(Of Short), _
62: ByVal original_reorderLevel As Nullable(Of Short), _
63: ByVal original_discontinued As Boolean, _
64: ByVal original_productID As Integer) _
65: As Boolean
66: 'STEP 1: Read in the current database product information
67: Dim products As _
68: NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
69: Adapter.GetProductByProductID(original_productID)
70: If products.Count = 0 Then
71: ' no matching record found, return false
72: Return False
73: End If
74: Dim product As _
75: NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = products(0)
76: 'STEP 2: Assign the original values to the product instance
77: AssignAllProductValues( _
78: product, original_productName, original_supplierID, _
79: original_categoryID, original_quantityPerUnit, original_unitPrice, _
80: original_unitsInStock, original_unitsOnOrder, original_reorderLevel, _
81: original_discontinued)
82: 'STEP 3: Accept the changes
83: product.AcceptChanges()
84: 'STEP 4: Assign the new values to the product instance
85: AssignAllProductValues( _
86: product, productName, supplierID, categoryID, quantityPerUnit, unitPrice, _
87: unitsInStock, unitsOnOrder, reorderLevel, discontinued)
88: 'STEP 5: Update the product record
89: Dim rowsAffected As Integer = Adapter.Update(product)
90: ' Return true if precisely one row was updated, otherwise false
91: Return rowsAffected = 1
92: End Function
Step 4: Passing the Original and New Values From the ASP.NET Page to the BLL Methods
With the DAL and BLL complete, all that remains is to create an ASP.NET page that can utilize the optimistic concurrency logic built in to the system. Specifically, the data Web control (the GridView, DetailsView, or FormView) must remember its original values and the ObjectDataSource must pass both sets of values to the Business Logic Layer. Furthermore, the ASP.NET page must be configured to gracefully handle concurrency violations.
Start by opening the OptimisticConcurrency.aspx
page in the EditInsertDelete
folder and adding a GridView to the Designer, setting its ID
property to ProductsGrid
. From the GridView’s smart tag, opt to create a new ObjectDataSource named ProductsOptimisticConcurrencyDataSource
. Since we want this ObjectDataSource to use the DAL that supports optimistic concurrency, configure it to use the ProductsOptimisticConcurrencyBLL
object.
Figure 13: Have the ObjectDataSource Use the ProductsOptimisticConcurrencyBLL
Object (Click to view full-size image)
Choose the GetProducts
, UpdateProduct
, and DeleteProduct
methods from drop-down lists in the wizard. For the UpdateProduct method, use the overload that accepts all of the product’s data fields.
Configuring the ObjectDataSource Control’s Properties
After completing the wizard, the ObjectDataSource’s declarative markup should look like the following:
[!code-aspxMain]
1: <asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
2: DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
3: SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
4: UpdateMethod="UpdateProduct">
5: <DeleteParameters>
6: <asp:Parameter Name="original_productID" Type="Int32" />
7: <asp:Parameter Name="original_productName" Type="String" />
8: <asp:Parameter Name="original_supplierID" Type="Int32" />
9: <asp:Parameter Name="original_categoryID" Type="Int32" />
10: <asp:Parameter Name="original_quantityPerUnit" Type="String" />
11: <asp:Parameter Name="original_unitPrice" Type="Decimal" />
12: <asp:Parameter Name="original_unitsInStock" Type="Int16" />
13: <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
14: <asp:Parameter Name="original_reorderLevel" Type="Int16" />
15: <asp:Parameter Name="original_discontinued" Type="Boolean" />
16: </DeleteParameters>
17: <UpdateParameters>
18: <asp:Parameter Name="productName" Type="String" />
19: <asp:Parameter Name="supplierID" Type="Int32" />
20: <asp:Parameter Name="categoryID" Type="Int32" />
21: <asp:Parameter Name="quantityPerUnit" Type="String" />
22: <asp:Parameter Name="unitPrice" Type="Decimal" />
23: <asp:Parameter Name="unitsInStock" Type="Int16" />
24: <asp:Parameter Name="unitsOnOrder" Type="Int16" />
25: <asp:Parameter Name="reorderLevel" Type="Int16" />
26: <asp:Parameter Name="discontinued" Type="Boolean" />
27: <asp:Parameter Name="productID" Type="Int32" />
28: <asp:Parameter Name="original_productName" Type="String" />
29: <asp:Parameter Name="original_supplierID" Type="Int32" />
30: <asp:Parameter Name="original_categoryID" Type="Int32" />
31: <asp:Parameter Name="original_quantityPerUnit" Type="String" />
32: <asp:Parameter Name="original_unitPrice" Type="Decimal" />
33: <asp:Parameter Name="original_unitsInStock" Type="Int16" />
34: <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
35: <asp:Parameter Name="original_reorderLevel" Type="Int16" />
36: <asp:Parameter Name="original_discontinued" Type="Boolean" />
37: <asp:Parameter Name="original_productID" Type="Int32" />
38: </UpdateParameters>
39: </asp:ObjectDataSource>
As you can see, the DeleteParameters
collection contains a Parameter
instance for each of the ten input parameters in the ProductsOptimisticConcurrencyBLL
class’s DeleteProduct
method. Likewise, the UpdateParameters
collection contains a Parameter
instance for each of the input parameters in UpdateProduct
.
For those previous tutorials that involved data modification, we’d remove the ObjectDataSource’s OldValuesParameterFormatString
property at this point, since this property indicates that the BLL method expects the old (or original) values to be passed in as well as the new values. Furthermore, this property value indicates the input parameter names for the original values. Since we are passing in the original values into the BLL, do not remove this property.
[!NOTE] The value of the
OldValuesParameterFormatString
property must map to the input parameter names in the BLL that expect the original values. Since we named these parametersoriginal_productName
,original_supplierID
, and so on, you can leave theOldValuesParameterFormatString
property value asoriginal_{0}
. If, however, the BLL methods’ input parameters had names likeold_productName
,old_supplierID
, and so on, you’d need to update theOldValuesParameterFormatString
property toold_{0}
.
There’s one final property setting that needs to be made in order for the ObjectDataSource to correctly pass the original values to the BLL methods. The ObjectDataSource has a ConflictDetection property that can be assigned to one of two values:
OverwriteChanges
- the default value; does not send the original values to the BLL methods’ original input parametersCompareAllValues
- does send the original values to the BLL methods; choose this option when using optimistic concurrency
Take a moment to set the ConflictDetection
property to CompareAllValues
.
Configuring the GridView’s Properties and Fields
With the ObjectDataSource’s properties properly configured, let’s turn our attention to setting up the GridView. First, since we want the GridView to support editing and deleting, click the Enable Editing and Enable Deleting checkboxes from the GridView’s smart tag. This will add a CommandField whose ShowEditButton
and ShowDeleteButton
are both set to true
.
When bound to the ProductsOptimisticConcurrencyDataSource
ObjectDataSource, the GridView contains a field for each of the product’s data fields. While such a GridView can be edited, the user experience is anything but acceptable. The CategoryID
and SupplierID
BoundFields will render as TextBoxes, requiring the user to enter the appropriate category and supplier as ID numbers. There will be no formatting for the numeric fields and no validation controls to ensure that the product’s name has been supplied and that the unit price, units in stock, units on order, and reorder level values are both proper numeric values and are greater than or equal to zero.
As we discussed in the Adding Validation Controls to the Editing and Inserting Interfaces and Customizing the Data Modification Interface tutorials, the user interface can be customized by replacing the BoundFields with TemplateFields. I’ve modified this GridView and its editing interface in the following ways:
- Removed the
ProductID
,SupplierName
, andCategoryName
BoundFields - Converted the
ProductName
BoundField to a TemplateField and added a RequiredFieldValidation control. - Converted the
CategoryID
andSupplierID
BoundFields to TemplateFields, and adjusted the editing interface to use DropDownLists rather than TextBoxes. In these TemplateFields’ItemTemplates
, theCategoryName
andSupplierName
data fields are displayed. - Converted the
UnitPrice
,UnitsInStock
,UnitsOnOrder
, andReorderLevel
BoundFields to TemplateFields and added CompareValidator controls.
Since we’ve already examined how to accomplish these tasks in previous tutorials, I’ll just list the final declarative syntax here and leave the implementation as practice.
[!code-aspxMain]
1: <asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
2: DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
3: OnRowUpdated="ProductsGrid_RowUpdated">
4: <Columns>
5: <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
6: <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
7: <EditItemTemplate>
8: <asp:TextBox ID="EditProductName" runat="server"
9: Text='<%# Bind("ProductName") %>'></asp:TextBox>
10: <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
11: ControlToValidate="EditProductName"
12: ErrorMessage="You must enter a product name."
13: runat="server">*</asp:RequiredFieldValidator>
14: </EditItemTemplate>
15: <ItemTemplate>
16: <asp:Label ID="Label1" runat="server"
17: Text='<%# Bind("ProductName") %>'></asp:Label>
18: </ItemTemplate>
19: </asp:TemplateField>
20: <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
21: <EditItemTemplate>
22: <asp:DropDownList ID="EditCategoryID" runat="server"
23: DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
24: DataTextField="CategoryName" DataValueField="CategoryID"
25: SelectedValue='<%# Bind("CategoryID") %>'>
26: <asp:ListItem Value=">(None)</asp:ListItem>
27: </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
28: runat="server" OldValuesParameterFormatString="original_{0}"
29: SelectMethod="GetCategories" TypeName="CategoriesBLL">
30: </asp:ObjectDataSource>
31: </EditItemTemplate>
32: <ItemTemplate>
33: <asp:Label ID="Label2" runat="server"
34: Text='<%# Bind("CategoryName") %>'></asp:Label>
35: </ItemTemplate>
36: </asp:TemplateField>
37: <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
38: <EditItemTemplate>
39: <asp:DropDownList ID="EditSuppliersID" runat="server"
40: DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
41: DataTextField="CompanyName" DataValueField="SupplierID"
42: SelectedValue='<%# Bind("SupplierID") %>'>
43: <asp:ListItem Value=">(None)</asp:ListItem>
44: </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
45: runat="server" OldValuesParameterFormatString="original_{0}"
46: SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
47: </asp:ObjectDataSource>
48: </EditItemTemplate>
49: <ItemTemplate>
50: <asp:Label ID="Label3" runat="server"
51: Text='<%# Bind("SupplierName") %>'></asp:Label>
52: </ItemTemplate>
53: </asp:TemplateField>
54: <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
55: SortExpression="QuantityPerUnit" />
56: <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
57: <EditItemTemplate>
58: <asp:TextBox ID="EditUnitPrice" runat="server"
59: Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
60: <asp:CompareValidator ID="CompareValidator1" runat="server"
61: ControlToValidate="EditUnitPrice"
62: ErrorMessage="Unit price must be a valid currency value without the
63: currency symbol and must have a value greater than or equal to zero."
64: Operator="GreaterThanEqual" Type="Currency"
65: ValueToCompare="0">*</asp:CompareValidator>
66: </EditItemTemplate>
67: <ItemTemplate>
68: <asp:Label ID="Label4" runat="server"
69: Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
70: </ItemTemplate>
71: </asp:TemplateField>
72: <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
73: <EditItemTemplate>
74: <asp:TextBox ID="EditUnitsInStock" runat="server"
75: Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
76: <asp:CompareValidator ID="CompareValidator2" runat="server"
77: ControlToValidate="EditUnitsInStock"
78: ErrorMessage="Units in stock must be a valid number
79: greater than or equal to zero."
80: Operator="GreaterThanEqual" Type="Integer"
81: ValueToCompare="0">*</asp:CompareValidator>
82: </EditItemTemplate>
83: <ItemTemplate>
84: <asp:Label ID="Label5" runat="server"
85: Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
86: </ItemTemplate>
87: </asp:TemplateField>
88: <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
89: <EditItemTemplate>
90: <asp:TextBox ID="EditUnitsOnOrder" runat="server"
91: Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
92: <asp:CompareValidator ID="CompareValidator3" runat="server"
93: ControlToValidate="EditUnitsOnOrder"
94: ErrorMessage="Units on order must be a valid numeric value
95: greater than or equal to zero."
96: Operator="GreaterThanEqual" Type="Integer"
97: ValueToCompare="0">*</asp:CompareValidator>
98: </EditItemTemplate>
99: <ItemTemplate>
100: <asp:Label ID="Label6" runat="server"
101: Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
102: </ItemTemplate>
103: </asp:TemplateField>
104: <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
105: <EditItemTemplate>
106: <asp:TextBox ID="EditReorderLevel" runat="server"
107: Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
108: <asp:CompareValidator ID="CompareValidator4" runat="server"
109: ControlToValidate="EditReorderLevel"
110: ErrorMessage="Reorder level must be a valid numeric value
111: greater than or equal to zero."
112: Operator="GreaterThanEqual" Type="Integer"
113: ValueToCompare="0">*</asp:CompareValidator>
114: </EditItemTemplate>
115: <ItemTemplate>
116: <asp:Label ID="Label7" runat="server"
117: Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
118: </ItemTemplate>
119: </asp:TemplateField>
120: <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
121: SortExpression="Discontinued" />
122: </Columns>
123: </asp:GridView>
We’re very close to having a fully-working example. However, there are a few subtleties that will creep up and cause us problems. Additionally, we still need some interface that alerts the user when a concurrency violation has occurred.
[!NOTE] In order for a data Web control to correctly pass the original values to the ObjectDataSource (which are then passed to the BLL), it’s vital that the GridView’s
EnableViewState
property is set totrue
(the default). If you disable view state, the original values are lost on postback.
Passing the Correct Original Values to the ObjectDataSource
There are a couple of problems with the way the GridView has been configured. If the ObjectDataSource’s ConflictDetection
property is set to CompareAllValues
(as is ours), when the ObjectDataSource’s Update()
or Delete()
methods are invoked by the GridView (or DetailsView or FormView), the ObjectDataSource attempts to copy the GridView’s original values into its appropriate Parameter
instances. Refer back to Figure 2 for a graphical representation of this process.
Specifically, the GridView’s original values are assigned the values in the two-way databinding statements each time the data is bound to the GridView. Therefore, it’s essential that the required original values all are captured via two-way databinding and that they are provided in a convertible format.
To see why this is important, take a moment to visit our page in a browser. As expected, the GridView lists each product with an Edit and Delete button in the leftmost column.
Figure 14: The Products are Listed in a GridView (Click to view full-size image)
If you click the Delete button for any product, a FormatException
is thrown.
Figure 15: Attempting to Delete Any Product Results in a FormatException
(Click to view full-size image)
The FormatException
is raised when the ObjectDataSource attempts to read in the original UnitPrice
value. Since the ItemTemplate
has the UnitPrice
formatted as a currency (<%# Bind("UnitPrice", "{0:C}") %>
), it includes a currency symbol, like $19.95. The FormatException
occurs as the ObjectDataSource attempts to convert this string into a decimal
. To circumvent this problem, we have a number of options:
- Remove the currency formatting from the
ItemTemplate
. That is, instead of using<%# Bind("UnitPrice", "{0:C}") %>
, simply use<%# Bind("UnitPrice") %>
. The downside of this is that the price is no longer formatted. - Display the
UnitPrice
formatted as a currency in theItemTemplate
, but use theEval
keyword to accomplish this. Recall thatEval
performs one-way databinding. We still need to provide theUnitPrice
value for the original values, so we’ll still need a two-way databinding statement in theItemTemplate
, but this can be placed in a Label Web control whoseVisible
property is set tofalse
. We could use the following markup in the ItemTemplate:
[!code-aspxMain]
1: <ItemTemplate>
2: <asp:Label ID="DummyUnitPrice" runat="server"
3: Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
4: <asp:Label ID="Label4" runat="server"
5: Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
6: </ItemTemplate>
- Remove the currency formatting from the
ItemTemplate
, using<%# Bind("UnitPrice") %>
. In the GridView’sRowDataBound
event handler, programmatically access the Label Web control within which theUnitPrice
value is displayed and set itsText
property to the formatted version. - Leave the
UnitPrice
formatted as a currency. In the GridView’sRowDeleting
event handler, replace the existing originalUnitPrice
value ($19.95) with an actual decimal value usingDecimal.Parse
. We saw how to accomplish something similar in theRowUpdating
event handler in the Handling BLL- and DAL-Level Exceptions in an ASP.NET Page tutorial.
For my example I chose to go with the second approach, adding a hidden Label Web control whose Text
property is two-way data bound to the unformatted UnitPrice
value.
After solving this problem, try clicking the Delete button for any product again. This time you’ll get an InvalidOperationException
when the ObjectDataSource attempts to invoke the BLL’s UpdateProduct
method.
Figure 16: The ObjectDataSource Cannot Find a Method with the Input Parameters it Wants to Send (Click to view full-size image)
Looking at the exception’s message, it’s clear that the ObjectDataSource wants to invoke a BLL DeleteProduct
method that includes original_CategoryName
and original_SupplierName
input parameters. This is because the ItemTemplate
s for the CategoryID
and SupplierID
TemplateFields currently contain two-way Bind statements with the CategoryName
and SupplierName
data fields. Instead, we need to include Bind
statements with the CategoryID
and SupplierID
data fields. To accomplish this, replace the existing Bind statements with Eval
statements, and then add hidden Label controls whose Text
properties are bound to the CategoryID
and SupplierID
data fields using two-way databinding, as shown below:
[!code-aspxMain]
1: <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
2: <EditItemTemplate>
3: ...
4: </EditItemTemplate>
5: <ItemTemplate>
6: <asp:Label ID="DummyCategoryID" runat="server"
7: Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
8: <asp:Label ID="Label2" runat="server"
9: Text='<%# Eval("CategoryName") %>'></asp:Label>
10: </ItemTemplate>
11: </asp:TemplateField>
12: <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
13: <EditItemTemplate>
14: ...
15: </EditItemTemplate>
16: <ItemTemplate>
17: <asp:Label ID="DummySupplierID" runat="server"
18: Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
19: <asp:Label ID="Label3" runat="server"
20: Text='<%# Eval("SupplierName") %>'></asp:Label>
21: </ItemTemplate>
22: </asp:TemplateField>
With these changes, we are now able to successfully delete and edit product information! In Step 5 we’ll look at how to verify that concurrency violations are being detected. But for now, take a few minutes to try updating and deleting a few records to ensure that updating and deleting for a single user works as expected.
Step 5: Testing the Optimistic Concurrency Support
In order to verify that concurrency violations are being detected (rather than resulting in data being blindly overwritten), we need to open two browser windows to this page. In both browser instances, click on the Edit button for Chai. Then, in just one of the browsers, change the name to “Chai Tea” and click Update. The update should succeed and return the GridView to its pre-editing state, with “Chai Tea” as the new product name.
In the other browser window instance, however, the product name TextBox still shows “Chai”. In this second browser window, update the UnitPrice
to 25.00
. Without optimistic concurrency support, clicking update in the second browser instance would change the product name back to “Chai”, thereby overwriting the changes made by the first browser instance. With optimistic concurrency employed, however, clicking the Update button in the second browser instance results in a DBConcurrencyException.
Figure 17: When a Concurrency Violation is Detected, a DBConcurrencyException
is Thrown (Click to view full-size image)
The DBConcurrencyException
is only thrown when the DAL’s batch update pattern is utilized. The DB direct pattern does not raise an exception, it merely indicates that no rows were affected. To illustrate this, return both browser instances’ GridView to their pre-editing state. Next, in the first browser instance, click the Edit button and change the product name from “Chai Tea” back to “Chai” and click Update. In the second browser window, click the Delete button for Chai.
Upon clicking Delete, the page posts back, the GridView invokes the ObjectDataSource’s Delete()
method, and the ObjectDataSource calls down into the ProductsOptimisticConcurrencyBLL
class’s DeleteProduct
method, passing along the original values. The original ProductName
value for the second browser instance is “Chai Tea”, which doesn’t match up with the current ProductName
value in the database. Therefore the DELETE
statement issued to the database affects zero rows since there’s no record in the database that the WHERE
clause satisfies. The DeleteProduct
method returns false
and the ObjectDataSource’s data is rebound to the GridView.
From the end user’s perspective, clicking on the Delete button for Chai Tea in the second browser window caused the screen to flash and, upon coming back, the product is still there, although now it’s listed as “Chai” (the product name change made by the first browser instance). If the user clicks the Delete button again, the Delete will succeed, as the GridView’s original ProductName
value (“Chai”) now matches up with the value in the database.
In both of these cases, the user experience is far from ideal. We clearly don’t want to show the user the nitty-gritty details of the DBConcurrencyException
exception when using the batch update pattern. And the behavior when using the DB direct pattern is somewhat confusing as the users command failed, but there was no precise indication of why.
To remedy these two issues, we can create Label Web controls on the page that provide an explanation to why an update or delete failed. For the batch update pattern, we can determine whether or not a DBConcurrencyException
exception occurred in the GridView’s post-level event handler, displaying the warning label as needed. For the DB direct method, we can examine the return value of the BLL method (which is true
if one row was affected, false
otherwise) and display an informational message as needed.
Step 6: Adding Informational Messages and Displaying Them in the Face of a Concurrency Violation
When a concurrency violation occurs, the behavior exhibited depends on whether the DAL’s batch update or DB direct pattern was used. Our tutorial uses both patterns, with the batch update pattern being used for updating and the DB direct pattern used for deleting. To get started, let’s add two Label Web controls to our page that explain that a concurrency violation occurred when attempting to delete or update data. Set the Label control’s Visible
and EnableViewState
properties to false
; this will cause them to be hidden on each page visit except for those particular page visits where their Visible
property is programmatically set to true
.
[!code-aspxMain]
1: <asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
2: EnableViewState="False" CssClass="Warning"
3: Text="The record you attempted to delete has been modified by another user
4: since you last visited this page. Your delete was cancelled to allow
5: you to review the other user's changes and determine if you want to
6: continue deleting this record." />
7: <asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
8: EnableViewState="False" CssClass="Warning"
9: Text="The record you attempted to update has been modified by another user
10: since you started the update process. Your changes have been replaced
11: with the current values. Please review the existing values and make
12: any needed changes." />
In addition to setting their Visible
, EnabledViewState
, and Text
properties, I’ve also set the CssClass
property to Warning
, which causes the Label’s to be displayed in a large, red, italic, bold font. This CSS Warning
class was defined and added to Styles.css back in the Examining the Events Associated with Inserting, Updating, and Deleting tutorial.
After adding these Labels, the Designer in Visual Studio should look similar to Figure 18.
Figure 18: Two Label Controls Have Been Added to the Page (Click to view full-size image)
With these Label Web controls in place, we’re ready to examine how to determine when a concurrency violation has occurred, at which point the appropriate Label’s Visible
property can be set to true
, displaying the informational message.
Handling Concurrency Violations When Updating
Let’s first look at how to handle concurrency violations when using the batch update pattern. Since such violations with the batch update pattern cause a DBConcurrencyException
exception to be thrown, we need to add code to our ASP.NET page to determine whether a DBConcurrencyException
exception occurred during the update process. If so, we should display a message to the user explaining that their changes were not saved because another user had modified the same data between when they started editing the record and when they clicked the Update button.
As we saw in the Handling BLL- and DAL-Level Exceptions in an ASP.NET Page tutorial, such exceptions can be detected and suppressed in the data Web control’s post-level event handlers. Therefore, we need to create an event handler for the GridView’s RowUpdated
event that checks if a DBConcurrencyException
exception has been thrown. This event handler is passed a reference to any exception that was raised during the updating process, as shown in the event handler code below:
[!code-vbMain]
1: Protected Sub ProductsGrid_RowUpdated _
2: (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
3: Handles ProductsGrid.RowUpdated
4: If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
5: If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
6: ' Display the warning message and note that the exception has
7: ' been handled...
8: UpdateConflictMessage.Visible = True
9: e.ExceptionHandled = True
10: End If
11: End If
12: End Sub
In the face of a DBConcurrencyException
exception, this event handler displays the UpdateConflictMessage
Label control and indicates that the exception has been handled. With this code in place, when a concurrency violation occurs when updating a record, the user’s changes are lost, since they would have overwritten another user’s modifications at the same time. In particular, the GridView is returned to its pre-editing state and bound to the current database data. This will update the GridView row with the other user’s changes, which were previously not visible. Additionally, the UpdateConflictMessage
Label control will explain to the user what just happened. This sequence of events is detailed in Figure 19.
Figure 19: A User s Updates are Lost in the Face of a Concurrency Violation (Click to view full-size image)
[!NOTE] Alternatively, rather than returning the GridView to the pre-editing state, we could leave the GridView in its editing state by setting the
KeepInEditMode
property of the passed-inGridViewUpdatedEventArgs
object to true. If you take this approach, however, be certain to rebind the data to the GridView (by invoking itsDataBind()
method) so that the other user’s values are loaded into the editing interface. The code available for download with this tutorial has these two lines of code in theRowUpdated
event handler commented out; simply uncomment these lines of code to have the GridView remain in edit mode after a concurrency violation.
Responding to Concurrency Violations When Deleting
With the DB direct pattern, there is no exception raised in the face of a concurrency violation. Instead, the database statement simply affects no records, as the WHERE clause does not match with any record. All of the data modification methods created in the BLL have been designed such that they return a Boolean value indicating whether or not they affected precisely one record. Therefore, to determine if a concurrency violation occurred when deleting a record, we can examine the return value of the BLL’s DeleteProduct
method.
The return value for a BLL method can be examined in the ObjectDataSource’s post-level event handlers through the ReturnValue
property of the ObjectDataSourceStatusEventArgs
object passed into the event handler. Since we are interested in determining the return value from the DeleteProduct
method, we need to create an event handler for the ObjectDataSource’s Deleted
event. The ReturnValue
property is of type object
and can be null
if an exception was raised and the method was interrupted before it could return a value. Therefore, we should first ensure that the ReturnValue
property is not null
and is a Boolean value. Assuming this check passes, we show the DeleteConflictMessage
Label control if the ReturnValue
is false
. This can be accomplished by using the following code:
[!code-vbMain]
1: Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
2: (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
3: Handles ProductsOptimisticConcurrencyDataSource.Deleted
4: If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
5: Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
6: If deleteReturnValue = False Then
7: ' No row was deleted, display the warning message
8: DeleteConflictMessage.Visible = True
9: End If
10: End If
11: End Sub
In the face of a concurrency violation, the user’s delete request is canceled. The GridView is refreshed, showing the changes that occurred for that record between the time the user loaded the page and when he clicked the Delete button. When such a violation transpires, the DeleteConflictMessage
Label is shown, explaining what just happened (see Figure 20).
Figure 20: A User s Delete is Canceled in the Face of a Concurrency Violation (Click to view full-size image)
Summary
Opportunities for concurrency violations exist in every application that allows multiple, concurrent users to update or delete data. If such violations are not accounted for, when two users simultaneously update the same data whoever gets in the last write “wins,” overwriting the other user’s changes changes. Alternatively, developers can implement either optimistic or pessimistic concurrency control. Optimistic concurrency control assumes that concurrency violations are infrequent and simply disallows an update or delete command that would constitute a concurrency violation. Pessimistic concurrency control assumes that concurrency violations are frequent and simply rejecting one user’s update or delete command is not acceptable. With pessimistic concurrency control, updating a record involves locking it, thereby preventing any other users from modifying or deleting the record while it is locked.
The Typed DataSet in .NET provides functionality for supporting optimistic concurrency control. In particular, the UPDATE
and DELETE
statements issued to the database include all of the table’s columns, thereby ensuring that the update or delete will only occur if the record’s current data matches with the original data the user had when performing their update or delete. Once the DAL has been configured to support optimistic concurrency, the BLL methods need to be updated. Additionally, the ASP.NET page that calls down into the BLL must be configured such that the ObjectDataSource retrieves the original values from its data Web control and passes them down into the BLL.
As we saw in this tutorial, implementing optimistic concurrency control in an ASP.NET web application involves updating the DAL and BLL and adding support in the ASP.NET page. Whether or not this added work is a wise investment of your time and effort depends on your application. If you infrequently have concurrent users updating data, or the data they are updating is different from one another, then concurrency control is not a key issue. If, however, you routinely have multiple users on your site working with the same data, concurrency control can help prevent one user’s updates or deletes from unwittingly overwriting another’s.
Happy Programming!
About the Author
Scott Mitchell, author of seven ASP/ASP.NET books and founder of 4GuysFromRolla.com, has been working with Microsoft Web technologies since 1998. Scott works as an independent consultant, trainer, and writer. His latest book is Sams Teach Yourself ASP.NET 2.0 in 24 Hours. He can be reached at mitchell@4GuysFromRolla.com. or via his blog, which can be found at http://ScottOnWriting.NET.
|