sábado, 27 de marzo de 2010

[WinForms] ComboBox Anidados

 

Introducción


En este articulo se mostrara un ejemplo concreto de como trabajar con combobox que deben cargarse en forma anidada, o sea cuando el valor seleccionado de uno es usado como filtro en los datos del siguiente.

Este vendría a ser la representación en versión Winforms, del articulo anterior en asp.net:

[ASP.NET] DropDownList anidados

Hay ciertos detalles que en un desarrollo winforms hay que tener en cuenta, como por ejemplo la asignación de los handler de los eventos.

Para la demostración se utilizara la misma capa de datos usada en el ejemplo en asp.net

 

Cascada de Eventos


Uno de los principales punto que hay que analizar es como trabaja cada evento al cambiar la selección del usuario.

private void Form1_Load(object sender, EventArgs e)
{
    LoadComboArtist();
}

private void cbArtist_SelectionChangeCommitted(object sender, EventArgs e)
{
    int ArtistId = Convert.ToInt32(cbArtist.SelectedValue);

    LoadComboAlbum(ArtistId);
}

private void cbAlbum_SelectionChangeCommitted(object sender, EventArgs e)
{
    int AlbumId = Convert.ToInt32(cbAlbum.SelectedValue);

    LoadComboTrack(AlbumId);
}

private void cbTrack_SelectionChangeCommitted(object sender, EventArgs e)
{
    int TrackId = Convert.ToInt32(cbTrack.SelectedValue);

    LoadGridViewCustomer(TrackId);
}

El código deja bien claro como un evento invoca el método encargado de cargar los ítems combo siguiente.

private void LoadComboArtist()
{
    cbArtist.DataSource = ChinookDAL.GellAllArtist();
    cbArtist.DisplayMember = "Name";
    cbArtist.ValueMember = "ArtistId";

    if (cbArtist.Items.Count != 0)
    {
        int ArtistId = Convert.ToInt32(cbArtist.SelectedValue);

        LoadComboAlbum(ArtistId);
    }
    else
    {
        cbAlbum.DataSource = null; 
        cbTrack.DataSource = null; 
        dgvCustomer.DataSource = null;
    }
    
}

private void LoadComboAlbum(int ArtistId)
{
    cbAlbum.DataSource = ChinookDAL.GellAlbumByArtist(ArtistId);
    cbAlbum.DisplayMember = "Title";
    cbAlbum.ValueMember = "AlbumId";


    if (cbAlbum.Items.Count != 0)
    {
        int AlbumId = Convert.ToInt32(cbAlbum.SelectedValue);

        LoadComboTrack(AlbumId);
    }
    else
    {
        cbTrack.DataSource = null; 
        dgvCustomer.DataSource = null;
    }

}

private void LoadComboTrack(int AlbumId)
{
    cbTrack.DataSource = ChinookDAL.GellTrackByAlbum(AlbumId);
    cbTrack.DisplayMember = "Name";
    cbTrack.ValueMember = "TrackId";

    if (cbTrack.Items.Count != 0)
    {
        int TrackId = Convert.ToInt32(cbTrack.SelectedValue);

        LoadGridViewCustomer(TrackId);
    }
    else
    {
        dgvCustomer.DataSource = null;
    }

}

Un punto muy importante a remarcar en el código presentado es el uso del evento:

SelectionChangeCommitted

este solo se ejecuta cuando una acción del usuario selecciona un ítem del combo, pero solo es una acción desde la interfaz del formulario, este no se ejecuta cuando se cambia desde código la selección del combo, lo cual si sucede si se usara el evento:

SelectedIndexChanged

 

DataGridView especificar varios campos en una celda


Un punto interesante que resultaba simple en el desarrollo en asp.net, ya que el control GridView cuenta con el uso de Eval() para poder tomar los valores bindeados a cada row, lo cual facilita la creación de celdas con datos compuestos de varias propiedades.

El mismo comportamiento puede replicarse en el DataGridView, por medio de la propiedad DataBoundItem de DataGridViewRow.

private void dgvCustomer_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
    Customer cust = dgvCustomer.Rows[e.RowIndex].DataBoundItem as Customer;

    if (dgvCustomer.Columns[e.ColumnIndex].Name == "Nombre")
        e.Value = string.Format("{0}, {1}", cust.FirstName, cust.LastName);


    if (dgvCustomer.Columns[e.ColumnIndex].Name == "Direccion")
        e.Value = string.Format("{0} - {1}, {2}", cust.Address, cust.City, cust.State);

}

A la grilla le estamos asignando una colección o lista del tipo Customer, es por eso que mediante la propiedad DataBoundItem  se puede recuperar esta entidad y utilizarla para armar el valor de la celda combinando varias propiedades de la entidad.

Cómo: Obtener acceso a objetos enlazados a filas DataGridView de formularios Windows Forms

En en el mismo evento se pregunta a que columna pertenece la celda que esta lanzando el evento y se procede a utilizar la entidad Customer bindeada para la fila del DataGridView actual.

 

Aclaraciones


La base de datos utilizada en el ejemplo fue obtenido del sitio codeplex: Chinook

Para poder ejecutar la aplicación simplemente deben tener el Sql Server Express 2008 ejecutándose en la PC local

 

[C#]
[VB.NET]

miércoles, 24 de marzo de 2010

C# - [ASP.NET] DropDownList anidados

 

Introducción


El objetivo de este articulo será el de demostrar con un ejemplo completo como cargar controles que dependen de la información de toros controles para desplegar la información.

Se usara para este ejemplo tres controles DropDownList que irán filtrando en cascada, hasta llegar al una grilla final, implementada con el GridView, la cual mostrara los clientes que realizaron la compra.

 

Cascada de Eventos


El punto importante en el ejemplo es analizar como los eventos de los controles interactúan entre ellos.

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                LoadComboArtist();
            }
        }

        protected void ddlArtist_SelectedIndexChanged(object sender, EventArgs e)
        {
            int ArtistId = Convert.ToInt32(ddlArtist.SelectedValue);

            LoadComboAlbum(ArtistId);
            
        }

        protected void ddlAlbum_SelectedIndexChanged(object sender, EventArgs e)
        {
            int AlbumId = Convert.ToInt32(ddlAlbum.SelectedValue);

            LoadComboTrack(AlbumId);
        }

        protected void ddlTrack_SelectedIndexChanged(object sender, EventArgs e)
        {
            int TrackId = Convert.ToInt32(ddlTrack.SelectedValue);

            LoadGridViewCustomer(TrackId);
        }

Como se observa en el código, cada evento toma el id de la entidad que representa e inmediatamente después llama a la funcionalidad que carga el combo que le precede.

private void LoadComboArtist()
        {
            ddlArtist.DataSource = ChinookDAL.GellAllArtist();
            ddlArtist.DataTextField = "Name";
            ddlArtist.DataValueField = "ArtistId";
            ddlArtist.DataBind();

            if (ddlArtist.Items.Count != 0)
            {
                int ArtistId = Convert.ToInt32(ddlArtist.SelectedValue);

                LoadComboAlbum(ArtistId);
            }
            else
            {
                ddlAlbum.Items.Clear();
                ddlTrack.Items.Clear();
                dvCustomer.DataSource = null;
                dvCustomer.DataBind();
            }
        }

        private void LoadComboAlbum(int ArtistId)
        {
            ddlAlbum.DataSource = ChinookDAL.GellAlbumByArtist(ArtistId);
            ddlAlbum.DataTextField = "Title";
            ddlAlbum.DataValueField = "AlbumId";
            ddlAlbum.DataBind();


            if (ddlAlbum.Items.Count != 0)
            {
                int AlbumId = Convert.ToInt32(ddlAlbum.SelectedValue);

                LoadComboTrack(AlbumId);
            }
            else
            {
                ddlTrack.Items.Clear();
                dvCustomer.DataSource = null;
                dvCustomer.DataBind();
            }
        }

        private void LoadComboTrack(int AlbumId)
        {
            ddlTrack.DataSource = ChinookDAL.GellTrackByAlbum(AlbumId);
            ddlTrack.DataTextField = "Name";
            ddlTrack.DataValueField = "TrackId";
            ddlTrack.DataBind();

            if (ddlTrack.Items.Count != 0)
            {
                int TrackId = Convert.ToInt32(ddlTrack.SelectedValue);

                LoadGridViewCustomer(TrackId);
            }
            else
            {
                dvCustomer.DataSource = null;
                dvCustomer.DataBind();
            }
        }

        private void LoadGridViewCustomer(int TrackId)
        {
            dvCustomer.DataSource = ChinookDAL.GellCustomerByTrack(TrackId);
            dvCustomer.DataBind();
        }

Cada método invoca a la funcionalidad de la DAL (Data Access Layer), para recuperar la información.

Se valida que el combo tenga ítem, y en caso de tenerlos invoca la carga del control que sigue en la jerarquía de controles anidados.

En caso de no tener ítem, realiza el borrado de todos los controles que depende de este en la cascada.

 

Aclaraciones


La base de datos utilizada en el ejemplo fue obtenido del sitio codeplex: Chinook

Para poder ejecutar la aplicación simplemente deben tener el Sql Server Express corriendo en la PC local

 

[C#]
[VB.NET]

sábado, 20 de marzo de 2010

[DataGridView] - ComboBox y evento SelectedIndexChanged

 

Introducción


Uno de los problemas al trabajar con el DataGridView y los combos en las celdas, es que no hay un eventos preciso que sea lanzado al cambiar la selección por parte del usuario.

El evento CellValueChanged se podría decir que es el mas cercano a utilizar, pero este solo se produce cuando la celda se deja de editar, o sea hay que salir de la edición de la celda, y además haber cambiado el ítem seleccionado para que el evento se produzca.

Es por este punto que este articulo explicara como adjuntar el combo definido en una columna de tipo DataGridViewComboBoxColumn, al evento SelectedIndexChanged, el cual de forma estándar no esta disponible en la grilla.

 

Planteo del problema


Se dispone de una grilla, la cual presenta un combo en una de sus columnas, y un check que habilita la selección de la lista de productos.

El usuario al cambiar la selección del combo, de forma automática el sistema debería marcarse el checkbox en la misma fila en edición.

Primer planteo de solución


Para resolver el problema serán necesarios dos eventos:

- EditingControlShowing, el cual se lanza cuando la celda entre en estado de edición

- SelectedIndexChanged, el cual será asignado al control combo de la celda que se este editando

 

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    DataGridViewComboBoxEditingControl dgvCombo = e.Control as DataGridViewComboBoxEditingControl;  
  
    if (dgvCombo != null)            
    {   
        //
        // se remueve el handler previo que pudiera tener asociado, a causa ediciones previas de la celda
        // evitando asi que se ejecuten varias veces el evento
        //
        dgvCombo.SelectedIndexChanged -= new  EventHandler(dvgCombo_SelectedIndexChanged);                
                   
        dgvCombo.SelectedIndexChanged += new EventHandler(dvgCombo_SelectedIndexChanged);            
    }

}

private void dvgCombo_SelectedIndexChanged(object sender, EventArgs e) 
{
    //
    // se recupera el valor del combo
    // a modo de ejemplo se escribe en consola el valor seleccionado
    //
    ComboBox combo = sender as ComboBox;

    Console.WriteLine(combo.SelectedValue);

    //
    // se accede a la fila actual, para trabajr con otor de sus campos
    // en este caso se marca el check si se cambia la seleccion
    //
    DataGridViewRow row = dataGridView1.CurrentRow; 

    DataGridViewCheckBoxCell cell = row.Cells["Seleccionado"] as DataGridViewCheckBoxCell;
    cell.Value = true;
}

Aquí hay algunos puntos a detallar:

- Como se observa en el evento EditingControlShowing, este tiene un argumento en el evento que permite tomar que control esta siendo editado, puntualmente el e.Control, el cual puede ser convertido a un tipo especifico se quiere trabajar, en este caso el combobox, cualquier otra celda no será del mismo tipo por lo tanto la conversión devolverá null.

Vale aclarar que en este caso usar esta línea:

DataGridViewComboBoxEditingControl dgvCombo = e.Control as DataGridViewComboBoxEditingControl; 

o esta otra:

ComboBox dgvCombo = e.Control as ComboBox;

es indiferente, con ambas funciona correctamente.

- Seguramente se preguntaran porque se esta realizando la desasignación del evento, cuando en la línea siguiente se vuelve adjunta. Esto básicamente se realiza porque si en varias oportunidades es editada la misma celda, en cada ingreso al evento se asignaría un nuevo handler, o sea no es pisado el previo o existente, provocando que se lance mas de una vez el mismo evento, lo cual no es el efecto deseado.

- En este ejemplo no se hizo, pero podría haberse preguntado si el control en edición es del tipo ComboBox, mediante el agregado de if, y el uso del is, para luego en caso de ser afirmativo en ese caso si convertir al tipo necesario.

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
 {
     if (e.Control is ComboBox)            
     {
         DataGridViewComboBoxEditingControl dgvCombo = e.Control as DataGridViewComboBoxEditingControl;  
         
         //
         // se remueve el handler previo que pudiera tener asociado, a causa ediciones previas de la celda
         // evitando asi que se ejecuten varias veces el evento
         //
         dgvCombo.SelectedIndexChanged -= new  EventHandler(dvgCombo_SelectedIndexChanged);                
                    
         dgvCombo.SelectedIndexChanged += new EventHandler(dvgCombo_SelectedIndexChanged);            
     }

 }

 

[C#]
[VB.NET]

 

Problema detectado en la primer solución


Si bien el ejemplo anterior funciona correctamente a primera vista, hay un efecto que se puede llegar a manifestarse, el cual no es nada deseable.

Resulta que en ciertas ocasiones luego de haber editado una de las celdas del combo y seleccionado un ítem, esta funciono correctamente y marco el check de la fila.

Pero al editar otra celda en una fila distinta, sin haber cambiado opción alguna, se dispara el evento del combo, marcando el check, cuando no debería hacerlo en ese momento, ya que no hubo cambio de selección alguna.

Esto se debe a que el combo queda con el evento asignado, y lo lanza cuando entra en edición.

 

Segundo Planteo de solución


Este escenario, si bien resuelve el efecto en la selección descripto en los pasos previo, tiene un punto no tan bonito en el código, ya que debe conservar el control que se esta editando de forma global al formulario.

Básicamente la resolución del problema es realizada mediante la quita del evento del combo cuando se deja de editar la celda, para lo cual se agrega el evento CellEndEdit.

 

DataGridViewComboBoxEditingControl dgvCombo;

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    dgvCombo = e.Control as DataGridViewComboBoxEditingControl;  
  
    if (dgvCombo != null)            
    {   
        dgvCombo.SelectedIndexChanged += new EventHandler(dvgCombo_SelectedIndexChanged);            
    }

}

private void dvgCombo_SelectedIndexChanged(object sender, EventArgs e) 
{
    //
    // se recupera el valor del combo
    // a modo de ejemplo se escribe en consola el valor seleccionado
    //
    ComboBox combo = sender as ComboBox;

    Console.WriteLine(combo.SelectedValue);

    //
    // se accede a la fila actual, para trabajr con otor de sus campos
    // en este caso se marca el check si se cambia la seleccion
    //
    DataGridViewRow row = dataGridView1.CurrentRow; 

    DataGridViewCheckBoxCell cell = row.Cells["Seleccionado"] as DataGridViewCheckBoxCell;
    cell.Value = true;
}

private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{

    if (dgvCombo != null)
        dgvCombo.SelectedIndexChanged -= new EventHandler(dvgCombo_SelectedIndexChanged);                

}

Puntos a remarcar:

- Se debe conservar el control combobox editado de forma global del formulario, de esta forma al terminar la edición, poder remover el evento.

Este problema se presenta ya que no existe algún otro evento en la grilla, en donde su argumento devuelva el control que lo genera, de la misma forma en que lo hace el evento EditingControlShowing, con su argumento e.Control

- En el evento EditingControlShowing solo hace falta agregar el handler al evento, ya que la remoción se realiza en esta oportunidad cuando es terminada la edición en el evento CellEndEdit

 

[C#]
[VB.NET]

C# - Buscar en una lista contenido repetido

 

Introducción

Sucede muchas veces que tenemos una lista con diversos ítems y es necesario saber cuales se repiten en base a un criterio especifico.

Si s esta desarrollando una aplicación con .net 3.5 o superior, lo primero que viene a la mente es utilizar linq, pero que sucede si se desarrolla sin esta tecnología, como resolver el problema.

Es por ello que este articulo propondrá un caso practico en donde se intentara brindar una solución.

El problema

Se dispone de una lista de archivos en una variable del tipo List<string>, y de esta se quiere saber cuantos ítems con distinta extensión se encuentran cargados.

Usando Linq

Esta primera solución seria la clásica, si uno dispone de Linq, como se observara es muy simple.

[C#]

static void Main(string[] args)
{

    List<string> lista = new List<string>()
    {
        "produtos.txt", "reporte.doc", 
        "listado.txt", "ventas.xls", 
        "servicios.doc", "detalles.txt"
    };

    var query = from item in lista
                let extension = item.Split('.')[1]
                group item by extension into g
                select new { Key = g.Key, Values = g };

    foreach (var item in query)
    {
        Console.WriteLine(string.Format("{0} - {1}", item.Key, item.Values.Count()));
    }

    Console.ReadLine();

}

[VB.NET]

Friend Shared Sub Main(ByVal args As String())

    Dim lista As New List(Of String)()
    lista.Add("produtos.txt")
    lista.Add("reporte.doc")
    lista.Add("listado.txt")
    lista.Add("ventas.xls")
    lista.Add("servicios.doc")
    lista.Add("detalles.txt")


    Dim query = From item In lista _
                 Let extension = item.Split("."c)(1) _
                 Group item By Key = extension Into Group _
                 Select Key, Values = Group

    For Each item In query
        Console.WriteLine(String.Format("{0} - {1}", item.Key, item.Values.Count()))
    Next

    Console.ReadLine()

End Sub

 

Usando Dictionary<>

La alternativa sin Linq, involucra el uso de diccionarios genéricos, al cual mediante un proceso secuencial será cargado con la información agrupada.

[C#]

static void Main(string[] args)
{
    List<string> lista = new List<string>()
    {
        "produtos.txt", "reporte.doc", 
        "listado.txt", "ventas.xls", 
        "servicios.doc", "detalles.txt"
    };

    Dictionary<string, int> contador = new Dictionary<string, int>();

    foreach(string item in lista)
    {
        string extension = item.Split('.')[1];

        if (contador.ContainsKey(extension))
            contador[extension]++;
     else
            contador.Add(extension, 1);
      
    }

    //
    // se muestra el resultado en pantalla
    //
    foreach (KeyValuePair<string, int> item in contador)
    {
     Console.WriteLine(string.Format("{0} - {1}", item.Key, item.Value));
    }

    Console.ReadLine();
}

[VB.NET]

Friend Shared Sub Main(ByVal args As String())

    Dim lista As New List(Of String)()
    lista.Add("produtos.txt")
    lista.Add("reporte.doc")
    lista.Add("listado.txt")
    lista.Add("ventas.xls")
    lista.Add("servicios.doc")
    lista.Add("detalles.txt")

    Dim contador As New Dictionary(Of String, Integer)()

    For Each item As String In lista
        Dim extension As String = item.Split("."c)(1)

        If contador.ContainsKey(extension) Then
            contador(extension) += 1
        Else
            contador.Add(extension, 1)
        End If
    Next

    '
    ' se muestra el resultado en pantalla
    '
    For Each item As KeyValuePair(Of String, Integer) In contador
        Console.WriteLine(String.Format("{0} - {1}", item.Key, item.Value))
    Next

    Console.ReadLine()
End Sub

En el ejemplo se se recorre la lista preguntando si la extensión ya ha sido registrada, para esto se hace uso del método ContainsKey() que proporciona el diccionario.

Si el ítem se encuentra se incremente en uno, sino se agrega por primera vez a la colección.

Al final se recorre los acumulado y se despliega en pantalla.

lunes, 15 de marzo de 2010

[ASP.NET] – Pasar valores entre dos GridView

 

Introducción


El objetivo de este articulo es demostrar como pasar valores entre dos controles GridView.

El pasaje de las filas entre ambos controles será soportada gracias al uso de objetos Session de asp.net, y al uso de DataSet para conservar el conjunto de registros.

Para darle algo mas de estabilidad a lo desarrollado se opto por la utilización de dataset tipados.

 

imagen

 

Definición de las grillas


Ambas grillas son idénticas en cuanto es definición, poseen los mismo campos, y controles de selección, en este caso se opto además por usar el mismo estilo.

La deferencia radica en el nombre de cada grilla, y en el mensaje cuando estas no contienen registros.

 <asp:GridView ID="gvArticulos" runat="server" AutoGenerateColumns="False" 
        CellPadding="4" ForeColor="#333333" GridLines="None" DataKeyNames="Id">
    <RowStyle BackColor="#EFF3FB" />
    <Columns>
        <asp:TemplateField>
        
            <ItemTemplate>
                <asp:CheckBox ID="chkSeleccion" runat="server" />
            </ItemTemplate>
        
        </asp:TemplateField>
        <asp:BoundField DataField="Descripcion" HeaderText="Descripcion" />
        <asp:BoundField DataField="Precio" HeaderText="Precio" />
    </Columns>
    <FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
    <PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign="Center" />
     <EmptyDataTemplate>
         No hay registros
     </EmptyDataTemplate>
    <SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True" ForeColor="#333333" />
    <HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
    <EditRowStyle BackColor="#2461BF" />
    <AlternatingRowStyle BackColor="White" />
</asp:GridView>

Es importante notar la definición del checkbox, para marcar cada ítem que se seleccionara para pasar a la otra lista.

 

Cargar Grids


El evento Pag_Load, realizara la inicialización de los datos de ambas grillas.

La que listara los artículos necesitara de un método que devuelve la información, por un tema de practicidad se están armando en runtime, pero nada impide que provengan de una base de datos, o algún otro origen.

La lista de selección, será inicializada sin registros, simplemente para que muestre la leyenda que se ha configurado cuando esta no posee datos.

Por ultimo la información visualizada también es asignada a los correspondientes objetos de Session para conservar la información y poder trabajarla entre request de la pagina.

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        gvArticulos.DataSource = ObtenerLista();
        gvArticulos.DataBind();

        //
        // Esta linea es suada para inicializar la grilla de seleccion
        // y que esta muestrre el mensaje informando que no hay items
        // seleccionados
        //
        gvSeleccion.DataSource = new dtArticulos.ArticulosDataTable();
        gvSeleccion.DataBind();

        Session["articulos"] = ObtenerLista(); ;
        Session["seleccion"] = new dtArticulos.ArticulosDataTable();
    }
}

Selecciona Items


La selección y pasaje del ítem marcado a la otra grilla requiere de varios pasos:

- Se recuperan los datos de cada grilla, que han sido guardados en los objetos de Session

- se recorre cada fila de la grilla de origen, recuperando y verificando si el checkbox de esa fila ha sido marcado

- si el registro fue seleccionado, se arma con la información del gridview un nuevo row, agregando el resultado al previamente recuperado datatable desde la Session

- se busca por el Id el registro en el conjunto de datos original, y se procede a quitarlo del conjunto de datos

- por ultimo se bindean las grillas con los dataset resultantes para que permitan visualizar el resultado y se guarda en la Session

 

protected void btnSeleccionar_Click(object sender, EventArgs e)
{
    dtArticulos.ArticulosDataTable dtArt = Session["articulos"] as dtArticulos.ArticulosDataTable;
    dtArticulos.ArticulosDataTable dtSel = Session["seleccion"] as dtArticulos.ArticulosDataTable;
    
    foreach (GridViewRow row in gvArticulos.Rows)
    {
        CheckBox check = row.FindControl("chkSeleccion") as CheckBox;
        
        if(check.Checked)
        {
            //
            // Se arma la fila para el DataSet de seleccion
            //
            dtArticulos.ArticulosRow articulo = dtSel.NewArticulosRow();
            articulo.Id = Convert.ToInt32(gvArticulos.DataKeys[row.RowIndex].Value);
            articulo.Descripcion = row.Cells[1].Text;
            articulo.Precio = Convert.ToInt32(row.Cells[2].Text);

            dtSel.Rows.Add(articulo);

            //
            // Se busca el registro en el DataTabla por el Id
            // este metodo devolvera una coleccion de filas, pero es sabido 
            // de antemano que solo sera encontrado una row para el filtro utilizado
            //
            DataRow[] rowdelete = dtArt.Select(string.Format("Id={0}", articulo.Id));
            dtArt.Rows.Remove(rowdelete[0]);

        }
    }

    gvArticulos.DataSource = dtArt;
    gvArticulos.DataBind();

    gvSeleccion.DataSource = dtSel;
    gvSeleccion.DataBind();

    Session["seleccion"] = dtSel;
    Session["articulos"] = dtArt;

}

 

[C#]
[VB.NET]

miércoles, 3 de marzo de 2010

[ASP.NET] - Guardar Imagen base de datos

 

Introducción


Este articulo tendrá por objetivo demostrar algunas técnicas de tratamiento de archivos cuando es necesario manipularlos para que el cliente desde una página web tenga acceso a ellos.

Entre las operaciones no solo se permitirá la descarga sino que además el cliente podrá subir documentos, y guardar estos en una base de datos.

 

Listar archivos en una carpeta del sitio


Para este trabajo se hará uso del control GridView el cual listara los archivos contenidos en la carpeta del sitio.

Debe remarcarse que siempre que se trabaje con archivos que son subidos o trabajados en el sitio deberá usarse carpetas que estén dentro de la estructura de dicho sitio, no es una buena practica usar rutas como ser c:\, o D:\temp, si el sitio no ha sido creado allí.

public partial class ListarImagenes : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            DirectoryInfo dirInfo = new DirectoryInfo(Server.MapPath("~/files"));
            FileInfo[] fileInfo = dirInfo.GetFiles("*.*", SearchOption.AllDirectories);

            GridView1.DataSource = fileInfo;
            GridView1.DataBind();
        }
    }

    protected void btnSubirArchivo_Click(object sender, EventArgs e)
    {
        if (FileUpload1.HasFile)
        {
            string fullPath = Path.Combine(Server.MapPath("~/files"), FileUpload1.FileName);
            FileUpload1.SaveAs(fullPath);
        }
    }

}

En el evento Page_Load puede observarse como se hace uso de la funcionalidad de System.IO para recuperar el listado de archivos que se encuentran en la carpeta de nombre “files”.

En este caso para poder recuperar el path físico se hace uso de Server.MapPath(), y se referencia la ruta mediante ~/ ya que al encontrarse la carpeta en un nivel distinto es necesario hace referencia desde el root del sitio, en este caso usar ../file también hubiera funcionado.

Al ejecutar la pagina ~/ListarArchivos/ListarImagenes.aspx, se visualizara un resultado como el siguiente:

Imagen1

La grilla toma los valores de las propiedades del objeto FileInfo, es por eso que se hace uso de nombres como Name o Length para mostrar la información de la tabla.

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4"
    ForeColor="#333333" GridLines="None">
    <RowStyle BackColor="#EFF3FB" />
    <Columns>
        <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">
            <ItemTemplate>
                <asp:HyperLink ID="descarga" runat="server" NavigateUrl='<%# Eval("Name", "~/ListarArchivos/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>

Además debe diferenciarse entre los dos link visualizados en la grilla, ya que estos trabajan de distintas formas,

-el primero es un link ordinario que apunta directo al archivos en la carpeta,

-el segundo es un link que fuerza la descarga del archivo a la PC del usuario, para su pre-visualización o guardar en disco.

Es interesante analizar el segundo caso, para este se hace uso de una pagina adicional que recibe como parámetro por QueryString el nombre del archivos.

Esta pagina buscara este valor pasado por parámetro en la carpeta del sitio donde se alojan las imágenes, y mediante el uso del objeto Response pondrá el archivo como respuesta del cliente.

La pagina que realiza esta tareas, es ~/ListarArchivos/Download.aspx :

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));

        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("~/files", filename)));

        Response.End();

    }
}

De esta pagina se detecta al tipo de archivo mediante la extensión, especificando el ContentType correcto, debe remarcarse que esta técnica no solo funcionaria con documentos de imágenes, si el en futuro es necesario adaptarlo para la descarga de otro tipo de archivos, solo será cuestión de agregar el soporte a nuevas extensiones en el switch.

 

Subir Imágenes a una Base de Datos


Subir una imagen que será persistida en un campo en una base de datos requerirá de algunas vueltas mas en el procesamiento del archivo que es subido al servidor.

Para esto se ha creado una clase que actuara a manera de Data Access y contendrá las consultas que inserten la información en la tabla.

Para guardar la información se hará uso del siguiente método

public static void GuardarImagen(string nombrearchivo, int length, byte[] imagen)
{
    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string query = @"INSERT INTO Imagenes (nombre, length, imagen)
                         VALUES (@name, @length, @imagen)";
        
        SqlCommand cmd = new SqlCommand(query, conn);
        
        cmd.Parameters.AddWithValue("@name", nombrearchivo);
        cmd.Parameters.AddWithValue("@length", length);

        SqlParameter imageParam = cmd.Parameters.Add("@imagen", System.Data.SqlDbType.Image);
        imageParam.Value = imagen;

        cmd.ExecuteNonQuery();

    }

}

la pagina ~/GuardarImagen/SubirImagen.aspx  contiene un control FileUpload que seleccionara el archivo a subir en el cliente y mediante un botón se realizara la siguiente acción:

protected void btnGuardar_Click(object sender, EventArgs e)
{

    if (FileUpload1.HasFile)
    {
        using (BinaryReader reader = new BinaryReader(FileUpload1.PostedFile.InputStream))
        {
            byte[] image = reader.ReadBytes(FileUpload1.PostedFile.ContentLength);

            ImagenesDAL.GuardarImagen(FileUpload1.FileName, FileUpload1.PostedFile.ContentLength , image);
            
        }

        CargarListadImagenes();
    }
    

}

Como puede visualizarse en el código se toma la información proveniente del upload y se invoca al método creado en el paso previo para insertar el registro, a este método se le pasa la imagen como un array de byte.

 

Listar archivos desde la base de datos


Bien ahora toca el momento de mostrar la información que es subida en el paso previo, para ello nuevamente se dispone de un GridView que es cargada en el Page_Load

public partial class SubirImagen : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
            CargarListadImagenes(); 
    }

    

    private void CargarListadImagenes()
    {
        GridView1.DataSource = ImagenesDAL.GetImagenList();
        GridView1.DataBind(); 
    }

}

Lo interesante de este código esta en la línea 13, en donde nuevamente se hace uso de de un método provisto por la clase de acceso a datos.

public static List<Imagenes> GetImagenList()
{
    List<Imagenes> lista = new List<Imagenes>();

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string query = @"SELECT Id, Nombre, Length  
                            FROM Imagenes";

        SqlCommand cmd = new SqlCommand(query, conn);

        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            Imagenes img = new Imagenes(
                                    Convert.ToInt32(reader["Id"]),
                                    Convert.ToString(reader["nombre"]),
                                    Convert.ToInt32(reader["length"]));
            lista.Add(img);
        }

    }

    return lista;

}

 

Este método tiene algo particular devuelve una lista de un tipo de nombre “Imagenes”, la cual resulta ser una clase custom creada para hacer mas simple trabajar con la información obtenida en la consulta.

public class Imagenes
{
    public Imagenes(int id, string nombre, int length)
    {
        this.Id = id;
        this.Nombre = nombre;
        this.Length = length;
    }
    public int Id { get; set; }
    public int Length { get; set; }
    public string Nombre { get; set; }

    public byte[] Imagen { get; set; }
}

El control de grilla hará uso de las propiedades de esta clase para mostrar la información

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4"
    ForeColor="#333333" GridLines="None">
    <RowStyle BackColor="#EFF3FB" />
    <Columns>
        <asp:BoundField DataField="Nombre" HeaderText="Nombre Archivo" />
        <asp:BoundField DataField="Length" HeaderText="Tamaño" />
        <asp:TemplateField HeaderText="Descargar">
            <ItemTemplate>
                <asp:HyperLink ID="descarga" runat="server" NavigateUrl='<%# Eval("Id", "~/GuardarImagen/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>

 

En esta oportunidad hay un detalle a remarcar en cuanto a la descarga, o acceso a la imagen desde el sitio, existen dos formas de tratar el archivo

- por medio de una pagina que reciba el id del registro que contiene la imagen

- por medio de un hadler de asp.net

 

Descargar la imagen por medio de una pagina


En los tag que forman el GridView seguro se ha observado que el link de descarga apunta a una url : ~/GuardarImagen/Download.aspx?id={0}

este es el documento que tomara el id informado por QueryString, realizaría la consulta tomando el archivo de imagen y lo pondrá en el objeto Response listo para enviarlo al cliente.

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

        Imagenes imagen = ImagenesDAL.GetImagenById(id);

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

        switch (Path.GetExtension(imagen.Nombre).ToLower())
        {
            case ".jpg":
                Response.ContentType = "image/jpg";
                break;
            case ".gif":
                Response.ContentType = "image/gif";
                break;
            case ".png":
                Response.ContentType = "image/png";
                break;
        }

        Response.BinaryWrite(imagen.Imagen);
        Response.End();

    }
}

Es interesante remarcar que aquí se hace uso de otro método de la clase de acceso a datos, en este caso para tomar solo la información de una imagen.

 

public static Imagenes GetImagenById(int Id)
{
    Imagenes img = null;

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string query = @"SELECT Id, Nombre, Length, Imagen
                        FROM Imagenes
                        WHERE Id = @id";

        SqlCommand cmd = new SqlCommand(query, conn);
        cmd.Parameters.AddWithValue("@id", Id);

        SqlDataReader reader = cmd.ExecuteReader();

        if (reader.Read())
        {
            img = new Imagenes(
                            Convert.ToInt32(reader["Id"]),
                            Convert.ToString(reader["nombre"]),
                            Convert.ToInt32(reader["length"]));

            img.Imagen = (byte[])reader["Imagen"];

        }

    }

    return img;

}

Descargar la imagen por medio de un handler


Si bien el código interno que usara el handler será igual al de la pagina, este requieres algunos comentarios.

El primero es que a modo de ejemplo el uso de handler se desarrollo en la pagina ~/GuardarImagen/ListarImagenes.aspx

Esta pagina posee una grilla en donde una de las columnas hay un control de imagen:

    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
        CellPadding="4" ForeColor="#333333" GridLines="None">
    <RowStyle BackColor="#EFF3FB" />
    <Columns>
        <asp:BoundField HeaderText="Nombre" DataField="nombre"  />
        <asp:TemplateField HeaderText="Imagen">
            <ItemTemplate>
                <img alt="" src="<%# Eval("Id", "Image.ashx?id={0}") %>" width="200px" height="200px" />
            </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>

la línea importante es

<img alt="" src="<%# Eval("Id", "Image.ashx?id={0}") %>" width="200px" height="200px" />

en esta se hace uso de un nombre y extensión especifico, en realidad lo importante es la extensión, el nombre en este caso puede ser cualquier otro y seguiría funcionando.

La imagen en este caso esta utilizando un handler para cargar la imagen, el mismo esta en la clase: HttpImageHandler.cs

public class HttpImageHandler : IHttpHandler
{

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

        Imagenes imagen = ImagenesDAL.GetImagenById(id);

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

        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;
        }

        context.Response.BinaryWrite(imagen.Imagen);
        context.Response.End();
    }

    public bool IsReusable
    {
        get { return false; }
    }

}

Lo interesando te este código es que al igual que la pagina de descarga, se tiene acceso al objeto Reponse, solo que en esta oportunidad es a través de HttpContext.

Además debe tenerse en cuenta para que el handler funcione es necesario agregar una línea en el archivo web.config

en este debería agregarse:

<add verb="*" path="*.ashx" type="GuardarImagenBaseDatos.GuardarImagen.HttpImageHandler"/>

El atributo type debe incluir todo el namespace, mas el nombre de la clase, debe tenerse en cuenta que esto puede variar en cada desarrollo dependiendo de donde se ubique la clase del handler.

Aclaración


Para que el ejemplo funcione es necesario contar con Sql Server Express instalado en la PC donde se ejecute el código, y el servicio debe estar iniciado

[C#]
[VB.NET]

lunes, 1 de marzo de 2010

[DataGridView] – Cálculos Totales en las filas y columnas

 

Introducción


Muchas de las veces que se opera con el control DataGridView es necesario realizar cálculos sobre el mismo, por lo general estos requieres del input del usuario de ciertos valores que trabajaran sobre otros ya cargados en el control

En este articulo tratare de de mostrar como hacer uso del control DataGridView para poder realizar estos cálculos, reflejando el resultado como totales de filas y columnas.

 

Carga de los datos en la grilla


Esta será la primer operación ha realizar, la carga de los datos de los productos en la grilla.

private void frmPedidos_Load(object sender, EventArgs e)
{
    //
    // Se recupera los datos de los productos desde la tabla
    //
    dtoProductos datos =  ProductoDAL.ProductosGetAll();

    //
    // Se agrega un registro adicional al DataTable, para representar la fila de totales
    //
    dtoProductos.ProductosRow rowTotal = datos.Productos.NewProductosRow();
    datos.Productos.Rows.Add(rowTotal);  

    //
    // Se bindean los datos a la grilla
    //
    dataGridView1.AutoGenerateColumns = false;
    dataGridView1.DataSource = datos;
    dataGridView1.DataMember = "Productos";

    //
    // Se selecciona la ultima fila de Totales y se marca como readonly
    // para evitar la seleccion por el usuario
    //
    DataGridViewRow row = dataGridView1.Rows[dataGridView1.Rows.Count - 1];
    row.ReadOnly = true;

    //
    // Se asigna el evento para detectar los cambios que el usuario realice
    //
    dataGridView1.CellValueChanged +=new DataGridViewCellEventHandler(dataGridView1_CellValueChanged); 
}

Como se puede apreciar se realizan algunas operaciones programáticamente sobre los datos antes de bindearlos, por ejemplo una de las principales es al agregado de una fila adicional al final del datatable ,esta operación es importante ya que permitirá visualizar la fila de totales al final de la grilla.

Otra operación importante es realizada luego de bindear, en donde se pone en readonly la ultima fila para evitar que el usuario la edite.

Algo a remarcar es la asignación del evento manuablemente en la ultima línea del evento Load del formulario, esta asignación es realizada en este punto ya que si se realiza por medio del cuadro de propiedades del Visual Studio, el evento “CellValueChanged” será lanzado varias veces cuando se carga la grilla, lo cual se evita al no asignar el el evento al comienzo, este evento solo es necesario ante la edición del usuario y no en la carga del mismo.

 

Calculo de totales


Ante la edición del campo de “pedido” o la selección de uno de los check de la columna de “selección”, es que se disparara este evento.

 

private void dataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
    //
    // Solo se trabaja ante los cambios en la columan de los checkbox 
    // y el ingreso de una canifad por el usuario
    //
    if (dataGridView1.Columns[e.ColumnIndex].Name == "Seleccion" ||
        dataGridView1.Columns[e.ColumnIndex].Name == "Pedido")
    {
        decimal totalColumna = 0;

        //
        // Se recorre fila a fila para recalcular el total despues del cambio
        //
        foreach (DataGridViewRow row in dataGridView1.Rows)
        {
            //
            // Se selecciona la celda del checkbox
            //
            DataGridViewCheckBoxCell cellSelecion = row.Cells["Seleccion"] as DataGridViewCheckBoxCell;

            //
            // Se valida si esta checkeada
            //
            if (Convert.ToBoolean(cellSelecion.Value))
            {
                //
                // Se valida si el usuario ingreso un valor en la celda de pedido
                //
                decimal pedido = 0;
                if (!decimal.TryParse(Convert.ToString(row.Cells["Pedido"].Value), out pedido))
                    continue;

                //
                // Se realiza el calculo para la fila, asignado el total en la celda "Total"
                // de la misma 
                //
                decimal totalFila = Convert.ToDecimal(row.Cells["PrecioUnitario"].Value) * pedido;
                row.Cells["Total"].Value = totalFila;

                //
                // Se aumula el total de cada una de las filas
                //
                totalColumna += totalFila;
            }
        }

        //
        // Se toma la ultima fila del total general, asignando el valor acumulado en el calculo
        //
        DataGridViewRow rowTotal = dataGridView1.Rows[dataGridView1.Rows.Count - 1];
        rowTotal.Cells["Total"].Value = totalColumna;

    }
}

En este evento se recorrerá cada una de las filas de la grilla realizando los cálculos a nivel de la propia fila, pero también de la columna de totales.

Adicionalmente se agrego el evento de validación, ante una entrada incorrecta del usuario en la celda de pedidos, si el usuario ingresa letras se mostrara un alerta en la fila.

private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
    if (dataGridView1.Columns[e.ColumnIndex].Name == "Pedido")
    {
        //
        // Si el campo esta vacio no lo marco como error
        //
        if (string.IsNullOrEmpty(e.FormattedValue.ToString()))
            return;

        //
        // Solo se valida ante el ingreso de un valor en el campo
        //
        decimal pedido = 0;
        if (!decimal.TryParse(e.FormattedValue.ToString(), out pedido))
        {
            DataGridViewRow row = dataGridView1.Rows[e.RowIndex];
            row.ErrorText = "Debe ingresar un número valido";
            e.Cancel = true;
        }
    }
}

//
// Este evento es usado al presiona ESC cancelando la edicion
// se elimine el mensaje de error en la fila
//
private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
    dataGridView1.Rows[e.RowIndex].ErrorText = String.Empty;
}

 

Nota: Hay un problema en  las validaciones en la grilla. Si por alguna razón cuando usa las validaciones en el DataGridView, no visualiza el icono con el mensaje del error esto se puede deber a que la propiedad AutoSizeRowsMode  no esta asignada con el valor None.

DataGridView1.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None

tenga en cuanta este punto cuando use las validaciones

 

[C#]
[VB.NET]