Compartir a través de


Implementar la simultaneidad optimista (C#)

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 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 para aquellos que incluyen un solo usuario que puede modificar los datos, no hay ninguna amenaza de dos usuarios simultáneos sobrescribiendo accidentalmente los cambios del otro. 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 confirme sus cambios por última vez invalidará los cambios realizados por el primero.

Imagine que dos usuarios, Jisun y Sam, estaban visitando una página en una aplicación que permitía a los visitantes actualizar y eliminar 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 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 un campo, 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. Sin embargo, 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 de UPDATE enviada a la base de datos que establece el nombre del producto en "Chai", el id. de categoría CategoryID de 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, hay posibles cambios de un usuario para sobrescribir los otros (haga clic para ver la imagen en 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: si los usuarios simultáneos modifican el mismo registro, deje que la última confirmación gane (el comportamiento predeterminado)
  • Simultaneidad optimista: supongamos que, aunque puede haber conflictos de simultaneidad cada vez, la gran mayoría del tiempo no surgirán estos conflictos; por lo 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: 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 comienza a actualizar un registro, bloquéelo, lo que impide que otros usuarios editen o eliminen ese registro hasta que el usuario confirme sus modificaciones.

Todos nuestros tutoriales han usado hasta ahora la estrategia de resolución de simultaneidad predeterminada, es decir, hemos dejado que la última edición gane. En este tutorial examinaremos cómo implementar el control de simultaneidad optimista.

Nota:

No veremos ejemplos de simultaneidad pesimista en esta serie de tutoriales. La simultaneidad pesimista rara vez se usa porque estos bloqueos, si no se renuncian 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: Ver cómo se implementa 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 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 TextBoxes 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 empezó 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 en tamaño completo)

Hay varios enfoques para implementar la simultaneidad optimista (consulte Peter A. Brombergla lógica de actualización de simultaneidad optimista para obtener un breve vistazo a una serie de opciones). ADO.NET DataSet con tipo proporciona una implementación que se puede configurar con solo marcar una casilla. Al habilitar la simultaneidad optimista para un TableAdapter en el DataSet con tipo, se aumentan las instrucciones UPDATE y DELETE de TableAdapter para incluir una comparación de todos los valores originales de 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 recuperaron originalmente al actualizar el registro en 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 cargaron 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 implicada, 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 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 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 BLL para usar el patrón de actualización por lotes (las sobrecargas de método Update() 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 DataSet con tipo denominado NorthwindOptimisticConcurrency, al que agregaremos un TableAdapter Products que use la simultaneidad optimista. Después, crearemos 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, estaremos listos para crear la página de ASP.NET.

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

Para crear un nuevo 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 vimos en el primer tutorial, al hacerlo se agregará un nuevo TableAdapter al DataSet con tipo, iniciando 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 la configuración NORTHWNDConnectionString de Web.config.

Connect to the Same Northwind Database

Figura 3: Conectar a la misma base de datos Northwind (haga clic para ver la imagen en 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.

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

Figura 4: Especificar los datos a recuperar mediante una instrucción SQL ad-hoc (haga clic para ver la imagen en 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 TableAdapter Products de nuestra 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: Usar la misma consulta SQL de TableAdapter Products en la DAL original (haga clic para ver la imagen en tamaño completo)

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

Enable Optimistic Concurrency Control by Checking the

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

Por último, indique que TableAdapter debe usar los patrones de acceso a datos que rellenan DataTable y devuelven una 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 usamos en nuestra 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 en tamaño completo)

Después de completar el asistente, DataSet Designer incluirá una DataTable Products fuertemente tipada y 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.

A DataTable and TableAdapter Have Been Added to the Typed DataSet

Figura 8: Se han añadido una DataTable y TableAdapter al DataSet tipado (haga clic para ver la imagen en tamaño completo)

Para ver las diferencias entre las consultas UPDATE y DELETE entre TableAdapter ProductsOptimisticConcurrency (que usa la simultaneidad optimista) y Products TableAdapter (que no), haga clic en TableAdapter y vaya a la ventana Propiedades. En las subpropiedades de las propiedades DeleteCommand y UpdateCommand, CommandText, 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 el TableAdapter ProductsOptimisticConcurrency, la instrucción DELETE 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 instrucción DELETE 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 WHERE de la instrucción DELETE para 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 GridView (o DetailsView o FormView). Dado que todos los campos distintos de ProductID, ProductNamey Discontinued pueden tener valores NULL, se incluyen parámetros y comprobaciones adicionales para comparar correctamente los valores NULL de la cláusula WHERE.

No agregaremos DataTables adicionales al conjunto de datos habilitado para simultaneidad optimista 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 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. 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 método GetProductByProductID(productID) mediante una instrucción SQL ad-hoc (vea la figura 4). Dado que 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: 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.

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

Figura 10: Agregar una cláusula WHERE 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.

Rename the Methods to FillByProductID and GetProductByProductID

Figura 11: Cambiar el nombre de los métodos a FillByProductID y GetProductByProductID (haga clic para ver la imagen en 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 de negocios para la DAL habilitada para simultaneidad optimista

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 usa el patrón directo de base de datos, llamando al método Delete(productID) de TableAdapter.

Con el nuevo 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 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, solo eliminando el registro especificado si los valores actuales de la base de datos se asignan a los originales.

Aunque la firma del método para el 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 lo tanto, en lugar de intentar usar la DAL habilitada para simultaneidad optimista con nuestra clase existente ProductsBLL, vamos a crear una nueva clase de capa lógica de negocios para trabajar con nuestra 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: Agregar la clase ProductsOptimisticConcurrencyBLL a la carpeta BLL

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

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

Anote el uso de la instrucción NorthwindOptimisticConcurrencyTableAdapters 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 ProductsOptimisticConcurrencyBLL de Adapter proporciona acceso rápido a una instancia de la clase ProductsOptimisticConcurrencyTableAdapter y sigue el patrón usado en nuestras clases BLL originales (ProductsBLL, CategoriesBLL, etc.). Por último, el método GetProducts() simplemente llama al método de DAL GetProducts() y devuelve un objeto ProductsOptimisticConcurrencyDataTable rellenado con una instancia 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 simultaneidad optimista, los métodos deben pasar los valores nuevos y originales. Para la eliminación, 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 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 DAL, como se muestra en el código siguiente:

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = 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;
}

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 cláusula WHERE no coincidirá con ningún registro de base de datos y no afectará a ningún registro. Por lo tanto, el método de TableAdapter Delete devolverá 0 y el método DeleteProduct de BLL devolverá false.

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

Como se indicó anteriormente, 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 emplea o no la simultaneidad optimista. Es decir, el método Update 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. Cuando 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 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 mediante el método GetProductByProductID(productID) de TableAdapter.
  2. Asigna los nuevos valores a la instancia ProductRow del paso 1
  3. Llame al método Update de TableAdapter y pasa la instancia ProductRow

Sin embargo, esta secuencia de pasos no admitirá correctamente la simultaneidad optimista porque el 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 a GridView al principio del proceso de edición. En su lugar, al usar una DAL habilitada para simultaneidad optimista, es necesario modificar las sobrecargas del método UpdateProduct para usar los pasos siguientes:

  1. Lea la información actual del producto de la base de datos en una instancia ProductsOptimisticConcurrencyRow mediante el método GetProductByProductID(productID) de TableAdapter.
  2. Asigne los valores originales a la instancia ProductsOptimisticConcurrencyRow del paso 1
  3. Llame al método ProductsOptimisticConcurrencyRow de la instancia AcceptChanges(), que indica al DataRow que sus valores actuales son los "originales".
  4. Asigne los nuevos valores a la instancia ProductsOptimisticConcurrencyRow
  5. Llame al método Update de TableAdapter y pase 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 sobrecarga 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 asignados los valores originales 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 y se pasa DataRow.

El código siguiente muestra la sobrecarga 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 UpdateProduct que acepta solo el nombre y el precio del producto como parámetros de entrada.

protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
    // new parameter values
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued, int productID,
    // original parameter values
    string original_productName, int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued,
    int original_productID)
{
    // STEP 1: Read in the current database product information
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
        Adapter.GetProductByProductID(original_productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = 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
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

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 página OptimisticConcurrency.aspx en la carpeta EditInsertDelete y agregue GridView al Diseñador, estableciendo su propiedad ID en ProductsGrid. En la etiqueta inteligente GridView, elija 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 objeto ProductsOptimisticConcurrencyBLL.

Have the ObjectDataSource Use the ProductsOptimisticConcurrencyBLL Object

Figura 13: Solicitar a ObjectDataSource que use el objeto ProductsOptimisticConcurrencyBLL (haga clic aquí 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 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, quitaríamos la propiedad OldValuesParameterFormatString de ObjectDataSource 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 propiedad OldValuesParameterFormatString debe asignarse a los nombres de parámetro de entrada del BLL que esperan los valores originales. Puesto que denominamos estos parámetros original_productName, original_supplierID, etc., puede dejar el valor de propiedad OldValuesParameterFormatString como original_{0}. Sin embargo, si los parámetros de entrada de los métodos BLL tenían nombres como old_productName, old_supplierID, etc., tendría 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 GridView. Esto agregará un CommandField cuyo ShowEditButton y ShowDeleteButton están 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 BoundFields 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 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:

  • He quitado los BoundFields ProductID, SupplierName y CategoryName
  • He convertido ProductName BoundField en TemplateField y agregado un control RequiredFieldValidation.
  • He convertido los BoundFields CategoryID y SupplierID en TemplateFields, y he ajustado la interfaz de edición para que utilice DropDownLists en lugar de cuadros de texto. En estos campos ItemTemplates de TemplateFields, se muestran los campos de datos CategoryName y SupplierName.
  • He convertido UnitPrice, UnitsInStock, UnitsOnOrder, y ReorderLevel BoundFields en TemplateFields y añadido controles CompareValidator.

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 de trabajo completo. Sin embargo, hay algunas sutilezas que pueden aparecer y causar 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 EnableViewState de GridView esté establecida en true (valor predeterminado). Si deshabilita el estado de vista, los valores originales se pierden en postback.

Pasar 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 la nuestra), cuando el Update() de ObjectDataSource o los métodos Delete() son invocados por GridView (o DetailsView o FormView), ObjectDataSource intenta copiar los valores originales de GridView en sus instancias Parameter adecuadas. 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 mediante el 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.

The Products are Listed in a GridView

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

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

Attempting to Delete Any Product Results in a FormatException

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

FormatException se genera cuando ObjectDataSource intenta leer en el valor original UnitPrice. Puesto que ItemTemplate tiene el formato de moneda UnitPrice (<%# 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:

  • Quitar el formato de moneda de 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 tiene formato.
  • Mostrar el formato de moneda UnitPrice en ItemTemplate; pero use la palabra clave Eval para lograrlo. 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 necesitaremos 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í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 mediante <%# Bind("UnitPrice") %>. En el controlador de eventos RowDataBound de GridView, acceda mediante programación al control web Label dentro del cual se muestra el valor UnitPrice y establezca su propiedad Text en la versión con formato.
  • Deje el UnitPrice con formato de moneda. En el controlador de eventos RowDeleting de GridView, reemplace el valor original existente UnitPrice ($19,95) por un valor decimal real mediante Decimal.Parse. Hemos visto cómo lograr algo similar en el controlador de eventos RowUpdating en el tutorial Control de excepciones de nivel BLL y DAL en un tutorial de página de ASP.NET.

En mi ejemplo, elegí optar por el segundo enfoque, agregando un control web Label oculto cuya propiedad Text tiene un enlace de datos bidireccional al formato UnitPrice sin valor.

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

Al examinar el mensaje de la excepción, está claro que ObjectDataSource quiere invocar un método BLL DeleteProduct que incluya parámetros de entrada original_CategoryName y original_SupplierName. Esto se debe a que los elementos ItemTemplate para CategoryID y SupplierID TemplateFields 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 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 compatibilidad con simultaneidad 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 previo, con "Chai Tea" como el nuevo nombre del producto.

Sin embargo, 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. Sin embargo, con la simultaneidad optimista en uso, al hacer clic en el botón Actualizar de la segunda instancia del explorador, se produce 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 produce una excepción DBConcurrencyException (haga clic para ver la imagen en tamaño completo).

Solo se produce DBConcurrencyException cuando se utiliza el patrón de actualización por lotes de 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 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 vuelve a exponer, GridView invoca el método Delete() de ObjectDataSource y ObjectDataSource llama al método de la clase ProductsOptimisticConcurrencyBLL, 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 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 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 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 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 Label 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 excepción DBConcurrencyException 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 en caso de infracción de simultaneidad

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: el patrón de actualización por lotes se usa para actualizar y el patrón directo de base de datos se usa para eliminar. Para empezar, vamos a agregar dos controles web Label 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 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 he establecido la propiedad CssClass en Warning, lo que hace que la etiqueta se muestre en una fuente grande, roja, cursiva y negrita. Esta clase CSS Warning se definió y agregó a Styles.css de nuevo en el tutorial 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.

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, 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 de Label adecuada Visible en true, mostrando el mensaje informativo.

Control de infracciones de simultaneidad 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 excepción DBConcurrencyException, es necesario agregar código a nuestra página de ASP.NET para determinar si se produjo una excepción DBConcurrencyException 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 de nivel BLL y DAL en una página de ASP.NET, estas excepciones se pueden detectar y suprimir en los controladores de eventos posteriores del control web de datos. Por lo 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 generó durante el proceso de actualización, como se muestra en el código del controlador de eventos siguiente:

protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

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

Nota:

Como alternativa, en lugar de devolver GridView al estado de edición previa, podríamos dejar GridView en su estado de edición estableciendo la propiedad KeepInEditMode del objeto GridViewUpdatedEventArgs pasado en true. Sin embargo, si adopta este enfoque, asegúrese de volver a enlazar los datos a GridView (invocando su método DataBind()) 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 controlador de eventos RowUpdated 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 al eliminar

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 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 BLL DeleteProduct.

El valor devuelto de un método BLL se puede examinar en los controladores de eventos posteriores de ObjectDataSource a través de la propiedad ReturnValue del objeto ObjectDataSourceStatusEventArgs pasado al controlador de eventos. Puesto que estamos interesados en 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 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 propiedad ReturnValue no es null y es un valor booleano. Suponiendo que se supere esta comprobación, se muestra el control Label DeleteConflictMessage si ReturnValue es false. Esto se puede lograr mediante código como el siguiente:

protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

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 etiqueta DeleteConflictMessage, que 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", 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 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 en .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 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 este trabajo agregado es una inversión inteligente del tiempo y el 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. 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 sobrescriban 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 trabajado 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.