3 de diciembre de 2015

Guardar - Mostrar imagenes en ASP.NET

Una imagen puede guardarse tanto en una base de datos como en una carpeta dedicada a guardar imágenes en el servidor, un punto importante es que a diferencia de las aplicaciones de escritorio en donde el componente OpenFileDialog permitía filtrar los tipos de archivos a seleccionar con el FileUpload esto no es posible, el usuario podrá seleccionar el tipo de archivo que desee, así que queda en manos del programador el definir los tipos de archivos permitidos.

Si se guarda en la base de datos debe especificarse el tipo de dato como Varbinary(MAX) y no como tipo IMAGE ya que según documentación de Microsoft este tipo de datos está próximo a desaparecer, igual se debe tener en cuenta el peso que supone a una base de datos el guardar imágenes, además la imagen a guardar se debe convertir en un array de bytes para almacenar el array en la base de datos.

A mi punto de vista, es mejor guardar las imágenes en una carpeta que subirlas a la base de datos. Sería más carga para el servidor de BD cargar ahí las imágenes, además de que se ocupa más procesamiento para descargarlas y nuevamente convertirlas a imagen. Unos datos antes de pasar a código:

*Es mejor guardar en el disco duro la imagen y en la base de datos, la ruta a dicha imagen.
*Nuestra imagen, contenida en el objeto HttpPostedFile dentro de un objeto de clase FileStream, deberá ser convertida a un conjunto de bytes para enviarlo a la base de datos. Dicho de otro modo, convertiremos el objeto de tipo Stream a un arreglo de bytes.
*Un stream es parámetro del constructor de binaryreader.
*En el Web.config se debe especificar el tamaño máximo permitido de las imágenes:

    

    
    

* Utilizar los siguientes espacios de nombre, útiles para tratar con archivos, datos, con la conexión y objetos de la base de datos SQL Server
using System.IO;
using System.Data;
using System.Data.SqlClient;

Ahora si, lo primero será agregar un control FileUpload a la página, este control permite al usuario examinar los directorios a fin de buscar la imagen que desea cargar en la base de datos, para este ejemplo también se agrega un control Button que al presionarlo guardara la imagen ya sea en un directorio especifico o en la base de datos y un control Label que presentara mensajes de guardado o error al usuario.



Ahora se va a validar que el archivo que se subió utilizando el control FileUpload sea en realidad una imagen, para eso se crea un array con los formatos que son permitidos y se compara con el formato del archivo que el cliente subió, este código va en el evento click del botón BDFile y al examinarlo notaran que hace una llamada al método GuardarArchivo(FileUpload.PostedFile) al que le enviamos el archivo que se ha cargado, este método guardar la imagen en una carpeta dentro del directorio raíz de nuestro proyecto
protected void BDFile_Click(object sender, EventArgs e) {
 string[] validFileTypes = { ".bmp", ".gif", ".png", ".jpg", ".jpeg" };
 
// Validar que exista un archivo cargado
if (fileUpload.HasFile) {
 string ext = System.IO.Path.GetExtension(fileUpload.PostedFile.FileName).ToLower();
 bool isValidFile = false;

// Validar extension de archivo cargado sea tipo imagen  
for (int i = 0; i < validFileTypes.Length; i++) {
 if (ext == validFileTypes[i]) {
  isValidFile = true;
 break;
 }
}

// En caso no tener una extension permitida 
 if (!isValidFile) {
   msg.Visible = true;
   msg.ForeColor = System.Drawing.Color.Red;
   msg.Text = "File format not recognised." +
              " Upload only Image formats";
               }

// metodo que guarda la imagen en una carpeta
   GuardarArchivo(fileUpload.PostedFile);
 }
 else {
    msg.Visible = true;
    msg.ForeColor = System.Drawing.Color.Red;
    msg.Text = "No se ha cargado ningun archivo !!!";
 }
}
Este código guarda la imagen en una carpeta dentro del directorio raíz del servidor, de momento lo trabajo en mi computadora personal pero al subir el proyecto al servidor recuerda que debes tener privilegios de lectura y escritura sobre esta carpeta
private void GuardarArchivo(HttpPostedFile file) {
  
// Se carga la ruta física de la carpeta temp del sitio
  string ruta = Server.MapPath("~/temp");

// Si el directorio no existe, crearlo
  if (!Directory.Exists(ruta))
       Directory.CreateDirectory(ruta);

//string archivo = String.Format("{0}\\{1}", ruta, file.FileName);
  string fullPath = Path.Combine(Server.MapPath("~/temp"), file.FileName);
          
// Verificar que el archivo no exista
 if (File.Exists(fullPath)){
  lblMessage.ForeColor = System.Drawing.Color.Red;
  lblMessage.Text = "ya existe el archivo " +file.FileName;
// en caso quisiera eliminar utilizaria esta linea de codigo
// if (File.Exists(archivo)) File.Delete(archivo);
  } 
  else
  {
  file.SaveAs(fullPath);
  lblMessage.ForeColor = System.Drawing.Color.Blue;
  lblMessage.Text = "se guardo el archivo " +file.FileName;
  }
}
Para esta entrada se guarda una imagen en la base de datos utilizando el código que muestro a continuación, agregue como definir un ContentType para los archivos de Word y Excel de Microsoft pues estos son diferentes a los archivos de imágenes o de PDF, esto es solo para explicar como tratar estos tipos de archivos ya que al estudiar el código se darán cuenta que el ContentType siempre se definirá del tipo imagen.


Recordar cambiar el nombre del método a utilizar en el evento click del botón BDFile, además presento la imagen de la tabla que se utiliza para guardar la imagen como un array de bytes(el id es autoincrement 1-1) y el código utilizado
private void GuardarBD(HttpPostedFile imagen) {

/* la cadena de conexión esta definida en Web.config, aunque recomiendo utilizar SQLconectionStingBuilder para evitar problemas de seguridad */           
 string conexion = WebApplication1.Properties.Settings.Default.cnn.ToString();
 Byte[] bytes = null;
// obtiene el nombre del archivo cargado por ej: pablo.docx - pablo.pdf - pablo.bmp
 string filename = imagen.FileName;
// obtiene la longitud del archivo cargado
 int img_len = imagen.ContentLength;
// obtiene a extension del archivo .png .jpg .bmp
 string ext = Path.GetExtension(filename).ToLower();
 string ext1 = Path.GetExtension(imagen.FileName).ToLower();
//obtiene el tipo de archivo cargado, es diferente a la extension, por ej: image/png - text/html - text/plain - application/pdf  
 string contenttype = String.Empty;

// definir el contentType basado en la extension del archivo
  switch (ext) {
   case ".doc":
    contenttype = "application/vnd.ms-word";
    break;
   case ".docx":
    contenttype = "application/vnd.ms-word";
    break;
   case ".xls":
    contenttype = "application/vnd.ms-excel";
    break;
   case ".xlsx":
    contenttype = "application/vnd.ms-excel";
    break;
   default:
    contenttype = imagen.ContentType.ToString();
    break;
   }
  if (contenttype != String.Empty)
   {
// Obtener el objeto stream a fin de leer su contenido
   Stream fs = imagen.InputStream;
// para guardar una imagen lo primero es tranformar esa imagen a binario para guardarlo en tipo BLOB
   BinaryReader br = new BinaryReader(fs);
   bytes = br.ReadBytes((Int32)fs.Length);
                
  SqlConnection cnx = new SqlConnection(conexion);
   try {
    cnx.Open();
    SqlCommand cmd = cnx.CreateCommand();
    cmd.CommandText =
    "INSERT INTO archivos (nombre, contentType, datos, lenght, ext) " +
    "VALUES (@nombre, @content, @datos, @lenght, @ext)";
    cmd.Parameters.AddWithValue("@nombre", filename);
    cmd.Parameters.AddWithValue("@content", contenttype);
    cmd.Parameters.Add("@datos", SqlDbType.VarBinary).Value = bytes;
    cmd.Parameters.AddWithValue("@lenght", img_len);
    cmd.Parameters.AddWithValue("@ext", ext);
    cmd.ExecuteNonQuery();
    lblMessage.ForeColor = System.Drawing.Color.Green;
    lblMessage.Text = "File Uploaded Successfully";
 }
   catch (Exception ex) { throw ex; }
  }
}
Ya guardamos ahora toca mostrar la imagen recuperando esta desde la carpeta temp dentro del directorio raíz de nuestro proyecto, para eso me creare un nuevo archivo WebForm(.aspx) al que llamare recuperarImagen.aspx Para mostrar el archivo de imagen agregare al proyecto un WebForm llamado MostrarImagen.aspx y sobre este agregare un control image referenciando a mi archivo recuperarImagen.aspx

Este es el código de recuperarImagen.aspx en el evento Load, como verán recupero una imagen especifica en la línea string filename = "logo_UCA.png"; pero en una aplicación real ustedes deben enviar el parámetro que sirva para distinguir y recuperar una imagen específica, explico el proceso al final del post.
protected void Page_Load(object sender, EventArgs e) {
 string filename = "logo_UCA.png";
 Response.Clear();
 Response.AddHeader("content-disposition", string.Format("inline;filename={0}", filename));
 //Response.AddHeader("content-disposition", string.Format("attachment;filename={0}", filename));

 switch (Path.GetExtension(filename).ToLower()) {
   case ".jpg":
    Response.ContentType = "image/jpg";
    break;
   case ".gif":
    Response.ContentType = "image/gif";
    break;
   case ".png":
    Response.ContentType = "image/png";
    break;
  }
    
   Response.WriteFile(Server.MapPath(Path.Combine("~/temp", filename)));
   Response.End();
}

Utilizando Handler Generico .ashx Ahora vamos a recuperar la imagen guardada en la base de datos, lo primero será crear una entidad que represente la tabla que guarda la imagen, para eso creo un archivo llamado imágenes.cs
public class imagenes {
 public imagenes() { }
 public imagenes(int id, string nombre, int length, string content, string Ext) {
  this.Id = id;
  this.Nombre = nombre;
  this.Length = length;
  this.contentType = content;
  this.ext = Ext;
    }
    
  public int Id { get; set; }
  public string contentType { get; set; }
  public int Length { get; set; }
  public string Nombre { get; set; }
  public byte[] datos { get; set; }
  public string ext { get; set; }
}

Ahora toca agregar un Handler generico con nombre ImageHandler tal como se muestra

                               

Y el ImageHandler.ashx.cs debe quedar de la siguiente manera, especifico la imagen que quiero recuperar en la línea de código GetImagenById(1) que significa que recuperara la imagen con código id=1 pero en una aplicación real ustedes deben enviar el parámetro que sirva para distinguir y recuperar una imagen específica, explico el proceso al final del post...notar el uso de context antes del objeto Response al utilizar un Handler generico
public class ImageHandler : IHttpHandler {
 public void ProcessRequest(HttpContext context) {
 imagenes imagen = GetImagenById(1);

// la respuesta mostrara la imagen
 context.Response.Clear();
 context.Response.AddHeader("content-disposition", string.Format("inline;filename={0}", imagen.Nombre));

// especificando la extension al archivo que se va presentar
switch (Path.GetExtension(imagen.Nombre).ToLower()) {
  case ".jpg":
   context.Response.ContentType = "image/jpg";
   break;
  case ".gif":
   context.Response.ContentType = "image/gif";
   break;
  case ".png":
   context.Response.ContentType = "image/png";
   break;
}

// se envia la imagen
  context.Response.BinaryWrite(imagen.datos);
  context.Response.End();
      }

// busca una imagen especifica en la base de datos
private imagenes GetImagenById(int p) {
  imagenes img = null;
  string conexion = GuardarImagenes.Properties.Settings.Default.cnn.ToString();
  using (SqlConnection conn = new SqlConnection(conexion))
   {
  conn.Open();
  string query = @"SELECT id, nombre, contentType, datos, lenght, ext
                   FROM archivos
                   WHERE id = @id";
  SqlCommand cmd = new SqlCommand(query, conn);
  cmd.Parameters.AddWithValue("@id", p);
  SqlDataReader reader = cmd.ExecuteReader();

  if (reader.Read()) {
   img = new imagenes();
   img.Id = Convert.ToInt32(reader["id"]);
   img.Nombre = Convert.ToString(reader["nombre"]);
   img.Length = Convert.ToInt32(reader["lenght"]);
   img.contentType = Convert.ToString(reader["contentType"]);
   img.ext = Convert.ToString(reader["ext"]);
   img.datos = (byte[])reader["datos"];

  }
 }

 return img;
 }

public bool IsReusable {
 get
  {
  return false;
  }
}

Y desde el control image del WebForm MostrarImagen.aspx especificamos

Como vieron anteriormente, para recuperar una imagen se utiliza el control image y se especifica el archivo en el atributo src, este archivo se pasa como un link, entonces para enviar información sobre la imagen que se desea recuperar ya sea hacia el archivo .aspx o hacia el archivo .ashx se puede utilizar esta linea de código(si e que estas seguro que el id y por ende la imagen existe):
src="Image.ashx? Id=<%# Eval("Id")%>"

o esta linea de código que en caso de no existir un id para la imagen en la base de datos mostrara una imagen por defecto
src="<%# Eval("Id") == DbNull.Value ? "ImagenDisco.jpg" :  Eval("Id", "Image.ashx? Id={0}") %>"

como ven estamos utilizando el metodo GET para enviar la informacion asi que para recuperar el valor enviado -en este cado Id- desde un Handler generico se debe utilizar
int id = Convert.ToInt32(context.Request.Params["id"]);

y para recuperar el Id desde un archivo .aspx se puede utilizar
int id = Convert.ToInt32(Request.QueryString["id"]);