Controles ComboBox relacionados
Por Enrique Martínez Montejo
Última revisión: 08/01/2011
 

A menudo disponemos de un control ComboBox relleno de elementos, y queremos que al seleccionar uno de ellos nos muestre, en otro control ComboBox diferente, los registros que se encuentren relacionados con el elemento seleccionado en el primer control ComboBox. Una especie de relación principal - secundaria (o maestra - detalle), pero que se muestren los datos en controles ComboBox, digamos que en controles ComboBox relacionados.

Esto se puede hacer fácilmente, siempre y cuando se encuentren previamente relacionados los datos que se van a mostrar en los diferentes controles ComboBox que se desean relacionar. Si no se encuentran relacionados los datos, va a ser complicado establecer la relación, porque ésta no se haría de manera automática, si no que la tendríamos que implementar a base de búsquedas.

Como personalmente no me gustan las cosas complicadas, procuro esforzarme para hacerlas de una manera fácil y sencilla, de tal manera que con el mínimo esfuerzo obtenga el mejor resultado óptimo posible.

Como normalmente tenemos los datos almacenados en una base de datos, simplemente sería cuestión de rellenar objetos DataTable, añadirlos a un objeto DataSet, y establecer en éste último objeto la relación adecuada entre los objetos DataTable. Pero en ésta ocasión, voy a suponer que el lector no tiene los datos en una base de datos, si no que los tiene desperdigados por cualquier lado. ¿Habría una solución para poner orden a éste desorden? La solución se encuentra en el objeto DataSet.

Muchos usuarios de Visual Basic, y de otros lenguajes .NET, creen que el objeto DataSet únicamente se utiliza cuando tenemos los datos en una base de datos, motivo por el cual desconocen las posibilidades de éste objeto, que se encuentra en lo más alto de la pirámide de lo que se conoce como la arquitectura desconectada de ADO .NET.

Imagine un objeto DataSet como si fuera una base de datos, pero que en lugar de estar localizada físicamente en un archivo de nuestro disco duro, se encuentra en la memoria de nuestro sistema, por lo que podemos guardar los datos en objetos DataTable (al igual que lo almacenaríamos en una tabla de una base de datos), y podemos relacionar los objetos DataTable entre sí (como también haríamos con las tablas de una base de datos relacional).

El ejemplo que presento a continuación, enseña la manera de crear un objeto DataSet, crear los objetos DataTable, rellenarlos de datos, y por último, relacionarlos entre sí. Para ello, vamos a mostrar en un primer control ComboBox (digamos en el control maestro) el nombre de cinco continentes, y en otro control ComboBox (el que actúa de detalle) los países que pertenecen al continente seleccionado en el primer control ComboBox. Obviamente, no espere que liste aquí todos los países de los cinco continentes, porque se trata solamente de un ejemplo.

En primer lugar, vamos a comenzar por escribir el procedimiento que se encargará de crear el objeto DataSet:

Private Function CreateDataSet() As DataSet

    Dim ds As New DataSet()

    ' Añadimos la tabla de Continentes.
    '
   
ds.Tables.Add(CreateDataTableContinents())

    ' Añadimos la tabla de Países.
    '
   
ds.Tables.Add(CreateDataTableCountries())

    ' Referenciamos las tablas maestra y detalle existentes
    ' en el objeto DataSet.
    '

    Dim
parentTable As DataTable = ds.Tables("Continentes")

    Dim childTable As DataTable = ds.Tables("Paises")

    ' Creamos una relación entre los objetos DataTable a través
    ' del campo IdContinente común en ambos objetos.
    '
   
Dim rel As New DataRelation( _
        "Continentes_Paises", _
        parentTable.Columns("IdContinente"), _
        childTable.Columns("IdContinente"))

    ' Añadimos la relación al objeto DataSet.
    '
   
ds.Relations.Add(rel)

    ' Devolvemos el objeto DataSet.
    '

    Return
ds

End Function

Observará que desde el procedimiento CreateDataSet, se está llamando a otros dos métodos, cada cual devolverá su respectivo objeto DataTable. Al final del procedimiento, se establece la relación entre los objetos DataTable, a través de un campo común, en el ejemplo, el campo IdContinente existente en los dos objetos DataTable. Esto no significa que los campos que forman la relación, tengan idénticos nombres en los dos objetos DataTable; mientras que los tipos de datos de los dos campos o columnas sean iguales, no importa el nombre que tenga cada uno de ellos.

A continuación, éste será el procedimiento para crear el objeto DataTable de Continentes:

Private Function CreateDataTableContinents() As DataTable

    Dim
dt As DataTable = New DataTable("Continentes")

    ' Añadimos la columna que va a identificar de manera
    ' única a cada Continente.
   
'
    Dim
dc As DataColumn = New DataColumn("IdContinente", GetType(Integer))

    ' Añadimos la columna al objeto DataTable.
    '
   
dt.Columns.Add(dc)

    ' Indicamos que ésta columna será la clave principal
    ' del objeto DataTable.
   
'
    dt.PrimaryKey = {dc}

    ' Añadimos otra columna para el nombre del Continente.
   
'
    dc = New DataColumn("Nombre", GetType(String))
    dt.Columns.Add(dc)

    ' Añadimos los registros al objeto DataTable.
   
'
    Dim
row As DataRow = dt.NewRow()
    row("IdContinente") = 1
    row("Nombre") = "Europa"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 2
    row("Nombre") = "América"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 3
    row("Nombre") = "África"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 4
    row("Nombre") = "Asia"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 5
    row("Nombre") = "Oceania"
   
dt.Rows.Add(row)

    Return
dt

End Function

Fíjese que el objeto DataTable resultante dispone solamente de dos campos o columnas: el campo IdContinente, que a su vez será la clave principal del objeto DataTable, y el campo Nombre.

Continuamos con el procedimiento para crear el objeto DataTable correspondiente a los países:

Private Function CreateDataTableCountries() As DataTable

    Dim
dt As DataTable = New DataTable("Paises")

    ' Añadimos una columna autonumérica que va a identificar de manera
   
' única a cada País.
    '

    Dim
dc As DataColumn = New DataColumn("IdPais", GetType(Integer))
    dc.AutoIncrement = True
   
dc.AutoIncrementStep = 1
    dc.AutoIncrementSeed = 1

    ' Añadimos la columna al objeto DataTable.
    '
   
dt.Columns.Add(dc)

    ' Indicamos que ésta columna será la clave principal
    ' del objeto DataTable.
    '
    dt.PrimaryKey = {dc}

    ' Añadimos la columna que representará el identificador del
    ' continente al que pertenece el país.
    '

    dc = New DataColumn("IdContinente", GetType(Integer))
    dt.Columns.Add(dc)

    ' Añadimos la columna para el nombre del país.
    '

    dc = New DataColumn("Nombre", GetType(String))
    dt.Columns.Add(dc)

    ' Añadimos los registros al objeto DataTable. Observe que no es
    ' necesario especificar los valores de la columna autonumérica.
    '

    Dim
row As DataRow = dt.NewRow()
    row("IdContinente") = 1
    row("Nombre") = "España"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 1
    row("Nombre") = "Francia"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 1
    row("Nombre") = "Alemania"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 1
    row("Nombre") = "Italia"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 2
    row("Nombre") = "Brasil"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 2
    row("Nombre") = "Colombia"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 2
    row("Nombre") = "Estados Unidos"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 2
    row("Nombre") = "Chile"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 2
    row("Nombre") = "México"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 3
    row("Nombre") = "Marruecos"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 3
    row("Nombre") = "Libia"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 3
    row("Nombre") = "Egipto"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 4
    row("Nombre") = "China"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 4
    row("Nombre") = "Japón"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 4
    row("Nombre") = "India"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 5
    row("Nombre") = "Australia"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 5
    row("Nombre") = "Nueva Zelanda"
   
dt.Rows.Add(row)

    row = dt.NewRow()
    row("IdContinente") = 5
    row("Nombre") = "Papúa Nueva Guinea"
   
dt.Rows.Add(row)

    Return
dt

End Function

El objeto DataTable de Países tendrá tres campos: IdPais (que será un campo autonumérico y actuará de clave principal de la tabla), IdContinente (para relacionarlo con el mismo campo de la tabla de Continentes), y Nombre, que como es de esperar, guardará el nombre del país.

¡Bueno! Pues ya estamos en condiciones de llamar al método CreateDataSet desde el evento Load del formulario que contiene los dos objetos ComboBox, para crear el objeto DataSet y establecer la relación entre ambos objetos DataTable:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load

    ' Obtenemos un objeto DataSet
   
'
    Dim ds As DataSet = CreateDataSet()

    ' Configuramos el control BindingSource principal
   '

    Dim bsPrincipal As New BindingSource()
    bsPrincipal.DataSource = ds
    bsPrincipal.DataMember = "Continentes"

    ' Configuramos el control ComboBox principal
    '

    With ComboBox1
        .DataSource = bsPrincipal
        .DisplayMember = "Nombre"
        .ValueMember = "IdContinente"
    End With

    ' Configuramos el control BindigSource secundario
   '

    Dim bsDetalle As New BindingSource()
    bsDetalle.DataSource = bsPrincipal
    bsDetalle.DataMember = "Continentes_Paises"

    ' Configuramos el control ComboBox secundario
   '

    With ComboBox2
        .DataSource = bsDetalle
        .DisplayMember = "Nombre"
        .ValueMember = "IdPais"
    End With

    ' Instalamos el controlador para el evento SelectedValueChanged
    ' del control ComboBox de países.
   
'
    AddHandler ComboBox2.SelectedValueChanged, AddressOf ComboBoxOnSelectedValueChanged

End Sub

El mecanimo para que funcione la relación entre las tablas Continentes y Paises, se encuentra en la utilización de dos objetos BindingSource, los cuales se encargarán de encapsular los orígenes de datos de los dos controles ComboBox. La configuración de los controles BindingSource deberá de adaptarse al siguiente esquema:

Y el esquema de configuración de los controles ComboBox relacionados sería éste otro:

Por último, vamos a escribir el procedimiento que se utilizará para el controlador del evento SelectedValueChanged del control ComboBox de países, el cual se desencadenará cuando seleccionemos un elemento del citado control (cuando cambie el valor de su propiedad SelectedValue), teniendo la posibilidad de obtener el identificador del país seleccionado:

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

    ' Referenciamos el control ComboBox que ha
    ' desencadenado el evento.
    '

    Dim cb As ComboBox = DirectCast(sender, ComboBox)

    Dim value As Object = cb.SelectedValue

    If (TypeOf value Is DataRowView) Then Return

    ' Obtenemos el identificador del país seleccionado.
    '

    Dim idPais As Integer = CInt(value)

    MessageBox.Show(CStr(idPais))

End Sub

¡Eso es todo! Como habrá tenido ocasión de comprobar, mediante la utilización de una relación existente en un objeto DataSet, hemos sido capaces de relacionar de una manera automática, los elementos existentes en dos controles ComboBox diferentes.

 

Otros enlaces de interés:

Índice de la colección de ejemplos de las clases del marco de trabajo de .NET


Enrique Martínez Montejo - 2011

NOTA: La información contenida en este artículo, así como el código fuente incluido en el mismo, se proporciona COMO ESTÁ, sin garantías de ninguna clase, y no otorga derecho alguno. Usted asume cualquier riesgo al poner en práctica, utilizar o ejecutar lo explicado, recomendado o sugerido en el presente artículo.

NOTE: The information contained in this article and source code included therein, is provided AS IS without warranty of any kind, and confers no rights. You assume any risk to implement, use or run it explained, recommended or suggested in this article.