Compartir a través de


Implementar la simultaneidad optimista (VB)

de 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 implementaremos 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 aquellas que tienen un único usuario que puede modificar los datos, no hay amenaza de que dos usuarios simultáneos sobrescriban accidentalmente los cambios entre ellos. Sin embargo, 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 ninguna directiva de simultaneidad vigente, cuando dos usuarios editan simultáneamente un único registro, el usuario que confirma sus cambios por última vez invalidará los cambios realizados por el primero.

Por ejemplo, imagine que dos usuarios, Jisun y Sam, estaban visitando una página en nuestra aplicación que permitía a los visitantes actualizar y eliminar los productos a través de un control GridView. Ambos hacen clic en el botón Editar de 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 UPDATE instrucción que se envía a la base de datos, que establece todos los campos actualizables del producto (aunque Jisun solo ha actualizado un campo, ProductName). En este momento, la base de datos tiene los valores "Chai Tea", la categoría Bebidas, el proveedor Exotic Liquids, y así sucesivamente para este producto en particular. Sin embargo, la GridView en la pantalla de Sam todavía muestra 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 UPDATE instrucción enviada a la base de datos que establece el nombre del producto en "Chai", el CategoryID al identificador de la categoría de Bebidas correspondiente, etc. Los cambios de Jisun en el nombre del producto han sido reemplazados. En la figura 1 se muestra gráficamente esta serie de eventos.

Cuando dos usuarios actualizan simultáneamente un registro, existe la posibilidad de que los cambios de un usuario sobrescriban los del otro.

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, un usuario podría estar en medio de la actualización de un registro cuando otro usuario lo elimina. O bien, entre cuando 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 -if los usuarios simultáneos modifican el mismo registro, permitir que la última confirmación gane (el comportamiento predeterminado)
  • Simultaneidad optimista - suponga que aunque puede haber conflictos de simultaneidad de vez en cuando, la gran mayoría del tiempo, estos conflictos no surgirán; por lo tanto, si surge un conflicto, simplemente informe al usuario que sus cambios no se pueden guardar porque otro usuario ha modificado los mismos datos.
  • Simultaneidad pesimista : supongamos que los conflictos de simultaneidad son comunes y que los usuarios no tolerarán que se les diga que los cambios no se guardaron debido a la actividad simultánea de otro usuario; por lo tanto, cuando un usuario inicia la actualización de un registro, bloquee dicho registro, lo que impide que otros usuarios editen o eliminen ese registro hasta que el usuario confirme sus modificaciones.

Hasta ahora, todos nuestros tutoriales han utilizado la estrategia de resolución de simultaneidad predeterminada, es decir, hemos dejado que la última escritura prevalezca. En este tutorial examinaremos cómo implementar el control de simultaneidad optimista.

Nota:

No veremos ejemplos de simultaneidad pesimistas en esta serie de tutoriales. La concurrencia pesimista rara vez se usa porque estos bloqueos, si no se liberan correctamente, pueden impedir que otros usuarios actualicen los datos. Por ejemplo, si un usuario bloquea un registro para su edición y, a continuación, sale durante el día antes de desbloquearlo, ningún otro usuario podrá actualizar ese registro hasta que el usuario original devuelva y complete su actualización. Por lo 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 de pedido, es un ejemplo de control de simultaneidad pesimista.

Paso 1: Examinar la implementación de la gestión de concurrencia 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 inicia el proceso de actualización o eliminación. Por ejemplo, al hacer clic en el botón Editar de una gridView editable, los valores del registro se leen de la base de datos y se muestran en cuadros de texto 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 valores se envían a la capa de lógica de negocios y, a continuación, 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 inició la edición 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.

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.

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 de tamaño completo).

Hay varios enfoques para implementar la simultaneidad optimista (consulte La lógica de actualización de simultaneidad optimista de Peter A. Bromberg para obtener una breve visión de una serie de opciones). El DataSet tipado de ADO.NET proporciona una implementación que se puede configurar simplemente marcando una casilla. Al habilitar la simultaneidad optimista para un TableAdapter en el DataSet Tipado, se aumentan las instrucciones de UPDATE y DELETE para incluir una comparación de todos los valores originales en la cláusula WHERE. La instrucción siguiente 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 recuperaron originalmente al actualizar el registro en GridView. Los @ProductName parámetros y @UnitPrice contienen los nuevos valores especificados por el usuario, mientras @original_ProductName que y @original_UnitPrice contienen los valores que originalmente se cargaron 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 UPDATE instrucción se ha simplificado para mejorar la legibilidad. En la práctica, la UnitPrice comprobación de la WHERE cláusula sería más compleja, ya que UnitPrice puede contener NULL s y comprobar si NULL = NULL siempre devuelve Falso (en su lugar, debe usar IS NULL).

Además de usar una instrucción subyacente UPDATE diferente, 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 de nuestro 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 BLL para usar el patrón de actualización por lotes (las Update() sobrecargas de método que aceptan DataRows y DataTables en lugar de valores escalares) también deben cambiarse.

En lugar de ampliar los TableAdapters de la DAL existente para usar la simultaneidad optimista (lo que requeriría cambiar el BLL para dar cabida), vamos a crear un nuevo Conjunto de datos con tipo denominado NorthwindOptimisticConcurrency, al que agregaremos un Products TableAdapter que use la simultaneidad optimista. Después, crearemos una ProductsOptimisticConcurrencyBLL clase de capa lógica de negocios que tenga las modificaciones adecuadas para admitir la dal de simultaneidad optimista. Una vez que se haya establecido esta base, estaremos listos para crear la página de ASP.NET.

Paso 2: Crear una capa de acceso a datos que admita la concurrencia optimista

Para crear un nuevo conjunto de datos con tipo, haga clic con el botón derecho en la DAL carpeta dentro de la App_Code carpeta y agregue un nuevo conjunto de datos denominado NorthwindOptimisticConcurrency. Como vimos en el primer tutorial, al hacerlo se agregará un nuevo TableAdapter al Dataset tipado, iniciando automáticamente el Asistente de configuración del 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 la NORTHWNDConnectionString configuración de Web.config.

Conexión a la misma base de datos Northwind

Figura 3: Conectarse a la misma base de datos Northwind (haga clic para ver la imagen de tamaño completo)

A continuación, se le pedirá cómo consultar los datos: a través de una instrucción SQL ad hoc, un nuevo procedimiento almacenado o un procedimiento almacenado existente. Puesto que usamos consultas SQL ad hoc en nuestra DAL original, use esta opción aquí también.

Especificar los datos que se van a recuperar mediante una instrucción SQL ad hoc

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

En la siguiente pantalla, escriba la consulta SQL que se usará para recuperar la información del producto. Vamos a usar la misma consulta SQL exacta que se usa para Products TableAdapter de nuestra DAL original, que devuelve todas las Product columnas 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

Usar la misma consulta SQL de Products TableAdapter en la DAL original

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

Antes de pasar a la siguiente pantalla, haga clic en el botón Opciones avanzadas. Para que tableAdapter emplee el control de simultaneidad optimista, simplemente active la casilla "Usar simultaneidad optimista".

Habilitar el control de simultaneidad optimista comprobando la casilla

Figura 6: Habilitar el control de simultaneidad optimista comprobando la casilla "Usar simultaneidad optimista" (haga clic para ver la imagen de tamaño completo)

Por último, indique que el TableAdapter debe utilizar los patrones de acceso a datos que tanto rellenan un DataTable como devuelven un DataTable; indique también que se deben crear los métodos directos de la base de datos. Cambie el nombre del método para devolver un patrón DataTable de GetData a GetProducts, de modo que refleje las convenciones de nomenclatura que usamos en nuestra DAL original.

Hacer que TableAdapter use todos los patrones de acceso a datos

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

Después de completar el asistente, el Diseñador de DataSet incluirá un DataTable fuertemente tipado Products y un TableAdapter. Dedique un momento a cambiar el nombre de DataTable de Products a ProductsOptimisticConcurrency, lo que puede hacer haciendo clic con el botón derecho en la barra de título de DataTable y seleccionando Cambiar nombre en el menú contextual.

Se han agregado DataTable y TableAdapter al DataSet tipado

Figura 8: Se ha agregado una DataTable y TableAdapter al DataSet tipado (haga clic para ver la imagen de tamaño completo)

Para ver las diferencias entre las consultas UPDATE y DELETE del TableAdapter ProductsOptimisticConcurrency (que usa la simultaneidad optimista) y del TableAdapter de Productos (que no), haga clic en el TableAdapter y vaya a la ventana de Propiedades. En las subpropiedades de las propiedades DeleteCommand y UpdateCommandCommandText 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 DELETE instrucción usada es:

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 DELETE declaración de Product TableAdapter en nuestra DAL original es mucho más sencilla:

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

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

No agregaremos DataTables adicionales al conjunto de datos con simultaneidad optimista habilitada para este tutorial, ya que nuestra página de ASP.NET solo proporcionará la actualización y eliminación de la información del producto. Sin embargo, todavía es necesario agregar el GetProductByProductID(productID) método a ProductsOptimisticConcurrency TableAdapter.

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

Marque el tipo de consulta como

Figura 9: Marcar el tipo de consulta como "SELECT que devuelve filas" (Haga clic para ver la imagen de 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.

Agregar una cláusula WHERE a la consulta precargada para devolver un registro de producto específico

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

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

Cambiar el nombre de los métodos a FillByProductID y GetProductByProductID

Figura 11: Cambiar el nombre de los métodos a FillByProductID y GetProductByProductID (Haga clic para ver la imagen de 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: Crear una capa de lógica empresarial para la DAL optimista Concurrency-Enabled

Nuestra clase existente ProductsBLL 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 UpdateProduct usan el patrón de actualización por lotes, pasando una instancia ProductRow al método Update de TableAdapter. Por otro lado, el método DeleteProduct utiliza el patrón directo de BD, llamando al método Delete(productID) del TableAdapter.

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

Aunque la firma del método del método de Update TableAdapter utilizado en el patrón de actualización por lotes no ha cambiado, el código necesario para registrar los valores originales y nuevos sí lo ha hecho. Por lo tanto, en lugar de intentar utilizar el DAL con compatibilidad para concurrencia optimista con nuestra clase existente ProductsBLL, vamos a crear una nueva clase de capa de lógica de negocio para trabajar con nuestro nuevo DAL.

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

Agregue la clase ProductsOptimisticConcurrencyBLL a la carpeta BLL.

Figura 12: Agregar la ProductsOptimisticConcurrencyBLL clase a la carpeta BLL

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

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 using NorthwindOptimisticConcurrencyTableAdapters antes de iniciar la declaración de clase. El NorthwindOptimisticConcurrencyTableAdapters espacio de nombres contiene la ProductsOptimisticConcurrencyTableAdapter clase , que proporciona los métodos de la DAL. También antes de la declaración de clase encontrará el System.ComponentModel.DataObject atributo , que indica a Visual Studio que incluya esta clase en la lista desplegable del asistente ObjectDataSource.

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

Eliminación de un producto utilizando el patrón DB Direct con concurrencia optimista

Cuando se utiliza el patrón de base de datos directo contra un DAL que emplea concurrencia optimista, se deben proporcionar los valores nuevos y originales a los métodos. Para eliminar, no hay nuevos valores, por lo que solo se deben pasar los valores originales. En nuestro BLL, debemos aceptar todos los parámetros originales como parámetros de entrada. Vamos a hacer que el DeleteProduct método de la ProductsOptimisticConcurrencyBLL clase 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 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 valores que se cargaron por última vez en 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 WHERE cláusula no coincidirá con ningún registro de base de datos y no se verá afectado ningún registro. Por lo tanto, el método de Delete TableAdapter devolverá 0 y el método de DeleteProduct BLL devolverá false.

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

Como se señaló anteriormente, el método Update del TableAdapter para el patrón de actualización por lotes tiene la misma firma de método independientemente de si se emplea o no la concurrencia optimista. Es decir, el Update método espera un DataRow, una matriz de DataRows, una DataTable o un DataSet con tipo. No hay parámetros de entrada adicionales para especificar los valores originales. Esto es posible porque DataTable realiza un seguimiento de los valores originales y modificados de sus DataRow(s). Cuando dal emite su UPDATE instrucción, los @original_ColumnName parámetros se rellenan con los valores originales de DataRow, mientras que los @ColumnName parámetros se rellenan con los valores modificados de DataRow.

En la ProductsBLL clase (que usa nuestra 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, nuestro código realiza la siguiente secuencia de eventos:

  1. Lee la información actual del producto de la base de datos en una instancia ProductRow utilizando el método GetProductByProductID(productID) de TableAdapter.
  2. Asignación de los nuevos valores a la ProductRow instancia del paso 1
  3. Llame al método Update del TableAdapter, pasando la instancia ProductRow.

No obstante, esta secuencia de pasos no admitirá correctamente la concurrencia optimista porque el ProductRow del paso 1 se completa 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 aquellos que estaban enlazados a GridView al inicio del proceso de edición. En su lugar, al usar una DAL habilitada para simultaneidad optimista, es necesario modificar las sobrecargas del UpdateProduct método para usar los pasos siguientes:

  1. Lee la información actual del producto de la base de datos en una instancia ProductsOptimisticConcurrencyRow utilizando el método GetProductByProductID(productID) de TableAdapter.
  2. Asignación de los valores originales a la ProductsOptimisticConcurrencyRow instancia del paso 1
  3. Llame al método ProductsOptimisticConcurrencyRow de la instancia AcceptChanges(), el cual indica al DataRow que sus valores actuales son los "originales".
  4. Asigna los nuevos valores a la ProductsOptimisticConcurrencyRow instancia
  5. Llame al método Update del TableAdapter, pasando la instancia ProductsOptimisticConcurrencyRow.

El paso 1 lee todos los valores actuales de la base de datos para el registro de producto especificado. Este paso es superfluo en la UpdateProduct sobrecarga que actualiza todas las columnas del 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 los valores originales han sido asignados a la instancia 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, pasando la fila de datos.

El código siguiente muestra la UpdateProduct sobrecarga que acepta todos los campos de datos del producto como parámetros de entrada. Aunque no se muestra aquí, la ProductsOptimisticConcurrencyBLL clase incluida en la descarga de este tutorial también contiene una UpdateProduct sobrecarga 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: Pasar los valores originales y nuevos de la página de ASP.NET a los métodos BLL

Con dal y BLL completados, todo lo que queda es crear una página de 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 debe configurarse para controlar correctamente las infracciones de simultaneidad.

Para empezar, abra la OptimisticConcurrency.aspx página en la EditInsertDelete carpeta y agregue GridView al Diseñador, estableciendo su ID propiedad en ProductsGrid. En la etiqueta inteligente de GridView, opte por crear un objeto ObjectDataSource denominado ProductsOptimisticConcurrencyDataSource. Dado que queremos que este ObjectDataSource use la DAL que admite la simultaneidad optimista, configúrela para usar el ProductsOptimisticConcurrencyBLL objeto .

Hacer que ObjectDataSource utilice el objeto ProductsOptimisticConcurrencyBLL

Figura 13: Hacer que objectDataSource use el objeto (ProductsOptimisticConcurrencyBLL la imagen de 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 de ObjectDataSource debe ser 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 DeleteParameters colección contiene una Parameter instancia de cada uno de los diez parámetros de entrada del método de ProductsOptimisticConcurrencyBLL la DeleteProduct clase. Del mismo modo, la UpdateParameters colección contiene una Parameter instancia para cada uno de los parámetros de entrada en UpdateProduct.

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

Nota:

El valor de la OldValuesParameterFormatString propiedad debe corresponder a los nombres de los parámetros de entrada del BLL que esperan los valores originales. Puesto que nombramos estos parámetros como original_productName, original_supplierID, etc., puede dejar el valor de la propiedad como OldValuesParameterFormatStringoriginal_{0}. Sin embargo, si los parámetros de entrada de los métodos BLL tuvieran nombres como old_productName, old_supplierID, etc., tendrías que actualizar la propiedad OldValuesParameterFormatString a old_{0}.

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

  • OverwriteChanges : el valor predeterminado; no envía los valores originales a los parámetros de entrada originales de los métodos BLL.
  • CompareAllValues - envía los valores originales a los métodos 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, vamos a poner nuestra atención en la configuración de GridView. En primer lugar, dado que queremos que GridView admita la edición y eliminación, haga clic en las casillas Habilitar edición y Habilitar eliminación de la etiqueta inteligente de GridView. Esto agregará un CommandField cuyo ShowEditButton y ShowDeleteButton están establecidos en true.

Cuando se enlaza a ProductsOptimisticConcurrencyDataSource ObjectDataSource, 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 de ninguna manera aceptable. CategoryID y SupplierID BoundFields se mostrarán como TextBoxes, lo que requiere que el usuario ingrese la categoría y el proveedor adecuados como números de ID. 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 en orden y los valores de nivel de reordenación son valores numéricos adecuados y son mayores o iguales a cero.

Como hemos explicado en los tutoriales Agregar 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 reemplazando BoundFields por TemplateFields. He modificado esta GridView y su interfaz de edición de las maneras siguientes:

  • Se han quitado ProductID, SupplierName y CategoryName campos asociados
  • Convirtió boundField ProductName en templateField y agregó un control RequiredFieldValidation.
  • Se convirtieron los CategoryID y SupplierID BoundFields en TemplateFields, y se ajustó la interfaz de edición para usar listas desplegables en lugar de cuadros de texto. En estos TemplateFields ItemTemplates, se muestran los campos de datos CategoryName y SupplierName.
  • Convirtió los UnitPrice, UnitsInStock, UnitsOnOrder y ReorderLevel BoundFields en TemplateFields y agregó controles de validación por comparación.

Puesto que ya hemos examinado cómo realizar estas tareas en tutoriales anteriores, solo enumeraré la sintaxis declarativa final aquí y 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>

Estamos muy cerca de tener un ejemplo plenamente funcional. Sin embargo, hay algunas sutilezas que surgirán y nos causarán problemas. Además, todavía necesitamos alguna interfaz que alerte al usuario cuando se ha producido una infracción de simultaneidad.

Nota:

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

Pasar los valores originales correctos al objectDataSource

Hay un par de problemas con la forma en que se ha configurado GridView. Si la propiedad ConflictDetection de ObjectDataSource está establecida como CompareAllValues (como en nuestro caso), cuando los métodos Update() o Delete() de ObjectDataSource son invocados por GridView (o DetailsView o FormView), ObjectDataSource intenta copiar los valores originales de GridView en sus instancias adecuadas Parameter. Consulte la figura 2 para obtener 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 bidireccionales cada vez que los datos están enlazados a GridView. Por lo tanto, es esencial que todos los valores originales necesarios se capturen a través del enlace de datos bidireccional y que se proporcionen en un formato convertible.

Para ver por qué esto es importante, dedique un momento a visitar nuestra 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.

Los productos se enumeran en una vista de cuadrícula

Figura 14: Los productos aparecen en una vista GridView (haga clic para ver la imagen de tamaño completo)

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

Si se intenta eliminar cualquier producto, se produce una excepción FormatException

Figura 15: Intentar eliminar cualquier producto resulta en un FormatException (haga clic para ver la imagen de tamaño completo)

FormatException se genera cuando ObjectDataSource intenta leer el valor originalUnitPrice. ItemTemplate Puesto que tiene el UnitPrice 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, tenemos una serie de opciones:

  • Quite el formato de moneda del ItemTemplate. Es decir, en lugar de usar <%# Bind("UnitPrice", "{0:C}") %>, simplemente use <%# Bind("UnitPrice") %>. La desventaja de esto es que el precio ya no está formateado.
  • Muestra el UnitPrice formateado como moneda en ItemTemplate, pero utiliza la palabra clave Eval para lograrlo. Recuerde que Eval realiza el enlace de datos unidireccional. Todavía necesitamos proporcionar el valor UnitPrice para los valores originales, por lo que seguiremos necesitando una instrucción de enlace de datos bidireccional en ItemTemplate, pero esto se puede colocar en un control Web de etiqueta cuya propiedad Visible esté ajustada a false. Podríamos 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, usando <%# Bind("UnitPrice") %>. En el controlador de eventos de RowDataBound del GridView, acceda programáticamente al control Web Label donde se muestra el valor UnitPrice y establezca su propiedad Text en la versión con formato.
  • Deje el UnitPrice formateado como moneda. En el controlador de eventos de RowDeleting GridView, reemplace el valor original UnitPrice existente ($19,95) por un valor decimal real mediante Decimal.Parse. Hemos visto cómo lograr algo similar en el RowUpdating controlador de eventos en el tutorial Control de excepciones BLL y DAL-Level en una página de ASP.NET .

En mi ejemplo, elegí ir con el segundo enfoque, agregando un control web de etiqueta oculto cuya Text propiedad es datos bidireccionales UnitPrice enlazados al valor 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 el ObjectDataSource intente invocar el método UpdateProduct de la BLL.

ObjectDataSource no puede encontrar un método con los parámetros de entrada que desea enviar

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

Al examinar el mensaje de la excepción, está claro que ObjectDataSource quiere invocar un método BLL DeleteProduct que incluya original_CategoryName y original_SupplierName parámetros de entrada. Esto se debe a que los ItemTemplate para los CategoryID y SupplierID TemplateFields actualmente contienen declaraciones de enlace bidireccional con los campos de datos CategoryName y SupplierName. En cambio, debemos incluir Bind instrucciones junto con los campos de datos CategoryID y SupplierID. Para ello, reemplace las instrucciones Bind existentes por Eval instrucciones y agregue controles Label ocultos cuyas Text propiedades están enlazadas a los CategoryID campos de datos 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 podemos eliminar y editar correctamente la información del producto. En el paso 5 veremos 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 soporte para concurrencia optimista

Para comprobar que se detectan infracciones de simultaneidad (en lugar de provocar que los datos se sobrescriban ciegamente), 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. A continuación, en solo uno de los exploradores, cambie el nombre a "Chai Tea" y haga clic en Actualizar. La actualización debe realizarse correctamente y devolver GridView a su estado de edición previa, con "Chai Tea" como el nuevo nombre del producto.

Sin embargo, en la otra instancia de ventana del explorador, el nombre del producto TextBox sigue apareciendo "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 navegador, se cambiaría el nombre del producto a "Chai", sobrescribiendo así los cambios realizados por la primera instancia del navegador. Sin embargo, con la simultaneidad optimista empleada, al hacer clic en el botón Actualizar de la segunda instancia del explorador, se produce una excepción DBConcurrencyException.

Cuando se detecta una infracción de simultaneidad, se lanza una excepción DBConcurrencyException.

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

DBConcurrencyException solo se produce cuando se utiliza el patrón de actualización por lotes del 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 gridView de ambas instancias del explorador a su estado de edición previa. A continuación, en la primera instancia del explorador, haga clic en el botón Editar y cambie el nombre del producto de "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 se vuelve a enviar, GridView invoca el método Delete() del ObjectDataSource y ObjectDataSource llama al método ProductsOptimisticConcurrencyBLL de la clase DeleteProduct, pasando los valores originales. El valor original ProductName de la segunda instancia del explorador es "Chai Tea", que no coincide con el valor actual ProductName de la base de datos. Por lo tanto, la DELETE instrucción emitida a la base de datos afecta a cero filas, ya que no hay ningún registro en la base de datos que cumpla la cláusula WHERE. El DeleteProduct método 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 parpadeó 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, eliminar se realizará correctamente, ya que el valor original ProductName de GridView ("Chai") ahora coincide con el valor de la base de datos.

En ambos casos, la experiencia del usuario está lejos de ser ideal. Claramente no queremos mostrar al usuario los detalles nitty-gritty de la DBConcurrencyException excepción 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 produjo un error en el comando users, pero no hubo ninguna indicación precisa de por qué.

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

Paso 6: Agregar mensajes informativos y mostrarlos ante una violación de concurrencia

Cuando se produce una infracción de simultaneidad, el comportamiento mostrado depende de si se usó la actualización por lotes de la DAL o el patrón directo de base de datos. En nuestro tutorial se usan ambos patrones, con el patrón de actualización por lotes que se usa para actualizar y el patrón directo de base de datos que se usa para eliminar. Para empezar, vamos a agregar dos controles Web de etiqueta a nuestra página que explican que se produjo 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 oculten en cada visita de página, excepto para aquellas visitas a páginas concretas donde 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 Visible, EnabledViewState y Text, también he establecido la propiedad CssClass a Warning, lo que hace que el rótulo se muestre en una fuente grande, roja, cursiva y negrita. ** Esta clase CSS Warning se definió y se añadió a Styles.css en el tutorial titulado Examinar 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.

Se han agregado dos controles de etiqueta a la página

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

Con estos controles Web de etiqueta establecidos, estamos listos para examinar cómo determinar cuándo se ha producido una infracción de simultaneidad, en cuyo punto se puede establecer la propiedad adecuada de la etiqueta Visible en true, mostrando el mensaje informativo.

Manejo de violaciones de concurrencia al actualizar

Veamos primero cómo controlar las infracciones de simultaneidad al usar el patrón de actualización por lotes. Dado que estas infracciones con el patrón de actualización por lotes hacen que se produzca una DBConcurrencyException excepción, es necesario agregar código a nuestra página de ASP.NET para determinar si se produjo una DBConcurrencyException excepción durante el proceso de actualización. Si es así, deberíamos mostrar un mensaje al usuario que explica que sus cambios no se guardaron 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 vimos en el tutorial Control de excepciones BLL y DAL-Level en una página de ASP.NET, estas excepciones se pueden detectar y suprimir en los manejadores de eventos de nivel post del control web de datos. Por lo tanto, es necesario crear un controlador de eventos para el evento de RowUpdated GridView que comprueba si se ha producido una DBConcurrencyException excepción. Este controlador de eventos se le pasa una referencia a cualquier excepción que se generó durante el proceso de actualización, como se muestra en el siguiente código del controlador de eventos:

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 DBConcurrencyException excepción, este controlador de eventos muestra el UpdateConflictMessage control Label e indica que la excepción ha sido gestionada. 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 de edición previa y se enlaza a los datos de la base de datos actual. Esto actualizará la fila GridView con los cambios del otro usuario, que anteriormente no eran visibles. Además, el control de etiqueta UpdateConflictMessage explicará al usuario lo que acaba de suceder. Esta secuencia de eventos se detalla en la figura 19.

Las actualizaciones de un usuario se pierden ante una violación de concurrencia

Figura 19: Las actualizaciones de un usuario se pierden debido a una violación de concurrencia (haga clic para ver la imagen de tamaño completo)

Nota:

Como alternativa, en lugar de devolver el GridView al estado previo a la edición, podríamos dejar el GridView en su estado de edición estableciendo la propiedad KeepInEditMode del objeto pasado en GridViewUpdatedEventArgs a verdadero. Sin embargo, si adopta este enfoque, asegúrese de volver a enlazar los datos a GridView (invocando su DataBind() método) para que los valores del otro usuario se carguen en la interfaz de edición. El código disponible para su descarga con este tutorial tiene estas dos líneas de código en el RowUpdated controlador de eventos comentadas; simplemente descomente estas líneas de código para que GridView permanezca en el modo de edición después de una violación de concurrencia.

Responder a violaciones de concurrencia al eliminar

Con el patrón directo de base de datos, no se produce ninguna excepción ante una infracción de concurrencia. 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 precisamente a un registro. Por lo tanto, para determinar si se produjo una infracción de simultaneidad al eliminar un registro, podemos examinar el valor devuelto del método de DeleteProduct BLL.

El valor devuelto de un método BLL se puede examinar en los manejadores de eventos posteriores de ObjectDataSource a través de la propiedad ReturnValue del objeto ObjectDataSourceStatusEventArgs pasado al manejador de eventos. Puesto que estamos interesados en determinar el valor devuelto del DeleteProduct método , es necesario crear un controlador de eventos para el evento ObjectDataSource Deleted . La ReturnValue propiedad es de tipo object y puede ser null si se generó una excepción y el método se interrumpió antes de que pudiera devolver un valor. Por lo tanto, primero debemos asegurarnos de que la ReturnValue propiedad no sea null y que sea un valor booleano. Suponiendo que se supere esta comprobación, mostramos el control de etiqueta DeleteConflictMessage si ReturnValue es false. Esto se puede lograr mediante el código 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. GridView se actualiza, mostrando los cambios que se produjeron para ese registro entre el momento en que el usuario cargó la página y cuando hizo clic en el botón Eliminar. Cuando se produce una infracción de este tipo, se muestra la DeleteConflictMessage etiqueta, que explica lo que acaba de suceder (vea la figura 20).

La eliminación de un usuario se cancela debido a una violación de concurrencia

Figura 20: La eliminación de un usuario se cancela debido a una violación de concurrencia (haga clic para ver la imagen de 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 violaciones, cuando dos usuarios actualizan simultáneamente los mismos datos, el que realiza la última escritura "gana", sobrescribiendo 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 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 tipado en .NET ofrece soporte para el control de concurrencia optimista. En concreto, las UPDATE instrucciones 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 configurado el DAL para admitir la simultaneidad optimista, es necesario actualizar los métodos BLL. Además, la página ASP.NET que llama al BLL debe configurarse de forma que ObjectDataSource recupere los valores originales de su control web de datos y los pase al BLL.

Como vimos en este tutorial, la implementación del control de simultaneidad optimista en una aplicación web de ASP.NET implica actualizar la DAL y BLL y agregar compatibilidad en la página de ASP.NET. Si invertir tiempo y esfuerzo en este trabajo adicional es una decisión sabia depende de cómo lo apliques. 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. Sin embargo, 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 sobrescriba involuntariamente las de otro.

¡Feliz programación!

Acerca del autor

Scott Mitchell, autor de siete libros de ASP/ASP.NET y fundador de 4GuysFromRolla.com, ha estado trabajando 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 en 24 horas. Se puede contactar con él en mitchell@4GuysFromRolla.com.