Serializar un control TreeView

Descripción general

El problema a resolver es serializar /Deserializar el contenido de un control TreView

[TOC] Tabla de Contenidos


↑↑↑

Introducción

El problema de serializar un árbol es relativamente complejo, la parte fácil es volcar la información del árbol a un soporte, la parte difícil es recomponer el árbol a partir de la información guardada.

Recuerda que un árbol es una estructura que contiene nodos, y cada uno de los nodos a su vez contiene nodos hijos, formando una estructura que se asemeja a un árbol

Falta el texto Alt de la imagen

Imagen copiada de:http://sujitpal.blogspot.com/2006/05/java-data-structure-generic-tree.html

El problema que se plantea es generar una estructura de datos que represente cada nodo del árbol y que se pueda guardar en el disco en un fichero, y a su vez que permita recuperar el árbol usando esa información

Buscando por internet, se encuentran varias soluciones posibles, por ejemplo,

En este documento he estudiado otra posibilidad, que consiste en guardar la información del árbol en un fichero de texto puro, guardando la información de cada nodo del árbol en formato CSV.


↑↑↑

Crear la estructrura de datos

Una de las formas es utilizar una estructura de datos que consiste en asignar a cada nodo tres valores, un valor ID único que lo identifica, el valor del ID del nodo padre y los datos que queremos aguardar del nodo, en este ejemplo solo guardaremos la propiedad Text del nodo, es decir el valor de texto que aparece en el control TreeView en cada uno de sus nodos

Falta el texto Alt de la imagen

Imagen copiada de:http://sujitpal.blogspot.com/2006/05/java-data-structure-generic-tree.html

'*********************************
'     | ID  | 
' ID  |Padre| Text
'---------------------------------
'    1|    0|1.- Animales        
'    2|    1|1.1- Mamíferos      
'    3|    2|1.1.1- Perros       
'    4|    3|1.1.1.1- Pastores   
'    5|    3|1.1.1.2- Chiguaguas 
'    6|    3|1.1.1.3- Compañía   
'    7|    1|1.2-Serpientes 

ID debe contener un numero único que identifica a cada nodo.

ID Padre: debe contener el ID del nodo padre del cual "Cuelga" este nodo

Text: Es el valor de la propiedad Text del objeto TreeNode. Si queremos guardar mas información del nodo, deberemos crear una columna adicional por cada dato que queramos guardar

Para manejar esta información utilizo una estructura, que llamare [EstructruraNodoSerialiar] que contiene la información de cada nodo, y también contiene una propiedad llamada [CSV] que se encarga de volcar los datos de la estructura en una cadena CSV para guardarla en un fichero de texto, y al revés, cuando recibe una cadena que contiene la información en formato CSV, es capaz de leerla y cargar los datos en la estructura.

El proceso de serializar la información es el siguiente

El proceso de Deserializar es el siguiente

Observacion: El problema que presenta este enfoque es que se leen varias veces lo nodos para darlos de alta, por lo que he habilitado un "semáforo" en la estructura que indica si ese "nodo" se ha creado a no.

La estructura [EstructruraNodoSerialiar] tiene la siguiente interface:

Public Structure EstructruraNodoSerialiar

    ''' <summary>El id UNICO del nodo</summary>
    Public Property ID() As Integer

    ''' <summary>El id del nodo padre</summary>
    Public Property IDPadre() As Integer

    ''' <summary>El valor de la propiedad Text del nodo</summary>
    Public Property Text() As String

    ''' <summary>
    ''' Se emplea en el proceso de carga del árbol, 
    ''' y es un valor lógico que indica si el nodo se 
    ''' ha creado en el árbol (valor true) o no
    ''' </summary>
    Public Property Grabado() As Boolean


    ''' <summary>
    '''  Constructor de la estructra
    ''' </summary>
    ''' <param name="id">El id UNICO del nodo</param>
    ''' <param name="idPadre"> El id del nodo padre</param>
    ''' <param name="text"> 
    ''' El valor de la propiedad Text del nodo</param>
    Public Sub New(ByVal id As Integer, _
                   ByVal idPadre As Integer, _
                   ByVal text As String)

    ''' <summary>
    ''' Devuelve los datos de la estructura en un 
    ''' formato legible por el usuario
    ''' </summary>
    ''' <returns>
    ''' Una cadena con la información de la estructura 
    ''' por ejemplo ID = 5; ID Padre = 2; Text = pajaros; Grabado = False
    ''' </returns>
    Public Overrides Function ToString() As String


    ''' <summary>
    ''' Obtiene / establece los valores de la estructura
    ''' en una cadena con formato CSV
    ''' </summary>
    Public Property CSV() As String

La propiedad CSV tiene el siguiente código:

''' <summary>
''' Obtiene / establece los valores de la estructura 
''' en una cadena con formato CSV
''' </summary>
Public Property CSV() As String
    Get
        Dim provider As IFormatProvider
        provider = System.Globalization.CultureInfo.CurrentCulture
        'La sintaxis de un elemento de formato es
        ' {index[,alignment][:formatString]},
        Dim formato As String = "{0,5:0}|{1,5:0}|{2,-20}"
        Dim salida As String
        '----------------------
        ' montar la cadena de salida con el salto de linea al final
        salida = String.Format( _
                 provider, formato, ID, IDPadre, Text) & _
                 Environment.NewLine
        '----------------------
        Return salida
    End Get
    Set(ByVal value As String)
        If String.IsNullOrEmpty(value) Then
            Throw New ArgumentException( _
           "El valor de la cadena no puede ser nulo o vacio", "CSV")
        End If
        Dim matrizElementos() As String
        Dim matrizSeparadores() As Char = {"|"c}
        Try
            matrizElementos = _ 
                  value.Split(matrizSeparadores, _
                              StringSplitOptions.RemoveEmptyEntries)
            Me.ID = Integer.Parse(matrizElementos(0))
            Me.IDPadre = Integer.Parse(matrizElementos(1))
            Me.Text = matrizElementos(2)
            Me.Grabado = False
        Catch ex As Exception
            Throw
        End Try
    End Set
End Property


↑↑↑

Serializar el Árbol

Si observas detenidamente, el código se encarga de recorrer todos y cada uno de los nodos del árbol, asignando a cada uno de ellos un numero único que es el ID del nodo, y a continuación se encarga de recorrer los nodos hijos. Por cada nodo, se carga la estructura con la información y se genera una cadena en formato CSV que es la que se escribe en un fichero de texto, el resultado de ese fichero es algo parecido a esto...

'********************************
'     | ID  | 
' ID  |Padre| Text
'--------------------------------
'    1|    0|1.- Animales        
'    2|    1|1.1- Mamíferos      
'    3|    2|1.1.1- Perros       
'    4|    3|1.1.1.1- Pastores   
'    5|    3|1.1.1.2- Chiguaguas 
'    6|    3|1.1.1.3- Compañía   
'    7|    1|1.2-Serpientes      
'    8|    1|1.3-Aves            
'    9|    8|1.3.1- Voladoras    
'   10|    8|1.3.2- Caminantes   
'   11|    1|1.4-Peces           
'   12|   11|1.4.1- Luchadores   
'   13|   12|1.4.1.1- Azules     
'   14|   12|1.4.1.2- Colorados  
'   15|   11|1.4.2- Gupis        
'   16|   11|1.4.3- Truchas      
'   17|    0|2.- Hongos          
'   18|    0|3.- Plantas         
'   19|   18|3.1- Hierbas        
'   20|   18|3.2- Arbustos       
'   21|   18|3.3- Arboles        
'   22|    0|4.- Minerales       
'   23|   22|4.1- Piedras        

EL proceso de grabar en disco la información tiene el siguiente código:

#Region "Save"

    ''' <summary>
    ''' Guarda un arbol en un fichero en formato CSV
    ''' </summary>
    ''' <param name="unArbol">el arbol que hay que guardar</param>
    ''' <param name="nombreDeFichero">
    '''  El nombre completo del fichero</param>
    Public Sub Save( _
           ByVal unArbol As System.Windows.Forms.TreeView, _
           ByVal nombreDeFichero As String)

        '-------------------------------------
        ' para evitar errores tontos
        ' si el directorio no existe crearlo
        If IO.Directory.Exists( _ 
           IO.Path.GetDirectoryName(nombreDeFichero)) = False Then
                  IO.Directory.CreateDirectory( _ 
                  IO.Path.GetDirectoryName(nombreDeFichero))
        End If
        '-------------------------------------
        ' Recorrer los nodos raices del arbol
        Using SW As New System.IO.StreamWriter( _
                    nombreDeFichero, False, codificacion)

            For Each TreeNode As TreeNode In unArbol.Nodes
                AuxSaveRecorrerNodosHijos (0, TreeNode, SW)
            Next

        End Using
    End Sub


    ''' <summary>
    '''  Funcion recursiva que recorre los nodos hijos de cada nodo
    ''' </summary>
    ''' <param name="IDNodoPadre">
    ''' El ID del nodo padre</param>
    ''' <param name="nodo">
    ''' El Objeto [TreeNode] que se esta estudiando </param>
    ''' <param name="SW">
    ''' El objeto [StreamWriter] usado para grabar 
    ''' la informacion en el disco
    ''' </param>
    Private Sub AuxSaveRecorrerNodosHijos ( _
                ByVal IDNodoPadre As Integer, _
                ByVal nodo As TreeNode, _
                ByRef SW As System.IO.StreamWriter)
        '--------------------------------
        ' el contador para generar un numero correlativo de nodos
        Static contadorIDNodo As Integer = 0
        ' Calcular y asigbar el ID unico a este nodo
        contadorIDNodo += 1
        Dim IDEsteNodoHijo As Integer = contadorIDNodo
        ' almacenar los valores del nodo
        Dim estruNodo As New EstructruraNodoSerialiar( _
                IDEsteNodoHijo, IDNodoPadre, nodo.Text)

        ' montar la cadena de salida con el salto de linea al final
#If DEBUG Then
        ' para control de depuracion
        Debug.Write(estruNodo.CSV)
#End If
        ' escribir el nodo en el fichero
        SW.Write(estruNodo.CSV)

        ' recorrer los nodos hijos
        For Each unNodo As TreeNode In nodo.Nodes
            saveRecorrerNodosHijos(IDEsteNodoHijo, unNodo, SW)
        Next
    End Sub

#End Region


↑↑↑

Deserializar el Árbol


↑↑↑

Leer el fichero del disco

Para leer la información el primer paso es recuperar la información del fichero de texto y cargarla en la memoria. El proceso se realiza en la función que se muestra a continuación, que lee el fichero y carga una lista genérica que contiene una lista de estructuras [EstructruraNodoSerialiar]. Cada una de las estructuras de la lista, contiene la información necesaria y suficiente para reconstruir el nodo del árbol. El proceso de reconstrucción se realiza en otras funciones

''' <summary>
''' Esta función lee el fichero de texto y carga 
''' la información en una lista genérica
''' </summary>
''' <param name="nombreDeFichero">
''' El nombre del fichero de texto donde está 
''' la información del árbol serializado
''' </param>
''' <returns>
''' Devuelve una lista genérica que contiene estructuras 
''' de datos del tipo [EstructruraNodoSerialiar] de manera 
''' que cada una de las estructuras contiene la información 
''' de un nodo del árbol. En otro proceso, usando esa 
''' información, se reconstruye el árbol
''' </returns>
Private Function AuxLoadCargarLista( _
        ByVal nombreDeFichero As String) _
        As List(Of EstructruraNodoSerialiar)
    '-----------------------------------------------
    ' control de la existencia del fichero en el disco
    If IO.File.Exists(nombreDeFichero) = False Then
        Throw New System.IO.FileNotFoundException( _
        "El fichero no existe en el disco", nombreDeFichero)
    End If
    '----------------------------------------
    Dim listaNodos As New List(Of EstructruraNodoSerialiar)
    Dim estructuraUnNodo As EstructruraNodoSerialiar = Nothing

#If DEBUG Then
    Debug.WriteLine("*********************************")
    Debug.WriteLine("     | ID  | ")
    Debug.WriteLine(" ID  |Padre| Text")
    Debug.WriteLine("---------------------------------")
#End If

    '----------------------------------------
    ' cargar la lista leyendo el fichero de texto
    Using SR As New System.IO.StreamReader( _
                nombreDeFichero, codificacion, True)
        ' leer la linea del fichero
        Dim input As String
        input = SR.ReadLine()
        While Not input Is Nothing

#If DEBUG Then
            Debug.WriteLine(input)
#End If
            ' escribir el nodo en un fichero
            estructuraUnNodo.CSV = input
            listaNodos.Add(estructuraUnNodo)
            input = SR.ReadLine()
        End While
        SR.Close()
    End Using
    Return listaNodos
End Function


↑↑↑

Carga del árbol

El proceso de carga del fichero lo realiza la siguiente función:

''' <summary>
'''  Deserializa un árbol guardado en un fichero de texto
''' </summary>
''' <param name="outputArbolSalida">
''' El árbol que se va a cargar con la información 
''' que existe en el fichero de texto</param>
''' <param name="nombreDeFichero">
''' El nombre del fichero en el disco que contiene 
''' la información del árbol</param>
Public Sub Load( _
           ByRef outputArbolSalida As System.Windows.Forms.TreeView, _
           ByVal nombreDeFichero As String)
    Try
        '----------------------------------------
        ' PASO UNO cargar el texto en una lista
        '----------------------------------------
        Dim listaNodos As New List(Of EstructruraNodoSerialiar)
        listaNodos = AuxLoadCargarLista(nombreDeFichero)

        '----------------------------------------
        ' PASO DOS cargar el arbol a partir de la lista
        ' busco el nodo raiz (o los nodos) lo grabo 
        ' y despues busco sus hijos
        '---------------------------------------
        Call AuxLoadCargaNodosRaiz(outputArbolSalida, listaNodos)
    Catch ex As Exception
        Throw
    End Try
End Sub

El proceso [AuxLoadCargarLista] que se encarga de cargar en una lista genérica la información del disco ya lo hemos visto

El proceso [AuxLoadCargaNodosRaiz] se encarga de recorrer la lista buscando los nodos raíces (aquellos que en su campo [IDPadre =0]. Cuando encuentra uno, se cuelga del árbol, se marca como colgado (grabado=true) y después se buscan en el árbol los nodos hijos que pueda tener

Nota

El proceso de búsqueda de nodos se hace mediante unos bucles que recorren toda la lista de nodos. Para hacer esta búsqueda más eficaz, lo primero que se comprueba es que el nodo no este [Grabado=False], es decir, que el nodo NO se haya colgado del árbol. Si un nodo se cuelga del árbol quiere decir que ya ha sido tratado antes por esta función, y no solo se ha colgado del árbol el nodo, sino que también se han buscado (y tratado) ya todos sus hijos, por lo que estén nodo no hay que volver a tratarlo, y podemos saltárnoslo.

''' <summary>
'''    Lee en la lista de nodos los nodos raíces, 
'''    los cuelga del árbol y a continuación busca 
'''    los nodos hijos de ese nodo raíz
''' </summary>
''' <param name="outputArbolSalida">
'''    El árbol que se va a cargar con la información 
'''    que existe en la lista genérica</param>
''' <param name="listaNodos">
'''    La lista genérica que contiene las estructuras 
'''    con la información de cada nodo</param>
Private Sub AuxLoadCargaNodosRaiz( _
        ByRef outputArbolSalida As System.Windows.Forms.TreeView, _
        ByRef listaNodos As List(Of EstructruraNodoSerialiar))

    Dim estrucNodoPadre As EstructruraNodoSerialiar = Nothing
    '----------------------------------------
    'busco el nodo raíz (o los nodos) lo grabo 
    'busco y después busco sus hijos
    '----------------------------------------
    outputArbolSalida.Nodes.Clear()
    Dim nuevoNodoRaiz As TreeNode
    For indexLista As Integer = 0 To listaNodos.Count - 1
        estrucNodoPadre = listaNodos.Item(indexLista)
        ' si no esta grabado
        If estrucNodoPadre.Grabado = False Then
            ' si es un nodo raiz
            If estrucNodoPadre.IDPadre = 0 Then
                nuevoNodoRaiz = New TreeNode(estrucNodoPadre.Text)
                outputArbolSalida.Nodes.Add(nuevoNodoRaiz)
#If DEBUG Then
                ' control de depuracion
                Debug.Write("Alta Raiz " & estrucNodoPadre.ToString)
#End If

                ' marcarlo como colgado del arbol
                estrucNodoPadre.Grabado = True
                ' buscar y grabar sus hijos
                AuxLoadCargaNodosHijos( _
                    nuevoNodoRaiz, estrucNodoPadre, listaNodos)
            End If
        End If
    Next
End Sub

El proceso [AuxLoadCargaNodosHijos] se encarga de colgar el nodo padre que se recibe del árbol (si aun no está colgado) y a continuación, de forma recursiva, buscar los nodos hijos de este nodo.

''' <summary>
'''    Busca los nodos hijos de un nodo en la lista de nodos, 
'''    cuando encuentra uno, lo cuelga del árbol, 
'''    lo marca como colgado del árbol, 
'''    y de forma recursiva busca sus hijos
''' </summary>
''' <param name="nodoPadre">
'''    El objeto nodo padre, es decir el nodo del cual 
'''    estamos buscando sus hijos</param>
''' <param name="estrucNodoPadre">
'''    La estructura que contiene la información del nodo padre. 
'''    Se pasa la estructuras porque necesito conocer 
'''    los ID de los nodos</param>
''' <param name="listaNodos">
'''    La lista genérica que contiene las estructuras 
'''    con la información de cada nodo</param>
Private Sub AuxLoadCargaNodosHijos( _
            ByRef nodoPadre As TreeNode, _
            ByRef estrucNodoPadre As EstructruraNodoSerialiar, _
            ByRef listaNodos As List(Of EstructruraNodoSerialiar))

    ' a partir de ese nodo buscar los hijos
    'Dim NodoSalida As TreeNode = Nothing
    For indexLista As Integer = estrucNodoPadre.IDPadre + 1 _
        To listaNodos.Count – 1

        Dim estrucNodoHijo As EstructruraNodoSerialiar = Nothing
        estrucNodoHijo = listaNodos.Item(indexLista)
        ' si no esta grabado Grabarlo como hijo de su padre
        If estrucNodoHijo.Grabado = False Then
            If estrucNodoHijo.IDPadre = estrucNodoPadre.ID Then

               Dim nuevoNodoHijo As New TreeNode(estrucNodoHijo.Text)
               nodoPadre.Nodes.Add(nuevoNodoHijo)
               ' control de depuracion
#If DEBUG Then
               Debug.Write("Alta Nodo " & estrucNodoHijo.ToString)
#End If

               ' marcarlo como grabado
               estrucNodoHijo.Grabado = True

               ' buscar sus hijos
               ' llamada recursiva a esta funcion
               AuxLoadCargaNodosHijos( _
                  nuevoNodoHijo, estrucNodoHijo, listaNodos)
            End If
        End If
    Next
End Sub


↑↑↑

Conclusión

El uso de llamadas recursivas a las funciones permite, con el uso de muy pocas líneas de código, crear los nodos del árbol, sin conocer la profundidad que pueda llegar a tener el contenido del árbol


↑↑↑

Código Fuente

Puedes descargarte un fichero ZIP que contiene el código descrito en este documento. Esta escrito en (.NET Framework 3.5) - Visual Basic .NET (versión 9.0) - 2010


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Documento Start]
[Imprimir el Documento]
© 1997 - - La Güeb de Joaquín
Joaquin Medina Serrano
Ésta página es española