Errores de navegación por un mal uso del objeto session

Un uso inadecuado del objeto Session puede llevarnos a situaciones no deseadas y que pueden producir errores cuando navegamos utilizando los botones hacia adelante y hacia atrás del navegador.
Antes de usar Session indiscriminadamente hay que entender lo qué se desea conseguir y no usar este objeto por pura inercia.

Analizaré este escenario con un ejemplo y simplificaré al máximo para destacar lo más importante.

Se trata de un sitio Web para consultar listas de contratación. Estas listas son gestionadas por dos ámbitos, el de Educación y el de Salud. En La primera pantalla existen dos enlaces, uno para cada ámbito, Salud = 1 y Educación = 2. Cuando pulsamos en un enlace nos redirecciona a la segunda página Web pasando por GET el identificador del ámbito. Así que tendremos una URL de este estilo …Pagina2.aspx?ambito=1.

screenshot.34

El segundo Web Form tiene un botón que al pulsarlo nos genera un listado PDF. Esta lista la gestiona únicamente el ámbito elegido.

screenshot.36

También tiene un enlace que recarga la misma página pero con los datos del otro ámbito.

screenshot.35

A continuación se muestra el código de la página Inicio.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Inicio.aspx.cs" Inherits="Inicio" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1 transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Inicio</title>
</head>
<body>
<form id="form1" runat="server">
   <ul>
      <li>
         <asp:HyperLink ID="hyperlink1" NavigateUrl="Pagina2.aspx?ambito=1" Text="Salud" runat="server" />
      </li>
      <li>
         <asp:HyperLink ID="hyperlink2" NavigateUrl="Pagina2.aspx?ambito=2" Text="Educación" runat="server" />
      </li>
   </ul>
</form>
</body>
</html>

y el código de la página Pagina2.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Pagina2.aspx.cs" Inherits="Pagina2" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Búsqueda por Puesto</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:HyperLink ID="hyperlinkMayorPeso" Text="Mayor peso" runat="server" />
    </div>
    <div>
        <asp:Label runat="server" ID="lblAmbitoActual"></asp:Label>
        <br />
        <asp:Button runat="server" ID="btnPdf" OnClick="BtnPdfClick" Text="Temporales" />
        <br />
        <br />
        <asp:Label runat="server" ID="lblResultado"></asp:Label>
    </div>
    </form>
</body>
</html>

Cuando accedemos desde la página de Inicio a la que contiene la lista ésta nos muestra datos sobre el acceso elegido, el nombre del ámbito, etc.
En este momento el servidor no debe saber nada del cliente ¿para que guardamos en servidor -en session- el IdAmbito? ¡guardémoslo en cliente! al igual que el cliente tiene un label con el texto Salud o Educación.
Cuando el cliente haga click, en ese instante y no antes, habrá que enviar al servidor los datos necesarios para que atienda nuestra solicitud.

La situación ideal es una “desconexión” total entre cliente y servidor, eso es el protocolo HTTP ¡un protocolo sin estado!  no hagamos trampas, aunque ASP.NET está lleno de ellas.

En el código se puede comprobar el funcionamiento erróneo si utilizamos Session, en cambio se comprueba el funcionamiento deseado si utilizamos el ViewState.

Primero haré la prueba guardando el identificador que viene por GET en Session

//En este caso no se debería usar session. Comentar este código.
private int Ambito
{
   get
   {
      if (Session["Ambito"] == null)
         return 0;
      return (int)Session["Ambito"];
   }
   set
   {
      Session["Ambito"] = value;
   }
}

En el evento Load se recoge el parámetro y se valida, además se realizan las acciones necesarias para completar el formulario.

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            //Cogemos el parámetro que viene por GET y lo validamos por si lo toca el usuario.
            //Si no está dentro de los valores permitidos redirigimos al inicio.
            int ambito = 0;
            int.TryParse(Request.QueryString["departamento"], out ambito);
            Ambito = ambito;
            if (Ambito != 1 && Ambito != 2)
                Response.Redirect("Pagina1.aspx");

            //Añado un enlace al otro ámbito. Para simular que voy a redirigir a las listas del otro ámbito.
            int otroAmbito = Ambito == 1 ? 2 : 1;
            PonerMayorPeso(otroAmbito.ToString());

            //Labels del formulario
            RellenarFormulario();

            //Simulo un botón de generación de PDF de una lista de temporales, por ejemplo.
            //Si entro a Salud la lista es la PkLista = 50 y si entro a Educación es la PkLista = 60.
            btnPdf.CommandName = "PkLista";
            btnPdf.CommandArgument = Ambito == 1 ? "50" : "60";
        }
    }

El método PonerMayorPeso configura el enlace para recargar la página con los datos del otro ámbito y el método RellenarFormulario añade un label con el nombre del ámbito en el que nos encontramos, más que nada para saber dónde estamos.

private void PonerMayorPeso(string ambito)
{
    hyperlinkMayorPeso.NavigateUrl = string.Format("Pagina2.aspx?ambito={0}", ambito);
    hyperlinkMayorPeso.Text += " en " + (ambito == "1" ? "Salud" : "Educación");
}

private void RellenarFormulario()
{
    lblAmbitoActual.Text = string.Format("Listas en {0}", Ambito == 1 ? "Salud" : "Educación");
}

El evento que se ejecuta al pulsar el botón Temporales recoge el identificador de la lista que queremos generar. En una situación más real hay varios botones cada uno para una lista.

protected void BtnPdfClick(object sender, EventArgs e)
    {
        var pkLista = 0;
        if (sender == null) return;
        var btn = (Button)sender;
        switch (btn.CommandName)
        {
            case "PkLista":
                pkLista = int.Parse(btn.CommandArgument);
                break;
        }

        GetLista(pkLista);
    }

Finalmente en este método se realizarían las acciones oportunas para generar el listado

private void GetLista(int pkLista)
{
    int ambito = Ambito;
    int lista = pkLista;
    //Ir a la BD...
    lblResultado.Text = string.Format("Voy a por la lista con PkLista = {0} de {1}", lista,
                                          ambito == 1 ? "Salud" : "Educación");
}

Probamos. Entro a Salud y obtengo esto:

screenshot.37

pulso en el enlace para ir a Educación y obtengo esto:

screenshot.39

Por ahora va todo bien, pero voy a ir para atrás desde el navegador, debería ir a Salud de nuevo:

screenshot.40

Sí estoy en Salud pero al pulsar para generar la lista me ha generado la de Educación!! ¿Qué ha pasado? Que hemos guardado en session información que pensábamos que nos iba a hacer falta cuando el usuario actuase sobre los botones pero esto no debería hacerse así ya que estamos en un ambiente distribuido y sin estado, el escenario ideal sería petición respuesta y nada más. El cliente pide con los datos necesarios para que el servidor sea capaz de alcanzar el objetivo y el servidor devuelve el resultado y finaliza la petición.
Si en vez de guardar el parámetro en Session lo guardamos en el cliente cada vez que éste haga una solicitud enviará el parámetro correcto. Para esto utilizaré el ViewState de la siguiente manera:

//En este caso no se debería usar session. Comentar este código.
//Si se debería usar ViewState. Descomentar este código.
    private int Ambito
    {
        get
        {
            if (ViewState["Ambito"] == null)
                return 0;
            return (int)ViewState["Ambito"];
        }
        set
        {
            ViewState["Ambito"] = value;
        }
    }

¿Ah entonces hay que usar el ViewState en vez de session? No, cada objeto en su momento, para eso existen los dos!!
Hay que usarlos con precaución, el ViewState carga el peso de la página y Session consume memoria en el servidor. Así que no habría que poner, por ejemplo documentos o listas con datos de la BD en el ViewState, que hay quien los pone…, ni todas o casi todas las variables de la aplicación en Session.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s