Eliminación por lotes (C#)

por Scott Mitchell

Descargar PDF

Aprenda a eliminar varios registros de base de datos en una misma operación. En la capa de interfaz de usuario compilamos a partir de una clase GridView mejorada creada en un tutorial anterior. En la capa de acceso a datos, se encapsulan las diversas operaciones de inserción dentro de una transacción para garantizar que todas las inserciones se realizan correctamente o que todas se revierten.

Introducción

En el tutorial anterior se ha explorado cómo crear una interfaz de edición por lotes mediante GridView totalmente modificable. En situaciones en las que los usuarios suelen editar muchos registros a la vez, una interfaz de edición por lotes requerirá muchos menos postback y modificadores de contexto de teclado a mouse, con lo cual mejorará la eficacia del usuario final. Esta técnica es útil de forma similar para las páginas en las que es habitual que los usuarios eliminen muchos registros de una sola vez.

Cualquier usuario que haya usado un cliente de correo electrónico en línea ya estará familiarizado con una de las interfaces de eliminación por lotes más comunes: una casilla en cada fila de una cuadrícula con un botón Eliminar todos los elementos activados correspondiente (vea la figura 1). Este tutorial es bastante corto porque ya hemos realizado todo el trabajo complejo en tutoriales anteriores, para crear la interfaz basada en web y un método para eliminar una serie de registros como una sola operación atómica. En el tutorial Agregar una columna GridView de casillas hemos creado una clase GridView con una columna de casillas y en el tutorial Ajustar modificaciones de base de datos en una transacción hemos creado un método en BLL que usará una transacción para eliminar List<T> de valores ProductID. En este tutorial, crearemos y combinaremos nuestras experiencias anteriores para crear un ejemplo de eliminación por lotes en funcionamiento.

Each Row Includes a Checkbox

Figura 1: Cada fila incluye una casilla (Haga clic para ver la imagen a tamaño completo)

Paso 1: Creación de la interfaz de eliminación por lotes

Puesto que ya hemos creado la interfaz de eliminación por lotes en el tutorial Agregar una columna GridView de casillas, podemos copiarla en BatchDelete.aspx en lugar de crearla desde cero. Para empezar, abra la página BatchDelete.aspx en la carpeta BatchData y la página CheckBoxField.aspx en la carpeta EnhancedGridView. En la página CheckBoxField.aspx, vaya a la vista de Origen y copie el marcado entre las etiquetas <asp:Content>, como se muestra en la figura 2.

Copy the Declarative Markup of CheckBoxField.aspx to the Clipboard

Figura 2: Copiar el marcado declarativo de CheckBoxField.aspx en el Portapapeles (Haga clic para ver la imagen a tamaño completo)

A continuación, vaya a la vista de Origen en BatchDelete.aspx y pegue el contenido del Portapapeles dentro de las etiquetas <asp:Content>. Copie y pegue también el código desde dentro de la clase de código subyacente en CheckBoxField.aspx.cs dentro de la clase de código subyacente en BatchDelete.aspx.cs (el controlador de eventos Click del botón DeleteSelectedProducts, el método ToggleCheckState y los controladores de eventos Click para los botones CheckAll y UncheckAll). Después de copiar este contenido, la clase de código subyacente de la página BatchDelete.aspx debe contener el código siguiente:

using System;
using System.Data;
using System.Configuration;
using System.Collections;
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;
public partial class BatchData_BatchDelete : System.Web.UI.Page
{
    protected void DeleteSelectedProducts_Click(object sender, EventArgs e)
    {
        bool atLeastOneRowDeleted = false;
        // Iterate through the Products.Rows property
        foreach (GridViewRow row in Products.Rows)
        {
            // Access the CheckBox
            CheckBox cb = (CheckBox)row.FindControl("ProductSelector");
            if (cb != null && cb.Checked)
            {
                // Delete row! (Well, not really...)
                atLeastOneRowDeleted = true;
                // First, get the ProductID for the selected row
                int productID = Convert.ToInt32(Products.DataKeys[row.RowIndex].Value);
                // "Delete" the row
                DeleteResults.Text += string.Format
                    ("This would have deleted ProductID {0}<br />", productID);
                //... To actually delete the product, use ...
                //ProductsBLL productAPI = new ProductsBLL();
                //productAPI.DeleteProduct(productID);
                //............................................
            }
        }
        // Show the Label if at least one row was deleted...
        DeleteResults.Visible = atLeastOneRowDeleted;
    }
    private void ToggleCheckState(bool checkState)
    {
        // Iterate through the Products.Rows property
        foreach (GridViewRow row in Products.Rows)
        {
            // Access the CheckBox
            CheckBox cb = (CheckBox)row.FindControl("ProductSelector");
            if (cb != null)
                cb.Checked = checkState;
        }
    }
    protected void CheckAll_Click(object sender, EventArgs e)
    {
        ToggleCheckState(true);
    }
    protected void UncheckAll_Click(object sender, EventArgs e)
    {
        ToggleCheckState(false);
    }
}

Después de copiar el marcado declarativo y el código fuente, tómese un momento para probar BatchDelete.aspx mediante un explorador. Debería ver un control GridView que enumera los diez primeros productos de GridView con cada fila que muestra el nombre, la categoría y el precio del producto junto con una casilla. Debe haber tres botones: Activar todo, Desactivar todo y Eliminar productos seleccionados. Al hacer clic en el botón Activar todo, se seleccionan todas las casillas, mientras que Desactivar todo anula la selección de todas las casillas. Al hacer clic en Eliminar productos seleccionados, se visualiza un mensaje que muestra los valores ProductID de los productos seleccionados, pero no elimina realmente los productos.

The Interface from CheckBoxField.aspx has been Moved to BatchDeleting.aspx

Figura 3: La interfaz de CheckBoxField.aspx se ha movido a BatchDeleting.aspx (Haga clic para ver la imagen a tamaño completo)

Paso 2: Eliminar los productos activados mediante transacciones

Con la interfaz de eliminación por lotes copiada correctamente en BatchDeleting.aspx, todo lo que queda es actualizar el código para que el botón Eliminar productos seleccionados elimine los productos activados mediante el método DeleteProductsWithTransaction de la clase ProductsBLL. Este método, agregado en el tutorial Ajustar modificaciones de base de datos en una transacción, acepta como entrada List<T> de valores ProductID y elimina cada uno de los correspondientes ProductID dentro del ámbito de una transacción.

El controlador de eventos Click del botón DeleteSelectedProducts actualmente usa el siguiente bucle foreach para recorrer en iteración cada fila de GridView:

// Iterate through the Products.Rows property
foreach (GridViewRow row in Products.Rows)
{
    // Access the CheckBox
    CheckBox cb = (CheckBox)row.FindControl("ProductSelector");
    if (cb != null && cb.Checked)
    {
        // Delete row! (Well, not really...)
        atLeastOneRowDeleted = true;
        // First, get the ProductID for the selected row
        int productID = Convert.ToInt32(Products.DataKeys[row.RowIndex].Value);
        // "Delete" the row
        DeleteResults.Text += string.Format
            ("This would have deleted ProductID {0}<br />", productID);
        //... To actually delete the product, use ...
        //ProductsBLL productAPI = new ProductsBLL();
        //productAPI.DeleteProduct(productID);
        //............................................
    }
}

Para cada fila, se hace referencia mediante programación al control web CheckBox ProductSelector. Si está activada, la fila ProductID se recupera de la colección DataKeys y la propiedad Text de la etiqueta DeleteResults se actualiza para incluir un mensaje que indica que la fila se seleccionó para su eliminación.

El código anterior no elimina realmente ningún registro, ya que la llamada al método Delete de la clase ProductsBLL se comenta. Si se aplica esta lógica de eliminación, el código eliminará los productos, pero no dentro de una operación atómica. Es decir, si las primeras eliminaciones de la secuencia se realizaron correctamente, pero se produjo un error posterior (quizá debido a una infracción de restricción de clave externa), se producirá una excepción, pero esos productos ya eliminados se eliminarán.

Para garantizar la atomicidad, necesitamos usar en su lugar el método DeleteProductsWithTransaction de la clase ProductsBLL. Dado que este método acepta una lista de valores ProductID, primero necesitamos compilar esta lista desde la cuadrícula y, a continuación, pasarla como parámetro. Primero creamos una instancia de List<T> de tipo int. Dentro del bucle foreach, necesitamos agregar los valores ProductID de productos seleccionados a este List<T>. Después del bucle, List<T> se debe pasar al método DeleteProductsWithTransaction de la clase ProductsBLL. Actualizamos el controlador de eventos Click del botón DeleteSelectedProducts con el siguiente código:

protected void DeleteSelectedProducts_Click(object sender, EventArgs e)
{
    // Create a List to hold the ProductID values to delete
    System.Collections.Generic.List<int> productIDsToDelete = 
        new System.Collections.Generic.List<int>();
    // Iterate through the Products.Rows property
    foreach (GridViewRow row in Products.Rows)
    {
        // Access the CheckBox
        CheckBox cb = (CheckBox)row.FindControl("ProductSelector");
        if (cb != null && cb.Checked)
        {
            // Save the ProductID value for deletion
            // First, get the ProductID for the selected row
            int productID = Convert.ToInt32(Products.DataKeys[row.RowIndex].Value);
            // Add it to the List...
            productIDsToDelete.Add(productID);
            // Add a confirmation message
            DeleteResults.Text += string.Format
                ("ProductID {0} has been deleted<br />", productID);
        }
    }
    // Call the DeleteProductsWithTransaction method and show the Label 
    // if at least one row was deleted...
    if (productIDsToDelete.Count > 0)
    {
        ProductsBLL productAPI = new ProductsBLL();
        productAPI.DeleteProductsWithTransaction(productIDsToDelete);
        DeleteResults.Visible = true;
        // Rebind the data to the GridView
        Products.DataBind();
    }
}

El código actualizado crea un List<T> de tipo int (productIDsToDelete) y lo rellena con los valores ProductID que se van a eliminar. Después del bucle foreach, si hay al menos un producto seleccionado, se llama al método DeleteProductsWithTransaction de la clase ProductsBLL y se pasa esta lista. La etiqueta DeleteResults también se muestra y los datos se vuelven a enlazar a GridView (de modo que los registros recién eliminados ya no aparezcan como filas en la cuadrícula).

En la figura 4 se muestra GridView después de seleccionar una serie de filas para su eliminación. En la figura 5 se muestra la pantalla inmediatamente después de hacer clic en el botón Eliminar productos seleccionados. Tenga en cuenta que en la figura 5, los valores ProductID de los registros eliminados se muestran en la etiqueta situada debajo de GridView y esas filas ya no están en GridView.

The Selected Products Will Be Deleted

Figura 4: Se eliminarán los productos seleccionados (Haga clic para ver la imagen a tamaño completo)

The Deleted Products ProductID Values are Listed Beneath the GridView

Figura 5: Los valores de productos eliminados ProductID se enumeran debajo de GridView (Haga clic para ver la imagen a tamaño completo)

Nota:

Para probar la atomicidad del método DeleteProductsWithTransaction, agregue manualmente una entrada para un producto en la tabla Order Details e intente eliminar ese producto (junto con otros). Recibirá un aviso de infracción de restricción de clave externa al intentar eliminar el producto con un pedido asociado, pero tenga en cuenta cómo se revierten las otras eliminaciones de productos seleccionados.

Resumen

La creación de una interfaz de eliminación por lotes implica agregar GridView con una columna de casillas y un control web de botón que, al hacer clic sobre él, eliminará todas las filas seleccionadas como una sola operación atómica. En este tutorial hemos creado una interfaz de este tipo mediante la unión del trabajo realizado en dos tutoriales anteriores, Agregar una columna GridView de casillas y Ajustar modificaciones de base de datos en una transacción. En el primer tutorial hemos creado una clase GridView con una columna de casillas y, en este último, hemos implementado un método en BLL que, cuando se pasa List<T> de valores ProductID, los elimina todos dentro del ámbito de una transacción.

En el siguiente tutorial, crearemos una interfaz para realizar inserciones por lotes.

¡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 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.

Agradecimientos especiales a

Esta serie de tutoriales contó con la revisión de muchos revisores que fueron de gran ayuda. Los revisores principales de este tutorial fueron Hilton Giesenow y Teresa Murphy. ¿Le interesaría revisar mis próximos artículos de MSDN? Si fuera así, escríbame a mitchell@4GuysFromRolla.com.