Implementar la simultaneidad optimista (VB)

por Scott Mitchell

Descargar PDF

En el caso de una aplicación web que permita a varios usuarios editar datos, existe el riesgo de que dos usuarios puedan editar los mismos datos al mismo tiempo. En este tutorial se implementará el control de simultaneidad optimista para controlar este riesgo.

Introducción

En el caso de las aplicaciones web que solo permiten a los usuarios ver datos o para las que incluyen un solo usuario que puede modificar los datos, no hay ninguna amenaza de dos usuarios simultáneos que sobrescriban accidentalmente los cambios del otro. Pero en el caso de las aplicaciones web que permiten a varios usuarios actualizar o eliminar datos, existe la posibilidad de que las modificaciones de un usuario entren en conflicto con las de otro usuario simultáneo. Sin una directiva de simultaneidad vigente, cuando dos usuarios editan simultáneamente un único registro, el último usuario que confirme los cambios invalidará los cambios realizados por el primero.

Imagine que dos usuarios, Jisun y Sam, visitan una página en una aplicación que permite a los visitantes actualizar y eliminar productos desde un control GridView. Los dos hacen clic en el botón Editar del control GridView aproximadamente al mismo tiempo. Jisun cambia el nombre del producto a "Chai Tea" y hace clic en el botón Actualizar. El resultado neto es una instrucción UPDATE que se envía a la base de datos, que establece todos los campos actualizables del producto (aunque Jisun solo ha actualizado uno, ProductName). En este momento, la base de datos tiene los valores "Chai Tea", la categoría Beverages (Bebidas), el proveedor Exotic Liquids, etc. para este producto en particular. Pero el control GridView en la pantalla de Sam sigue mostrando el nombre del producto en la fila GridView editable como "Chai". Unos segundos después de confirmar los cambios de Jisun, Sam actualiza la categoría a Condiments y hace clic en Actualizar. Esto da como resultado una instrucción UPDATE que se envía a la base de datos que establece el nombre del producto en "Chai", CategoryID en el id. de categoría Beverages correspondiente, etc. Los cambios de Jisun en el nombre del producto se han sobrescrito. En la figura 1 se muestra gráficamente esta serie de eventos.

When Two Users Simultaneously Update a Record There s Potential for One User 's Changes to Overwrite the Other 's

Figura 1: Cuando dos usuarios actualizan simultáneamente un registro, existe la posibilidad de que los cambios de un usuario sobrescriban los del otro (Haga clic para ver la imagen a tamaño completo)

De forma similar, cuando dos usuarios visitan una página, uno podría estar en medio de la actualización de un registro cuando el otro usuario lo elimina. O bien, entre el momento que un usuario carga una página y cuando hace clic en el botón Eliminar, es posible que otro usuario haya modificado el contenido de ese registro.

Hay tres estrategias de control de simultaneidad disponibles:

  • No hacer nada: si los usuarios simultáneos modifican el mismo registro, deje que gane la última confirmación (el comportamiento predeterminado)
  • Simultaneidad optimista: imagine que, aunque en ocasiones puede haber conflictos de simultaneidad, la gran mayoría del tiempo no surgirán; por tanto, si surge un conflicto, simplemente informe al usuario de que sus cambios no se pueden guardar porque otro usuario ha modificado los mismos datos
  • Simultaneidad pesimista: imagine que los conflictos de simultaneidad son habituales y que los usuarios no tolerarán que se les diga que los cambios no se han guardado debido a la actividad simultánea de otro usuario; por tanto, cuando un usuario comienza a actualizar un registro, bloquéelo, lo que impide que otros usuarios editen o eliminen ese registro hasta que el primero confirme sus modificaciones

En todos los tutoriales vistos hasta ahora se ha usado la estrategia de resolución de simultaneidad predeterminada, es decir, se ha dejado que gane la última edición. En este tutorial verá cómo implementar el control de simultaneidad optimista.

Nota:

En esta serie de tutoriales no se verán ejemplos de simultaneidad pesimista. La simultaneidad pesimista rara vez se usa porque si no se renuncia correctamente a estos bloqueos, pueden impedir que otros usuarios actualicen los datos. Por ejemplo, si un usuario bloquea un registro para su edición y, después, se marcha sin desbloquearlo, ningún otro usuario podrá actualizar ese registro hasta que el usuario original vuelva y complete su actualización. Por tanto, en situaciones en las que se usa la simultaneidad pesimista, normalmente hay un tiempo de espera que, si se alcanza, cancela el bloqueo. Los sitios web de venta de entradas, que bloquean una ubicación de asiento determinada durante un breve período mientras el usuario completa el proceso del pedido, son un ejemplo de control de simultaneidad pesimista.

Paso 1: Análisis de la implementación de la simultaneidad optimista

El control de simultaneidad optimista funciona asegurándose de que el registro que se actualiza o elimina tiene los mismos valores que cuando se ha iniciado el proceso de actualización o eliminación. Por ejemplo, al hacer clic en el botón Editar de un control GridView editable, los valores del registro se leen de la base de datos y se muestran en controles TextBox y otros controles web. GridView guarda estos valores originales. Más adelante, después de que el usuario realice sus cambios y haga clic en el botón Actualizar, los valores originales más los nuevos se envían a la capa de lógica de negocios y, luego, a la capa de acceso a datos. La capa de acceso a datos debe emitir una instrucción SQL que solo actualizará el registro si los valores originales que el usuario ha empezado a editar son idénticos a los valores que todavía están en la base de datos. En la figura 2 se muestra esta secuencia de eventos.

For the Update or Delete to Succeed, the Original Values Must Be Equal to the Current Database Values

Figura 2: Para que la actualización o eliminación se realice correctamente, los valores originales deben ser iguales a los valores actuales de la base de datos (Haga clic para ver la imagen a tamaño completo)

Hay varios enfoques para implementar la simultaneidad optimista (vea La lógica de actualización de la simultaneidad optimista de Peter A. Bromberg para obtener un breve resumen de varias opciones). El elemento DataSet con tipo de ADO.NET proporciona una implementación que se puede configurar con solo activar una casilla. Al habilitar la simultaneidad optimista para un elemento TableAdapter en el objeto DataSet con tipo, se aumentan las instrucciones UPDATE y DELETE de TableAdapter para incluir una comparación de todos los valores originales en la cláusula WHERE. La siguiente instrucción UPDATE, por ejemplo, actualiza el nombre y el precio de un producto solo si los valores de base de datos actuales son iguales a los valores que se han recuperado originalmente al actualizar el registro en el control GridView. Los parámetros @ProductName y @UnitPrice contienen los nuevos valores especificados por el usuario, mientras que @original_ProductName y @original_UnitPrice contienen los valores que se han cargado originalmente en GridView cuando se hizo clic en el botón Editar:

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

Nota:

Esta instrucción UPDATE se ha simplificado para mejorar la legibilidad. En la práctica, la comprobación de UnitPrice de la cláusula WHERE sería más compleja, ya que UnitPrice puede contener NULL y comprobar si NULL = NULL siempre devuelve False (en su lugar, debe usar IS NULL).

Además de usar otra instrucción UPDATE subyacente, la configuración de TableAdapter para usar la simultaneidad optimista también modifica la firma de sus métodos directos de base de datos. Recuerde del primer tutorial, Creación de una capa de acceso a datos, que los métodos directos de base de datos eran aquellos que aceptan una lista de valores escalares como parámetros de entrada (en lugar de una instancia de DataRow o DataTable fuertemente tipada). Al usar la simultaneidad optimista, los métodos directos Update() y Delete() de base de datos incluyen también parámetros de entrada para los valores originales. Además, el código de la BLL para usar el patrón de actualización por lotes (las sobrecargas de método Update() que aceptan instancias de DataRow y DataTable en lugar de valores escalares) también se debe cambiar.

En lugar de ampliar los elementos TableAdapter de la DAL existente para usar la simultaneidad optimista (lo que requeriría cambiar la BLL), se creará un objeto DataSet con tipo denominado NorthwindOptimisticConcurrency, al que se agregará un elemento TableAdapter Products que use la simultaneidad optimista. Después, se creará una clase de capa lógica de negocios ProductsOptimisticConcurrencyBLL que tenga las modificaciones adecuadas para admitir la DAL de simultaneidad optimista. Una vez que se haya establecido esta base, ya se puede crear la página ASP.NET.

Paso 2: Creación de una capa de acceso a datos que admita la simultaneidad optimista

Para crear un objeto DataSet con tipo, haga clic con el botón derecho en la carpeta DAL dentro de la carpeta App_Code y agregue un nuevo conjunto de datos denominado NorthwindOptimisticConcurrency. Como ha visto en el primer tutorial, al hacerlo se agregará un nuevo elemento TableAdapter al objeto DataSet con tipo y se iniciará automáticamente el Asistente para configuración de TableAdapter. En la primera pantalla, se le pedirá que especifique la base de datos a la que conectarse: conéctese a la misma base de datos Northwind mediante el valor NORTHWNDConnectionString de Web.config.

Connect to the Same Northwind Database

Figura 3: Conexión a la misma base de datos Northwind (Haga clic para ver la imagen a tamaño completo)

A continuación, se le pedirá cómo consultar los datos: desde una instrucción SQL ad-hoc, un nuevo procedimiento almacenado o un procedimiento almacenado existente. Como se han usado consultas SQL ad-hoc en la DAL original, use esta opción aquí también.

Specify the Data to Retrieve Using an Ad-Hoc SQL Statement

Figura 4: Especificación de los datos que se van a recuperar mediante una instrucción SQL ad-hoc (Haga clic para ver la imagen a tamaño completo)

En la siguiente pantalla, escriba la consulta SQL que se usará para recuperar la información del producto. Se usará la misma consulta SQL que para TableAdapter Products de la DAL original, que devuelve todas las columnas Product junto con los nombres de proveedor y categoría del producto:

SELECT   ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
           UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
           (SELECT CategoryName FROM Categories
              WHERE Categories.CategoryID = Products.CategoryID)
              as CategoryName,
           (SELECT CompanyName FROM Suppliers
              WHERE Suppliers.SupplierID = Products.SupplierID)
              as SupplierName
FROM     Products

Use the Same SQL Query from the Products TableAdapter in the Original DAL

Figura 5: Uso de la misma consulta SQL de TableAdapter Products en la DAL original (Haga clic para ver la imagen a tamaño completo)

Antes de pasar a la pantalla siguiente, pulse el botón Opciones avanzadas. Para que TableAdapter use el control de simultaneidad optimista, simplemente active la casilla "Usar simultaneidad optimista".

Enable Optimistic Concurrency Control by Checking the

Figura 6: Habilitación del control de simultaneidad optimista con la activación de la casilla "Usar simultaneidad optimista" (Haga clic para ver la imagen a tamaño completo)

Por último, indique que TableAdapter debe usar los patrones de acceso a datos que rellenan DataTable y devuelven una instancia de DataTable; indique también que se deben crear los métodos directos de base de datos. Cambie el nombre del método para el patrón Return a DataTable de GetData a GetProducts, de modo que refleje las convenciones de nomenclatura que se usan en la DAL original.

Have the TableAdapter Utilize All Data Access Patterns

Figura 7: Hacer que TableAdapter use todos los patrones de acceso a datos (Haga clic para ver la imagen a tamaño completo)

Después de completar el asistente, el diseñador de DataSet incluirá una instancia Products de DataTable fuertemente tipada y una instancia de TableAdapter. Dedique un momento a cambiar el nombre de DataTable de Products a ProductsOptimisticConcurrency; para ello, haga clic con el botón derecho en la barra de título de DataTable y seleccione Cambiar nombre en el menú contextual.

A DataTable and TableAdapter Have Been Added to the Typed DataSet

Figura 8: Adición de instancias de DataTable y TableAdapter al objeto DataSet con tipo (Haga clic para ver la imagen a tamaño completo)

Para ver las diferencias entre las consultas UPDATE y DELETE entre el elemento TableAdapter ProductsOptimisticConcurrency (que usa la simultaneidad optimista) y elemento TableAdapter Products (que no), haga clic en TableAdapter y vaya a la ventana Propiedades. En las subpropiedades CommandText de las propiedades DeleteCommand y UpdateCommand, puede ver la sintaxis SQL real que se envía a la base de datos cuando se invocan los métodos relacionados con la actualización o eliminación de la DAL. Para TableAdapter ProductsOptimisticConcurrency, la instrucción DELETE usada es la siguiente:

DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

Mientras que la instrucción DELETE de TableAdapter Product en la DAL original es mucho más sencilla:

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

Como puede ver, la cláusula WHERE de la instrucción DELETE para la instancia de TableAdapter que usa la simultaneidad optimista incluye una comparación entre cada uno de los valores de columna existentes de la tabla Product y los valores originales en el momento en que se ha rellenado el control GridView (o DetailsView o FormView). Como todos los campos distintos de ProductID, ProductName y Discontinued pueden tener valores NULL, se incluyen parámetros y comprobaciones adicionales para comparar correctamente los valores NULL de la cláusula WHERE.

No se agregarán elementos DataTable adicionales al conjunto de datos habilitado para la simultaneidad optimista en este tutorial, ya que la página ASP.NET solo proporcionará la actualización y eliminación de la información del producto. Pero todavía es necesario agregar el método GetProductByProductID(productID) a TableAdapter ProductsOptimisticConcurrency.

Para ello, haga clic con el botón derecho en la barra de título de TableAdapter (el área situada encima de los nombres de método Fill y GetProducts), y elija Agregar consulta en el menú contextual. Esto iniciará el Asistente para configuración de consultas de TableAdapter. Al igual que con la configuración inicial de TableAdapter, opte por crear el método GetProductByProductID(productID) mediante una instrucción SQL ad-hoc (vea la figura 4). Como el método GetProductByProductID(productID) devuelve información sobre un producto determinado, indique que esta consulta es un tipo de consulta SELECT que devuelve filas.

Mark the Query Type as a

Figura 9: Marcado del tipo de consulta como "SELECT que devuelve filas" (Haga clic para ver la imagen a tamaño completo)

En la siguiente pantalla se le pedirá que use la consulta SQL, con la consulta predeterminada de TableAdapter precargada. Aumente la consulta existente para incluir la cláusula WHERE ProductID = @ProductID, como se muestra en la figura 10.

Add a WHERE Clause to the Pre-Loaded Query to Return a Specific Product Record

Figura 10: Adición de una cláusula WHERE a la consulta precargada para devolver un registro de producto específico (Haga clic para ver la imagen a tamaño completo)

Por último, cambie los nombres de método generados por FillByProductID y GetProductByProductID.

Rename the Methods to FillByProductID and GetProductByProductID

Figura 11: Cambio del nombre de los métodos a FillByProductID y GetProductByProductID (Haga clic para ver la imagen a tamaño completo)

Con este asistente completado, TableAdapter ahora contiene dos métodos para recuperar datos: GetProducts(), que devuelve todos los productos; y GetProductByProductID(productID), que devuelve el producto especificado.

Paso 3: Creación de una capa de lógica de negocios para la DAL habilitada para la simultaneidad optimista

La clase ProductsBLL existente tiene ejemplos de uso de la actualización por lotes y los patrones directos de base de datos. Tanto el método AddProduct como las sobrecargas de UpdateProduct usan el patrón de actualización por lotes, y se pasa una instancia de ProductRow al método Update de TableAdapter. Por otro lado, el método DeleteProduct usa el patrón directo de base de datos y se llama al método Delete(productID) de TableAdapter.

Con el nuevo elemento TableAdapter ProductsOptimisticConcurrency, los métodos directos de la base de datos ahora requieren que también se pasen los valores originales. Por ejemplo, el método Delete espera ahora diez parámetros de entrada: el valor ProductID original, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel y Discontinued. Usa estos valores de parámetros de entrada adicionales en la cláusula WHERE de la instrucción DELETE enviada a la base de datos y solo se elimina el registro especificado si los valores actuales de la base de datos coinciden con los originales.

Aunque la firma del método Update de TableAdapter usado en el patrón de actualización por lotes no ha cambiado, el código necesario para registrar los valores originales y nuevos sí ha cambiado. Por tanto, en lugar de intentar usar la DAL habilitada para la simultaneidad optimista con la clase ProductsBLL existente, se creará una clase de capa lógica de negocios para trabajar con la nueva DAL.

Agregue una clase denominada ProductsOptimisticConcurrencyBLL a la carpeta BLL dentro de la carpeta App_Code.

Add the ProductsOptimisticConcurrencyBLL Class to the BLL Folder

Figura 12: Adición de la clase ProductsOptimisticConcurrencyBLL a la carpeta BLL

A continuación, agregue el código siguiente a la clase ProductsOptimisticConcurrencyBLL:

Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
    Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
    Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
        Get
            If _productsAdapter Is Nothing Then
                _productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
            End If
            Return _productsAdapter
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
        Return Adapter.GetProducts()
    End Function
End Class

Observe la instrucción NorthwindOptimisticConcurrencyTableAdapters using encima del inicio de la declaración de clase. El espacio de nombres NorthwindOptimisticConcurrencyTableAdapters contiene la clase ProductsOptimisticConcurrencyTableAdapter, que proporciona los métodos de la DAL. Además, antes de la declaración de clase encontrará el atributo System.ComponentModel.DataObject, que indica a Visual Studio que incluya esta clase en la lista desplegable del asistente de ObjectDataSource.

La propiedad Adapter de ProductsOptimisticConcurrencyBLL proporciona acceso rápido a una instancia de la clase ProductsOptimisticConcurrencyTableAdapter y sigue el patrón usado en las clases de la BLL originales (ProductsBLL, CategoriesBLL, etc.). Por último, el método GetProducts() simplemente llama al método GetProducts() de la DAL y devuelve un objeto ProductsOptimisticConcurrencyDataTable rellenado con una instancia de ProductsOptimisticConcurrencyRow de cada registro de producto de la base de datos.

Eliminación de un producto mediante el patrón directo de base de datos con simultaneidad optimista

Cuando se usa el patrón directo de base de datos en una DAL que usa la simultaneidad optimista, los métodos deben pasar los valores nuevos y los originales. Para la eliminación, no hay valores nuevos, por lo que solo se deben pasar los valores originales. En la BLL, se deben aceptar todos los parámetros originales como parámetros de entrada. Hará que el método DeleteProduct de la clase ProductsOptimisticConcurrencyBLL use el método directo de base de datos. Esto significa que este método debe tomar los diez campos de datos del producto como parámetros de entrada y pasarlos a la DAL, como se muestra en el código siguiente:

<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
    ByVal original_productID As Integer, ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean) _
    As Boolean
    Dim rowsAffected As Integer = Adapter.Delete(
                                    original_productID, _
                                    original_productName, _
                                    original_supplierID, _
                                    original_categoryID, _
                                    original_quantityPerUnit, _
                                    original_unitPrice, _
                                    original_unitsInStock, _
                                    original_unitsOnOrder, _
                                    original_reorderLevel, _
                                    original_discontinued)
    ' Return true if precisely one row was deleted, otherwise false
    Return rowsAffected = 1
End Function

Si los valores originales, los últimos que se han cargado en el control GridView (o DetailsView o FormView), difieren de los valores de la base de datos cuando el usuario hace clic en el botón Eliminar, la cláusula WHERE no coincidirá con ningún registro de base de datos y no afectará a ningún registro. Por tanto, el método Delete de TableAdapter devolverá 0 y el método DeleteProduct de la BLL devolverá false.

Actualización de un producto mediante el patrón de actualización por lotes con simultaneidad optimista

Como se ha indicado antes, el método Update de TableAdapter para el patrón de actualización por lotes tiene la misma firma de método independientemente de si se usa o no la simultaneidad optimista. Es decir, el método Update espera un objeto DataRow, una matriz de objetos DataRow, un objeto DataTable o un objeto DataSet con tipo. No hay parámetros de entrada adicionales para especificar los valores originales. Esto es posible porque DataTable realiza el seguimiento de los valores originales y modificados de sus objetos DataRow. Cuando la DAL emite su instrucción UPDATE, los parámetros @original_ColumnName se rellenan con los valores originales de DataRow, mientras que los parámetros @ColumnName se rellenan con los valores modificados de DataRow.

En la clase ProductsBLL (que usa la DAL de simultaneidad original y no optimista), cuando se usa el patrón de actualización por lotes para actualizar la información del producto, el código realiza la siguiente secuencia de eventos:

  1. Lee la información actual del producto de la base de datos en una instancia de ProductRow mediante el método GetProductByProductID(productID) de TableAdapter
  2. Asigna los nuevos valores a la instancia de ProductRow del paso 1
  3. Llama al método Update de TableAdapter y le pasa la instancia de ProductRow

Pero esta secuencia de pasos no admitirá correctamente la simultaneidad optimista porque el elemento ProductRow rellenado en el paso 1 se rellena directamente desde la base de datos, lo que significa que los valores originales usados por DataRow son los que existen actualmente en la base de datos y no los que estaban enlazados al control GridView al principio del proceso de edición. En su lugar, al usar una DAL habilitada para la simultaneidad optimista, es necesario modificar las sobrecargas del método UpdateProduct para usar los pasos siguientes:

  1. Leer la información actual del producto de la base de datos en una instancia de ProductsOptimisticConcurrencyRow mediante el método GetProductByProductID(productID) de TableAdapter
  2. Asignar los valores originales a la instancia de ProductsOptimisticConcurrencyRow del paso 1
  3. Llamar al método AcceptChanges() de la instancia de ProductsOptimisticConcurrencyRow, que indica a DataRow que sus valores actuales son los "originales"
  4. Asignar los nuevos valores a la instancia de ProductsOptimisticConcurrencyRow
  5. Llamar al método Update de TableAdapter y pasar la instancia de ProductsOptimisticConcurrencyRow

En el paso 1 se leen todos los valores actuales de la base de datos para el registro de producto especificado. Este paso es superfluo en la sobrecarga de UpdateProduct que actualiza todas las columnas de producto (ya que estos valores se sobrescriben en el paso 2), pero es esencial para esas sobrecargas en las que solo se pasa un subconjunto de los valores de columna como parámetros de entrada. Una vez que se asignan los valores originales a la instancia de ProductsOptimisticConcurrencyRow, se llama al método AcceptChanges(), que marca los valores actuales de DataRow como los valores originales que se usarán en los parámetros @original_ColumnName de la instrucción UPDATE. A continuación, los nuevos valores de parámetro se asignan a ProductsOptimisticConcurrencyRow y, por último, se invoca el método Update y se pasa el objeto DataRow.

En el código siguiente se muestra la sobrecarga de UpdateProduct que acepta todos los campos de datos del producto como parámetros de entrada. Aunque no se muestra aquí, la clase ProductsOptimisticConcurrencyBLL incluida en la descarga de este tutorial también contiene una sobrecarga de UpdateProduct que acepta solo el nombre y el precio del producto como parámetros de entrada.

Protected Sub AssignAllProductValues( _
    ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean)
    product.ProductName = productName
    If Not supplierID.HasValue Then
        product.SetSupplierIDNull()
    Else
        product.SupplierID = supplierID.Value
    End If
    If Not categoryID.HasValue Then
        product.SetCategoryIDNull()
    Else
        product.CategoryID = categoryID.Value
    End If
    If quantityPerUnit Is Nothing Then
        product.SetQuantityPerUnitNull()
    Else
        product.QuantityPerUnit = quantityPerUnit
    End If
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    If Not unitsOnOrder.HasValue Then
        product.SetUnitsOnOrderNull()
    Else
        product.UnitsOnOrder = unitsOnOrder.Value
    End If
    If Not reorderLevel.HasValue Then
        product.SetReorderLevelNull()
    Else
        product.ReorderLevel = reorderLevel.Value
    End If
    product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean, ByVal productID As Integer, _
    _
    ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean, _
    ByVal original_productID As Integer) _
    As Boolean
    'STEP 1: Read in the current database product information
    Dim products As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
        Adapter.GetProductByProductID(original_productID)
    If products.Count = 0 Then
        ' no matching record found, return false
        Return False
    End If
    Dim product As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = products(0)
    'STEP 2: Assign the original values to the product instance
    AssignAllProductValues( _
        product, original_productName, original_supplierID, _
        original_categoryID, original_quantityPerUnit, original_unitPrice, _
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel, _
        original_discontinued)
    'STEP 3: Accept the changes
    product.AcceptChanges()
    'STEP 4: Assign the new values to the product instance
    AssignAllProductValues( _
        product, productName, supplierID, categoryID, quantityPerUnit, unitPrice, _
        unitsInStock, unitsOnOrder, reorderLevel, discontinued)
    'STEP 5: Update the product record
    Dim rowsAffected As Integer = Adapter.Update(product)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

Paso 4: Transferencia de los valores originales y nuevos de la página ASP.NET a los métodos de la BLL

Con la DAL y la BLL completadas, solo falta crear una página ASP.NET que pueda usar la lógica de simultaneidad optimista integrada en el sistema. En concreto, el control web de datos (GridView, DetailsView o FormView) debe recordar sus valores originales y ObjectDataSource debe pasar ambos conjuntos de valores a la capa lógica de negocios. Además, la página ASP.NET se debe configurar para controlar correctamente las infracciones de simultaneidad.

Para empezar, abra la página OptimisticConcurrency.aspx en la carpeta EditInsertDelete, agregue un control GridView al Diseñador y establezca su propiedad ID en ProductsGrid. Desde la etiqueta inteligente de GridView, elija crear un objeto ObjectDataSource con el nombre ProductsOptimisticConcurrencyDataSource. Como quiere que esta instancia de ObjectDataSource use la DAL que admite la simultaneidad optimista, configúrelo para usar el objeto ProductsOptimisticConcurrencyBLL.

Have the ObjectDataSource Use the ProductsOptimisticConcurrencyBLL Object

Figura 13: ObjectDataSource usa el objeto ProductsOptimisticConcurrencyBLL (Haga clic para ver la imagen a tamaño completo)

Elija los métodos GetProducts, UpdateProduct y DeleteProduct en las listas desplegables del asistente. Para el método UpdateProduct, use la sobrecarga que acepta todos los campos de datos del producto.

Configuración de las propiedades del control ObjectDataSource

Después de completar el asistente, el marcado declarativo del control ObjectDataSource debería tener un aspecto similar al siguiente:

<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
    DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
    UpdateMethod="UpdateProduct">
    <DeleteParameters>
        <asp:Parameter Name="original_productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
        <asp:Parameter Name="original_productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

Como puede ver, la colección DeleteParameters contiene una instancia de Parameter de cada uno de los diez parámetros de entrada de la clase ProductsOptimisticConcurrencyBLL del método DeleteProduct. Del mismo modo, la colección UpdateParameters contiene una instancia de Parameter para cada uno de los parámetros de entrada en UpdateProduct.

En los tutoriales anteriores que implicaban la modificación de datos, se quitaba la propiedad OldValuesParameterFormatString de ObjectDataSource en este momento, ya que esta propiedad indica que el método de la BLL espera que se pasen los valores antiguos (u originales) así como los nuevos valores. Además, este valor de propiedad indica los nombres de parámetros de entrada para los valores originales. Como se pasan los valores originales a la BLL, no quite esta propiedad.

Nota:

El valor de la propiedad OldValuesParameterFormatString se debe asignar a los nombres de parámetro de entrada de la BLL que esperan los valores originales. Como los nombres de estos parámetros son original_productName, original_supplierID, etc., puede dejar el valor de propiedad OldValuesParameterFormatString como original_{0}. Pero si los parámetros de entrada de los métodos de la BLL tuvieran nombres como old_productName, old_supplierID, etc., tendría que actualizar la propiedad OldValuesParameterFormatString a old_{0}.

Hay una última configuración de propiedad que se debe realizar para que ObjectDataSource pase correctamente los valores originales a los métodos de la BLL. ObjectDataSource tiene una propiedad ConflictDetection que se puede asignar a uno de dos valores:

  • OverwriteChanges: el valor predeterminado; no envía los valores originales a los parámetros de entrada originales de los métodos de la BLL
  • CompareAllValues: envía los valores originales a los métodos de la BLL; elija esta opción al usar la simultaneidad optimista

Dedique un momento a establecer la propiedad ConflictDetection en CompareAllValues.

Configuración de las propiedades y campos de GridView

Con las propiedades de ObjectDataSource configuradas correctamente, se centrará en la configuración de GridView. En primer lugar, como quiere que GridView admita la edición y eliminación, haga clic en las casillas Habilitar edición y Habilitar eliminación de su etiqueta inteligente. Esto agregará un elemento CommandField con ShowEditButton y ShowDeleteButton establecidos en true.

Cuando se enlaza a ObjectDataSource ProductsOptimisticConcurrencyDataSource, GridView contiene un campo para cada uno de los campos de datos del producto. Aunque este tipo de GridView se puede editar, la experiencia del usuario no es aceptable. Los controles BoundField CategoryID y SupplierID se representarán como cuadros de texto, lo que requiere que el usuario escriba la categoría y el proveedor adecuados como números de identificador. No habrá ningún formato para los campos numéricos y ningún control de validación para asegurarse de que se ha proporcionado el nombre del producto y de que el precio unitario, las unidades en existencias, las unidades del pedido y los valores de nivel de reordenación son valores numéricos adecuados y son mayores o iguales a cero.

Como se ha explicado en los tutoriales Adición de controles de validación a las interfaces de edición e inserción y Personalización de la interfaz de modificación de datos, la interfaz de usuario se puede personalizar si se reemplazan los controles BoundField por controles TemplateField. Se ha modificado este control GridView y su interfaz de edición de las maneras siguientes:

  • Se han quitado los controles BoundField ProductID, SupplierName y CategoryName
  • Se ha convertido el control BoundField ProductName en TemplateField y se ha agregado un control RequiredFieldValidation.
  • Se han convertido los controles BoundField CategoryID y SupplierID en controles TemplateField, y se ha ajustado la interfaz de edición para que utilice controles DropDownList en lugar de cuadros de texto. En estos campos ItemTemplates de TemplateField, se muestran los campos de datos CategoryName y SupplierName.
  • Se han convertido los controles BoundField UnitPrice, UnitsInStock, UnitsOnOrder y ReorderLevel en controles TemplateField y se han agregado controles CompareValidator.

Como en tutoriales anteriores ya ha visto cómo realizar estas tareas, aquí solo se mostrará la sintaxis declarativa final y se dejará la implementación como práctica.

<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
    OnRowUpdated="ProductsGrid_RowUpdated">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="EditProductName" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:TextBox>
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="EditProductName"
                    ErrorMessage="You must enter a product name."
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditCategoryID" runat="server"
                    DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
                    DataTextField="CategoryName" DataValueField="CategoryID"
                    SelectedValue='<%# Bind("CategoryID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetCategories" TypeName="CategoriesBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server"
                    Text='<%# Bind("CategoryName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditSuppliersID" runat="server"
                    DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
                    DataTextField="CompanyName" DataValueField="SupplierID"
                    SelectedValue='<%# Bind("SupplierID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label3" runat="server"
                    Text='<%# Bind("SupplierName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitPrice" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
                <asp:CompareValidator ID="CompareValidator1" runat="server"
                    ControlToValidate="EditUnitPrice"
                    ErrorMessage="Unit price must be a valid currency value without the
                    currency symbol and must have a value greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Currency"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label4" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsInStock" runat="server"
                    Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator2" runat="server"
                    ControlToValidate="EditUnitsInStock"
                    ErrorMessage="Units in stock must be a valid number
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label5" runat="server"
                    Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsOnOrder" runat="server"
                    Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator3" runat="server"
                    ControlToValidate="EditUnitsOnOrder"
                    ErrorMessage="Units on order must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label6" runat="server"
                    Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
            <EditItemTemplate>
                <asp:TextBox ID="EditReorderLevel" runat="server"
                    Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator4" runat="server"
                    ControlToValidate="EditReorderLevel"
                    ErrorMessage="Reorder level must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label7" runat="server"
                    Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

Está muy cerca de obtener un ejemplo operativo completo. Pero hay algunos matices que pueden aparecer y causar problemas. Además, todavía necesita una interfaz que alerte al usuario si se produce una infracción de simultaneidad.

Nota:

Para que un control web de datos pase correctamente los valores originales a ObjectDataSource (que luego se pasan a la BLL), es fundamental que la propiedad EnableViewState de GridView esté establecida en true (valor predeterminado). Si deshabilita el estado de visualización, los valores originales se pierden durante el postback.

Transferencia de los valores originales correctos a ObjectDataSource

Hay un par de problemas con la forma en que se ha configurado GridView. Si la propiedad ConflictDetection de ObjectDataSource está establecida en CompareAllValues (como en este caso), cuando Update() de ObjectDataSource o los métodos Delete() se invocan en el control GridView (o DetailsView o FormView), ObjectDataSource intenta copiar los valores originales de GridView en sus instancias de Parameter adecuadas. Consulte la figura 2 para ver una representación gráfica de este proceso.

En concreto, a los valores originales de GridView se les asignan los valores de las instrucciones de enlace de datos bidireccional cada vez que los datos están enlazados a GridView. Por tanto, es esencial que todos los valores originales necesarios se capturen mediante el enlace de datos bidireccional y que se proporcionen en un formato convertible.

Para ver la importancia de esto, visite la página en un explorador. Como se esperaba, GridView enumera cada producto con un botón Editar y Eliminar en la columna situada más a la izquierda.

The Products are Listed in a GridView

Figura 14: Los productos aparecen en un control GridView (Haga clic para ver la imagen a tamaño completo)

Si hace clic en el botón Eliminar de cualquier producto, se inicia una excepción FormatException.

Attempting to Delete Any Product Results in a FormatException

Figura 15: El intento de eliminación de cualquier producto genera FormatException (Haga clic para ver la imagen a tamaño completo)

FormatException se genera cuando ObjectDataSource intenta leer el valor UnitPrice original. Como ItemTemplate tiene UnitPrice con formato de moneda (<%# Bind("UnitPrice", "{0:C}") %>), incluye un símbolo de moneda, como 19,95 $. FormatException se produce cuando ObjectDataSource intenta convertir esta cadena en decimal. Para eludir este problema, hay una serie de opciones:

  • Quitar el formato de moneda de ItemTemplate. Es decir, en lugar de usar <%# Bind("UnitPrice", "{0:C}") %>, simplemente use <%# Bind("UnitPrice") %>. La desventaja es que el precio ya no tiene formato.
  • Mostrar UnitPrice con formato de moneda en ItemTemplate; pero usar la palabra clave Eval para hacerlo. Recuerde que Eval realiza el enlace de datos unidireccional. Todavía es necesario proporcionar el valor UnitPrice de los valores originales, por lo que todavía necesitará una instrucción de enlace de datos bidireccional en ItemTemplate, pero esto se puede colocar en un control web Label cuya propiedad Visible esté establecida en false. Podría usar el marcado siguiente en ItemTemplate:
<ItemTemplate>
    <asp:Label ID="DummyUnitPrice" runat="server"
        Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
    <asp:Label ID="Label4" runat="server"
        Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
  • Quite el formato de moneda de ItemTemplate mediante <%# Bind("UnitPrice") %>. En el controlador de eventos RowDataBound de GridView, acceda mediante programación al control web Label dentro del que se muestra el valor UnitPrice y establezca su propiedad Text en la versión con formato.
  • Deje UnitPrice con formato de moneda. En el controlador de eventos RowDeleting de GridView, reemplace el valor UnitPrice original existente ($19,95) por un valor decimal real mediante Decimal.Parse. Ha visto cómo lograr algo similar en el controlador de eventos RowUpdating en el tutorial Control de excepciones de nivel BLL y DAL en una página ASP.NET.

En el ejemplo, se ha optado por el segundo enfoque, agregando un control web Label oculto cuya propiedad Text tiene un enlace de datos bidireccional al valor UnitPrice sin formato.

Después de resolver este problema, intente hacer clic en el botón Eliminar de nuevo para cualquier producto. Esta vez obtendrá un InvalidOperationException cuando ObjectDataSource intente invocar el método UpdateProduct de la BLL.

The ObjectDataSource Cannot Find a Method with the Input Parameters it Wants to Send

Figura 16: ObjectDataSource no puede encontrar un método con los parámetros de entrada que quiere enviar (Haga clic para ver la imagen a tamaño completo)

Al examinar el mensaje de la excepción, está claro que ObjectDataSource quiere invocar un método DeleteProduct de la BLL que incluya parámetros de entrada original_CategoryName y original_SupplierName. Esto se debe a que los elementos ItemTemplate para los controles TemplateField CategoryID y SupplierID contienen actualmente instrucciones de enlace bidireccionales con los campos de datos CategoryName y SupplierName. En su lugar, es necesario incluir instrucciones Bind con los campos de datos CategoryID y SupplierID. Para ello, reemplace las instrucciones de enlace existentes por instrucciones Eval y agregue controles Label ocultos cuyas propiedades Text estén enlazadas a los campos de datos CategoryID y SupplierID mediante el enlace de datos bidireccional, como se muestra a continuación:

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummyCategoryID" runat="server"
            Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label2" runat="server"
            Text='<%# Eval("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummySupplierID" runat="server"
            Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label3" runat="server"
            Text='<%# Eval("SupplierName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

Con estos cambios, ahora puede eliminar y editar correctamente la información del producto. En el paso 5 verá cómo comprobar que se detectan infracciones de simultaneidad. Pero, por ahora, dedique unos minutos a intentar actualizar y eliminar algunos registros para asegurarse de que la actualización y eliminación de un solo usuario funciona según lo previsto.

Paso 5: Prueba de compatibilidad con la simultaneidad optimista

Para comprobar que se detectan infracciones de simultaneidad (en lugar de provocar que los datos se sobrescriban a ciegas), es necesario abrir dos ventanas del explorador en esta página. En ambas instancias del explorador, haga clic en el botón Editar para Chai. Después, en uno de los exploradores, cambie el nombre a "Chai Tea" y haga clic en Actualizar. La actualización se debe realizar correctamente y devolver el control GridView a su estado anterior a la edición, con "Chai Tea" como el nuevo nombre del producto.

Pero en la otra instancia de ventana del explorador, el cuadro de texto del nombre del producto sigue mostrando "Chai". En esta segunda ventana del explorador, actualice UnitPrice a 25.00. Sin compatibilidad con la simultaneidad optimista, al hacer clic en Actualizar en la segunda instancia del explorador, se cambiaría el nombre del producto a "Chai", lo que sobrescribe los cambios realizados por la primera instancia del explorador. Pero con la simultaneidad optimista en uso, al hacer clic en el botón Actualizar de la segunda instancia del explorador, se inicia una excepción DBConcurrencyException.

When a Concurrency Violation is Detected, a DBConcurrencyException is Thrown

Figura 17: Cuando se detecta una infracción de simultaneidad, se inicia una excepción DBConcurrencyException (Haga clic para ver la imagen a tamaño completo)

DBConcurrencyException solo se inicia cuando se utiliza el patrón de actualización por lotes de la DAL. El patrón directo de base de datos no genera una excepción, simplemente indica que no se ha visto afectada ninguna fila. Para ilustrar esto, devuelva el control GridView de ambas instancias del explorador a su estado anterior a la edición. A continuación, en la primera instancia del explorador, haga clic en el botón Editar y cambie el nombre del producto "Chai Tea" de nuevo a "Chai" y haga clic en Actualizar. En la segunda ventana del navegador, haga clic en el botón Eliminar de Chai.

Al hacer clic en Eliminar, la página genera un postback, GridView invoca el método Delete() de ObjectDataSource y ObjectDataSource llama al método DeleteProduct de la clase ProductsOptimisticConcurrencyBLL, y pasa los valores originales. El valor ProductName original de la segunda instancia del explorador es "Chai Tea", que no coincide con el valor ProductName actual de la base de datos. Por tanto, la instrucción DELETE emitida a la base de datos afecta a cero filas, ya que no hay ningún registro en la base de datos que cumple la cláusula WHERE. El método DeleteProduct devuelve false y los datos de ObjectDataSource se vuelven a enlazar a GridView.

Desde la perspectiva del usuario final, al hacer clic en el botón Eliminar de Chai Tea en la segunda ventana del navegador, la pantalla parpadea y, al volver, el producto todavía está allí, aunque ahora aparece como "Chai" (el cambio de nombre del producto realizado por la primera instancia del explorador). Si el usuario hace clic de nuevo en el botón Eliminar, la eliminación se realizará correctamente, ya que el valor ProductName original de GridView ("Chai") ahora coincide con el valor de la base de datos.

En ambos casos, la experiencia del usuario está lejos de ser la idónea. Claramente no quiere mostrar al usuario los detalles específicos de la excepción DBConcurrencyException al usar el patrón de actualización por lotes. Y el comportamiento al usar el patrón directo de base de datos es algo confuso, ya que se produce un error en el comando del usuario, pero no ha habido ninguna indicación precisa del motivo.

Para solucionar estos dos problemas, puede crear controles web Label en la página que proporcionen una explicación de por qué se produce un error de actualización o eliminación. Para el patrón de actualización por lotes, puede determinar si se ha producido o no una excepción DBConcurrencyException en el controlador de eventos de nivel posterior de GridView, y mostrar la etiqueta de advertencia según sea necesario. Para el método directo de base de datos, se puede examinar el valor devuelto del método de la BLL (que es true si una fila se ha visto afectada, o false de lo contrario) y mostrar un mensaje informativo según sea necesario.

Paso 6: Adición de mensajes informativos y representarlos en caso de una infracción de simultaneidad

Cuando se produce una infracción de simultaneidad, el comportamiento mostrado depende de si se ha usado la actualización por lotes de la DAL o el patrón directo de base de datos. En este tutorial se usan ambos patrones: el patrón de actualización por lotes se usa para la actualización y el patrón directo de base de datos se usa para la eliminación. Para empezar, se agregarán dos controles web Label a la página para explicar que se ha producido una infracción de simultaneidad al intentar eliminar o actualizar datos. Establezca las propiedades Visible y EnableViewState del control Label en false; esto hará que se oculte en cada visita de página, excepto para las visitas a páginas concretas en las que su propiedad Visible esté establecida mediante programación en true.

<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to delete has been modified by another user
           since you last visited this page. Your delete was cancelled to allow
           you to review the other user's changes and determine if you want to
           continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to update has been modified by another user
           since you started the update process. Your changes have been replaced
           with the current values. Please review the existing values and make
           any needed changes." />

Además de establecer sus propiedades Visible, EnabledViewState y Text, también se ha establecido la propiedad CssClass en Warning, lo que hace que la etiqueta se muestre en una fuente grande, roja, en cursiva y negrita. Esta clase Warning CSS se ha definido y agregado a Styles.css en el tutorial Examen de los eventos asociados con la inserción, actualización y eliminación.

Después de agregar estas etiquetas, el Diseñador de Visual Studio debe tener un aspecto similar a la figura 18.

Two Label Controls Have Been Added to the Page

Figura 18: Se han agregado dos controles Label a la página (Haga clic para ver la imagen a tamaño completo)

Con estos controles web Label establecidos, ya se puede examinar cómo determinar cuándo se ha producido una infracción de simultaneidad, en cuyo punto se puede establecer la propiedad Visible de Label adecuada en true y mostrar el mensaje informativo.

Control de infracciones de simultaneidad durante la actualización

Primero se verá cómo controlar las infracciones de simultaneidad al usar el patrón de actualización por lotes. Como estas infracciones con el patrón de actualización por lotes hacen que se produzca una excepción DBConcurrencyException, es necesario agregar código a la página ASP.NET para determinar si se ha producido una excepción DBConcurrencyException durante el proceso de actualización. Si es así, se debería mostrar un mensaje al usuario en el que explique que sus cambios no se han guardado porque otro usuario había modificado los mismos datos entre cuando comenzó a editar el registro y cuando hizo clic en el botón Actualizar.

Como ha visto en el tutorial Control de excepciones de nivel de BLL y DAL en una página ASP.NET, estas excepciones se pueden detectar y suprimir en los controladores de eventos de nivel posterior del control web de datos. Por tanto, es necesario crear un controlador de eventos para el evento RowUpdated de GridView que compruebe si se ha producido una excepción DBConcurrencyException. Este controlador de eventos pasa una referencia a cualquier excepción que se genere durante el proceso de actualización, como se muestra en el código del controlador de eventos siguiente:

Protected Sub ProductsGrid_RowUpdated _
        (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
        Handles ProductsGrid.RowUpdated
    If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
        If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
            ' Display the warning message and note that the exception has
            ' been handled...
            UpdateConflictMessage.Visible = True
            e.ExceptionHandled = True
        End If
    End If
End Sub

En el caso de una excepción DBConcurrencyException, este controlador de eventos muestra el control Label UpdateConflictMessage e indica que se ha controlado la excepción. Con este código implementado, cuando se produce una infracción de simultaneidad al actualizar un registro, se pierden los cambios del usuario, ya que habrían sobrescrito las modificaciones de otro usuario al mismo tiempo. En concreto, GridView se devuelve a su estado anterior a la edición y se enlaza a los datos de la base de datos actual. Esto actualizará la fila de GridView con los cambios del otro usuario, que anteriormente no eran visibles. Además, el control Label UpdateConflictMessage explicará al usuario lo que acaba de suceder. Esta secuencia de eventos se detalla en la figura 19.

A User s Updates are Lost in the Face of a Concurrency Violation

Figura 19: Se muestra un mensaje en caso de una infracción de simultaneidad (Haga clic para ver la imagen a tamaño completo)

Nota:

Como alternativa, en lugar de devolver GridView al estado anterior a la edición, se podría dejar en su estado de edición y establecer la propiedad KeepInEditMode del objeto GridViewUpdatedEventArgs pasado en true. Pero si adopta este enfoque, asegúrese de volver a enlazar los datos a GridView (mediante la invocación de su método DataBind()) para que los valores del otro usuario se carguen en la interfaz de edición. En el código disponible para descargar con este tutorial estas dos líneas del controlador de eventos RowUpdated están comentadas; simplemente quite la marca de comentario de estas líneas de código para que GridView permanezca en modo de edición después de una infracción de simultaneidad.

Respuesta a infracciones de simultaneidad durante la eliminación

Con el patrón directo de base de datos, no se produce ninguna excepción en caso de una infracción de simultaneidad. En su lugar, la instrucción de base de datos simplemente no afecta a ningún registro, ya que la cláusula WHERE no coincide con ningún registro. Todos los métodos de modificación de datos creados en la BLL se han diseñado de forma que devuelvan un valor booleano que indica si afectan o no a un registro concreto. Por tanto, para determinar si se ha producido una infracción de simultaneidad al eliminar un registro, se puede examinar el valor devuelto del método DeleteProduct de la BLL.

El valor devuelto de un método de la BLL se puede examinar en los controladores de eventos de nivel posterior de ObjectDataSource mediante la propiedad ReturnValue del objeto ObjectDataSourceStatusEventArgs pasado al controlador de eventos. Como le interesa determinar el valor devuelto del método DeleteProduct, es necesario crear un controlador de eventos para el evento Deleted de ObjectDataSource. La propiedad ReturnValue es de tipo object y puede ser null si se ha generado una excepción y el método se ha interrumpido antes de que pudiera devolver un valor. Por tanto, primero se debe asegurar de que la propiedad ReturnValue no es null y es un valor booleano. Si se supera esta comprobación, se muestra el control Label DeleteConflictMessage si ReturnValue es false. Esto se puede lograr mediante código como el siguiente:

Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
        (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
        Handles ProductsOptimisticConcurrencyDataSource.Deleted
    If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
        Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
        If deleteReturnValue = False Then
            ' No row was deleted, display the warning message
            DeleteConflictMessage.Visible = True
        End If
    End If
End Sub

En el caso de una infracción de simultaneidad, se cancela la solicitud de eliminación del usuario. El control GridView se actualiza, y se muestran los cambios que se han producido para ese registro entre el momento en que el usuario ha cargado la página y cuando ha hecho clic en el botón Eliminar. Cuando se produce una infracción de este tipo, se muestra la etiqueta DeleteConflictMessage, en la que se explica lo que acaba de suceder (vea la figura 20).

A User s Delete is Canceled in the Face of a Concurrency Violation

Figura 20: Se cancela la eliminación de un usuario en caso de una infracción de simultaneidad (Haga clic para ver la imagen a tamaño completo)

Resumen

Existen oportunidades de infracciones de simultaneidad en cada aplicación que permite a varios usuarios simultáneos actualizar o eliminar datos. Si no se tienen en cuenta estas infracciones, cuando dos usuarios actualizan simultáneamente los mismos datos, la última edición "gana", y se sobrescriben los cambios del otro usuario. Como alternativa, los desarrolladores pueden implementar el control de simultaneidad optimista o pesimista. El control de simultaneidad optimista supone que las infracciones de simultaneidad son poco frecuentes y simplemente no permite utilizar un comando de actualización o eliminación que constituiría una infracción de simultaneidad. El control de simultaneidad pesimista supone que las infracciones de simultaneidad son frecuentes y simplemente rechazar el comando de actualización o eliminación de un usuario no es aceptable. Con el control de simultaneidad pesimista, actualizar un registro implica bloquearlo, lo que impide que otros usuarios modifiquen o eliminen el registro mientras está bloqueado.

El conjunto de datos con tipo de .NET proporciona funcionalidad para admitir el control de simultaneidad optimista. En concreto, las instrucciones UPDATE y DELETE emitidas a la base de datos incluyen todas las columnas de la tabla, lo que garantiza que la actualización o eliminación solo se produzca si los datos actuales del registro coinciden con los datos originales que tenía el usuario al realizar su actualización o eliminación. Una vez que se configura la DAL para admitir la simultaneidad optimista, es necesario actualizar los métodos de la BLL. Además, la página ASP.NET que llama a la BLL se debe configurar de forma que ObjectDataSource recupere los valores originales de su control web de datos y los pase a la BLL.

Como ha visto en este tutorial, la implementación del control de simultaneidad optimista en una aplicación web ASP.NET implica actualizar la DAL y la BLL, y agregar compatibilidad en la página ASP.NET. Si este trabajo adicional es una inversión inteligente de tiempo y esfuerzo depende de la aplicación. Si tiene usuarios simultáneos que actualizan datos con poca frecuencia o los datos que actualizan son diferentes entre sí, el control de simultaneidad no es un problema clave. Pero si habitualmente tiene varios usuarios en el sitio que trabajan con los mismos datos, el control de simultaneidad puede ayudar a evitar que las actualizaciones o eliminaciones de un usuario sobrescriban involuntariamente las de otro.

¡Feliz programación!

Acerca del autor

Scott Mitchell, autor de siete libros de ASP y ASP.NET, y fundador de 4GuysFromRolla.com, trabaja con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, entrenador y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Puede ponerse en contacto con él a través de mitchell@4GuysFromRolla.com. o de su blog, que se puede encontrar en http://ScottOnWriting.NET.