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]

37 comentarios:

  1. el selectedvalue al cambiarlo en codigo no activa el evento ? ... tengo ese problema ... solo el selectedindex puede entonces?

    Gracias

    ResponderEliminar
  2. hola Arx

    a que evento haces referencia ?

    es al SelectedChangeCommited, ese evento solo se dispara cuando hay una selecion por parte del usuario, asignacion desde codigo no lo modifica

    saludos

    ResponderEliminar
  3. Seguí el tutorial, pero me sale un error en linea de código y no se que sera ya que al comparar la del ejemplo con el mio son casi lo mismo, pero me sale el siguiente error:

    No se puede enlazar con el nuevo miembro de presentación.
    Nombre del parámetro: newDisplayMember

    ResponderEliminar
  4. hola Javier

    como es que obtienes un mensaje
    Nombre del parámetro: newDisplayMember

    si el DisplayMember es una propiedad no un obneto, este no se instancia con el new, hay algo raro en el codigo

    saludos

    ResponderEliminar
  5. Una pregunta, en el método GellTrackByAlbum para que lo usas; lo usas para que cuando yo seleccione el artista y luego el álbum se cargue las canciones asociadas a ese álbum escogido?

    Gracias

    ResponderEliminar
  6. hola Javier

    exacto
    como veras se usa en LoadComboTrack() y se asigna al combo cbTrack, filtrando por un AlbumId

    con este se respeta la cascada que se va cargando tomando el id de la entidad relacionada para traer los datos que relaciona

    saludos

    ResponderEliminar
  7. como se puede hacer lo mismo pero con linq to SQL, mi problema radica en el bucle while no se como se puede hacer para que muestre todos los datos el combobox.

    ResponderEliminar
  8. hola luis

    para que necesitas un while ?

    si suas linq to sql tendrias que tener una lista de tu entidad que puedes vincular directo al datasource del control

    usarias linq para armar la expression que filtre por la seleccion del combo relacionado, pro luego el resultado se asigna directo

    saludos

    ResponderEliminar
  9. esto es lo que ago lo cual me funciona pero con el metodo llenarLocalidades()
    lo que sucede, es que hace bien el filtrado pero en el combobox cmbLocalidad
    solo me muestra una localidad por provincias suponiendo tengo la provincia bs as
    y 3 localidades correspondientes a bs y solo me muestra la primera localidad
    y no las demas y con todas las provincias hace lo mismo.
    deberia ir un bucle que me agregue todas las ciudades pero no se como se puede hacer
    disculpas soy nuevo en esto.

    private void Form1_Load(object sender, EventArgs e)
    {
    var pro = from p in BD.PROVINCIAS
    select p;

    cmbProvincias.DataSource = pro;
    cmbProvincias.DisplayMember = "PROVINCIA";
    cmbProvincias.ValueMember = "PK_ID_PROVINCIA";
    //cmbProvincias.Items.Add(pro);
    llenarLocalidades();
    }

    private void cmbProvincias_SelectedIndexChanged(object sender, EventArgs e)
    {

    llenarLocalidades();

    }


    public void llenarLocalidades()
    {
    //Deberia ir un bucle?
    var Loc = from l in BD.LOCALIDADES
    where l.PK_ID_LOCALIDAD == Convert.ToInt32(cmbProvincias.SelectedValue)
    select l;

    cmbLocalidad.DataSource = Loc;
    cmbLocalidad.DisplayMember = "LOCALIDAD";
    }

    ResponderEliminar
  10. hola luis

    porque piensas que en llenarLocalidades() deberia ir un bucle, si alli filtras y asigna los datos al combo

    lo que si veo incorrecto es que si cargas localidades filtres el campo PK_ID_LOCALIDAD, se supone que deberias alli filtrar por el valor del combo anterior relacionado

    [WinForms] ComboBox Anidados

    o sea es una relacion, el filtro del combo siguiente lo armas con el valor de la seleccion del anterior

    saludos

    ResponderEliminar
  11. genial anduvo a la perfección gracias son de mucha ayuda tus respuestas y material que aportas.

    tengo una duda mas si no es molestia en la misma tabla localidades tengo un campo de codigo postal "CP" y cuando selecciono el combobox cmbLocalidad quiero que se muestre en un Textbox el codigo postal lo que ago es lo siguiente pero me sale error.

    public void llenarLocalidades()
    {
    var Loc = from l in BD.LOCALIDADES
    where l.FK_ID_PROVINCIA == Convert.ToInt32(cmbProvincias.SelectedValue)
    select l;

    cmbLocalidad.DataSource = Loc;
    cmbLocalidad.DisplayMember = "LOCALIDAD";
    //cmbLocalidad.ValueMember = "PK_ID_LOCALIDAD";//aqui dedusco deberia ir el campo"CP"
    //que es el que uso para asignar a txtcp pero me da error.
    }
    private void cmbLocalidad_SelectedIndexChanged(object sender, EventArgs e)
    {

    txtCP.Text = cmbLocalidad.SelectedValue.ToString();
    }

    ResponderEliminar
  12. hola luis

    es correcto que debas usar CP para el ValueMemeber

    quedaria
    cmbLocalidad.DisplayMember = "LOCALIDAD";
    cmbLocalidad.ValueMember = "CP"
    cmbLocalidad.DataSource = Loc;

    pero no uses el evento SelectedIndexChanged, usa el SelectedChangecommited
    porque este evento si es ejecutado solo ante una seleccion del usuario

    saludos

    ResponderEliminar
  13. un millón de gracias, me anduvo de 10. cualquier duda volveré a preguntar.

    ResponderEliminar
  14. Hola yo tengo una pregunta tengo un combobox que se llena con

    SqlDataAdapter daDos = new SqlDataAdapter(sqlUno, conection);
    DataTable dtDos = new DataTable();
    daDos.Fill(dtDos);
    ya tengo el dispalmember y value member
    ya me muestra los datos pero lo que quiero es que en el primmer item del combobox me aparesca el tipico texto(Seleecione o un espacio enblanco) y no o eh podido lograr espero me ayuden gracias por leer este comentario.

    ResponderEliminar
  15. hola jempdulintre

    no has analiza este articulo

    ComboBox - DropDownList – Opción “Todos”

    alli trato el tema

    saludos

    ResponderEliminar
  16. ChinookDAL.GellAllArtist()
    ME PODRIAS EXPLICAR QUE HACE ESTA SENTENCIA? SI CHINOOKDAL ES EL NOMBRE DE LA BASE DE DATOS Y LO DEMAS

    ResponderEliminar
  17. hola Allen

    te comento que lo que alli vidualizas el la clase que implemente ala capa de acceso a datos

    por supuesto lleva el mismo nombre que la db porque justamente trabaja contra esta (pero podria llevar cualqueir otro si lo necesitas), pero es la clase cuya responsabilidad sera permitir el acceso a las tabla de la db y abstraer al resto de la aplicacion de esta tarea

    mantiene ordenado el codigo separando las responsabilidades


    saludos

    ResponderEliminar
  18. hey tio como puedo hacer para obtener los codigos de las opciones seleccionadas (check) en un UltraComboeditor y almacenarlo en una caja de texto, todo esto es el C#. saludos

    ResponderEliminar
  19. hola

    la verdad nunca he usado el UltraComboeditor, pero imagino este tiene documentacion que explique como se utiliza

    no hay nada en la ayuda que se brinda del compomente, o quizas quien lo vende en la pagina tenga algun foro o mail de soporte donde consulta (aunque imagino se debera redactar en ingles la pregunta)

    saludos

    ResponderEliminar
  20. Hola tengo dos combobox en mi formulario, lo cuales lleno a partir de dos tablas en mi BD ,y lo hago de la siguiente forma:





    Sub CargarPersonal()
    Try

    Dim da As New SqlDataAdapter("Select * from Personal", dbalmacen)
    Dim ds As New DataSet
    da.Fill(ds, "Personal")
    Me.cmbprecibe.DataSource = ds.Tables("Personal")
    Me.cmbprecibe.DisplayMember = "Nombre"
    Me.cmbprecibe.ValueMember = "IdPersonal"
    'cmbprecibe.SelectedIndex = -1
    Catch ex As Exception
    MsgBox(ex.Message)

    End Try
    End Sub

    Sub CargarArea()
    Try
    Dim da As New SqlDataAdapter("Select * from Area", dbalmacen)
    Dim ds As New DataSet
    da.Fill(ds, "Area")
    Me.cmbarea.DataSource = ds.Tables("Area")
    Me.cmbarea.DisplayMember = "Descripcion"
    Me.cmbarea.ValueMember = "IdArea"
    'cmbarea.SelectedIndex = -1
    Catch ex As Exception
    MsgBox(ex.Message)

    End Try
    End Sub

    Lo que quiero lograr es que al seleccionar un elemento del combobox de personal, en el de área me aparezca automaticamente a que área pertenece,

    La estructura de las tablas la tengo asi:

    PERSONAL

    IdPersonal IdArea Nombre

    1 1 victor

    2 3 lupita

    3 2 karen

    4 4 mine

    AREA

    IdArea Descripcion

    1 Sistemas

    2 Patrimonial

    3 Copiado

    4 Contraloria

    Realice un ejemplo y modifique el codigo que carga el personal asi:

    Sub CargarPersonal()
    Try
    Dim da As New SqlDataAdapter("Select * from Personal Where personal.IdArea='" & cmbarea.SelectedValue & "'", dbalmacen)
    Dim ds As New DataSet
    da.Fill(ds, "Personal")
    Me.cmbprecibe.DataSource = ds.Tables("Personal")
    Me.cmbprecibe.DisplayMember = "Nombre"
    Me.cmbprecibe.ValueMember = "IdPersonal"
    'cmbprecibe.SelectedIndex = -1
    Catch ex As Exception
    MsgBox(ex.Message)

    End Try
    End Sub

    y en el evento SelectedIndexChanged del combo area escribí las siguientes lineas:

    If Not Me.cmbarea.SelectedValue.GetType Is GetType(DataRowView) Then
    CargarPersonal()
    End If

    Estos cambios hacen que al seleccionar un elemento del cobo de areas en el de personal me aparecen los elementos correspondiente a esa area, pero yo lo necesito hacer al revés, como puedo solucionarlo gracias!!

    ResponderEliminar
  21. hola Bere

    en realidad esta pregunta hubiera estado mejor palntearla desde el foro de vb.net, mas que nada porque permite trabajar con el codigo de forma mas simple

    lo que veo es que aqui
    Dim da As New SqlDataAdapter("Select * from Personal Where personal.IdArea='" & cmbarea.SelectedValue & "'", dbalmacen)

    no estas saudno parametros ,si analizas el ejemplo de este articulo veras que uso el Parameter del objeto command para asignar los valores de filtro a la query, eso mismo deberias hacer en este caso

    al reves no puedes hacerlo si la relacion es uno a muchos del area a las personas, si lo haces al contrario solo tendrias un unico area que seria la que tiene una persona vinculada, pero eso no seria valido para cargar los items del combo de area porque seria una unica row

    saludos

    ResponderEliminar
  22. Muchas gracias Ing. leandro.

    Su ejemplo funciona excelente y el contenido del blog me ayudo bastante.

    ResponderEliminar

  23. Buenas estimado, antes que nada agradecerte por tus ejemplos son de gran ayuda.

    Ahora al grano :o) podrias corregir los enlaces de descargas del combobox anidados para winforms en c#.

    Saludos.

    ResponderEliminar
  24. hola MatiCris

    revise el link y puse descargar correctamente el zip con el codigo,
    estan publicados en onedrive

    que error obtienes?

    saludos

    ResponderEliminar
  25. Hola Buenas tardes, yo estoy haciendo en visual studio tres combobox anidados, los cuales son tipo marca y modelo, y uno depende de la seleccion del anterior, el que me esta fallando es el ultimo que no me muestra bien los resultados. Tendrás algun mail para pasarte el codigo o te lo paso por acá? Te cuento que las consultas las hago con Store procedure.
    Gracias

    ResponderEliminar
  26. Hola Leandro, mira aplique muy bien tu ejemplo y si me quedo, sin embargo ahora busco aplicar un filtro en los comboBox anidados que hice, lo hice en el primero y funciono con el datagrid pero el segundo no se como insertarle el Item 0 para hacer las comparaciones, puesto que cuando lo hago se pierde el enlace con el primer combo, no se si me pudieras ayudar, de antemano muchas gracias

    ResponderEliminar
  27. hola Chirola07

    validaste que los valores selecciondos que tomas de los combos sean correctos y se los estes asignando a la invocacion del procedures como parametro

    tambien podrias ejecutar el procedure desde el Sql Server Managemetn Studio y ver si al ponerle valores retorna los registros que esperas obtener, con esto validas que el SP funciona correctamente

    saludos

    ResponderEliminar
  28. hola Samuel

    entiendo que la aplicacion es winforms, no? si es asi no veo que enlace hacer referencia ya que la deteccion de una seleccion la realizas mediante evento
    si detectas el evento SelectionChangeCommitted, entonces alli tomas la seleccion y cargas el proximo combo o grid

    si pones breakpoint en el evento del combo se detiene la ejecucion? sino lo hace quizas no este bien adjunto el evento

    saludos

    ResponderEliminar
  29. Quedo muy agradecido del extracto del ejemplo con C#, soluciones mi problema gracias a él
    Saludos desde Arica - Chile

    ResponderEliminar
  30. hola leandro excelente tu explicacion! ahora tengo un problema, cuando selecciono el combo por primera vez para q s me cargue todo funciona de diez y carga los combo perfectamente. El problema es cuando selecciono otro pais para q me muestre las provincias de ese en otro combo se me llena el combo con las provincias del pais anterior y las provincias con el combo del que seleccione ultimo. Me entendes? Como puedo hacer para que no me ocurra eso, estoy desesperado, estoy con mi proyecto de tesis y hace dos dias q estoy trabado en eso. Ya no se que vuelta darle.
    comboPais -----------------------comboProvincia

    ResponderEliminar
  31. hola leandro excelente tu explicacion! ahora tengo un problema, cuando selecciono combo por primera vez para q s me cargue todo funciona de diez y carga los combo perfectamente. El problema es cuando selecciono otro pais para q me muestre las provincias de ese en otro combo se me llena el combo con las provincias del pais anterior y las provincias con el combo del que seleccione ultimo. Me entendes? Como puedo hacer para que no me ocurra eso, estoy desesperado, estoy con mi proyecto de tesis y hace dos dias q estoy trabado en eso. Ya no se que vuelta darle.

    ResponderEliminar
  32. string selectProvincia = "select idProvincia, provincia from Provincia where idPais="+idPais+"";
    SqlCommand com = new SqlCommand(selectProvincia, con.con);
    con.con.Open();
    con.da.SelectCommand = com;
    con.da.Fill(con.ds, "Provincia");
    cbprovincia.DataSource = con.ds.Tables["Provincia"];
    cbprovincia.DisplayMember = con.ds.Tables["Provincia"].Columns["provincia"].ToString();
    cbprovincia.ValueMember = con.ds.Tables["Provincia"].Columns["idProvincia"].ToString();
    con.con.Close();

    asi es como lleno los combobox con el dateset y la propiedad datasource del combo. Si puedes dime en que estoy fallando o q me falta agregarle para que me filtre correctamente los datos de los combo sin que se repitan los elementos cuando selecciono del combo. Espero tu ayuda gracias!!!

    ResponderEliminar
  33. private void comboPais()
    {

    string selectPais = "select codigo, pais from pais";
    SqlCommand com = new SqlCommand(selectPais, con.con);
    con.con.Open();//abro conexio
    con.da.SelectCommand = com;//dataAdapter
    con.da.Fill(con.ds, "pais");//relleno dataAdapter con datos de ds(dataset)
    cbpais.DataSource = con.ds.Tables["pais"];
    cbpais.DisplayMember = con.ds.Tables["pais"].Columns["pais"].ToString();
    cbpais.ValueMember = con.ds.Tables["pais"].Columns["codigo"].ToString();
    con.con.Close();
    if (cbpais.Items.Count != 0)
    {
    int idPais = Convert.ToInt32(cbpais.SelectedValue);
    comboProvincia(idPais);
    }
    else
    {
    cbprovincia.DataSource = null;
    cbciudad.DataSource = null;
    }
    }

    private void comboProvincia(int idPais)
    {
    string selectProvincia = "select idProvincia, provincia from Provincia where idPais="+idPais+"";
    SqlCommand com = new SqlCommand(selectProvincia, con.con);
    con.con.Open();
    con.da.SelectCommand = com;
    con.da.Fill(con.ds, "Provincia");
    cbprovincia.DataSource = con.ds.Tables["Provincia"];
    cbprovincia.DisplayMember = con.ds.Tables["Provincia"].Columns["provincia"].ToString();
    cbprovincia.ValueMember = con.ds.Tables["Provincia"].Columns["idProvincia"].ToString();
    con.con.Close();
    if (cbprovincia.Items.Count != 0)
    {
    int idProv = Convert.ToInt32(cbprovincia.SelectedValue);
    comboCiudad(idProv);
    }
    else
    {
    cbciudad.DataSource = null;
    }
    }


    private void comboCiudad(int idProv)
    {
    string selectCiudad = "select idCiudad, nombre from Ciudad where idProvincia=" + idProv + "";
    SqlCommand com = new SqlCommand(selectCiudad, con.con);
    con.con.Open();
    con.da.SelectCommand = com;
    con.da.Fill(con.ds, "Ciudad");
    cbciudad.DataSource = con.ds.Tables["Ciudad"];
    cbciudad.DisplayMember = con.ds.Tables["Ciudad"].Columns["nombre"].ToString();
    cbciudad.ValueMember = con.ds.Tables["Ciudad"].Columns["idCiudad"].ToString();
    con.con.Close();
    if (cbciudad.Items.Count != 0)
    {
    int idCiudad = Convert.ToInt32(cbciudad.SelectedValue);
    }
    }

    ResponderEliminar
  34. consulta yo hice una funcion en winforms que es de 3 capas cuando quiero obtener el valor del indice para llevarlo a la funcion que me traiga los datos me da error con que el valor no es el mismo y todo es interger por eso no se en que le estoy errando

    ResponderEliminar
  35. Genial.. seguí el tuto y todo funciona ok.!

    gracias amigo.

    ResponderEliminar