domingo, 13 de marzo de 2011

[ASP.NET] Grabar Archivo en base de datos

 

Introducción


Este articulo podría considerarse la continuación de:

[ASP.NET] - Guardar Imagen base de datos

básicamente se extenderá el articulo anterior aportando los temas:

  • subir un archivo a una carpeta dentro del sitio
    • validación de las extensiones que puede subirse
    • listar los archivos de la carpeta
    • distintas formas de representar el link de descarga
      • link directo al archivo
      • link a una pagina que administrara la descarga
    • borrado del archivo de la carpeta del sitio
  • subir el archivos a una base de datos
    • listar los registro de la tabla
    • distintas formas de implementar la descarga
      • usando un handler
      • usando una página que administre la descarga
    • eliminar registro de la base de datos, por ende el archivo

 

Subir un archivo a una carpeta del sitio


Para esta primera sección se trabajara con el contenido desarrollado en la carpeta “GuardarEnCarpeta” del proyecto que se puede descargar en el articulo, la carpeta contiene la página “ListarArchivos.aspx”, cuya funcionalidad será la de subir (FileUpload) y además listas los archivos (GridView), de una carpeta determinada dentro de la estructura del sitio web.

    <form id="form1" runat="server">
    <div>
        <asp:FileUpload ID="FileUpload1" runat="server" />
        <br />
        <br />
        <asp:Button ID="btnSubirArchivo" runat="server" Text="Subir Archivo" 
            onclick="btnSubirArchivo_Click"  />
        <br />
        <asp:RegularExpressionValidator 
             id="RegularExpressionValidator1" runat="server" 
             ErrorMessage="Solo pdf, doc o xls son permitidos." 
             ValidationExpression="^(([a-zA-Z]:)|(\\{2}\w+)\$?)(\\(\w[\w].*))+(.pdf|.doc|xls)$" 
             ControlToValidate="FileUpload1">
            </asp:RegularExpressionValidator>
        <br />
        <br />
        <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4"
            ForeColor="#333333" GridLines="None" onrowdeleting="GridView1_RowDeleting" DataKeyNames="Name">
            <RowStyle BackColor="#EFF3FB" />
            <Columns>
               <asp:TemplateField HeaderText="Eliminar" ItemStyle-HorizontalAlign="Center">
                    <ItemTemplate>
                        <asp:ImageButton ID="imgEliminar" runat="server" CommandName="Delete"  ImageUrl="~/imagenes/delete.png" Width="24px" Height="24px"/>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField HeaderText="Nombre Archivo">
                    <ItemTemplate>
                        <asp:HyperLink ID="nombre" runat="server" NavigateUrl='<%# Eval("Name", "~/files/{0}") %>'
                            Text='<%# Eval("Name") %>'>
                        </asp:HyperLink>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:BoundField DataField="Length" HeaderText="Tamaño" />
                <asp:TemplateField HeaderText="Descargar" ItemStyle-HorizontalAlign="Center">
                    <ItemTemplate>
                        <asp:HyperLink ID="descarga" runat="server" NavigateUrl='<%# Eval("Name", "~/GuardarEnCarpeta/Download.aspx?filename={0}") %>'>
                               <img src="../imagenes/download.gif" alt="" width="30px" height="30px" style="border-width:0px;" />
                        </asp:HyperLink>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
            <FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
            <PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign="Center" />
            <SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True" ForeColor="#333333" />
            <HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
            <EditRowStyle BackColor="#2461BF" />
            <AlternatingRowStyle BackColor="White" />
        </asp:GridView>
    </div>
    </form>

 

Es importante resaltar algunos puntos de este código.

 

Validación en las extensión que pueden ser subidas al sitio

Se hace uso del control RegularExpressionValidator, para proporcionar una validación del lado del cliente, que impida subir archivos en una extensión invalida, o no deseada.

<asp:RegularExpressionValidator 
             id="RegularExpressionValidator1" runat="server" 
             ErrorMessage="Solo pdf, doc o xls son permitidos." 
             ValidationExpression="^(([a-zA-Z]:)|(\\{2}\w+)\$?)(\\(\w[\w].*))+(.pdf|.doc|xls)$" 
             ControlToValidate="FileUpload1">
</asp:RegularExpressionValidator>

Dos formas diferentes de exponer el link de descarga

El gridview que lista los archivos de la carpeta expone dos link de descarga del archivo listado, uno de ellos al presionar el nombre:

<asp:TemplateField HeaderText="Nombre Archivo">
    <ItemTemplate>
        <asp:HyperLink ID="nombre" runat="server" NavigateUrl='<%# Eval("Name", "~/files/{0}") %>'
            Text='<%# Eval("Name") %>'>
        </asp:HyperLink>
    </ItemTemplate>
</asp:TemplateField>

Prestar atención a la definición del NavidateUrl, en este la url especifica concretamente al archivo, por medio del path relativo usando el ~/, esto se puede lograr porque al archivo esta físicamente alojado en una carpeta en al estructura del sitio publicado.

La otra alternativa se presenta al presionar sobre la imagen de descarga:

<asp:TemplateField HeaderText="Descargar" ItemStyle-HorizontalAlign="Center">
    <ItemTemplate>
        <asp:HyperLink ID="descarga" runat="server" NavigateUrl='<%# Eval("Name", "~/GuardarEnCarpeta/Download.aspx?filename={0}") %>'>
               <img src="../imagenes/download.gif" alt="" width="30px" height="30px" style="border-width:0px;" />
        </asp:HyperLink>
    </ItemTemplate>
</asp:TemplateField>

En este otro caso se define una pagina web “Download.aspx” con un valor que pasara por querystring, esta pagina será la responsable de tomar el archivo desde su lugar físico y lo enviara en el Response.

Es importante en este punto analizar que acciones toma esta pagina.

public partial class Download : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string filename = Request.QueryString["filename"].ToString(); 

        Response.Clear();

        Response.AddHeader("content-disposition", string.Format("attachment;filename={0}", filename));
        Response.ContentType = "application/octet-stream";

        Response.WriteFile(Server.MapPath(Path.Combine("~/files", filename)));

        Response.End();

    }
}

El código que implementa es muy simple, solo toma el archivo que recibe por la url, define información para la respuesta y escribe en el Response el archivo para ser enviado.

Un punto no menor es la definición del ContentType del tipo “application/octet-stream”, este forzara la descarga mostrando el cuadro de download al usuario para que seleccione donde quiere descargarlo. Si en lugar de usar este se hubiera definido el tipo concreto para carda archivo, al realizarse la descarga se mostraría embebido el documento en el browser, por supuesto esto sucederá si el usuario posee un editor para este documento.

Eliminación del archivo subido el sitio

La eliminación del archivo esta implementada en los botones de gridview, pero para esto funcione se definen varios puntos, el primero tiene que ver con la columna en el grid

<asp:TemplateField HeaderText="Eliminar" ItemStyle-HorizontalAlign="Center">
     <ItemTemplate>
         <asp:ImageButton ID="imgEliminar" runat="server" CommandName="Delete"  ImageUrl="~/imagenes/delete.png" Width="24px" Height="24px"/>
     </ItemTemplate>
 </asp:TemplateField>

la cual es un ImagenButton con el CommandName = “Delete”, que lleve el nombre “Delete” no es arbitrario, sino que con este nombre lanzara el evento  RowDeleting del gridview, es por eso que se asocia el evento onrowdeleting="GridView1_RowDeleting" además de las propiedad DataKeyNames="Name", para poder tomar el nombre del archivo que se quiere eliminar.

Si ahora se analiza el evento:

protected void GridView1_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
    //se obtiene el nombre de campo definido en el DataKeyNames del gridview
    string fileName = Convert.ToString(GridView1.DataKeys[e.RowIndex].Value);
    //se define el path fisico del archivo
    string fullPath = Path.Combine(Server.MapPath("~/files"), fileName);

    File.Delete(fullPath);

    Response.Redirect("ListarArchivos.aspx");

}

en este se hace uso del DataKeys, para obtener el nombre del archivo, esto se logra gracias al DataKeyNames definido con el campo del nombre del archivo, ahora solo queda obtener la ruta física y proceder a eliminar.

 

Subir un archivo a la base de datos


En esta otra sección cambiaremos de carpeta del trabajo dentro del sitio, ahora sera “GuardarEnDb”, aquí también se cuenta con un GridView para listar los archivos registrados en la db, y el FileUpload para subirlos al sitio.

    <form id="form1" runat="server">
    <div>
        <asp:FileUpload ID="FileUpload1" runat="server" />
        <br />
        <br />
        <asp:Button ID="btnGuardar" runat="server" Text="Guardar Archivo" OnClick="btnGuardar_Click" />
        <br />
        <asp:RegularExpressionValidator 
             id="RegularExpressionValidator1" runat="server" 
             ErrorMessage="Solo pdf, doc o xls son permitidos." 
             ValidationExpression="^(([a-zA-Z]:)|(\\{2}\w+)\$?)(\\(\w[\w].*))+(.pdf|.doc|xls)$" 
             ControlToValidate="FileUpload1">
        </asp:RegularExpressionValidator>
        <br />
        <br />
        <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4"
            ForeColor="#333333" GridLines="None" DataKeyNames="Id" 
            onrowdeleting="GridView1_RowDeleting">
            <RowStyle BackColor="#EFF3FB" />
            <Columns>
                <asp:TemplateField HeaderText="Eliminar" ItemStyle-HorizontalAlign="Center">
                    <ItemTemplate>
                        <asp:ImageButton ID="imgEliminar" runat="server" CommandName="Delete"  ImageUrl="~/imagenes/delete.png" Width="24px" Height="24px"/>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField HeaderText="Nombre Archivo">
                    <ItemTemplate>
                        <asp:HyperLink ID="nombre" runat="server" NavigateUrl='<%# Eval("Id", "~/DescargaArchivo.ashx?id={0}") %>'
                            Text='<%# Eval("Nombre") %>'>
                        </asp:HyperLink>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:BoundField DataField="Length" HeaderText="Tamaño" />
                <asp:TemplateField HeaderText="Descargar">
                    <ItemTemplate>
                        <asp:HyperLink ID="descarga" runat="server" NavigateUrl='<%# Eval("Id", "~/GuardarEnDb/Download.aspx?id={0}") %>'>
                               <img src="../imagenes/download.gif" alt="" width="30px" height="30px" style="border-width:0px;" />
                        </asp:HyperLink>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
            <FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
            <PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign="Center" />
            <SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True" ForeColor="#333333" />
            <HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
            <EditRowStyle BackColor="#2461BF" />
            <AlternatingRowStyle BackColor="White" />
        </asp:GridView>
    </div>
    </form>

A simple vista pareciera idéntico al ejemplo tratado en el punto 2 del articulo, pero hay detalles que deben analizarse, porque hay unas cuantas diferencias.

Dos formas diferentes de exponer el link de descarga

Aquí también nos encontramos con distintas formas de descarga, pero en este caso al tratarse de una base de datos ya no se puede acceder directo al archivo por su url, será necesario contar con intermediarios que obtengan el archivo desde la db y lo envíen como respuesta.

Una de esta formas se define por medio de un handler, en donde la columna en el grid poseerá una url al mismo.

<asp:TemplateField HeaderText="Nombre Archivo">
    <ItemTemplate>
        <asp:HyperLink ID="nombre" runat="server" NavigateUrl='<%# Eval("Id", "~/DescargaArchivo.ashx?id={0}") %>'
            Text='<%# Eval("Nombre") %>'>
        </asp:HyperLink>
    </ItemTemplate>
</asp:TemplateField>

Nótese que lo importante aquí no es el nombre que se use para referirse al handler, ya que este puede ser cualquiera, lo importante es la extensión usada (en este caso ashx), ya que la misma será definida en el web.config, en la línea:

<httpHandlers>
	.
	.
	<add verb="*" path="*.ashx" type="GuardarArchivoBaseDatos.GuardarArchivo.HttpImageHandler"/>

</httpHandlers>

La implementación del handler se encuentra en la clase HttpImageHandler.cs, dentro del código del mismo se simula el comportamiento que tendría la definición del link si se lo define directo al archivo, es por eso que se define un ContentType concreto para cada extensión especifica de los archivos soportados.

public class HttpImageHandler : IHttpHandler
{

    public void ProcessRequest(HttpContext context)
    {
        int id = Convert.ToInt32(context.Request.Params["id"]);

        Archivo archivo = ArchivosDAL.GetById(id);

        context.Response.Clear();
        context.Response.AddHeader("content-disposition", string.Format("attachment;filename={0}", archivo.Nombre));

        switch (Path.GetExtension(archivo.Nombre).ToLower())
        {
            case ".pdf":
                context.Response.ContentType = "application/pdf";
                break;
            case ".doc":
                context.Response.ContentType = "application/msword";
                break;
            case ".xls":
                context.Response.ContentType = "application/vnd.ms-excel";
                break;
        }

        context.Response.BinaryWrite(archivo.ContenidoArchivo);
        context.Response.End();
    }

    public bool IsReusable
    {
        get { return false; }
    }

}

Nota: podrías haberse definido directamente el “application/octet-stream” en el hadler, pero se realizo de esta forma para mostrar distintas variantes que pueden aplicarse.

Es en la otra alternativa se define la página “Download.aspx”, pero esta tiene algunas diferencias con respeto a su semejante cuando se trabaja con archivos.

<asp:TemplateField HeaderText="Descargar">
    <ItemTemplate>
        <asp:HyperLink ID="descarga" runat="server" NavigateUrl='<%# Eval("Id", "~/GuardarEnDb/Download.aspx?id={0}") %>'>
               <img src="../imagenes/download.gif" alt="" width="30px" height="30px" style="border-width:0px;" />
        </asp:HyperLink>
    </ItemTemplate>
</asp:TemplateField>

Si bien pareciera engañar la igualdad con el ejemplo del punto 2.2, este tiene sus diferencias, una muy clara es el uso del campo “Id” como parámetro de la url del link, al usar una base de datos estamos trabajando con registro y los mismo se identifican por su clave.

public partial class Download : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int id = Convert.ToInt32(Request.QueryString["id"]);

        Archivo archivo = ArchivosDAL.GetById(id);

        Response.Clear();

        Response.AddHeader("content-disposition", string.Format("attachment;filename={0}", archivo.Nombre));
        Response.ContentType = "application/octet-stream";

        Response.BinaryWrite(archivo.ContenidoArchivo);
        Response.End();

    }
}

Se toma el id enviado en el querystring, se recupera la información del archivo por medio de una consulta a la DB y luego de definir el ContentType, se envía el array de byte por el Response, nótese que aquí no se escribe un archivo porque no se tiene uno, sino que se opera directamente con los byte.

Eliminación del archivo subido el sitio

Las diferencia en este punto con lo visto en el 2.3, es que aquí se debe hacer uso del id de la entidad que se quiere eliminar, y no el nombre del archivo, es por eso que se define el DataKeyNames, con el “Id”, y luego en el código:

protected void GridView1_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
    //se obtiene el nombre de campo definido en el DataKeyNames del gridview
    int id = Convert.ToInt32(GridView1.DataKeys[e.RowIndex].Value);

    ArchivosDAL.DeleteById(id);

    CargarListadImagenes();
}

se toma este para ejecuta la consulta de DELETE del registro en la db

 

Código de ejemplo


El desarrollo del ejemplo fue realizado con Visual Studio 2008, y la db empleada es Sql Server 2008 Express, el mdf de la misma se encuentra en la carpeta App_Data, y la cadena de conexión se define en el Web.config

 

[C#] 
[VB.NET] 
 

martes, 8 de marzo de 2011

[Análisis] CampusMVP - ADO.NET Entity Framework 4.0 - Aplicaciones y servicios centrados en datos

 

Diariamente durante la participación en los foros noto que tema recurrente esta relacionado con el accesos a datos, existen muchas dudas de las técnicas que se deben aplicar para trabajar la información de un repositorio.

En el blog trato de ejemplificar de forma practica las situaciones mas comunes que he visto planteadas en el foro, pero debo reconocer que son muchas y muy variadas como para tratarlas todas.

Mas que nada estas se generan por un nivel inicial de quién consulta, sumado a la no dedicación del tiempo suficiente para madurar la tecnología, quizás realizando practicas que vayan subiendo en complejidad de de forma progresiva muchas de las dudas nunca se hubieran planteado. Por lo general si se busca como guía un libro sobre el tema, esta podría ser una alternativa, pero seguramente esté en ingles, y muchas veces el no tener un gran dominio del idioma puede terminar restando, haciendo muy lento el aprendizaje.

Lo anteriormente dicho genera un problema : "falta de capacitación". La profesión de sistemas requiere de constante capacitación que permita obtener nuevas habilidades sobre un tema; sino se madura la tecnología es más que seguro que se encontrarán miles de trabas.

Es aquí donde CampusMVP puede aportar un gran beneficio, tuve la oportunidad gracias al contacto de Fred Lores de analizar unos de los cursos online que brindan
y la verdad me quedé maravillado, me refiero a este en particular:

Preparación de la certificación 70-516 TS: Accessing Data with Microsoft® .NET Framework 4

Como comenté más arriba el acceso a datos y manipulación de la información desde mi punto de vista es un aspecto importantísimo en el desarrollo de sistemas, ojo con esto no estoy minimizando que la presentación (WinForms, WFP, asp.net) y comunicación (WCF) sean aspectos menos importantes, pero sí noto que el trabajar con datos es uno de los aspecto que más dudas y consultas generan todos los días.

Esta inquietud se la plateé a Fred y es por eso que de todos los cursos expuestos en CampusMVP el de acceso a datos fue el que destaco.

Después de recorrerlo un buen rato destaco las características:

  • el curso está totalmente en español, lo cual no hay que menospreciar teniendo en cuenta que en sistemas no abunda documentación en nuestro idioma, el material comúnmente encontrado esta en inglés, lo cual implica ir algo más lento si es un idioma que no se domina del todo.
  • claramente explicado y con una lectura muy simple, algo que me llamó la atención es lo natural que se da la lectura de los temas, por supuesto esto también está relacionado a la experiencia previa que uno tenga. Si ya se han realizado algunas incursiones es lógico que resulte más simple la lectura, pero así y todo resultan muy claros los temas tratados. 
  • la complejidad de cada tema avanza de forma progresiva, a medida recorren los temas estos van aumentando de complejidad, el curso está estructurados de forma escalonada para que resulte práctico.
    Este punto, unido a los ejemplo de código que se brindan durante la explicación, aporta una clara comprensión de cada tema tratado.
  • tiene la cantidad justa de imágenes que explican paso a paso como realizar cada tarea, pareciera un dato menor pero es importante que las explicaciones escritas se complementen con imágenes y que ésto se haga de forma adecuada no es simple (como se dice: una imagen vale más de mil palabras, pero tampoco es cuestión de desbordar todos los temas con miles de print screen de pantallas).
    Además se cuenta con videos que complementan aquellos temas en donde se requiere mostrar un proceso completo.

En el link del curso se puede observar el temario tratado, pero este no solo incluye ADO.NET para el acceso a datos, sino que cubre en totalidad la certificación 70-516 (para aquel que quiera rendirla, lo cual es recomendable si se realiza el curso)
Es por eso que se incluyen puntos relacionado con ORM, más que útil si se quiere aprender las últimas novedades que agilicen el desarrollo:

  • Entity Framework
  • Linq to Sql

Es más, este va mas allá incluyendo tecnologías actuales como ser:

  • Sync Framework
  • WCF Data Service

Al acceder al curso se dispone de autoevaluaciones para afianzar lo aprendido durante la unidad.

Para coronar el postre con una frutilla, el acceso al curso permite la descarga de 3 libros en formato pdf, entre ellos el más destacado.

ADO.NET Entity Framework 4.0 - Aplicaciones y servicios centrados en datos

Conclusión, si se busca aprender o profundizar los conocimiento en el acceso a datos, en español, de simple lectura y comprensión, sumado a una buena estructura en el temario, este curso es el indicado.