Twitter Facebook Google + RSS Feed

Mantener el estado de un TreeView entre distintas páginas

3
.NET

miércoles, 10 de septiembre de 2008 a las 23:40hs por Gustavo Cantero (The Wolf)

Hace unos meses nos encargaron el desarrollo de una aplicación web, desde la cual se podría buscar Podcasts, ver una vista preliminar de éstos, bajarlos en distintos formatos para dispositivos portátiles, suscribirse a series, recomendarlos, buscar por autor, etc.  Estos Podcasts se guardan en “canales”, los cuales se muestran en un árbol (TreeView) en la parte izquierda de la página, la que en realidad está en el MasterPage del sitio.  La aplicación funcionaba perfecto, pero nos topamos con un inconveniente: cuando el usuario abría el árbol y seleccionaba un canal la página hacía un Postback y, como es de suponerse, el árbol mantenía su estado actual; pero si el usuario en lugar de pulsar un botón o generar de cualquier otra manera un evento que realizara un Postback pulsaba en un link hacia otra página, el estado de la página anterior y de todos sus controles se perdía incluyendo el del árbol, quedando todos sus nodos cerrados.
Para solucionar esto nos dimos cuenta que debíamos persistir el estado del árbol desde el cliente en cookies, pero aún no sabíamos que guardar!  Comenzamos revisando el código HTML generado por el TreeView para intentar obtener de alguna manera el estado de qué nodos estaban abiertos y cuales cerrados, y mientras revisábamos los tags de la página encontramos tres campos ocultos cuyo nombre tenían como prefijo el nombre del árbol:

<input type="hidden" name="ctl00_trvChannels_ExpandState" id="ctl00_ctlChannels_trvChannels_ExpandState" value="unnnunnuununnnnnnnnuuuunnunnnnnnnn" />
<input type="hidden" name="ctl00_trvChannels_SelectedNode" id="ctl00_ctlChannels_trvChannels_SelectedNode" value="" />
<input type="hidden" name="ctl00_trvChannels_PopulateLog" id="ctl00_ctlChannels_trvChannels_PopulateLog" value="" />

Al momento nos dimos cuenta que el árbol guardaba su estado en estos campos, y comprobamos que no lo hacía para comparar el actual con el de la invocación anterior, sino que lo hacía del lado del cliente para mantener su último estado!  Lo siguiente a hacer era claro: entender cómo guardaba su estado en estos campos.  En nuestro caso sólo nos interesaba saber qué nodos estaban expandidos, por lo cual nos enfocamos en el campo “ctl00_trvChannels_ExpandState”, y luego de “jugar” un rato con los estados llegamos a la conclusión de que cada carácter representa un nodo, ordenado desde el primero del árbol siguiendo por sus hijos (siempre de arriba hacia abajo) antes de seguir con el nodo “vecino”.  A continuación muestro el orden con un ejemplo gráfico:

Estos caracteres representan tres posibles estados de un nodo:

  1. n: el nodo no posee hijos, por lo tanto, no puede expandirse (non expandable)
  2. c: el nodo posee hijos pero está cerrado (collapsed)
  3. e: el nodo posee hijos y están visibles, o sea, el nodo está expandido (expanded)

Ahora nos quedaba hacer alguna rutina en JavaScript que guardara este campo en una cookie antes de que el usuario dejara la página actual, para luego leerla en el servidor, lo cual resolvimos con la función que detallo a continuación, a la que llamamos en el evento onbeforeunload del objeto BODY:

function saveTreeViewState(){
    document.cookie = "TreeViewState=" + escape(document.getElementById(idTreeViewState).value) + "; path=/;";
}

La variable idTreeViewState aquí utilizada la definimos en el evento Load de la página con el siguiente código:

Page.ClientScript.RegisterClientScriptBlock(
    this.GetType(),
    "TreeViewState",
    "var idTreeViewState='" + trvChannels.ClientID + "_ExpandState';",
    true);

Lo próximo a hacer fue modificar la rutina de carga del árbol del lado del servidor para que establezca el estado de los nodos en base al contenido de esta cookie.  Esta tarea fue relativamente sencilla, solamente creamos dos variables de clase; la primera llamada strState del tipo string para guardar el contenido de la cookie, y la segunda, llamada intStatePosition y del tipo entero, para utilizarla de contador.  Luego, si la página no estaba recibiendo un Postback, por cada nodo cargado en el árbol (en el orden descripto en la imagen anterior) hacemos una llamada a la siguiente función:

private void LoadNodeState(TreeNode Node) {
    if (strState != null && strState.Length > ++intStatePosition)
        Node.Expanded = strState[intStatePosition] == 'e';
}

Cabe mencionar que la variable intStatePosition la inicializamos en -1.
El código completo de esta página lo detallo a continuación.  Hay que tener en cuenta que en este ejemplo el árbol está en un MasterPage, pero no habría ningún problema si estuviera en la página.

public partial class Site1 : System.Web.UI.MasterPage {
    private string strState = null;
    private int intStatePosition = -1;

    protected void Page_Load(object sender, EventArgs e) {
        if (!IsPostBack) {
            //Estado del árbol almacenado en cookies
            if (Request.Cookies.Get("TreeViewState") != null)
                strState = Request.Cookies.Get("TreeViewState").Value;

            //Nodo raiz del árbol
            TreeNode objRootNode = new TreeNode("Nodo raíz");
            trvChannels.Nodes.Add(objRootNode);
            LoadNodeState(objRootNode);
            SubNodes(objRootNode);
        }

        //Guardo en una variable de JavaScript el ID de cliente del campo oculto
        //donde .NET guarda el estado del mismo
        Page.ClientScript.RegisterClientScriptBlock(
            typeof(Site1),
            "TreeViewState",
            "var idTreeViewState='" + trvChannels.ClientID +             "_ExpandState';",
            true);
    }

    /// <summary>
    /// Crea los subnodos de un nodo dado
    /// </summary>
    /// <param name="ParentNode">Nodo padre</param>
    private void SubNodes(TreeNode ParentNode) {
        for (int intPos = 1; intPos < 6; intPos++) {
            TreeNode objNewNode = new TreeNode();
            ParentNode.ChildNodes.Add(objNewNode);
            objNewNode.Text = "Nodo " + intPos.ToString();
            LoadNodeState(objNewNode);
            if (objNewNode.Depth < 4)
                SubNodes(objNewNode);
        }

    }

    /// <summary>
    /// Establece el estado de "expandido" o no al nodo
    /// dependiendo del valor de la cookie
    /// </summary>
    /// <param name="Node">Nodo</param>
    private void LoadNodeState(TreeNode Node) {
        if (strState != null && strState.Length > ++intStatePosition)
            Node.Expanded = strState[intStatePosition] == 'e';
    }

    protected void trvChannels_SelectedNodeChanged(object sender, EventArgs e) {
        lblSelectedNode.Text = "Nodo seleccionado: " + trvChannels.SelectedNode.ValuePath;
    }
}

A continuación les dejo un proyecto realizado con Visual Studio® 2008 con el ejemplo para que prueben el código.

Espero que esta idea les sea de utilidad, y los instamos a que nos dejen sus comentarios.


3 comentarios »

  1. lewis dice:

    Amigo muy buena tu explicacion todo paso a paso pero hay un pequeño detalle que no cuandra en mi aplicacion utilizando tu codigo yo ya tengo el menu pre armado no lo genero en el page load del master page si no ya tiene sus valores definidos y cuando uso tu codigo solo me funciona en el nodo 0 pero si doy click a otro nodo nada no funciona trate de modificar tu codigo pero me deja expandidos otros un favor ojal pudieras ayudarme.
    Atte Lewis

  2. Ciro dice:

    oye perdona las molestias pero tu codigo no me funciona en visual 2012, el problema que tengo es el mismo que tus mensionas, radica en el master page, si pudieras ayudarme te lo agradeceria mucho.
    el error me lo marca en el Site1.master en las linea:

    ciertamente son un poco nuevo en .net, asi que si me pudieras hechar la mano te lo agadeseria mucho

Deja un comentario

Buscar