Compartir a través de


Incluir una opción de carga de archivos al agregar un nuevo registro (VB)

por Scott Mitchell

Descargar PDF

En este tutorial se muestra cómo crear una interfaz web que permita al usuario escribir datos de texto y cargar archivos binarios. Para ilustrar las opciones disponibles a fin de almacenar datos binarios, se guardará un archivo en la base de datos mientras el otro se almacena en el sistema de archivos.

Introducción

En los dos tutoriales anteriores se han explorado técnicas para almacenar datos binarios asociados al modelo de datos de la aplicación, se ha examinado cómo usar el control FileUpload para enviar archivos desde el cliente al servidor web y se ha visto cómo presentar estos datos binarios en un control web de datos. Pero todavía hay que hablar sobre cómo asociar datos cargados con el modelo de datos.

En este tutorial creará una página web para agregar una nueva categoría. Además de controles TextBox para el nombre y la descripción de la categoría, esta página deberá incluir dos controles FileUpload, uno para la nueva imagen de la categoría y otro para el folleto. La imagen cargada se almacenará directamente en la nueva columna Picture del registro , mientras que el folleto se guardará en la carpeta ~/Brochures con la ruta de acceso al archivo guardado en la columna BrochurePath del nuevo registro.

Antes de crear esta página web, es necesario actualizar la arquitectura. La consulta principal de CategoriesTableAdapter no recupera la columna Picture. Por tanto, el método Insert generado automáticamente solo tiene entradas para los campos CategoryName, Description y BrochurePath. Por tanto, es necesario crear un método adicional en TableAdapter que solicite los cuatro campos Categories. La clase CategoriesBLL de la capa de lógica de negocios también tendrá que actualizarse.

Paso 1: Adición de un método InsertWithPicture a CategoriesTableAdapter

Cuando se creó CategoriesTableAdapter en el tutorial Creación de una capa de acceso a datos, se configuró para generar automáticamente instrucciones INSERT, UPDATE y DELETE basadas en la consulta principal. Además, se indicó a TableAdapter que usara el enfoque directo de base de datos, que creó los métodos Insert, Update y Delete. Estos métodos ejecutan las instrucciones INSERT, UPDATE y DELETE generadas automáticamente y, como consecuencia, aceptan parámetros de entrada basados en las columnas que devuelve la consulta principal. En el tutorial Carga de archivos, se aumentó la consulta principal de CategoriesTableAdapter para usar la columna BrochurePath.

Como la consulta principal de CategoriesTableAdapter no hace referencia a la columna Picture, no se puede agregar un nuevo registro ni actualizar un registro existente con un valor para la columna Picture. Para capturar esta información, puede crear un método en TableAdapter que se use específicamente a fin de insertar un registro con datos binarios, o bien personalizar la instrucción INSERT generada automáticamente. El problema con la personalización de la instrucción INSERT generada automáticamente es que se corre el riesgo de que el asistente sobrescriba las personalizaciones. Por ejemplo, imagine que ha personalizado la instrucción INSERT para incluir el uso de la columna Picture. Esto actualizaría el método Insert de TableAdapter a fin de incluir un parámetro de entrada adicional para los datos binarios de la imagen de la categoría. Después, podría crear un método en la capa de lógica de negocios para usar este método la DAL e invocar este método la BLL mediante la capa de presentación, y todo funcionaría a la perfección. Es decir, hasta la próxima vez que se configure TableAdapter mediante el Asistente para la configuración de TableAdapter. En cuanto se complete el asistente, las personalizaciones de la instrucción INSERT se sobrescribirían, el método Insert se revertiría a su formato anterior y el código ya no se compilaría.

Nota:

Esto supone un problema al usar procedimientos almacenados en lugar de instrucciones SQL ad hoc. En un tutorial posterior se explorará el uso de los procedimientos almacenados en lugar de instrucciones SQL ad hoc en la capa de acceso a datos.

Para evitar esta posible molestia, en lugar de personalizar las instrucciones SQL generadas automáticamente, se creará un método para TableAdapter. Este método, denominado InsertWithPicture, aceptará valores para las columnas CategoryName, Description, BrochurePath y Picture, y ejecutará una instrucción INSERT que almacena los cuatro valores de un nuevo registro.

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. Esto inicia el Asistente para la configuración de consultas de TableAdapter, que comienza preguntándole 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 crear una consulta para agregar un nuevo registro a la tabla Categories, elija INSERT y haga clic en Siguiente.

Select the INSERT Option

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

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

INSERT INTO [Categories] 
    ([CategoryName], [Description], [BrochurePath], [Picture]) 
VALUES 
    (@CategoryName, @Description, @BrochurePath, @Picture)

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

Name the New TableAdapter Method InsertWithPicture

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

Paso 2: Actualización de la capa de lógica de negocios

Como la capa de presentación solo debe interactuar con la capa de lógica de negocios en lugar de pasarla directamente a la capa de acceso a datos, es necesario crear un método de la BLL que invoque el método de la DAL que se acaba de crear (InsertWithPicture). En este tutorial, se creará un método en la clase CategoriesBLL con el nombre InsertWithPicture que acepte como entrada tres elementos String y una matriz Byte. Los parámetros de entrada de String 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. Como se muestra en el código siguiente, este método de la BLL invoca el método de la DAL correspondiente:

<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Insert, False)> _
Public Sub InsertWithPicture(categoryName As String, description As String, _
    brochurePath As String, picture() As Byte)
    
    Adapter.InsertWithPicture(categoryName, description, brochurePath, picture)
End Sub

Nota:

Asegúrese de haber guardado el conjunto de datos con tipo antes de agregar el método InsertWithPicture a la BLL. Como el código de clase CategoriesTableAdapter se genera automáticamente en función del conjunto de datos con tipo, si no guarda primero los cambios en el conjunto de datos con tipo, la propiedad Adapter no conocerá el método InsertWithPicture.

Paso 3: Representación de una lista de las categorías existentes y sus datos binarios

En este tutorial creará una página que permita a un usuario final agregar una nueva categoría al sistema, proporcionando una imagen y un folleto para la nueva categoría. En el tutorial anterior, se ha usado un objeto GridView con controles TemplateField e ImageField para mostrar cada nombre, descripción, imagen y vínculo de cada categoría a fin de descargar su folleto. En este tutorial se replicará esa funcionalidad y se creará una página que muestre todas las categorías existentes y permita crear otras.

Para empezar, abra la página DisplayOrDownload.aspx en la carpeta BinaryData. Vaya a la vista Origen, copie la sintaxis declarativa de GridView y ObjectDataSource y péguela en el elemento <asp:Content> en UploadInDetailsView.aspx. Además, no olvide copiar el método GenerateBrochureLink de la clase de código subyacente de DisplayOrDownload.aspx a UploadInDetailsView.aspx.

Copy and Paste the Declarative Syntax from DisplayOrDownload.aspx to UploadInDetailsView.aspx

Figura 3: Copia y pegado de la sintaxis declarativa de DisplayOrDownload.aspx en UploadInDetailsView.aspx (Haga clic para ver la imagen a tamaño completo)

Después de copiar la sintaxis declarativa y el método GenerateBrochureLink en la página UploadInDetailsView.aspx, vea la página mediante un explorador para asegurarse de que todo se ha copiado correctamente. Debería ver un control GridView en el que se muestran las ocho categorías que incluye un vínculo para descargar el folleto, así como la imagen de la categoría.

You Should Now See Each Category Along with Its Binary Data

Figura 4: Se debería ver cada categoría junto con sus datos binarios (Haga clic para ver la imagen a tamaño completo)

Paso 4: Configuración de CategoriesDataSource para admitir la inserción

La instancia de ObjectDataSource CategoriesDataSource que usa actualmente el control GridView Categories no proporciona la capacidad de insertar datos. Para admitir la inserción mediante este control de origen de datos, es necesario asignar su método Insert a un método en su objeto subyacente, CategoriesBLL. En concreto, querrá asignarlo al método CategoriesBLL que se ha agregado en el paso 2, InsertWithPicture.

Para empezar, haga clic en el vínculo Configurar origen de datos desde la etiqueta inteligente de ObjectDataSource. En la primera pantalla se muestra el objeto con el que el origen de datos está configurado para trabajar, CategoriesBLL. Deje esta configuración tal como está y haga clic en Siguiente para avanzar a la pantalla Definir métodos de datos. Vaya a la pestaña INSERT y elija el método InsertWithPicture de la lista desplegable. Haga clic en Finalizar para completar el asistente.

Configure the ObjectDataSource to use the InsertWithPicture Method

Figura 5: Configuración de ObjectDataSource para usar el método InsertWithPicture (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.

Después de completar el asistente, ObjectDataSource incluirá ahora un valor para su propiedad InsertMethod, así como InsertParameters para las cuatro columnas de categoría, como se muestra en el marcado declarativo siguiente:

<asp:ObjectDataSource ID="CategoriesDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetCategories" 
    TypeName="CategoriesBLL" InsertMethod="InsertWithPicture">
    <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>
</asp:ObjectDataSource>

Paso 5: Creación de la interfaz de inserción

Como se ha descrito en Introducción de la inserción, actualización y eliminación de datos, el control DetailsView proporciona una interfaz de inserción integrada que se puede usar al trabajar con un control de origen de datos que admite la inserción. Se agregará un control DetailsView a esta página encima de GridView que representará permanentemente su interfaz de inserción, lo que permite al usuario agregar rápidamente una nueva categoría. Al agregar una nueva categoría en DetailsView, el control GridView que hay debajo se actualizará automáticamente y mostrará la nueva categoría.

Para empezar, arrastre un control DetailsView desde el Cuadro de herramientas al Diseñador situado encima de GridView, establezca su propiedad ID en NewCategory y borre los valores de las propiedades Height y Width. Desde la etiqueta inteligente de DetailsView, vincúlelo al objeto CategoriesDataSource existente y active la casilla Habilitar inserción.

Screenshot of DetailsView with the CategoryID property set to NewCategory, the Height and Width property values are empty, and the Enable Inserting checkbox selected.

Figura 6: Enlace de DetailsView a CategoriesDataSource y habilitación de la inserción (Haga clic para ver la imagen a tamaño completo)

Para representar permanentemente el control DetailsView en su interfaz de inserción, establezca su propiedad DefaultMode en Insert.

Observe que DetailsView tiene cinco controles BoundField: CategoryID, CategoryName, Description, NumberOfProducts y BrochurePath, aunque el control BoundField CategoryID no se representa en la interfaz de inserción porque su propiedad InsertVisible está establecida en False. Estos controles BoundField existen porque son las columnas que devuelve el método GetCategories(), el que el elemento ObjectDataSource invoca para recuperar sus datos. Pero a fin de realizar la inserción, no quiere permitir que el usuario especifique un valor para NumberOfProducts. Además, es necesario permitirles cargar una imagen para la nueva categoría, así como cargar un PDF para el folleto.

Quite el control BoundField NumberOfProducts de DetailsView por completo y, después, actualice las propiedades HeaderText de los controles BoundField CategoryName y BrochurePath a Category y Brochure, respectivamente. A continuación, convierta el control BoundField BrochurePath en un control TemplateField y agregue un nuevo control TemplateField para la imagen, lo que le proporciona un valor HeaderText de Picture. Mueva el control TemplateField Picture para situarlo entre los controles TemplateField BrochurePath y CommandField.

Screenshot showing the fields window with TemplateField, Picture, and HeaderText highlighted.

Figura 7: Enlace de DetailsView a CategoriesDataSource y habilitación de la inserción

Si ha convertido el control BoundField BrochurePath en un control TemplateField mediante el cuadro de diálogo Editar campos, TemplateField incluye instancias de ItemTemplate, EditItemTemplate y InsertItemTemplate. Pero solo se necesita InsertItemTemplate, así que no dude en quitar las otras dos plantillas. En este momento, la sintaxis declarativa de DetailsView debe ser similar a la siguiente:

<asp:DetailsView ID="NewCategory" runat="server" AutoGenerateRows="False" 
    DataKeyNames="CategoryID" DataSourceID="CategoriesDataSource" 
    DefaultMode="Insert">
    <Fields>
        <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" 
            InsertVisible="False" ReadOnly="True" 
            SortExpression="CategoryID" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            SortExpression="CategoryName" />
        <asp:BoundField DataField="Description" HeaderText="Description" 
            SortExpression="Description" />
        <asp:TemplateField HeaderText="Brochure" SortExpression="BrochurePath">
            <InsertItemTemplate>
                <asp:TextBox ID="TextBox1" runat="server"
                    Text='<%# Bind("BrochurePath") %>'></asp:TextBox>
            </InsertItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Picture"></asp:TemplateField>
        <asp:CommandField ShowInsertButton="True" />
    </Fields>
</asp:DetailsView>

Adición de controles FileUpload para los campos Brochure y Picture

Actualmente, el objeto InsertItemTemplate de TemplateField BrochurePath contiene un control TextBox, mientras que el objeto Picture de TemplateField no contiene ninguna plantilla. Es necesario actualizar estos dos objetos InsertItemTemplate de TemplateField para usar controles FileUpload.

Desde la etiqueta inteligente DetailsView, elija la opción Editar plantillas y, después, seleccione el objeto InsertItemTemplate del control TemplateField BrochurePath en la lista desplegable. Quite el control TextBox y, después, arrastre un control FileUpload desde el Cuadro de herramientas a la plantilla. Establezca el valor ID del control FileUpload en BrochureUpload. Del mismo modo, agregue un control FileUpload al objeto InsertItemTemplate de TemplateField Picture. Establezca el valor ID de este control FileUpload en PictureUpload.

Add a FileUpload Control to the InsertItemTemplate

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

Después de realizar estas incorporaciones, la sintaxis declarativa de TemplateField será la siguiente:

<asp:TemplateField HeaderText="Brochure" SortExpression="BrochurePath">
    <InsertItemTemplate>
        <asp:FileUpload ID="BrochureUpload" runat="server" />
    </InsertItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Picture">
    <InsertItemTemplate>
        <asp:FileUpload ID="PictureUpload" runat="server" />
    </InsertItemTemplate>
</asp:TemplateField>

Cuando un usuario agrega una nueva categoría, querrá asegurarnos de que el folleto y la imagen sean del tipo de archivo correcto. Para el folleto, el usuario debe proporcionar un PDF. Para la imagen, es necesario que el usuario cargue un archivo de imagen, pero ¿se permite cualquier archivo de imagen o solo los de un tipo determinado, como GIF o JPG? Para permitir distintos tipos de archivo, es necesario ampliar el esquema Categories a fin de incluir una columna que capture el tipo de archivo para que este tipo se pueda enviar al cliente mediante Response.ContentType en DisplayCategoryPicture.aspx. Como este tipo de columna no existe, sería prudente restringir a los usuarios para que proporcionen solo un tipo de archivo de imagen específico. Las imágenes existentes de la tabla Categories son mapas de bits, pero JPG es un formato de archivo más adecuado para las imágenes que se sirven en la web.

Si un usuario carga un tipo de archivo incorrecto, es necesario cancelar la inserción y mostrar un mensaje que indique el problema. Agregue un control web Label debajo de DetailsView. Establezca su propiedad ID en UploadWarning, borre su propiedad Text, establezca la propiedad CssClass en Warning y las propiedades Visible y EnableViewState en False. La clase Warning de CSS se define en Styles.css y representa el texto en una fuente grande, roja, en cursiva y negrita.

Nota:

Idealmente, los controles BoundFields CategoryName y Description se convertirán en controles TemplateField y se personalizarán sus interfaces de inserción. Por ejemplo, la interfaz de inserción Description sería más adecuada mediante un cuadro de texto de varias líneas. Y como la columna CategoryName no acepta valores NULL, se debe agregar un objeto RequiredFieldValidator a fin de asegurarse de que el usuario proporciona un valor para el nombre de la nueva categoría. Estos pasos se dejan como ejercicio para el lector. Consulte Personalización de la interfaz de modificación de datos para obtener un análisis detallado al aumento de las interfaces de modificación de datos.

Paso 6: Guardado del folleto cargado en el sistema de archivos del servidor web

Cuando el usuario escribe los valores de una nueva categoría y hace clic en el botón Insertar, se produce un postback y se inicia el flujo de trabajo de inserción. En primer lugar, se desencadena el evento ItemInserting de DetailsView. A continuación, se invoca el método Insert() de ObjectDataSource, que da como resultado que se agregue un nuevo registro a la tabla Categories. Después, se desencadena el evento ItemInserted de DetailsView.

Antes de invocar el método Insert() de ObjectDataSource, debe asegurarse de que el usuario ha cargado los tipos de archivo adecuados y, después, guardar el PDF del folleto en el sistema de archivos del servidor web. Cree un controlador de eventos para el evento ItemInserting de DetailsView y agregue el código siguiente:

' Reference the FileUpload controls
Dim BrochureUpload As FileUpload = _
    CType(NewCategory.FindControl("BrochureUpload"), FileUpload)
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
        e.Cancel = True
        Exit Sub
    End If
End If

Para empezar, el controlador de eventos hace referencia al control FileUpload BrochureUpload desde las plantillas de DetailsView. Después, si se ha cargado un folleto, se examina la extensión del archivo cargado. Si la extensión no es .PDF, se muestra una advertencia, se cancela la inserción y finaliza la ejecución del controlador de eventos.

Nota:

Confiar en la extensión del archivo cargado no es una técnica segura para asegurarse de que el archivo cargado es un documento PDF. El usuario podría tener un documento PDF válido con la extensión .Brochure, o podría haber tomado un documento que no es PDF y proporcionarle una extensión .pdf. El contenido binario del archivo tendría que examinarse mediante programación para comprobar de forma más convincente el tipo de archivo. Pero estos enfoques exhaustivos suelen ser excesivos; comprobar la extensión es suficiente en la mayoría de escenarios.

Como se describe en el tutorial Carga de archivos, se debe tener cuidado al guardar archivos en el sistema de archivos para que una carga de un usuario no sobrescriba la de otro. En este tutorial intentará usar el mismo nombre que el archivo cargado. Pero si ya existe un archivo en el directorio ~/Brochures con ese mismo nombre de archivo, se agregará un número al final hasta que se encuentre un nombre único. Por ejemplo, si el usuario carga un archivo de folleto denominado Meats.pdf, pero ya hay un archivo denominado Meats.pdf en la carpeta ~/Brochures, el nombre del archivo guardado se cambiará a Meats-1.pdf. Si ya existe, se probará conMeats-2.pdf, etc., hasta que se encuentre un nombre de archivo único.

El código siguiente usa el método File.Exists(path) para determinar si ya existe un archivo con el nombre de archivo especificado. Si es así, continúa probando nuevos nombres de archivo para el folleto hasta que no se encuentre ningún conflicto.

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

Una vez que se encuentre un nombre de archivo válido, el archivo se debe guardar en el sistema de archivos y el valor brochurePath``InsertParameter de ObjectDataSource se debe actualizar para que este nombre de archivo se escriba en la base de datos. Como ha visto en el tutorial Carga de archivos, el archivo se puede guardar mediante el método SaveAs(path) del control FileUpload. Para actualizar el parámetro brochurePath de ObjectDataSource, use la colección e.Values.

' Save the file to disk and set the value of the brochurePath parameter
BrochureUpload.SaveAs(Server.MapPath(brochurePath))
e.Values("brochurePath") = brochurePath

Paso 7: Guardado de la imagen cargada en la base de datos

Para almacenar la imagen cargada en el nuevo registro Categories, es necesario asignar el contenido binario cargado al parámetro picture de ObjectDataSource en el evento ItemInserting de DetailsView. Pero antes de realizar esta asignación, debe asegurarse de que la imagen cargada sea JPG y no de otro tipo de imagen. Al igual que en el paso 6, se usará la extensión de archivo de la imagen cargada para determinar su tipo.

Aunque la tabla Categories permite valores NULL para la columna Picture, todas las categorías tienen actualmente una imagen. Se obligará al usuario a proporcionar una imagen al agregar una nueva categoría mediante esta página. El código siguiente comprueba si se ha cargado una imagen y que esta tiene una extensión adecuada.

' Reference the FileUpload controls
Dim PictureUpload As FileUpload = _
    CType(NewCategory.FindControl("PictureUpload"), FileUpload)
If PictureUpload.HasFile Then
    ' 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
        e.Cancel = True
        Exit Sub
    End If
Else
    ' No picture uploaded!
    UploadWarning.Text = _
        "You must provide a picture for the new category."
    UploadWarning.Visible = True
    e.Cancel = True
    Exit Sub
End If

Este código se debe colocar antes del código del paso 6 para que, si hay un problema con la carga de imágenes, el controlador de eventos finalice antes de que el archivo del folleto se guarde en el sistema de archivos.

Si se ha cargado un archivo adecuado, asigne el contenido binario cargado al valor del parámetro picture con la siguiente línea de código:

' Set the value of the picture parameter
e.Values("picture") = PictureUpload.FileBytes

Controlador de eventos ItemInserting completo

A efectos de integridad, este es el controlador de eventos ItemInserting en su totalidad:

Protected Sub NewCategory_ItemInserting _
    (sender As Object, e As DetailsViewInsertEventArgs) _
    Handles NewCategory.ItemInserting
    
    ' Reference the FileUpload controls
    Dim PictureUpload As FileUpload = _
        CType(NewCategory.FindControl("PictureUpload"), FileUpload)
    If PictureUpload.HasFile Then
        ' 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
            e.Cancel = True
            Exit Sub
        End If
    Else
        ' No picture uploaded!
        UploadWarning.Text = _
            "You must provide a picture for the new category."
        UploadWarning.Visible = True
        e.Cancel = True
        Exit Sub
    End If
    ' Set the value of the picture parameter
    e.Values("picture") = PictureUpload.FileBytes
    ' Reference the FileUpload controls
    Dim BrochureUpload As FileUpload = _
        CType(NewCategory.FindControl("BrochureUpload"), FileUpload)
    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
            e.Cancel = True
            Exit Sub
        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))
        e.Values("brochurePath") = brochurePath
    End If
End Sub

Paso 8: Corrección de la página DisplayCategoryPicture.aspx

Ahora probará la interfaz de inserción y el controlador de eventos ItemInserting creado en los últimos pasos. Visite la página UploadInDetailsView.aspx en un explorador e intente agregar una categoría, pero omita la imagen, o bien especifique una imagen que no sea JPG o un folleto que no sea PDF. En cualquiera de estos casos, se mostrará un mensaje de error y se cancelará el flujo de trabajo de inserción.

A Warning Message is Displayed If an Invalid File Type is Uploaded

Figura 9: Se muestra un mensaje de advertencia si se carga un tipo de archivo no válido (Haga clic para ver la imagen a tamaño completo)

Una vez que haya comprobado que la página requiere que se cargue una imagen y que no acepta archivos que no sean PDF o JPG, agregue una nueva categoría con una imagen JPG válida, y deje el campo Brochure vacío. Después de hacer clic en el botón Insertar, la página generará un postback y se agregará un nuevo registro a la tabla Categories con el contenido binario de la imagen cargada almacenado directamente en la base de datos. GridView se actualiza y muestra una fila para la categoría recién agregada, pero, como se muestra en la figura 10, la imagen de la nueva categoría no se representa correctamente.

The New Category s Picture is not Displayed

Figura 10: No se muestra la imagen de la nueva categoría (Haga clic para ver la imagen a tamaño completo)

La razón por la que no se muestra la nueva imagen es porque la página DisplayCategoryPicture.aspx que devuelve una imagen de categoría especificada está configurada para procesar mapas de bits que tienen un encabezado OLE. Este encabezado de 78 bytes se quita del contenido binario de la columna Picture antes de que se devuelva al cliente. Pero el archivo JPG que acaba de cargar para la nueva categoría no tiene este encabezado OLE; por tanto, se quitan los bytes necesarios y válidos de los datos binarios de la imagen.

Como ahora hay mapas de bits con encabezados OLE y JPG en la tabla Categories, es necesario actualizar DisplayCategoryPicture.aspx a fin de que haga la eliminación de encabezado OLE para las ocho categorías originales y omita esta eliminación para los registros de categoría más recientes. En el siguiente tutorial verá cómo actualizar una imagen de registro existente y se actualizarán todas las imágenes de categoría antiguas para que sean JPG. Pero por el momento, use el código siguiente en DisplayCategoryPicture.aspx a fin de quitar los encabezados OLE solo para esas ocho categorías originales:

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)
    If categoryID <= 8 Then
        ' Output HTTP headers providing information about the binary data
        Response.ContentType = "image/bmp"
        ' Output the binary data
        ' But first we need to strip out the OLE header
        Const OleHeaderLength As Integer = 78
        Dim strippedImageLength As Integer = _
            category.Picture.Length - OleHeaderLength
        Dim strippedImageData(strippedImageLength) As Byte
        Array.Copy(category.Picture, OleHeaderLength, _
            strippedImageData, 0, strippedImageLength)
        Response.BinaryWrite(strippedImageData)
    Else
        ' 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 If
End Sub

Con este cambio, la imagen JPG ahora se representa correctamente en GridView.

The JPG Images for New Categories are Correctly Rendered

Figura 11: Las imágenes JPG para nuevas categorías se representan correctamente (Haga clic para ver la imagen a tamaño completo)

Paso 9: Eliminación del folleto si se produce una excepción

Uno de los desafíos de almacenar datos binarios en el sistema de archivos del servidor web es que introduce una desconexión entre el modelo de datos y sus datos binarios. Por tanto, cada vez que se elimina un registro, también se deben quitar los datos binarios correspondientes en el sistema de archivos. Esto también puede pasar durante la inserción. Considere el siguiente escenario: un usuario agrega una nueva categoría y especifica una imagen y un folleto válidos. Al hacer clic en el botón Insertar, se produce un postback y se desencadena el evento ItemInserting de DetailsView, lo que hace que se guarde el folleto en el sistema de archivos del servidor web. A continuación, se invoca el método Insert() de ObjectDataSource que llama al método InsertWithPicture de la clase CategoriesBLL, que llama al método InsertWithPicture de CategoriesTableAdapter.

Ahora, ¿qué ocurre si la base de datos está sin conexión o si hay un error en la instrucción SQL INSERT? Claramente, se producirá un error en INSERT, por lo que no se agregará ninguna nueva fila de categoría a la base de datos. Pero el archivo de folleto cargado en el sistema de archivos del servidor web sigue presente. Este archivo debe eliminarse cuando se produzca una excepción durante el flujo de trabajo de inserción.

Como se ha explicado antes en el tutorial Control de excepciones de nivel de BLL y DAL en una página de ASP.NET, cuando se produce una excepción desde las profundidades de la arquitectura, se propaga porlas distintas capas. En la capa de presentación, se puede determinar si se ha producido una excepción desde el evento ItemInserted de DetailsView. Este controlador de eventos también proporciona los valores InsertParameters de ObjectDataSource. Por tanto, se puede crear un controlador de eventos para el evento ItemInserted que compruebe si se ha producido una excepción y, si es así, se elimine el archivo especificado por el parámetro brochurePath de ObjectDataSource:

Protected Sub NewCategory_ItemInserted _
    (sender As Object, e As DetailsViewInsertedEventArgs) _
    Handles NewCategory.ItemInserted
    
    If e.Exception IsNot Nothing Then
        ' Need to delete brochure file, if it exists
        If e.Values("brochurePath") IsNot Nothing Then
            System.IO.File.Delete(Server.MapPath _
                (e.Values("brochurePath").ToString()))
        End If
    End If
End Sub

Resumen

Hay una serie de pasos que se deben realizar para proporcionar una interfaz basada en web a fin de agregar registros que incluyan datos binarios. Si los datos binarios se almacenan directamente en la base de datos, es probable que tenga que actualizar la arquitectura y agregar métodos específicos para controlar el caso en el que se insertan datos binarios. Después de actualizar la arquitectura, el siguiente paso consiste en crear la interfaz de inserción, mediante un objeto DetailsView que se ha personalizado a fin de incluir un control FileUpload para cada campo de datos binario. Después, los datos cargados se pueden guardar en el sistema de archivos del servidor web o asignarse a un parámetro de origen de datos en el controlador de eventos ItemInserting de DetailsView.

Para guardar datos binarios en el sistema de archivos se necesita más planificación que para guardar datos directamente en la base de datos. Se debe elegir un esquema de nomenclatura para evitar que la carga de un usuario sobrescriba la de otro. Además, se deben realizar pasos adicionales para eliminar el archivo cargado si se produce un error en la inserción de la base de datos.

Ahora tiene la capacidad de agregar nuevas categorías al sistema con un folleto y una imagen, pero aún tiene que ver cómo actualizar los datos binarios de una categoría existente o cómo quitar correctamente los datos binarios de una categoría eliminada. Estos dos temas se abordarán en el tutorial siguiente.

¡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. Los revisores principales de este tutorial han sido Dave Bernard, Teresa Murphy y Bernadette Leigh. ¿Le interesaría revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.