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

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 hemos 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 tenemos que hablar sobre cómo asociar datos cargados con el modelo de datos.

En este tutorial crearemos una página web para agregar una nueva categoría. Además de TextBoxes 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 del registro Picture, mientras que el folleto se guardará en la carpeta ~/Brochures con la ruta de acceso al archivo guardado en la nueva columna BrochurePath del registro.

Antes de crear esta página web, es necesario actualizar la arquitectura. La consulta principal de CategoriesTableAdapter no recupera la columna Picture. Por lo tanto, el método Insert generado automáticamente solo tiene entradas para los campos CategoryName, Description y BrochurePath. Por lo 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: Agregar un método InsertWithPicture a CategoriesTableAdapter

Cuando se creó CategoriesTableAdapter de nuevo 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, hemos indicado a TableAdapter que emplee 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, aumentamos la consulta principal de CategoriesTableAdapter para usar la columna BrochurePath.

Puesto que la consulta principal de CategoriesTableAdapter no hace referencia a la columna Picture, no podemos agregar un nuevo registro ni actualizar un registro existente con un valor para la columna Picture. Para capturar esta información, podemos crear un método en TableAdapter que se usa específicamente a fin de insertar un registro con datos binarios o podemos personalizar la instrucción INSERT generada automáticamente. El problema con la personalización de la instrucción INSERT generada automáticamente es que corremos el riesgo de que el asistente sobrescriba nuestras personalizaciones. Por ejemplo, imagine que hemos 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íamos crear un método en la capa lógica de negocios para usar este método DAL e invocar este método BLL mediante la capa de presentación, y todo funcionaría a la perfección. Es decir, hasta la próxima vez que configuremos TableAdapter mediante el Asistente para la configuración de TableAdapter. En cuanto se complete el asistente, nuestras personalizaciones en 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:

Esta molestia es un problema al usar procedimientos almacenados en lugar de instrucciones SQL ad hoc. Un tutorial futuro explorará el uso de 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, vamos a 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á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 INSERTAR y haga clic en Siguiente.

Select the INSERT Option

Figura 1: selección de la opción INSERTAR (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)

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

Name the New TableAdapter Method InsertWithPicture

Figura 2: asignación de un nombre al método InsertWithPicture de TableAdapter (haga clic para ver la imagen a tamaño completo).

Paso 2: Actualizar la capa de lógica de negocios

Dado que 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 BLL que invoque el método DAL que acabamos de crear (InsertWithPicture). En este tutorial, se creará un método en la clase CategoriesBLL denominada InsertWithPicture que acepte como entrada tres elementos string y una matriz byte. 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. Como se muestra en el código siguiente, este método BLL invoca el método DAL correspondiente:

[System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Insert, false)] 
public void InsertWithPicture(string categoryName, string description, 
    string brochurePath, byte[] picture)
{
    Adapter.InsertWithPicture(categoryName, description, brochurePath, picture);
}

Nota:

Asegúrese de haber guardado el Conjunto de datos con tipo antes de agregar el método InsertWithPicture a BLL. Dado que 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: Mostrar una lista de las categorías existentes y sus datos binarios

En este tutorial crearemos 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, usamos un objeto GridView con TemplateField e ImageField para mostrar cada nombre, descripción, imagen y vínculo de cada categoría a fin de descargar su folleto. Vamos a replicar esa función para este tutorial, creando una página que muestre todas las categorías existentes y permita crear nuevas.

Para empezar, abra la página DisplayOrDownload.aspx en la carpeta BinaryData. Vaya a la vista Origen y copie la sintaxis declarativa de GridView y ObjectDataSource, pegándola en el elemento <asp:Content> en UploadInDetailsView.aspx. Además, no olvide copiar a través del 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 a 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 copió correctamente. Debería ver un objeto 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: zhora debería ver cada categoría junto con sus datos binarios (haga clic para ver la imagen a tamaño completo).

Paso 4: Configurar CategoriesDataSource para admitir la inserción

El ObjectDataSource CategoriesDataSource que usa actualmente 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, queremos asignarlo al método CategoriesBLL que se agregó en el Paso 2, InsertWithPicture.

Para empezar, haga clic en el vínculo Configurar origen de datos desde la etiqueta inteligente 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 INSERTAR 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í sobrescribirá 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: Crear la interfaz de inserción

Tal como se ha tratado primero en Una 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. Vamos a 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 objeto 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, estableciendo su propiedad ID en NewCategory y borrando los valores de las propiedades Height y Width. En la etiqueta inteligente DetailsView, vincúlelo al objeto CategoriesDataSource existente y active la casilla Habilitar inserción.

Screenshot showing DetailsView open with the CategoryID property set to NewCategory, empty Height and Width property values 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 DetailsView en su interfaz de inserción, establezca su propiedad DefaultMode en Insert.

Tenga en cuenta que DetailsView tiene cinco objetos BoundField: CategoryID, CategoryName, Description, NumberOfProducts y BrochurePath, aunque el objeto BoundField CategoryID no se representa en la interfaz de inserción porque su propiedad InsertVisible está establecida en false. Estos objetos BoundField existen porque son las columnas que devuelve el método GetCategories(), que es lo que el elemento ObjectDataSource invoca para recuperar sus datos. Pero a fin de realizar la inserción, no queremos 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 objeto BoundField NumberOfProducts de DetailsView por completo y, después, actualice las propiedades HeaderText de los objetos BoundField CategoryName y BrochurePath a Category y Brochure, respectivamente. A continuación, convierta el objeto BoundField BrochurePath en un objeto TemplateField y agregue un nuevo objeto TemplateField para la imagen, lo que proporciona a este nuevo TemplateField un valor HeaderText de Picture. Mueva el objeto TemplateField Picture para que esté entre TemplateField BrochurePath y CommandField.

Screenshot of 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 objeto BoundField BrochurePath en un objeto TemplateField mediante el cuadro de diálogo Editar campos, TemplateField incluye un objeto ItemTemplate, EditItemTemplate y InsertItemTemplate. Pero solo InsertItemTemplate es necesario, 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>

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

Actualmente, el objeto InsertItemTemplate de TemplateField BrochurePath contiene un objeto 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.

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

Add a FileUpload Control to the InsertItemTemplate

Figura 8: incorporació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, queremos 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, necesitamos que el usuario cargue un archivo de imagen, pero ¿se permite cualquier archivo de imagen o solo archivos de imagen 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. Puesto que no tenemos este tipo de columna, 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 a través de 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, desactive 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, cursiva y negrita.

Nota:

Idealmente, los objetos de BoundFields CategoryName y Description se convertirán en objetos TemplateField y sus interfaces de inserción personalizadas. Por ejemplo, la interfaz de inserción Description sería más adecuada mediante un cuadro de texto de varias líneas. Y dado que 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 vistazo detallado al aumento de las interfaces de modificación de datos.

Paso 6: Guardar el 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 despliega 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, primero debemos asegurarnos de que el usuario cargó 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 control
FileUpload BrochureUpload = 
    (FileUpload)NewCategory.FindControl("BrochureUpload");
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;
        e.Cancel = true;
        return;
    }
}

El controlador de eventos se inicia haciendo referencia al control de 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 otras. En este tutorial intentaremos usar el mismo nombre que el archivo cargado. Pero si ya existe un archivo en el directorio ~/Brochures con ese mismo nombre de archivo, agregaremos 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, cambiaremos el nombre del archivo guardado a Meats-1.pdf. Si ya existe, probaremos Meats-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 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++;
}

Una vez encontrado un nombre de archivo válido, el archivo debe guardarse en el sistema de archivos y el valor brochurePath``InsertParameter de ObjectDataSource debe actualizarse para que este nombre de archivo se escriba en la base de datos. Como vimos 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: Guardar 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, es necesario asegurarnos primero de que la imagen cargada sea un JPG y no otro tipo de imagen. Al igual que en el Paso 6, vamos a 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. Vamos a forzar 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
FileUpload PictureUpload = (FileUpload)NewCategory.FindControl("PictureUpload");
if (PictureUpload.HasFile)
{
    // 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;
        e.Cancel = true;
        return;
    }
}
else
{
    // No picture uploaded!
    UploadWarning.Text = 
        "You must provide a picture for the new category.";
    UploadWarning.Visible = true;
    e.Cancel = true;
    return;
}

Este código debe colocarse 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.

Suponiendo que 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 void NewCategory_ItemInserting(object sender, DetailsViewInsertEventArgs e)
{
    // Reference the FileUpload controls
    FileUpload PictureUpload = (FileUpload)NewCategory.FindControl("PictureUpload");
    if (PictureUpload.HasFile)
    {
        // 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;
            e.Cancel = true;
            return;
        }
    }
    else
    {
        // No picture uploaded!
        UploadWarning.Text = 
            "You must provide a picture for the new category.";
        UploadWarning.Visible = true;
        e.Cancel = true;
        return;
    }
    // Set the value of the picture parameter
    e.Values["picture"] = PictureUpload.FileBytes;
    
    
    // Reference the FileUpload controls
    FileUpload BrochureUpload = 
        (FileUpload)NewCategory.FindControl("BrochureUpload");
    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;
            e.Cancel = true;
            return;
        }
        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));
        e.Values["brochurePath"] = brochurePath;
    }
}

Paso 8: Corregir la página DisplayCategoryPicture.aspx

Dediquemos un momento a probar la interfaz de inserción y el controlador de eventos ItemInserting que se creó en los últimos pasos. Visite la página UploadInDetailsView.aspx a mediante 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 no acepte archivos que no sean PDF o JPG, agregue una nueva categoría con una imagen JPG válida, dejando el campo Brochure vacío. Después de hacer clic en el botón Insertar, la página devolverá 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: la imagen de la nueva categoría no se muestra (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 acabamos de cargar para la nueva categoría no tiene este encabezado OLE; por lo tanto, se quitan los bytes necesarios y válidos de los datos binarios de la imagen.

Dado que 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 nuestro siguiente tutorial examinaremos cómo actualizar una imagen de registro existente y actualizaremos 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 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];
    if (categoryID <= 8)
    {
        // For older categories, we must strip the OLE header... images are bitmaps
        // 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 int OleHeaderLength = 78;
        int strippedImageLength = category.Picture.Length - OleHeaderLength;
        byte[] strippedImageData = new byte[strippedImageLength];
        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);
    }
}

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: Eliminar el folleto frente a 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 lo 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 entrar en juego al insertarlo. Considere el siguiente escenario: un usuario agrega una nueva categoría, especificando 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 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 INSERTAR, por lo que no se agregará ninguna nueva fila de categoría a la base de datos. Pero todavía tenemos el archivo de folleto cargado en el sistema de archivos del servidor web. Este archivo debe eliminarse frente a una excepción durante el flujo de trabajo de inserción.

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

protected void NewCategory_ItemInserted
    (object sender, DetailsViewInsertedEventArgs e)
{
    if (e.Exception != null)
    {
        // Need to delete brochure file, if it exists
        if (e.Values["brochurePath"] != null)
            System.IO.File.Delete(Server.MapPath(
                e.Values["brochurePath"].ToString()));
    }
}

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, agregando métodos específicos para controlar el caso en el que se insertan datos binarios. Una vez actualizada la arquitectura, el siguiente paso consiste en crear la interfaz de inserción, que se puede realizar mediante un objeto DetailsView que se ha personalizado a fin de incluir un control FileUpload para cada campo de datos binario. 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.

Guardar datos binarios en el sistema de archivos requiere más planificación que guardar datos directamente en la base de datos. Se debe elegir un esquema de nomenclatura para evitar que una carga del usuario sobrescriba otra. 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 tenemos la capacidad de agregar nuevas categorías al sistema con un folleto y una imagen, pero aún tenemos 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. Trataremos estos dos temas en el tutorial siguiente.

¡Feliz programación!

Acerca del autor

Scott Mitchell, autor de siete libros de ASP/ASP.NET y fundador de 4GuysFromRolla.com, ha estado trabajando con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, entrenador y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Puede ponerse en contacto con él via 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. Los revisores principales de este tutorial fueron Dave Bernard, Teresa Murphy y Bernadette Leigh. ¿Le interesa revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.