domingo, 4 de abril de 2010

C# - [Winforms] Como usar el SelectedValue

 

Introducción

En varias oportunidades he notado que trae problemas el incorrecto uso de ciertas propiedades en controles como ser el ComboBox, ListBox, CheckBoxList especialmente cuando estos están bindeados a datos que usan datatables.

Es por eso que aquí se trataran ejemplo de distintas formas de trabajar estos controles unidos a datos, y que propiedades usar cuando se quiere trabajar con ellos.

Uno de los principales problemas es el uso de la propiedad SelectedValue, esta permite tomar el valor del ítem seleccionado por el usuario, pero como manipular esta propiedad depende de la forma en como se haya vinculado el control al origen de datos.

Las distintas formas de trabajar la propiedad estará dada por la asignación o no de un valor en la propiedad ValueMember

Sin asignación del ValueMember

En el Form1 se hace uso de un DataSet Tipado de nombre “ProductosDataSet”, el cual será responsable por medio del TableAdapter (creado por el asistente), de cargar los datos proveniente de la db.

Solo se ha especificado la propiedad DisplayMember, al momento de bindear el DataTable de producto, devuelto por el método creado para tal fin.

[C#]

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

    CargarListBox();

    CargarCheckBoxList();
}

private void CargarComboBox()
{
    cbProductos.DataSource = ObtenerProductos();
    cbProductos.DisplayMember = "Descripcion";
}

private void CargarListBox()
{
    lbProductos.DataSource = ObtenerProductos();
    lbProductos.DisplayMember = "Descripcion";
}

private void CargarCheckBoxList()
{
    ckbProductos.DataSource = ObtenerProductos();
    ckbProductos.DisplayMember = "Descripcion";
}

[VB.NET]

Private Sub Form1_Load(sender As Object, e As EventArgs)
	CargarComboBox()

	CargarListBox()

	CargarCheckBoxList()
End Sub

   Private Sub CargarComboBox()

       cbProductos.DataSource = ObtenerProductos()
       cbProductos.DisplayMember = "Descripcion"

   End Sub

   Private Sub CargarListBox()

       lbProductos.DataSource = ObtenerProductos()
       lbProductos.DisplayMember = "Descripcion"

   End Sub

   Private Sub CargarCheckBoxList()

       ckbProductos.DataSource = ObtenerProductos()
       ckbProductos.DisplayMember = "Descripcion"

   End Sub

En este caso al inspeccionar la propiedad SelectedValue, todos los controles devolverá un objeto del tipo DataRowView.

[C#]

private void cbProductos_SelectedIndexChanged(object sender, EventArgs e)
{
    DataRowView rowView = cbProductos.SelectedValue as DataRowView;
    
   
    lblSeleccionComboBox.Text = string.Format("Id: {0} \nDescripcion: {1} \nPrecio: {2}", 
                                        rowView["IdProducto"], 
                                        rowView["Descripcion"], 
                                        rowView["PrecioUnitario"]);
}

private void lbProductos_SelectedIndexChanged(object sender, EventArgs e)
{
    DataRowView rowView = lbProductos.SelectedValue as DataRowView;

    lblSeleccionListBox.Text = string.Format("Id: {0} \nDescripcion: {1} \nPrecio: {2}", 
                                        rowView["IdProducto"], 
                                        rowView["Descripcion"], 
                                        rowView["PrecioUnitario"]);

}

private void ckbProductos_SelectedIndexChanged(object sender, EventArgs e)
{
    DataRowView rowView = ckbProductos.SelectedValue as DataRowView;

    txtSeleccionCheckBoxList.Text = string.Format("Id: {0} \r\nDescripcion: {1} \r\nPrecio: {2}", 
                                        rowView["IdProducto"], 
                                        rowView["Descripcion"], 
                                        rowView["PrecioUnitario"]);
}

private void btnCheckedItems_Click(object sender, EventArgs e)
{
    txtSeleccionCheckBoxList.Text = "";

    foreach (DataRowView rowView in ckbProductos.CheckedItems)
    {
        txtSeleccionCheckBoxList.Text += string.Format("Id: {0} \r\nDescripcion: {1} \r\nPrecio: {2}\r\n\r\n", 
                                                            rowView["IdProducto"], 
                                                            rowView["Descripcion"], 
                                                            rowView["PrecioUnitario"]);
    }

}

[VB.NET]

Private Sub cbProductos_SelectedIndexChanged(sender As Object, e As EventArgs)
	Dim rowView As DataRowView = TryCast(cbProductos.SelectedValue, DataRowView)


       lblSeleccionComboBox.Text = String.Format("Id: {1} {0}Descripcion: {2} {0}Precio: {3}", _
                                                 Environment.NewLine, _
                                                 rowView("IdProducto"), _
                                                 rowView("Descripcion"), _
                                                 rowView("PrecioUnitario"))
End Sub

   Private Sub lbProductos_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)

       Dim rowView As DataRowView = TryCast(lbProductos.SelectedValue, DataRowView)

       lblSeleccionListBox.Text = String.Format("Id: {1} {0}Descripcion: {2} {0}Precio: {3}", _
                                                 Environment.NewLine, _
                                                 rowView("IdProducto"), _
                                                 rowView("Descripcion"), _
                                                 rowView("PrecioUnitario"))
   End Sub

   Private Sub ckbProductos_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)

       Dim rowView As DataRowView = TryCast(ckbProductos.SelectedValue, DataRowView)

       txtSeleccionCheckBoxList.Text = String.Format("Id: {1} {0}Descripcion: {2} {0}Precio: {3}", _
                                         Environment.NewLine, _
                                         rowView("IdProducto"), _
                                         rowView("Descripcion"), _
                                         rowView("PrecioUnitario"))
   End Sub

   Private Sub btnCheckedItems_Click(ByVal sender As Object, ByVal e As EventArgs)

       txtSeleccionCheckBoxList.Text = ""

       For Each rowView As DataRowView In ckbProductos.CheckedItems

           txtSeleccionCheckBoxList.Text += String.Format("Id: {1} {0}Descripcion: {2} {0}Precio: {3}{0}{0}", _
                                             Environment.NewLine, _
                                             rowView("IdProducto"), _
                                             rowView("Descripcion"), _
                                             rowView("PrecioUnitario"))
       Next

   End Sub

Seguramente uno se preguntara porque este dato en concreto, bien resulta que este representa una vista de la fila del DataTable, y como solo estamos seleccionado un ítem individual en el control, este devolverá solo una fila (row) del DataTable usado para vincular el dato de ese ítem en particular.

 

Asignando un valor al ValueMember

En el Form2, se define el valor de uno de los campos del origen de datos en la propiedad ValueMember, esto hace que la aplicación trabaje de una forma distinta.

[C#]

private void Form2_Load(object sender, EventArgs e)
{
    CargarComboBox();

    CargarListBox();

    CargarCheckBoxList();
}

private void CargarComboBox()
{
    cbProductos.DisplayMember = "Descripcion";
    cbProductos.ValueMember = "IdProducto";
    cbProductos.DataSource = ObtenerProductos();

}

private void CargarListBox()
{
    lbProductos.DisplayMember = "Descripcion";
    lbProductos.ValueMember = "IdProducto";
    lbProductos.DataSource = ObtenerProductos();

}

private void CargarCheckBoxList()
{
    ckbProductos.ValueMember = "IdProducto";
    ckbProductos.DataSource = ObtenerProductos();
    ckbProductos.DisplayMember = "Descripcion";
}

[VB.NET]

Private Sub Form2_Load(ByVal sender As Object, ByVal e As EventArgs)
    CargarComboBox()

    CargarListBox()

    CargarCheckBoxList()
End Sub

Private Sub CargarComboBox()

    cbProductos.DisplayMember = "Descripcion"
    cbProductos.ValueMember = "IdProducto"
    cbProductos.DataSource = ObtenerProductos()

End Sub

Private Sub CargarListBox()

    lbProductos.DisplayMember = "Descripcion"
    lbProductos.ValueMember = "IdProducto"
    lbProductos.DataSource = ObtenerProductos()

End Sub

Private Sub CargarCheckBoxList()

    ckbProductos.ValueMember = "IdProducto"
    ckbProductos.DataSource = ObtenerProductos()
    ckbProductos.DisplayMember = "Descripcion"

End Sub

Es importante destacar en este punto que la asignación de las propiedades debe realizarse antes de bindear la información, ya que si la propiedad ValueMember es asignada después esta no tendrá efecto.

[C#]

private void cbProductos_SelectedIndexChanged(object sender, EventArgs e)
{
    int IdProducto = Convert.ToInt32(cbProductos.SelectedValue);
    string Descripcion = Convert.ToString(cbProductos.Text);
    
   
    lblSeleccionComboBox.Text = string.Format("Id: {0} \nDescripcion: {1}",
                                        IdProducto, 
                                        Descripcion);
}

private void lbProductos_SelectedIndexChanged(object sender, EventArgs e)
{
    int IdProducto = Convert.ToInt32(lbProductos.SelectedValue);
    string Descripcion = Convert.ToString(lbProductos.Text);


    lblSeleccionListBox.Text = string.Format("Id: {0} \nDescripcion: {1}",
                                        IdProducto,
                                        Descripcion);

}

private void ckbProductos_SelectedIndexChanged(object sender, EventArgs e)
{
    int IdProducto = Convert.ToInt32(ckbProductos.SelectedValue);
    string Descripcion = Convert.ToString(ckbProductos.Text);


    txtSeleccionCheckBoxList.Text = string.Format("Id: {0} \r\nDescripcion: {1}",
                                        IdProducto,
                                        Descripcion);
}

private void btnCheckedItems_Click(object sender, EventArgs e)
{
    txtSeleccionCheckBoxList.Text = "";

    foreach (DataRowView rowView in ckbProductos.CheckedItems)
    {
        txtSeleccionCheckBoxList.Text += string.Format("Id: {0} \r\nDescripcion: {1} \r\nPrecio: {2}\r\n\r\n", 
                                                            rowView["IdProducto"], 
                                                            rowView["Descripcion"], 
                                                            rowView["PrecioUnitario"]);
    }

}

 

[VB.NET]

Private Sub cbProductos_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)

    Dim IdProducto As Integer = Convert.ToInt32(cbProductos.SelectedValue)
    Dim Descripcion As String = Convert.ToString(cbProductos.Text)


    lblSeleccionComboBox.Text = String.Format("Id: {1} {0}Descripcion: {2}", _
                                              Environment.NewLine, _
                                              IdProducto, _
                                              Descripcion)
End Sub

Private Sub lbProductos_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)

    Dim IdProducto As Integer = Convert.ToInt32(lbProductos.SelectedValue)
    Dim Descripcion As String = Convert.ToString(lbProductos.Text)


    lblSeleccionListBox.Text = String.Format("Id: {1} {0}Descripcion: {2}", _
                                              Environment.NewLine, _
                                              IdProducto, _
                                              Descripcion)
End Sub

Private Sub ckbProductos_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)

    Dim IdProducto As Integer = Convert.ToInt32(ckbProductos.SelectedValue)
    Dim Descripcion As String = Convert.ToString(ckbProductos.Text)


    txtSeleccionCheckBoxList.Text = String.Format("Id: {1} {0}Descripcion: {2}", _
                                      Environment.NewLine, _
                                      IdProducto, _
                                      Descripcion)
End Sub

Private Sub btnCheckedItems_Click(ByVal sender As Object, ByVal e As EventArgs)

    txtSeleccionCheckBoxList.Text = ""

    For Each rowView As DataRowView In ckbProductos.CheckedItems

        txtSeleccionCheckBoxList.Text += String.Format("Id: {1} {0}Descripcion: {2} {0}Precio: {3}{0}{0}", _
                                          Environment.NewLine, _
                                          rowView("IdProducto"), _
                                          rowView("Descripcion"), _
                                          rowView("PrecioUnitario"))
    Next

End Sub

Ahora si, al momento de recuperar el valor este retornara en la propiedad SelectedValue un dato simple, en este caso un entero por tratarse del Id del Producto.

Aquí ya no podrá recuperarse el precio en la misma acción, o al menos no con la propiedad SelectedValue, para poder tomar el DataRowView, se deberá recurrir al SelectedItem

 

Aclaración

Para cambiar la ejecución de un Formulario a otro, deberás modificar el archivo Program.cs o Program.vb según el lenguaje usado

cambiando la línea: Application.Run(new Form1());

por el nombre de la clase del formulario que quiere ejecutarse

 

[C#]
[VB.NET]

viernes, 2 de abril de 2010

C# – Eliminar ítems en una lista

 

Introducción

Existen muchas formas de trabajar con una lista, y realizar operaciones de búsqueda y eliminación de aquellos elementos que debe ser filtrados, pero hay que tener cuidado ya que no todas las operaciones son validas

En este articulo se analizaría las formas de buscar un elemento y removerlo de la colección, analizando además métodos no recomendados

El problema

Cuando se tiene que realizar una operación sobre una lista, filtrando sus datos, lo primero que viene a la mente es recorrerla y allí mismo eliminar los ítems que no coinciden con la búsqueda.

O sea implementar algo similar a esto:

private void btnEliminarConProblema_Click(object sender, EventArgs e)
{
    List<Producto> list = ObtenerLista();

    foreach (Producto item in list)
    {
        if (!item.Descripcion.ToLower().Contains(txtBusqueda.Text.ToLower()))
            list.Remove(item);
    }

    LoadListView(list);
}

Pero esta primera aproximación presenta un defecto técnico, resulta que las listas necesita mantener el índice intacto para poder mantener el ciclo, el cual estamos alterando si removemos un ítem.

En este caso veremos un error con el mensaje:

Collection was modified; enumeration operation may not execute.

Soluciones

Existe una buen variedad de soluciones a este problema, en donde la implementación de cada una dependerá del objetivo que se quiera alcanzar

Una de ellas podría ser implementando el ciclo por medio de un simple for

[Usando el For]

private void btnEliminarConFor_Click(object sender, EventArgs e)
{
    List<Producto> list = ObtenerLista();

    for (int i = 0; i < list.Count; i++)
    {
        if (!list[i].Descripcion.ToLower().Contains(txtBusqueda.Text.ToLower()))
        {
            list.RemoveAt(i);
            i--;
        }
    }

    LoadListView(list);
}

Como se observa este requiere trabar con los índices de la colección, pero para poder mantenerlo actualizado es preciso restar uno cuando un ítem es quitado.

Otra alternativa podrías ser usar una lista auxiliar que acumule los ítems que no coinciden con la búsqueda, para ser eliminado en un segundo ciclo de la lista

[Usando una lista auxiliar]

private void btnEliminarConColeccionAuxiliar_Click(object sender, EventArgs e)
{
    List<Producto> listAux = new List<Producto>();
    List<Producto> list = ObtenerLista();

    foreach (Producto item in list)
    {
        if (!item.Descripcion.ToLower().Contains(txtBusqueda.Text.ToLower()))
        {
            listAux.Add(item);
            continue;
        }

    }

    foreach (Producto item in listAux)
    {
        list.Remove(item);
    }

    LoadListView(list);

}

El inconveniente con esta aproximación es que se necesitan dos ciclos para terminar la tarea.

También existen alternativas algo mas performantes, como ser el uso de delegados, expresiones Lambda, o Linq

[Usando Delegados]

private void btnEliminarConDelegate_Click(object sender, EventArgs e)
{
    List<Producto> list = ObtenerLista();

    //
    // quitara aquellos productos en donde la funcion
    // definida en el delegado retorne un true
    //
    list.RemoveAll(delegate(Producto prod)
    {
        return !prod.Descripcion.ToLower().Contains(txtBusqueda.Text.ToLower());
    });


    LoadListView(list);

}

[Usando Expresiones Lambda]

private void btnEliminarConLambda_Click(object sender, EventArgs e)
{
    List<Producto> list = ObtenerLista();

    list.RemoveAll(x => !x.Descripcion.ToLower().Contains(txtBusqueda.Text.ToLower()));

    LoadListView(list);
}

[Usando Linq]

private void btnEliminarConLinq_Click(object sender, EventArgs e)
{
    List<Producto> list = ObtenerLista();

    list = (from item in list
            where item.Descripcion.ToLower().Contains(txtBusqueda.Text.ToLower())
            select item).ToList<Producto>();

    LoadListView(list);
}

 

[C#]
[VB.NET]

jueves, 1 de abril de 2010

C# - [Winforms] Seleccionar Fila con ENTER – DataGridView y ListView

 

Introducción

Este articulo es mostrara una alternativa de como se puede detectar la presión de una tecla (en este caso el ENTER) en un control determinado, pudiendo así trabajar con la selección.

En el ejemplo se trabajar con dos controles, el DataGridView y un ListView.

Básicamente la técnica para ambos ejemplos será similar, todo el trabajo se realizaría en el método ProcessCmdKey del formulario, (el cual es una sobrecarga), permitiendo atrapar la teclas pulsadas en el formulario, en este además se podrías validar que control esta activo en ese momento y en caso de ser el que resulta útil, procede a trabajar con la selección.

Para ambos ejemplo se permitirá optar (mediante radiobuttons) si la selección del ítem en la lista se mostrar en un mensaje, o será desplegado en otra ventana (formulario).

 

DataGridView - Selección de la fila

Como había comentado todo el trabajo esta en la función:

[C#]

protected override bool ProcessCmdKey(ref System.Windows.Forms.Message msg, System.Windows.Forms.Keys keyData)
{
    //
    // Si el control DataGridView no tiene el foco, 
    // se abandonamos el procedimiento, llamando al metodo base
    //
    if ((!dataGridView1.Focused)) 
        return base.ProcessCmdKey(ref msg, keyData);

    //
    // Si la tecla presionada es distinta al ENTER, 
    // se abandonamos el procedimiento, llamando al metodo base
    //
    if (keyData != Keys.Enter) 
        return base.ProcessCmdKey(ref msg, keyData);
    //
    // Obtenemos la fila actual 
    //
    DataGridViewRow row = dataGridView1.CurrentRow;

    if (rbMostrarMensaje.Checked)
    {
        MessageBox.Show(string.Format("Se ha seleccionado, Cuenta:'{0}' Descripcion:'{1}'", 
                                            row.Cells["cuenta"].Value, 
                                            row.Cells["descripcion"].Value));
    }
    else if (rbMostrarForm.Checked)
    {
        int cuenta = Convert.ToInt32(row.Cells["cuenta"].Value);
        string desc = Convert.ToString(row.Cells["descripcion"].Value);

        frmSeleccion frm = new frmSeleccion(cuenta, desc);
        frm.ShowDialog();
    }

    return true;
} 

[VB.NET]

Protected Overloads Overrides Function ProcessCmdKey(ByRef msg As System.Windows.Forms.Message, keyData As System.Windows.Forms.Keys) As Boolean
	'
	' Si el control DataGridView no tiene el foco, 
	' se abandonamos el procedimiento, llamando al metodo base
	'
	If (Not dataGridView1.Focused) Then
		Return MyBase.ProcessCmdKey(msg, keyData)
	End If

	'
	' Si la tecla presionada es distinta al ENTER, 
	' se abandonamos el procedimiento, llamando al metodo base
	'
	If keyData <> Keys.Enter Then
		Return MyBase.ProcessCmdKey(msg, keyData)
	End If
	'
	' Obtenemos la fila actual 
	'
	Dim row As DataGridViewRow = dataGridView1.CurrentRow

       If rbMostrarMensaje.Checked Then

           MessageBox.Show(String.Format("Se ha seleccionado, Cuenta:'{0}' Descripcion:'{1}'", row.Cells("cuenta").Value, row.Cells("descripcion").Value))

       ElseIf rbMostrarForm.Checked Then

           Dim cuenta As Integer = CInt(row.Cells("cuenta").Value)
           Dim desc As String = CStr(row.Cells("descripcion").Value)

           Dim frm As New frmSeleccion(cuenta, desc)
           frm.ShowDialog()

       End If

	Return True
End Function

- El primer punto es detectar si al presionar la tecla el foco lo tiene el control que se quiere trabajar, en este caso el DataGridView

- El segundo paso es determinar si la tecla presionada por el usuario es la que se desea controlar, en este caso será el ENTER

Si las dos validaciones anteriores pasaron, se puede recuperar la fila seleccionada en la grilla, y trabajar con la información que esta proporcione.

 

[C#]
[VB.NET]

 

ListView – Selección de la fila

La única diferencia entre este código y el usado en el DataGridview, será la forma en que se obtiene el ítem seleccionado.

Las validaciones en cuanto a la tecla presionada, y el foco en el control activo son idénticas para ambos casos.

[C#]

protected override bool ProcessCmdKey(ref System.Windows.Forms.Message msg, System.Windows.Forms.Keys keyData)
{
    //
    // Si el control DataGridView no tiene el foco, 
    // se abandonamos el procedimiento, llamando al metodo base
    //
    if ((!listView1.Focused)) 
        return base.ProcessCmdKey(ref msg, keyData);

    //
    // Si la tecla presionada es distinta al ENTER, 
    // se abandonamos el procedimiento, llamando al metodo base
    //
    if (keyData != Keys.Enter) 
        return base.ProcessCmdKey(ref msg, keyData);

    //
    // Sino hay item seleccinado en la lista
    // se abandonamos el procedimiento, llamando al metodo base
    //
    if(listView1.SelectedItems.Count == 0)
        return base.ProcessCmdKey(ref msg, keyData);

    //
    // Obtenemos la fila actual 
    //
    ListViewItem item = listView1.SelectedItems[0]; 

    if (rbMostrarMensaje.Checked)
    {
        MessageBox.Show(string.Format("Se ha seleccionado, Cuenta:'{0}' Descripcion:'{1}'",
                                            item.Text,
                                            item.SubItems[1].Text));
    }
    else if (rbMostrarForm.Checked)
    {
        int cuenta = Convert.ToInt32(item.Text);
        string desc = Convert.ToString(item.SubItems[1].Text);

        frmSeleccion frm = new frmSeleccion(cuenta, desc);
        frm.ShowDialog();
    }

    return true;
} 

[VB.NET]

Protected Overloads Overrides Function ProcessCmdKey(ByRef msg As System.Windows.Forms.Message, keyData As System.Windows.Forms.Keys) As Boolean
	'
	' Si el control DataGridView no tiene el foco, 
	' se abandonamos el procedimiento, llamando al metodo base
	'
	If (Not listView1.Focused) Then
		Return MyBase.ProcessCmdKey(msg, keyData)
	End If

	'
	' Si la tecla presionada es distinta al ENTER, 
	' se abandonamos el procedimiento, llamando al metodo base
	'
	If keyData <> Keys.Enter Then
		Return MyBase.ProcessCmdKey(msg, keyData)
	End If

	'
	' Sino hay item seleccinado en la lista
	' se abandonamos el procedimiento, llamando al metodo base
	'
	If listView1.SelectedItems.Count = 0 Then
		Return MyBase.ProcessCmdKey(msg, keyData)
	End If

	'
	' Obtenemos la fila actual 
	'
	Dim item As ListViewItem = listView1.SelectedItems(0)

       If rbMostrarMensaje.Checked Then

           MessageBox.Show(String.Format("Se ha seleccionado, Cuenta:'{0}' Descripcion:'{1}'", item.Text, item.SubItems(1).Text))

       ElseIf rbMostrarForm.Checked Then

           Dim cuenta As Integer = CInt(item.Text)
           Dim desc As String = CStr(item.SubItems(1).Text)

           Dim frm As New frmSeleccion(cuenta, desc)
           frm.ShowDialog()

       End If

       Return True

End Function

 

[C#]
[VB.NET]

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]