Incluir una opción de carga de archivos al agregar un nuevo registro (C#)
por Scott Mitchell
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.
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.
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
.
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.
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.
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.
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.
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
.
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.
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.
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.
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.