Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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.
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 de 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.
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
.
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.
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
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".
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.
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.
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 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 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
, ProductName
y 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.
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.
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
.
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 .
Figura 12: Agregar la ProductsOptimisticConcurrencyBLL
clase a la carpeta BLL
A continuación, agregue el código siguiente a la ProductsOptimisticConcurrencyBLL
clase :
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();
}
}
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 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 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:
- Lee la información actual del producto de la base de datos en una instancia
ProductRow
utilizando el métodoGetProductByProductID(productID)
de TableAdapter. - Asignación de los nuevos valores a la
ProductRow
instancia del paso 1 - Llame al método
Update
del TableAdapter, pasando la instanciaProductRow
.
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:
- Lee la información actual del producto de la base de datos en una instancia
ProductsOptimisticConcurrencyRow
utilizando el métodoGetProductByProductID(productID)
de TableAdapter. - Asignación de los valores originales a la
ProductsOptimisticConcurrencyRow
instancia del paso 1 - Llame al método
ProductsOptimisticConcurrencyRow
de la instanciaAcceptChanges()
, el cual indica al DataRow que sus valores actuales son los "originales". - Asigna los nuevos valores a la
ProductsOptimisticConcurrencyRow
instancia - Llame al método
Update
del TableAdapter, pasando la instanciaProductsOptimisticConcurrencyRow
.
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 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 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 .
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 OldValuesParameterFormatString
original_{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
yCategoryName
campos asociados - Convirtió boundField
ProductName
en templateField y agregó un control RequiredFieldValidation. - Se convirtieron los
CategoryID
ySupplierID
BoundFields en TemplateFields, y se ajustó la interfaz de edición para usar listas desplegables en lugar de cuadros de texto. En estos TemplateFieldsItemTemplates
, se muestran los campos de datosCategoryName
ySupplierName
. - Convirtió los
UnitPrice
,UnitsInStock
,UnitsOnOrder
yReorderLevel
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.
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
.
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 enItemTemplate
, pero utiliza la palabra claveEval
para lograrlo. Recuerde queEval
realiza el enlace de datos unidireccional. Todavía necesitamos proporcionar el valorUnitPrice
para los valores originales, por lo que seguiremos necesitando una instrucción de enlace de datos bidireccional enItemTemplate
, pero esto se puede colocar en un control Web de etiqueta cuya propiedadVisible
esté ajustada afalse
. 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 deRowDataBound
del GridView, acceda programáticamente al control Web Label donde se muestra el valorUnitPrice
y establezca su propiedadText
en la versión con formato. - Deje el
UnitPrice
formateado como moneda. En el controlador de eventos deRowDeleting
GridView, reemplace el valor originalUnitPrice
existente ($19,95) por un valor decimal real medianteDecimal.Parse
. Hemos visto cómo lograr algo similar en elRowUpdating
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.
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.
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.
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 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 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.
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 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 DeleteConflictMessage
etiqueta, que explica lo que acaba de suceder (vea la figura 20).
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.