Actualizar y eliminar los datos binarios existentes (VB)

Por Scott Mitchell

Descargar PDF

En tutoriales anteriores vimos cómo el control GridView facilita la edición y eliminación de datos de texto. En este tutorial, veremos 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 hemos agregado bastante funcionalidad para trabajar con datos binarios. Para empezar, hemos agregado una columna BrochurePath a la tabla Categories y hemos actualizado la arquitectura en consecuencia. También hemos 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. Hemos 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 hemos 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 haremos 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 generaron en función de la consulta principal de CategoriesTableAdapter, que no incluye la columna Picture. Por lo tanto, los métodos Insert y Update no incluyen parámetros para especificar los datos binarios para la imagen de la categoría. Igual que hicimos en el tutorial anterior, es necesario crear un nuevo 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 preguntándonos 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. Puesto que estamos creando 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 (una 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))

La pantalla final del asistente nos pide que asignemos un nombre al nuevo método TableAdapter. Escriba UpdateWithPicture y haga clic en Finalizar.

Name the New TableAdapter Method UpdateWithPicture

Figura 2: Asignación de un nombre al método UpdateWithPicture 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 para 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, podemos 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 Function DeleteCategory(ByVal categoryID As Integer) As Boolean
    Dim rowsAffected As Integer = Adapter.Delete(categoryID)
    ' Return true if precisely one row was deleted, otherwise false
    Return rowsAffected = 1
End Function

En este tutorial, vamos a crear dos métodos para actualizar una categoría: uno que espera los datos de imagen binarios e invoca el método UpdateWithPicture que acabamos 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á editando. Por lo tanto, queremos 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 Integer como parámetros de entrada; el segundo, solo tres objetos String y uno Integer. 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 Integer 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 Nothing:

<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Update, False)> _
Public Function UpdateCategory(categoryName As String, description As String, _
    brochurePath As String, picture() As Byte, categoryID As Integer) As Boolean
    
    ' If no picture is specified, use other overload
    If picture Is Nothing Then
        Return UpdateCategory(categoryName, description, brochurePath, categoryID)
    End If
    ' Update picture, as well
    Dim rowsAffected As Integer = Adapter.UpdateWithPicture _
        (categoryName, description, brochurePath, picture, categoryID)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function
<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateCategory(categoryName As String, description As String, _
    brochurePath As String, categoryID As Integer) As Boolean
    Dim rowsAffected As Integer = Adapter.Update _
        (categoryName, description, brochurePath, categoryID)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

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

En el tutorial anterior creamos 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 ampliaremos el control GridView para que admita la edición y eliminación. En lugar de continuar trabajando desde UploadInDetailsView.aspx, coloquemos en su lugar los cambios de este tutorial 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 del ObjectDataSource. Sin embargo, 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 marcamos estos métodos usando DataObjectMethodAttribute como el método predeterminado para actualizar y eliminar.

Por ahora, establezca la lista desplegable de la pestaña UPDATE en (Ninguno), pero deje la lista desplegable de la pestaña DELETE establecida en DeleteCategory. Volveremos 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í sobrescribirá 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 lo tanto, borre esta propiedad por completo o restablézcala al valor predeterminado, {0}. Si necesita repasar esta propiedad del control ObjectDataSource, consulte 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 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 el 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 obtendrá 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 proporcionándole un folleto y una imagen. Mi categoría de prueba, la cual 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 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 Delete 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, necesitamos 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 queremos 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
Private deletedCategorysPdfPath As String = Nothing
Protected Sub Categories_RowDeleting(sender As Object, e As GridViewDeleteEventArgs) _
    Handles Categories.RowDeleting
    
    ' Determine the PDF path for the category being deleted...
    Dim categoryID As Integer = Convert.ToInt32(e.Keys("CategoryID"))
    Dim categoryAPI As New CategoriesBLL()
    Dim categoriesData As Northwind.CategoriesDataTable = _
        categoryAPI.GetCategoryByCategoryID(categoryID)
    Dim category As Northwind.CategoriesRow = categoriesData(0)
    If category.IsBrochurePathNull() Then
        deletedCategorysPdfPath = Nothing
    Else
        deletedCategorysPdfPath = category.BrochurePath
    End If
End Sub
Protected Sub Categories_RowDeleted(sender As Object, e As GridViewDeletedEventArgs) _
    Handles Categories.RowDeleted
    
    ' Delete the brochure file if there were no problems deleting the record
    If e.Exception Is Nothing Then
        DeleteRememberedBrochurePath()
    End If
End Sub

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íamos 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 invocado 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. Esto 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

Tal y como se describe 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 del ObjectDataSource y continúe con el segundo paso. Dado que 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 lo 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 desde 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 TextBox a cuya propiedad Text se le asigna el valor del DataImageUrlField del 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, una que permita al usuario hacer una de las siguientes cosas:

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

También necesitamos actualizar la interfaz de edición del objeto ImageField Picture, pero llegaremos a esto en el paso 7.

En 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, estableciendo 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 establecido en 1, 2 y 3, respectivamente:

  • Use current brochure
  • Remove current brochure
  • Upload new brochure

Establezca la propiedad Selected del primer 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 Sub BrochureOptions_SelectedIndexChanged _
    (sender As Object, e As EventArgs)
    
    ' Get a reference to the RadioButtonList and its Parent
    Dim BrochureOptions As RadioButtonList = _
        CType(sender, RadioButtonList)
    Dim parent As Control = BrochureOptions.Parent
    ' Now use FindControl("controlID") to get a reference of the 
    ' FileUpload control
    Dim BrochureUpload As FileUpload = _
        CType(parent.FindControl("BrochureUpload"), FileUpload)
    ' Only show BrochureUpload if SelectedValue = "3"
    BrochureUpload.Visible = (BrochureOptions.SelectedValue = "3")
End Sub

Dado que los controles RadioButtonList y FileUpload están dentro de una plantilla, tenemos que escribir un poco de 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 tengamos 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 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 Edit. La figura 15 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, queremos seguir usando la misma configuración de BrochurePath. Por lo 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, queremos establecer el valor de BrochurePath del registro en NULL. Esto se puede lograr estableciendo 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 queremos hacerlo si la actualización se completa sin generar una excepción.
  • Si SelectedValue es 3, queremos asegurarnos de que el usuario ha cargado un archivo PDF y, después, queremos 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 queremos hacerlo si la actualización se completa sin generar una excepción.

Los pasos que es necesario 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 hemos agregado en el tutorial anterior. Por lo tanto, nos corresponde refactorizar esta funcionalidad en métodos independientes. En concreto, he 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.

El código de estos dos métodos se muestra más abajo. Observe la similitud entre ProcessBrochureUpload y el controlador de eventos ItemInserting del control DetailsView del tutorial anterior. En este tutorial he 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 Function ProcessBrochureUpload _
    (BrochureUpload As FileUpload, CancelOperation As Boolean) As String
    
    CancelOperation = False    ' by default, do not cancel operation
    If BrochureUpload.HasFile Then
        ' Make sure that a PDF has been uploaded
        If String.Compare(System.IO.Path.GetExtension(BrochureUpload.FileName), _
            ".pdf", True) <> 0 Then
            
            UploadWarning.Text = _
                "Only PDF documents may be used for a category's brochure."
            UploadWarning.Visible = True
            CancelOperation = True
            Return Nothing
        End If
        Const BrochureDirectory As String = "~/Brochures/"
        Dim brochurePath As String = BrochureDirectory + BrochureUpload.FileName
        Dim fileNameWithoutExtension As String = _
            System.IO.Path.GetFileNameWithoutExtension(BrochureUpload.FileName)
        Dim iteration As Integer = 1
        While System.IO.File.Exists(Server.MapPath(brochurePath))
            brochurePath = String.Concat(BrochureDirectory, _
                fileNameWithoutExtension, "-", iteration, ".pdf")
            iteration += 1
        End While
        ' 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 Nothing
    End If
End Function
Private Sub DeleteRememberedBrochurePath()
    ' Is there a file to delete?
    If deletedCategorysPdfPath IsNot Nothing Then
        System.IO.File.Delete(Server.MapPath(deletedCategorysPdfPath))
    End If
End Sub

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 Sub Categories_RowUpdating _
    (sender As Object, e As GridViewUpdateEventArgs) _
    Handles Categories.RowUpdating
    
    ' Reference the RadioButtonList
    Dim BrochureOptions As RadioButtonList = _
        CType(Categories.Rows(e.RowIndex).FindControl("BrochureOptions"), _
            RadioButtonList)
    ' Get BrochurePath information about the record being updated
    Dim categoryID As Integer = Convert.ToInt32(e.Keys("CategoryID"))
    Dim categoryAPI As New CategoriesBLL()
    Dim categoriesData As Northwind.CategoriesDataTable = _
        categoryAPI.GetCategoryByCategoryID(categoryID)
    Dim category As Northwind.CategoriesRow = categoriesData(0)
    If BrochureOptions.SelectedValue = "1" Then
        ' Use current value for BrochurePath
        If category.IsBrochurePathNull() Then
            e.NewValues("brochurePath") = Nothing
        Else
            e.NewValues("brochurePath") = category.BrochurePath
        End If
    ElseIf BrochureOptions.SelectedValue = "2" Then
        ' Remove the current brochure (set it to NULL in the database)
        e.NewValues("brochurePath") = Nothing
    ElseIf BrochureOptions.SelectedValue = "3" Then
        ' Reference the BrochurePath FileUpload control
        Dim BrochureUpload As FileUpload = _
            CType(categories.Rows(e.RowIndex).FindControl("BrochureUpload"), _
                FileUpload)
        ' Process the BrochureUpload
        Dim cancelOperation As Boolean = False
        e.NewValues("brochurePath") = _
            ProcessBrochureUpload(BrochureUpload, cancelOperation)
        e.Cancel = cancelOperation
    Else
        ' Unknown value!
        Throw New ApplicationException( _
            String.Format("Invalid BrochureOptions value, {0}", _
                BrochureOptions.SelectedValue))
    End If
    If BrochureOptions.SelectedValue = "2" OrElse _
        BrochureOptions.SelectedValue = "3" Then
        
        ' "Remember" that we need to delete the old PDF file
        If (category.IsBrochurePathNull()) Then
            deletedCategorysPdfPath = Nothing
        Else
            deletedCategorysPdfPath = category.BrochurePath
        End If
    End If
End Sub
Protected Sub Categories_RowUpdated _
    (sender As Object, e As GridViewUpdatedEventArgs) _
    Handles Categories.RowUpdated
    
    ' If there were no problems and we updated the PDF file, 
    ' then delete the existing one
    If e.Exception Is Nothing Then
        DeleteRememberedBrochurePath()
    End If
End Sub

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. En este tutorial, sin embargo, 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 nuestro 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. Sin embargo, 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 lo 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 queremos proporcionar un cuadro de texto en la interfaz de edición. En su lugar, queremos ofrecer un control FileUpload que el usuario final puede usar para cambiar la imagen de la categoría. A diferencia del valor BrochurePath, para estos tutoriales hemos decidido requerir que cada categoría tenga una imagen. Por lo 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 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. La plantilla 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>

Necesitamos actualizar la plantilla EditItemTemplate para usar un control FileUpload. En la etiqueta inteligente del control GridView, haga clic en el vínculo Editar plantillas y seleccione la plantilla 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. Sin embargo, si esta matriz es Nothing, se llama a la sobrecarga alternativa UpdateCategory, que emite la instrucción SQL UPDATE que no modifica la columna Picture, dejando intacta la imagen actual de la categoría. Por lo 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 queremos especificar un valor para el parámetro picture. Por otro lado, si se ha cargado un archivo en el control FileUpload PictureUpload, queremos asegurarnos de que es un archivo JPG. Si es así, podemos enviar su contenido binario al control ObjectDataSource mediante el parámetro picture.

Igual que sucede 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 lo tanto, he refactorizado la funcionalidad común en un nuevo método, ValidPictureUpload, y he actualizado el controlador de eventos ItemInserting para usar este método.

Agregue el código siguiente al inicio del controlador de eventos RowUpdating del GridView. Es importante que este código venga antes del código que guarda el archivo de folleto, ya que no queremos 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
Dim PictureUpload As FileUpload = _
    CType(categories.Rows(e.RowIndex).FindControl("PictureUpload"), _
        FileUpload)
If PictureUpload.HasFile Then
    ' Make sure the picture upload is valid
    If ValidPictureUpload(PictureUpload) Then
        e.NewValues("picture") = PictureUpload.FileBytes
    Else
        ' Invalid file upload, cancel update and exit event handler
        e.Cancel = True
        Exit Sub
    End If
End If

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 Nothing. 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 Function ValidPictureUpload(ByVal PictureUpload As FileUpload) As Boolean
    ' Make sure that a JPG has been uploaded
    If String.Compare(System.IO.Path.GetExtension(PictureUpload.FileName), _
        ".jpg", True) <> 0 AndAlso _
        String.Compare(System.IO.Path.GetExtension(PictureUpload.FileName), _
        ".jpeg", True) <> 0 Then
        
        UploadWarning.Text = _
            "Only JPG documents may be used for a category's picture."
        UploadWarning.Visible = True
        Return False
    Else
        Return True
    End If
End Function

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 hemos 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. Corrija esto quitando 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 Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
    Dim categoryID As Integer = _
        Convert.ToInt32(Request.QueryString("CategoryID"))
    ' Get information about the specified category
    Dim categoryAPI As New CategoriesBLL()
    Dim categories As Northwind.CategoriesDataTable = _
        categoryAPI.GetCategoryWithBinaryDataByCategoryID(categoryID)
    Dim category As Northwind.CategoriesRow = 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)
End Sub

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. Dado que 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. Dejo estos toques finales como un ejercicio para que practique.

Resumen

Con este tutorial finaliza nuestro análisis del trabajo con datos binarios. En este tutorial y los tres anteriores hemos 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. Sin embargo, 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 hemos explorado 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.

Nuestro siguiente conjunto de tutoriales explora 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/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 vía mitchell@4GuysFromRolla.com. o a través de su blog, que se puede encontrar en http://ScottOnWriting.NET.

Agradecimientos especiales a

Muchos revisores han evaluado esta serie de tutoriales. El revisor principal de este tutorial ha sido Teresa Murphy. ¿Le interesa revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.