Actualizar y eliminar los datos binarios existentes (C#)

por Scott Mitchell

Descargar PDF

En tutoriales anteriores se ha visto cómo el control GridView facilita la edición y eliminación de datos de texto. En este tutorial, verá cómo el control GridView también permite editar y eliminar datos binarios, tanto si esos datos binarios se guardan en la base de datos como si se almacenan en el sistema de archivos.

Introducción

En los últimos tres tutoriales se ha agregado bastante funcionalidad para trabajar con datos binarios. Para empezar, se ha agregado una columna BrochurePath a la tabla Categories y se ha actualizado la arquitectura en consecuencia. También se han agregado métodos de capa de acceso a datos y de capa de lógica de negocios para trabajar con la columna Picture existente de la tabla Categories, que contiene los datos binarios de un archivo de imagen. Se han creado páginas web para presentar los datos binarios en un control GridView con un vínculo de descarga del folleto, con la imagen de la categoría mostrada en un elemento <img>, y se ha agregado un control DetailsView para permitir a los usuarios agregar una nueva categoría y cargar sus datos de folleto e imagen.

Lo único que queda por implementar es la capacidad de editar y eliminar categorías existentes, y lo hará en este tutorial mediante las características integradas de edición y eliminación de GridView. Al editar una categoría, el usuario podrá cargar opcionalmente una imagen nueva o hacer que la categoría siga usando la existente. Para el folleto, puede optar por usar el folleto existente, cargar un nuevo folleto o indicar que la categoría ya no tiene un folleto asociado. Comencemos.

Paso 1: Actualización de la capa de acceso a datos

La capa de acceso a datos (DAL) tiene métodos Insert, Update y Delete generados automáticamente, pero estos métodos se han generado en función de la consulta principal de CategoriesTableAdapter, que no incluye la columna Picture. Por tanto, los métodos Insert y Update no incluyen parámetros para especificar los datos binarios para la imagen de la categoría. Como en el tutorial anterior, es necesario crear un método TableAdapter para actualizar la tabla Categories al especificar datos binarios.

Abra el conjunto de datos con tipo y, en el diseñador, haga clic con el botón derecho en el encabezado de CategoriesTableAdapter y elija Agregar consulta en el menú contextual para iniciar el Asistente para la configuración de consultas de TableAdapter. Este asistente comienza preguntando cómo debe acceder la consulta TableAdapter a la base de datos. Elija Usar instrucciones SQL y haga clic en Siguiente. En el paso siguiente se solicita el tipo de consulta que se va a generar. Como se va a crear una consulta para agregar un nuevo registro a la tabla Categories, elija UPDATE y haga clic en Siguiente.

Select the UPDATE Option

Figura 1: Selección de la opción UPDATE (Haga clic para ver la imagen a tamaño completo)

Ahora es necesario especificar la instrucción SQL UPDATE. El asistente sugiere automáticamente una instrucción UPDATE correspondiente a la consulta principal de TableAdapter (que actualiza los valores de CategoryName, Description y BrochurePath). Cambie la instrucción para que la columna Picture se incluya junto con un parámetro @Picture, de la siguiente manera:

UPDATE [Categories] SET 
    [CategoryName] = @CategoryName, 
    [Description] = @Description, 
    [BrochurePath] = @BrochurePath ,
    [Picture] = @Picture
WHERE (([CategoryID] = @Original_CategoryID))

En la pantalla final del asistente se pide asignar un nombre al nuevo método TableAdapter. Escriba UpdateWithPicture y haga clic en Finalizar.

Name the New TableAdapter Method UpdateWithPicture

Figura 2: Asignación del nombre UpdateWithPicture al método de TableAdapter (Haga clic para ver la imagen a tamaño completo)

Paso 2: Adición de los métodos de la capa de lógica de negocios

Además de actualizar la capa de acceso a datos, es necesario actualizar la capa de lógica de negocios a fin de incluir métodos para actualizar y eliminar una categoría. Estos métodos se invocarán desde la capa de presentación.

Para eliminar una categoría, puede usar el método Delete de CategoriesTableAdapter generado automáticamente. Agregue el siguiente método a la clase CategoriesBLL:

[System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteCategory(int categoryID)
{
    int rowsAffected = Adapter.Delete(categoryID);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

En este tutorial, se crearán dos métodos para actualizar una categoría: uno que espera los datos de imagen binarios e invoca el método UpdateWithPicture que se acaba de agregar a CategoriesTableAdapter, y otro que acepta solo los valores de CategoryName, Description y BrochurePath, y usa la instrucción Update generada automáticamente de la clase CategoriesTableAdapter. La lógica detrás del uso de dos métodos es que, en algunas circunstancias, es posible que un usuario quiera actualizar la imagen de la categoría junto con sus otros campos, en cuyo caso el usuario tendrá que cargar la nueva imagen. Los datos binarios de la imagen cargada se pueden usar en la instrucción UPDATE. En otros casos, es posible que el usuario solo esté interesado en actualizar el nombre y la descripción, por ejemplo. Pero si la instrucción UPDATE espera también los datos binarios de la columna Picture, también es necesario proporcionar esa información. Esto requeriría un viaje adicional a la base de datos para devolver los datos de imagen del registro que se está edita. Por lo tanto, querrá usar dos métodos UPDATE. La capa de lógica de negocios determinará cuál se usará en función de si se proporcionan datos de imagen al actualizar la categoría.

Para facilitar esto, agregue dos métodos a la clase CategoriesBLL, ambos denominados UpdateCategory. El primero debe aceptar tres objetos string, una matriz de byte y un objeto int como parámetros de entrada; el segundo, solo tres objetos string y uno int. Los parámetros string de entrada son para el nombre, la descripción y la ruta del archivo de folleto de la categoría, mientras que la matriz byte es para el contenido binario de la imagen de la categoría, y el objeto int identifica la propiedad CategoryID del registro que se va a actualizar. Observe que la primera sobrecarga invoca la segunda si la matriz byte pasada es null:

[System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Update, false)]
public bool UpdateCategory(string categoryName, string description, 
    string brochurePath, byte[] picture, int categoryID)
{
    // If no picture is specified, use other overload
    if (picture == null)
        return UpdateCategory(categoryName, description, brochurePath, categoryID);
    // Update picture, as well
    int rowsAffected = Adapter.UpdateWithPicture
        (categoryName, description, brochurePath, picture, categoryID);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateCategory(string categoryName, string description, 
    string brochurePath, int categoryID)
{
    int rowsAffected = Adapter.Update
        (categoryName, description, brochurePath, categoryID);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

Paso 3: Copia de la funcionalidad de inserción y visualización

En el tutorial anterior se ha creado una página denominada UploadInDetailsView.aspx que enumeraba todas las categorías de un control GridView y proporcionaba un control DetailsView para agregar nuevas categorías al sistema. En este tutorial se ampliará el control GridView para que admita la edición y eliminación. En lugar de continuar trabajando desde UploadInDetailsView.aspx, en su lugar los cambios de este tutorial se colocarán en la página UpdatingAndDeleting.aspx de la misma carpeta ~/BinaryData. Copie y pegue el marcado declarativo y el código de UploadInDetailsView.aspx a UpdatingAndDeleting.aspx.

Para empezar, abra la página UploadInDetailsView.aspx. Copie toda la sintaxis declarativa dentro del elemento <asp:Content>, como se muestra en la figura 3. Después, abra UpdatingAndDeleting.aspx y pegue este marcado dentro de su elemento <asp:Content>. Del mismo modo, copie el código de la clase de código subyacente de la página UploadInDetailsView.aspx en UpdatingAndDeleting.aspx.

Copy the Declarative Markup from UploadInDetailsView.aspx

Figura 3: Copia del marcado declarativo de UploadInDetailsView.aspx (Haga clic para ver la imagen a tamaño completo)

Después de copiar el código y el marcado declarativo, visite UpdatingAndDeleting.aspx. Debería ver la misma salida y tener la misma experiencia de usuario que con la página UploadInDetailsView.aspx del tutorial anterior.

Paso 4: Adición de compatibilidad con la eliminación a los controles ObjectDataSource y GridView

Como se ha explicado en el tutorial Información general sobre la inserción, actualización y eliminación de datos, el control GridView proporciona funcionalidades de eliminación integradas y estas funcionalidades se pueden habilitar con solo activar una casilla si el origen de datos subyacente de la cuadrícula admite la eliminación. Actualmente, el control ObjectDataSource al que está enlazado el control GridView (CategoriesDataSource) no admite la eliminación.

Para solucionar este problema, haga clic en la opción Configurar origen de datos de la etiqueta inteligente del ObjectDataSource para iniciar el asistente. La primera pantalla muestra que el control ObjectDataSource está configurado para trabajar con la clase CategoriesBLL. Presione Siguiente. Actualmente, solo se especifican las propiedades InsertMethod y SelectMethod de ObjectDataSource. Pero el asistente rellena automáticamente las listas desplegables en las pestañas UPDATE y DELETE con los métodos UpdateCategory y DeleteCategory, respectivamente. Esto se debe a que en la clase CategoriesBLL estos métodos se marcan mediante DataObjectMethodAttribute como el método predeterminado para actualizar y eliminar.

Por ahora, establezca la lista desplegable de la pestaña UPDATE en (None), pero deje la lista desplegable de la pestaña DELETE establecida en DeleteCategory. Volverá a este asistente en el paso 6 para agregar compatibilidad con la actualización.

Configure the ObjectDataSource to Use the DeleteCategory Method

Figura 4: Configuración de ObjectDataSource para usar el método DeleteCategory (Haga clic para ver la imagen a tamaño completo)

Nota:

Al completar el asistente, Visual Studio puede preguntar si quiere actualizar campos y claves, lo que regenerará los campos de controles web de datos. Elija No, ya que si elige Sí se sobrescribirán las personalizaciones de campo que haya realizado.

El control ObjectDataSource ahora incluirá un valor para su propiedad DeleteMethod, así como un objeto DeleteParameter. Recuerde que, cuando se usa el asistente para especificar los métodos, Visual Studio establece la propiedad OldValuesParameterFormatString del ObjectDataSource en original_{0}, lo que provoca problemas con las invocaciones de los métodos de actualización y eliminación. Por tanto, borre esta propiedad por completo o restablézcala al valor predeterminado, {0}. Si necesita repasar esta propiedad del control ObjectDataSource, vea el tutorial Información general sobre la inserción, actualización y eliminación de datos.

Después de completar el asistente y corregir la propiedad OldValuesParameterFormatString, el marcado declarativo del control ObjectDataSource debería ser similar al siguiente:

<asp:ObjectDataSource ID="CategoriesDataSource" runat="server" 
    OldValuesParameterFormatString="{0}" SelectMethod="GetCategories" 
    TypeName="CategoriesBLL" InsertMethod="InsertWithPicture" 
    DeleteMethod="DeleteCategory">
    <InsertParameters>
        <asp:Parameter Name="categoryName" Type="String" />
        <asp:Parameter Name="description" Type="String" />
        <asp:Parameter Name="brochurePath" Type="String" />
        <asp:Parameter Name="picture" Type="Object" />
    </InsertParameters>
    <DeleteParameters>
        <asp:Parameter Name="categoryID" Type="Int32" />
    </DeleteParameters>
</asp:ObjectDataSource>

Después de configurar el control ObjectDataSource, agregue funcionalidades de eliminación al control GridView activando la casilla Habilitar eliminación en la etiqueta inteligente del GridView. Esto agregará un elemento CommandField al control GridView cuya propiedad ShowDeleteButton está establecida en true.

Enable Support for Deleting in the GridView

Figura 5: Habilitación de la compatibilidad con la eliminación en GridView (Haga clic para ver la imagen a tamaño completo)

Dedique un momento a probar la funcionalidad de eliminación. Hay una clave externa entre la propiedad CategoryID de la tabla Products y la propiedad CategoryID de la tabla Categories, por lo que se producirá una excepción de infracción de la restricción de clave externa si intenta eliminar cualquiera de las ocho primeras categorías. Para probar esta funcionalidad, agregue una nueva categoría y proporcione un folleto y una imagen. La categoría de prueba, que se muestra en la figura 6, incluye un archivo de folleto de prueba denominado Test.pdf y una imagen de prueba. En la figura 7 se muestra el control GridView después de agregar la categoría de prueba.

Add a Test Category with a Brochure and Image

Figura 6: Adición de una categoría de prueba con un folleto y una imagen (Haga clic para ver la imagen a tamaño completo)

After Inserting the Test Category, it is Displayed in the GridView

Figura 7: Después de insertar la categoría de prueba, se muestra en el control GridView (Haga clic para ver la imagen a tamaño completo)

En Visual Studio, actualice el Explorador de soluciones. Ahora debería ver un nuevo archivo en la carpeta ~/Brochures, Test.pdf (vea la figura 8).

Después, haga clic en el vínculo Eliminar en la fila de la categoría de prueba, lo que hace que la página aplique un postback y se active el método DeleteCategory de la clase CategoriesBLL. Esto invocará el método Delete de la capa de acceso a datos, lo que hará que la instrucción DELETE adecuada se envíe a la base de datos. Los datos se vuelven a enlazar al control GridView y el marcado se devuelve al cliente sin la categoría de prueba.

Aunque el flujo de trabajo de eliminación ha quitado correctamente el registro de la categoría de prueba de la tabla Categories, no ha quitado su archivo de folleto del sistema de archivos del servidor web. Actualice el Explorador de soluciones y verá que Test.pdf todavía se encuentra en la carpeta ~/Brochures.

The Test.pdf File Was Not Deleted from the Web Server s File System

Figura 8: El archivo Test.pdf no se ha eliminado del sistema de archivos del servidor web

Paso 5: Eliminación del archivo de folleto de la categoría eliminada

Una de las desventajas de almacenar datos binarios externos a la base de datos es que se deben realizar pasos adicionales para limpiar estos archivos cuando se elimina el registro de la base de datos asociado. Los controles GridView y ObjectDataSource proporcionan eventos que se activan antes y después de que se haya ejecutado el comando de eliminación. Por tanto, es necesario crear controladores de eventos para los eventos previos y posteriores a la acción. Antes de eliminar el registro de Categories, es necesario determinar la ruta de acceso de su archivo PDF, pero no quiere eliminar el PDF antes de que se elimine la categoría para que no se produzca una excepción y la categoría no se elimine.

El evento RowDeleting del control GridView se desencadena antes de invocar el comando de eliminación del control ObjectDataSource, mientras que el evento RowDeleted se desencadena después. Cree controladores de eventos para estos dos eventos mediante el código siguiente:

// A page variable to "remember" the deleted category's BrochurePath value 
string deletedCategorysPdfPath = null;
protected void Categories_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
    // Determine the PDF path for the category being deleted...
    int categoryID = Convert.ToInt32(e.Keys["CategoryID"]);
    CategoriesBLL categoryAPI = new CategoriesBLL();
    Northwind.CategoriesDataTable categories = 
        categoryAPI.GetCategoryByCategoryID(categoryID);
    Northwind.CategoriesRow category = categories[0];
    if (category.IsBrochurePathNull())
        deletedCategorysPdfPath = null;
    else
        deletedCategorysPdfPath = category.BrochurePath;
}
protected void Categories_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
    // Delete the brochure file if there were no problems deleting the record
    if (e.Exception == null)
    {
        // Is there a file to delete?
        if (deletedCategorysPdfPath != null)
        {
            System.IO.File.Delete(Server.MapPath(deletedCategorysPdfPath));
        }
    }
}

En el controlador de eventos RowDeleting, la propiedad CategoryID de la fila que se va a eliminar se toma de la colección DataKeys del GridView, a la que se puede acceder en este controlador de eventos por medio de la colección e.Keys. Después, se invoca el método GetCategoryByCategoryID(categoryID) de la clase CategoriesBLL para devolver información sobre el registro que se va a eliminar. Si el objeto CategoriesDataRow devuelto tiene un valor distinto de NULL``BrochurePath, se almacena en la variable deletedCategorysPdfPath de página para que el archivo se pueda eliminar mediante el controlador de eventos RowDeleted.

Nota:

En lugar de recuperar los detalles de BrochurePath del registro de Categories que se va a eliminar en el controlador de eventos RowDeleting, podría agregar alternativamente el objeto BrochurePath a la propiedad DataKeyNames del control GridView y acceder al valor del registro por medio de la colección e.Keys. Esto aumentaría ligeramente el tamaño del estado de visualización del control GridView, pero reduciría la cantidad de código necesario y ahorraría un viaje a la base de datos.

Una vez que invoque el comando de eliminación subyacente del ObjectDataSource, se activa el controlador de eventos RowDeleted del GridView. Si no hay excepciones en la eliminación de los datos y hay un valor para deletedCategorysPdfPath, el PDF se elimina del sistema de archivos. Tenga en cuenta que este código adicional no es necesario para limpiar los datos binarios de la categoría asociados a su imagen. Se debe a que los datos de imagen se almacenan directamente en la base de datos, por lo que la eliminación de la fila de Categories también elimina los datos de imagen de esa categoría.

Después de agregar los dos controladores de eventos, vuelva a ejecutar este caso de prueba. Al eliminar la categoría, también se elimina su PDF asociado.

La actualización de los datos binarios asociados de un registro existente proporciona algunos desafíos interesantes. El resto de este tutorial profundiza en la adición de funcionalidades de actualización al folleto y la imagen. En el paso 6 se exploran las técnicas para actualizar la información del folleto, mientras que en el paso 7 se trata la actualización de la imagen.

Paso 6: Actualización del folleto de una categoría

Como se ha descrito en el tutorial Información general sobre la inserción, actualización y eliminación de datos, el control GridView ofrece compatibilidad de edición integrada a nivel de fila que se puede implementar mediante la activación de una casilla si su origen de datos subyacente está configurado correctamente. Actualmente, el control ObjectDataSource CategoriesDataSource aún no está configurado para incluir compatibilidad con la actualización, por lo que vamos a agregarla.

Haga clic en el vínculo Configurar origen de datos desde el asistente de ObjectDataSource y continúe con el segundo paso. Como DataObjectMethodAttribute se usa en CategoriesBLL, la lista desplegable UPDATE debería rellenarse automáticamente con la sobrecarga de UpdateCategory que acepta cuatro parámetros de entrada (para todas las columnas, excepto Picture). Cambie esto para que use la sobrecarga con cinco parámetros.

Configure the ObjectDataSource to Use the UpdateCategory Method that Includes a Parameter for Picture

Figura 9: Configuración de ObjectDataSource para usar el método UpdateCategory que incluye un parámetro para Picture (Haga clic para ver la imagen a tamaño completo)

El control ObjectDataSource ahora incluirá un valor para su propiedad UpdateMethod, así como los objetos UpdateParameter correspondientes. Como se indica en el paso 4, Visual Studio establece la propiedad OldValuesParameterFormatString del control ObjectDataSource en original_{0} cuando se usa el asistente para configurar orígenes de datos. Esto provocará problemas con las invocaciones de los métodos de actualización y eliminación. Por tanto, borre esta propiedad por completo o restablézcala al valor predeterminado, {0}.

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

<asp:ObjectDataSource ID="CategoriesDataSource" runat="server" 
    OldValuesParameterFormatString="{0}" SelectMethod="GetCategories" 
    TypeName="CategoriesBLL" InsertMethod="InsertWithPicture" 
    DeleteMethod="DeleteCategory" UpdateMethod="UpdateCategory">
    <InsertParameters>
        <asp:Parameter Name="categoryName" Type="String" />
        <asp:Parameter Name="description" Type="String" />
        <asp:Parameter Name="brochurePath" Type="String" />
        <asp:Parameter Name="picture" Type="Object" />
    </InsertParameters>
    <DeleteParameters>
        <asp:Parameter Name="categoryID" Type="Int32" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="categoryName" Type="String" />
        <asp:Parameter Name="description" Type="String" />
        <asp:Parameter Name="brochurePath" Type="String" />
        <asp:Parameter Name="picture" Type="Object" />
        <asp:Parameter Name="categoryID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

Para activar las características de edición integradas del control GridView, active la opción Habilitar edición desde la etiqueta inteligente del GridView. Esto establecerá la propiedad ShowEditButton de CommandField en true, lo que agregará un botón Edit (así como botones Update y Cancel para la fila que se está editando).

Configure the GridView to Support Editing

Figura 10: Configuración del control GridView para admitir la edición (Haga clic para ver la imagen a tamaño completo)

Visite la página en un explorador y haga clic en el botón Edit de una de las filas. Los objetos BoundField CategoryName y Description se representan como cuadros de texto. El objeto TemplateField BrochurePath carece de un objeto EditItemTemplate, por lo que continúa mostrando un vínculo al folleto en ItemTemplate. El objeto ImageField Picture se representa como un control TextBox a cuya propiedad Text se le asigna el valor del DataImageUrlField de ImageField, en este caso CategoryID.

The GridView Lacks an Editing Interface for BrochurePath

Figura 11: El control GridView carece de una interfaz de edición para BrochurePath (Haga clic para ver la imagen a tamaño completo)

Personalización de la interfaz de edición de BrochurePath

Es necesario crear una interfaz de edición para el objeto TemplateField BrochurePath, que permita al usuario hacer una de las siguientes cosas:

  • Dejar el folleto de la categoría como está,
  • Actualizar el folleto de la categoría mediante la carga de un nuevo folleto, o bien
  • Quitar el folleto de la categoría por completo (en caso de que la categoría ya no tenga un folleto asociado).

También es necesario actualizar la interfaz de edición del objeto ImageField Picture, pero lo hará en el paso 7.

Desde la etiqueta inteligente del control GridView, haga clic en el vínculo Editar plantillas y seleccione la plantilla EditItemTemplate del objeto TemplateField BrochurePath en la lista desplegable. Agregue un control web RadioButtonList a esta plantilla y establezca su propiedad ID en BrochureOptions y su propiedad AutoPostBack en true. En la ventana Propiedades, haga clic en los puntos suspensivos de la propiedad Items, lo que abrirá el editor de la colección ListItem. Agregue las tres opciones siguientes con Value establecidos en 1, 2 y 3, respectivamente:

  • Usar folleto actual
  • Quitar folleto actual
  • Cargar nuevo folleto

Establezca la propiedad Selected de la primera instancia de ListItem en true.

Add Three ListItems to the RadioButtonList

Figura 12: Adición de tres elementos ListItem al control RadioButtonList

Debajo del control RadioButtonList, agregue un control FileUpload denominado BrochureUpload. Establezca su propiedad Visible en false.

Add a RadioButtonList and FileUpload Control to the EditItemTemplate

Figura 13: Adición de controles RadioButtonList y FileUpload a EditItemTemplate (Haga clic para ver la imagen a tamaño completo)

El control RadioButtonList proporciona las tres opciones para el usuario. La idea es que el control FileUpload solo se muestre si se selecciona la última opción, Upload new brochure (Cargar un nuevo folleto). Para ello, cree un controlador de eventos para el evento SelectedIndexChanged del control RadioButtonList y agregue el código siguiente:

protected void BrochureOptions_SelectedIndexChanged(object sender, EventArgs e)
{
    // Get a reference to the RadioButtonList and its Parent
    RadioButtonList BrochureOptions = (RadioButtonList)sender;
    Control parent = BrochureOptions.Parent;
    // Now use FindControl("controlID") to get a reference of the 
    // FileUpload control
    FileUpload BrochureUpload = 
        (FileUpload)parent.FindControl("BrochureUpload");
    // Only show BrochureUpload if SelectedValue = "3"
    BrochureUpload.Visible = (BrochureOptions.SelectedValue == "3");
}

Como los controles RadioButtonList y FileUpload están dentro de una plantilla, hay que escribir código para acceder mediante programación a estos controles. Se pasa una referencia del control RadioButtonList al controlador de eventos SelectedIndexChanged en el parámetro de entrada sender. Para obtener el control FileUpload, es necesario obtener el control primario RadioButtonList y usar el método FindControl("controlID") desde allí. Una vez que tenga una referencia a los controles RadioButtonList y FileUpload, la propiedadVisible del control FileUpload se establece en true solo si el valor SelectedValue del control RadioButtonList es igual a 3, que es el Value del elemento ListItem Upload new brochure.

Con este código aplicado, dedique un momento a probar la interfaz de edición. Haga clic en el botón Edit de una fila. Inicialmente, se debe seleccionar la opción Use current brochure (Usar el folleto actual). Al cambiar el índice seleccionado, se produce un postback. Si se selecciona la tercera opción, se muestra el control FileUpload; de lo contrario, se oculta. En la figura 14 se muestra la interfaz de edición cuando se hace clic por primera vez en el botón Editar. En la figura 15 se muestra la interfaz después de seleccionar la opción Upload new brochure (Cargar un nuevo folleto).

Initially, the Use current brochure Option is Selected

Figura 14: Inicialmente, está seleccionada la opción Use current brochure (Haga clic para ver la imagen a tamaño completo)

Choosing the Upload new brochure Option Displays the FileUpload Control

Figura 15: La selección de la opción Upload new brochure muestra el control FileUpload (Haga clic para ver la imagen a tamaño completo)

Guardado del archivo de folleto y actualización de la columna BrochurePath

Cuando se hace clic en el botón Update del control GridView, se desencadena su evento RowUpdating. Se invoca el comando de actualización del control ObjectDataSource y, después, se desencadena el evento RowUpdated del control GridView. Igual que con el flujo de trabajo de eliminación, es necesario crear controladores de eventos para ambos eventos. En el controlador de eventos RowUpdating, es necesario determinar qué acción se realizará en función del valor SelectedValue del control RadioButtonList BrochureOptions:

  • Si SelectedValue es 1, querrá seguir usando la misma configuración de BrochurePath. Por tanto, es necesario establecer el parámetro brochurePath del control ObjectDataSource en el valor BrochurePath existente del registro que se está actualizando. El parámetro brochurePath del control ObjectDataSource se puede establecer mediante e.NewValues["brochurePath"] = value.
  • Si SelectedValue es 2, querrá establecer el valor de BrochurePath del registro en NULL. Esto se puede lograr si se establece el parámetro brochurePath del control ObjectDataSource en Nothing, lo que da como resultado que se use una base de datos NULL en la instrucción UPDATE. Si hay un archivo de folleto existente que se va a quitar, es necesario eliminar el archivo existente. Pero solo querrá hacerlo si la actualización se completa sin generar una excepción.
  • Si SelectedValue es 3, querrá asegurarse de que el usuario ha cargado un archivo PDF y, después, guardarlo en el sistema de archivos y actualizar el valor de la columna BrochurePath del registro. Además, si hay un archivo de folleto existente que se va a reemplazar, es necesario eliminar el archivo anterior. Pero solo querrá hacerlo si la actualización se completa sin generar una excepción.

Los pasos que se deben completar cuando el valor SelectedValue del control RadioButtonList es 3 son prácticamente idénticos a los que usa el controlador de eventos ItemInserting del control DetailsView. Este controlador de eventos se ejecuta cuando se agrega un nuevo registro de categoría desde el control DetailsView que se ha agregado en el tutorial anterior. Por tanto, debe refactorizar esta funcionalidad en métodos independientes. En concreto, se ha dividido la funcionalidad común en dos métodos:

  • ProcessBrochureUpload(FileUpload, out bool) acepta como entrada una instancia del control FileUpload y un valor booleano de salida que especifica si la operación de eliminación o edición debe continuar o si se debe cancelar debido a algún error de validación. Este método devuelve la ruta de acceso al archivo guardado o null si no se ha guardado ningún archivo.
  • DeleteRememberedBrochurePath elimina el archivo especificado por la ruta de acceso de la variable de página deletedCategorysPdfPath si deletedCategorysPdfPath no es null.

A continuación se muestra el código de estos dos métodos. Observe la similitud entre ProcessBrochureUpload y el controlador de eventos ItemInserting del control DetailsView del tutorial anterior. En este tutorial se han actualizado los controladores de eventos del control DetailsView para usar estos nuevos métodos. Descargue el código asociado a este tutorial para ver las modificaciones en los controladores de eventos del control DetailsView.

private string ProcessBrochureUpload
    (FileUpload BrochureUpload, out bool CancelOperation)
{
    CancelOperation = false;    // by default, do not cancel operation
    if (BrochureUpload.HasFile)
    {
        // Make sure that a PDF has been uploaded
        if (string.Compare(System.IO.Path.GetExtension(BrochureUpload.FileName), 
            ".pdf", true) != 0)
        {
            UploadWarning.Text = 
                "Only PDF documents may be used for a category's brochure.";
            UploadWarning.Visible = true;
            CancelOperation = true;
            return null;
        }
        const string BrochureDirectory = "~/Brochures/";
        string brochurePath = BrochureDirectory + BrochureUpload.FileName;
        string fileNameWithoutExtension = 
            System.IO.Path.GetFileNameWithoutExtension(BrochureUpload.FileName);
        int iteration = 1;
        while (System.IO.File.Exists(Server.MapPath(brochurePath)))
        {
            brochurePath = string.Concat(BrochureDirectory, fileNameWithoutExtension, 
                "-", iteration, ".pdf");
            iteration++;
        }
        // Save the file to disk and set the value of the brochurePath parameter
        BrochureUpload.SaveAs(Server.MapPath(brochurePath));
        return brochurePath;
    }
    else
    {
        // No file uploaded
        return null;
    }
}
private void DeleteRememberedBrochurePath()
{
    // Is there a file to delete?
    if (deletedCategorysPdfPath != null)
    {
        System.IO.File.Delete(Server.MapPath(deletedCategorysPdfPath));
    }
}

Los controladores de eventos RowUpdating y RowUpdated del control GridView usan los métodos ProcessBrochureUpload y DeleteRememberedBrochurePath, como se muestra en el código siguiente:

protected void Categories_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
    // Reference the RadioButtonList
    RadioButtonList BrochureOptions = 
        (RadioButtonList)Categories.Rows[e.RowIndex].FindControl("BrochureOptions");
    // Get BrochurePath information about the record being updated
    int categoryID = Convert.ToInt32(e.Keys["CategoryID"]);
    CategoriesBLL categoryAPI = new CategoriesBLL();
    Northwind.CategoriesDataTable categories = 
        categoryAPI.GetCategoryByCategoryID(categoryID);
    Northwind.CategoriesRow category = categories[0];
    if (BrochureOptions.SelectedValue == "1")
    {
        // Use current value for BrochurePath
        if (category.IsBrochurePathNull())
            e.NewValues["brochurePath"] = null;
        else
            e.NewValues["brochurePath"] = category.BrochurePath;
    }
    else if (BrochureOptions.SelectedValue == "2")
    {
        // Remove the current brochure (set it to NULL in the database)
        e.NewValues["brochurePath"] = null;
    }
    else if (BrochureOptions.SelectedValue == "3")
    {
        // Reference the BrochurePath FileUpload control
        FileUpload BrochureUpload = 
            (FileUpload)Categories.Rows[e.RowIndex].FindControl("BrochureUpload");
        // Process the BrochureUpload
        bool cancelOperation = false;
        e.NewValues["brochurePath"] = 
            ProcessBrochureUpload(BrochureUpload, out cancelOperation);
        e.Cancel = cancelOperation;
    }
    else
    {
        // Unknown value!
        throw new ApplicationException(
            string.Format("Invalid BrochureOptions value, {0}", 
                BrochureOptions.SelectedValue));
    }
    if (BrochureOptions.SelectedValue == "2" || 
        BrochureOptions.SelectedValue == "3")
    {
        // "Remember" that we need to delete the old PDF file
        if (category.IsBrochurePathNull())
            deletedCategorysPdfPath = null;
        else
            deletedCategorysPdfPath = category.BrochurePath;
    }
}
protected void Categories_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    // If there were no problems and we updated the PDF file, 
    // then delete the existing one
    if (e.Exception == null)
    {
        DeleteRememberedBrochurePath();
    }
}

Observe cómo el controlador de eventos RowUpdating usa una serie de instrucciones condicionales para realizar la acción adecuada en función del valor de la propiedad SelectedValue del control RadioButtonList BrochureOptions.

Con este código aplicado, puede editar una categoría y hacer que use su folleto actual, que no use ningún folleto o que cargue uno nuevo. Ahora, pruébelo. Establezca puntos de interrupción en los controladores de eventos RowUpdating y RowUpdated para hacerse una idea del flujo de trabajo.

Paso 7: Carga de una nueva imagen

La interfaz de edición del objeto ImageField Picture se representa como un cuadro de texto rellenado con el valor de su propiedad DataImageUrlField. Durante el flujo de trabajo de edición, GridView pasa un parámetro al control ObjectDataSource con el nombre del parámetro, el valor de la propiedad DataImageUrlField de ImageField y el valor especificado en el cuadro de texto de la interfaz de edición. Este comportamiento es adecuado cuando la imagen se guarda como un archivo en el sistema de archivos y la propiedad DataImageUrlField contiene la dirección URL completa de la imagen. En tales circunstancias, la interfaz de edición muestra la dirección URL de la imagen en el cuadro de texto, que el usuario puede cambiar y volver a guardar en la base de datos. Esta interfaz predeterminada no permite al usuario cargar una nueva imagen, pero sí permite cambiar la dirección URL de la imagen de su valor actual a otro. Pero en este tutorial la interfaz de edición predeterminada de ImageField no es suficiente porque los datos binarios de Picture se almacenan directamente en la base de datos y la propiedad DataImageUrlField solo contiene el valor CategoryID.

Para comprender mejor lo que sucede en el tutorial cuando un usuario edita una fila con un objeto ImageField, considere el ejemplo siguiente: un usuario edita una fila con CategoryID igual a 10, lo que hace que el objeto ImageField Picture se represente como un cuadro de texto con el valor 10. Imagine que el usuario cambia el valor de este cuadro de texto a 50 y hace clic en el botón Update. Se produce un postback y el control GridView crea inicialmente un parámetro denominado CategoryID con el valor 50. Pero antes de que el GridView envíe este parámetro (y los parámetros CategoryName y Description), agrega los valores de la colección DataKeys. Por tanto, sobrescribe el parámetro CategoryID con el valor de CategoryID subyacente de la fila actual, que es 10. En resumen, la interfaz de edición de ImageField no afecta al flujo de trabajo de edición de este tutorial porque los nombres de la propiedad DataImageUrlField de ImageField y el valor DataKey de la cuadrícula son iguales.

Aunque ImageField facilita la visualización de una imagen basada en datos de una base de datos, no quiere proporcionar un cuadro de texto en la interfaz de edición. En su lugar, quiere ofrecer un control FileUpload que el usuario final pueda usar para cambiar la imagen de la categoría. A diferencia del valor BrochurePath, para estos tutoriales se ha decidido requerir que cada categoría tenga una imagen. Por tanto, no es necesario permitir al usuario indicar que no hay ninguna imagen asociada, el usuario puede cargar una imagen nueva o dejar la imagen actual tal como está.

Para personalizar la interfaz de edición del objeto ImageField, es necesario convertirlo en un objeto TemplateField. En la etiqueta inteligente del control GridView, haga clic en el vínculo Editar columnas, seleccione el objeto ImageField y haga clic en el vínculo Convertir este campo en un TemplateField.

Convert the ImageField Into a TemplateField

Figura 16: Conversión del campo ImageField en TemplateField

La conversión del objeto ImageField en un objeto TemplateField de esta manera genera un elemento TemplateField con dos plantillas. Como se muestra en la siguiente sintaxis declarativa, el objeto ItemTemplate contiene un control web de imagen cuya propiedad ImageUrl se asigna mediante una sintaxis de enlace de datos basada en las propiedades DataImageUrlField y DataImageUrlFormatString del objeto ImageField. EditItemTemplate contiene un elemento TextBox cuya propiedad Text está enlazada al valor especificado por la propiedad DataImageUrlField.

<asp:TemplateField>
    <EditItemTemplate>
        <asp:TextBox ID="TextBox1" runat="server" 
            Text='<%# Eval("CategoryID") %>'></asp:TextBox>
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Image ID="Image1" runat="server" 
            ImageUrl='<%# Eval("CategoryID", 
                "DisplayCategoryPicture.aspx?CategoryID={0}") %>' />
    </ItemTemplate>
</asp:TemplateField>

Es necesario actualizar EditItemTemplate para usar un control FileUpload. En la etiqueta inteligente del control GridView, haga clic en el vínculo Editar plantillas y seleccione EditItemTemplate del objeto TemplateField Picture en la lista desplegable. En la plantilla debería ver un cuadro de texto, quítelo. Después, arrastre un control FileUpload desde el cuadro de herramientas a la plantilla y establezca su propiedad ID en PictureUpload. Agregue también a la plantilla el texto "To change the category's picture, specify a new picture. To keep the category s picture the same, leave the field empty" (Para cambiar la imagen de la categoría, proporcione una nueva imagen. Para mantener la imagen actual de la categoría, deje el campo vacío).

Add a FileUpload Control to the EditItemTemplate

Figura 17: Adición de un control FileUpload a EditItemTemplate (Haga clic para ver la imagen a tamaño completo)

Después de personalizar la interfaz de edición, vea el progreso en un explorador. Al ver una fila en modo de solo lectura, la imagen de la categoría se muestra como estaba antes, pero al hacer clic en el botón Edit se representa la columna de imagen como texto con un control FileUpload.

The Editing Interface Includes a FileUpload Control

Figura 18: La interfaz de edición incluye un control FileUpload (Haga clic para ver la imagen a tamaño completo)

Recuerde que el control ObjectDataSource está configurado para llamar al método UpdateCategory de la clase CategoriesBLL que acepta como entrada los datos binarios de la imagen como una matriz byte. Pero si esta matriz tiene un valor null, se llama a la sobrecarga de UpdateCategory a, que emite la instrucción SQL UPDATE que no modifica la columna Picture, y deja intacta la imagen actual de la categoría. Por tanto, en el controlador de eventos RowUpdating del control GridView, es necesario hacer referencia mediante programación al control FileUpload PictureUpload y determinar si se ha cargado un archivo. Si no se ha cargado ninguno, no quiere especificar un valor para el parámetro picture. Por otro lado, si se ha cargado un archivo en el control FileUpload PictureUpload, querrá asegurarse de que es un archivo JPG. Si es así, puede enviar su contenido binario al control ObjectDataSource mediante el parámetro picture.

Como con el código usado en el paso 6, gran parte del código necesario aquí ya existe en el controlador de eventos ItemInserting del control DetailsView. Por tanto, se ha refactorizado la funcionalidad común en un nuevo método, ValidPictureUpload, y se ha actualizado el controlador de eventos ItemInserting para usar este método.

Agregue el código siguiente al inicio del controlador de eventos RowUpdating de GridView. Es importante que este código sea anterior al código que guarda el archivo de folleto, ya que no quiere guardar el folleto en el sistema de archivos del servidor web si se carga un archivo de imagen no válido.

// Reference the PictureUpload FileUpload
FileUpload PictureUpload = 
    (FileUpload)Categories.Rows[e.RowIndex].FindControl("PictureUpload");
if (PictureUpload.HasFile)
{
    // Make sure the picture upload is valid
    if (ValidPictureUpload(PictureUpload))
    {
        e.NewValues["picture"] = PictureUpload.FileBytes;
    }
    else
    {
        // Invalid file upload, cancel update and exit event handler
        e.Cancel = true;
        return;
    }
}

El método ValidPictureUpload(FileUpload) toma un control FileUpload como único parámetro de entrada y comprueba la extensión del archivo cargado para asegurarse de que el archivo cargado es un archivo JPG; solo se llama a este método si se carga un archivo de imagen. Si no se carga ningún archivo, no se establece el parámetro picture y, por tanto, usa su valor predeterminado null. Si se carga una imagen y ValidPictureUpload devuelve true, al parámetro picture se le asignan los datos binarios de la imagen cargada. Si el método devuelve false, se cancela el flujo de trabajo de actualización y se cierra el controlador de eventos.

El código del método ValidPictureUpload(FileUpload), que se ha refactorizado desde el controlador de eventos ItemInserting del control DetailsView, es el siguiente:

private bool ValidPictureUpload(FileUpload PictureUpload)
{
    // Make sure that a JPG has been uploaded
    if (string.Compare(System.IO.Path.GetExtension(PictureUpload.FileName), 
            ".jpg", true) != 0 &&
        string.Compare(System.IO.Path.GetExtension(PictureUpload.FileName), 
            ".jpeg", true) != 0)
    {
        UploadWarning.Text = 
            "Only JPG documents may be used for a category's picture.";
        UploadWarning.Visible = true;
        return false;
    }
    else
    {
        return true;
    }
}

Paso 8: Reemplazo de las imágenes originales de las categorías por archivos JPG

Recuerde que las ocho imágenes originales de las categorías son archivos de mapa de bits encapsulados en un encabezado OLE. Ahora que ha agregado la capacidad de editar la imagen de un registro existente, dedique un momento a reemplazar estos mapas de bits por archivos JPG. Si quiere seguir usando las imágenes de categoría actuales, puede convertirlas a archivos JPG realizando los pasos siguientes:

  1. Guarde las imágenes de mapa de bits en el disco duro. Visite la página UpdatingAndDeleting.aspx en el explorador y, para cada una de las ocho primeras categorías, haga clic con el botón derecho en la imagen y elija guardar la imagen.
  2. Abra la imagen en el editor de imágenes que prefiera. Puede usar Microsoft Paint, por ejemplo.
  3. Guarde el mapa de bits como una imagen JPG.
  4. Actualice la imagen de la categoría con el archivo JPG desde la interfaz de edición.

Después de editar una categoría y cargar la imagen JPG, la imagen no se representará en el explorador porque la página DisplayCategoryPicture.aspx quita los primeros 78 bytes de las imágenes de las ocho primeras categorías. Para corregirlo, quite el código que realiza la eliminación del encabezado OLE. Después de hacerlo, el controlador de eventos DisplayCategoryPicture.aspx``Page_Load debería tener solo el código siguiente:

protected void Page_Load(object sender, EventArgs e)
{
    int categoryID = Convert.ToInt32(Request.QueryString["CategoryID"]);
    // Get information about the specified category
    CategoriesBLL categoryAPI = new CategoriesBLL();
    Northwind.CategoriesDataTable categories = _
        categoryAPI.GetCategoryWithBinaryDataByCategoryID(categoryID);
    Northwind.CategoriesRow category = categories[0];
    // For new categories, images are JPGs...
    
    // Output HTTP headers providing information about the binary data
    Response.ContentType = "image/jpeg";
    // Output the binary data
    Response.BinaryWrite(category.Picture);
}

Nota:

Las interfaces de inserción y edición de la página UpdatingAndDeleting.aspx podrían necesitar un poco más de trabajo. Los campos BoundField CategoryName y Description de los controles DetailsView y GridView deben convertirse en objetos TemplateField. Como CategoryName no admite valores NULL, se debe agregar un elemento RequiredFieldValidator. Y probablemente se debería convertir el cuadro de texto Description en un objeto TextBox de varias líneas. Estos retoques finales se dejan como un ejercicio para que practique.

Resumen

Con este tutorial finaliza el análisis del trabajo con datos binarios. En este tutorial y los tres anteriores ha visto cómo se pueden almacenar datos binarios en el sistema de archivos o directamente en la base de datos. Un usuario proporciona datos binarios al sistema seleccionando un archivo de su disco duro y cargándolo en el servidor web, desde donde se puede almacenar en el sistema de archivos o insertar en la base de datos. ASP.NET 2.0 incluye un control FileUpload que permite proporcionar una interfaz tan fácil de usar como arrastrar y colocar. Pero como se indica en el tutorial Carga de archivos, el control FileUpload solo es adecuado para cargas de archivos relativamente pequeñas, idealmente que no superen un megabyte. También ha visto cómo asociar los datos cargados con el modelo de datos subyacente, además de cómo editar y eliminar los datos binarios de los registros existentes.

En el siguiente conjunto de tutoriales se exploran varias técnicas de almacenamiento en caché. El almacenamiento en caché proporciona un medio para mejorar el rendimiento general de una aplicación tomando los resultados de operaciones costosas y almacenándolos en una ubicación a la que se puede acceder más rápidamente.

¡Feliz programación!

Acerca del autor

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

Agradecimientos especiales a

Esta serie de tutoriales fue revisada por muchos revisores. El revisor principal de este tutorial ha sido Teresa Murphy. ¿Le interesaría revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.