Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
lunes, 2 de noviembre de 2009

Hoy vamos a dedicar un rato a comentar una técnica que es considerada una buena práctica en el desarrollo de aplicaciones web: el patrón PRG o Post-Redirect-Get. Seguramente alguna vez lo haya citado por aquí, pero nunca lo había explicado en profundidad.

Por último, antes de entrar en materia, es conveniente indicar que lo que vamos a ver es válido para ASP.NET Webforms, ASP.NET MVC y, en general, para cualquier tecnología de construcción de sitios web que incluya componentes en servidor, puesto que se trata de una forma de hacer las cosas, no de la implementación de una solución. Ya en el último epígrafe veremos implementaciones concretas para Webforms y el framework MVC.

El problema

Es bastante habitual que al desarrollar aplicaciones para la web creemos un formulario de introducción de datos, y lo normal es que estos envíen los datos al servidor utilizando el verbo HTTP Post. Hasta aquí, bien.

Cuando desde el servidor se recibe una petición de este tipo normalmente se ejecuta un código, por ejemplo para almacenar la información en la base de datos y, de forma bastante frecuente, aprovechamos para enviar al cliente feedback de que su operación ha sido realizada con éxito. Y aquí es donde aparece el problema.

image Si el usuario, por esas ocurrencias que suele tener ;-), decide pulsar F5 o actualizar la página en el navegador, se van a producir dos efectos desagradables:

  • primero, se le mostrará al usuario un cuadro de diálogo informándolo de algo que difícilmente va a entender y que, en cualquier caso, le asustará bastante. El navegador le informa de que está realizando un reenvío completo de los datos del formulario.
  • segundo, una vez superado el escollo anterior, se volvería a ejecutar en servidor toda la lógica asociada a la recepción de datos del formulario con consecuencias, a veces terribles, como el almacenamiento de registros duplicados en la base de datos.

¿Cómo podemos evitar esto?

La solución: Post-Redirect-Get

El patrón PRG viene a indicarnos una forma de diseñar nuestras aplicaciones pensando en evitar los problemas descritos anteriormente, y, como veremos, es bastante simple. El procedimiento general a seguir sería:

  1. Recibimos la petición Post con los datos que ha introducido el usuario en un formulario.
  2. Ejecutamos la lógica asociada a la recepción de dicho formulario, por ejemplo, grabar en la base de datos.
  3. Enviamos al cliente una respuesta con código HTTP 30x (Redirect), indicando al agente de usuario que debe solicitar otra página, en la mostraremos un mensaje informando de que el proceso se ha realizado con éxito.
  4. El navegador obtiene dicha página mediante una petición de tipo Get.

En este punto, si el usuario decide (o el diablo que lleva dentro le ordena ;-)) refrescar la página, lo único que conseguirá será que su navegador vuelva a solicitar la página en la que le estamos informando de que el proceso ha sido satisfactorio. No se le muestra ningún cuadro de diálogo amenazante, ni se ejecuta la lógica de nuevo, ni hay ningún tipo de daños colaterales.

El siguiente diagrama, basado en el de la imprescindible Wikipedia, muestra gráficamente este proceso:

Secuencia en el patrón PRG

¿Un poco de código? Sí, por favor

ASP.NET WebForms

La abstracción sobre los protocolos montada por la tecnología Webforms hace más difícil reconocer los conceptos que estamos tratando, pero aún así es bastante sencillo aplicar el patrón.

En los formularios web, los postbacks se realizan a través de métodos Http POST, por lo que la implementación de la lógica y la redirección podremos realizarlas en la implementación del evento de servidor correspondiente, por ejemplo, el de pulsación de un botón:

protected void btnAceptar_Click(object sender, EventArgs e)
{
    if (Page.IsValid)
    {
        // Lógica 
        Cliente cliente = new Cliente(txtNombre.Text, txtApellidos.Text);
        GestorDeClientes.Salvar(cliente);
    
        // Redirección
        Response.Redirect("clientes.aspx", true);
    }
}

La página “clientes.aspx” podría ser ser el listado de clientes registrados en el sistema, por ejemplo.

ASP.NET MVC

En el framework MVC, por su cercanía a los protocolos, sí es fácil identificar los conceptos de petición y redirecciones. El siguiente código sería equivalente al anteriormente mostrado, pero enviando al usuario a la acción “Index”:

[HandleError]
public class ClientesController : Controller
{
    ... // Otras acciones
 
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Editar(Cliente cliente)
    {
        if (ModelState.IsValid)
        {
            GestorDeClientes.Grabar(cliente);
            return RedirectToAction("Index");
        }
        return View(cliente);
    }
}

Publicado en: Variable not found

8 Comentarios:

Gabriel Porras dijo...

Que idea tan sencilla y tan buena!
Gracias!

josé M. Aguilar dijo...

Sí, la verdad es que el concepto es simplísimo y muy fácil de implementar en cualquier plataforma. :-)

Gracias por comentar.

Elkin C dijo...

Bueno, y si el mensaje que quiero dar es en jquery o algo así... como se haría??? y en el caso de que necesite mantenerme en la misma pagina, que podríamos utilizar?

Gracias por enseñar estos conceptos.

josé M. Aguilar dijo...

Hola, Elkin.

Este patrón tiene sentido con envíos de página completa, que es cuando puede aparecer el problema al que se hace referencia en el artículo.

Al enviar datos con jQuery controlas todo el proceso mediante script, por lo que no es necesario redirigir el usuario a ninguna otra página, simplemente procesar el resultado obtenido con la llamada Ajax.

Un saludo & gracias por comentar.

Unknown dijo...

3. Enviamos al cliente una respuesta con código HTTP 30x (Redirect), indicando al agente de usuario que debe solicitar otra página, en la mostraremos un mensaje informando de que el proceso se ha realizado con éxito.

Ok entiendo hasta esta parte,¿ pero que pasa si el proceso no se realiza con exito?, yo lo que hago es un return view?
Ejemplo
[HttpGet]
public ActionResult altaPlantel()
{

return View(p);

}

[HttpPost]
public ActionResult altaPlantel(Model)
{
Logica......
if(logica==exito)
return RedirectToAction("altaPlantel");
else
return view(model)

}

En este proceso si la lgica dio error y actualizo la pagina, me muestra el mensaje que es necesario actualizar la pagina.
En realidad no entiendo lo que estoy haciendo mal,¿Alguna idea?

josé M. Aguilar dijo...

Hola, José Luis!

Lo estás implementando correctamente. En efecto, salvo que quieras complicarte bastante la vida, en ese caso verás aparecer el mensaje si actualizas la página.

La única forma de evitarlo sería crear una redirección hacia el mismo formulario cuando la validación falla, pero suministrándole los valores actuales de sus campos para que el usuario pueda corregir sin tener que volver a introducirlo todo. Normalmente no se justifica el hacerlo así.

El patrón PRG intenta evitar sobre todo los terribles efectos de una posible duplicidad en las acciones que se llevarían a cabo si los datos del formulario son correctos.

En el caso de que los datos sean incorrectos, el reenvío sólo provocaría que volviera a mostrarse el error de validación una y otra vez, lo cual no suele tener daños colaterales.

Saludos!


Unknown dijo...

a ok, estaba entendiendo mal el patrón, pensaba que la idea era evitar ese mensaje,en todo caso como dices, complicándome la vida lo solucione de la siguiente forma

[HttpGet]
public ActionResult altaPlantel(Model)
{
if(TempData["accion"]!=null)
{
ViewBag.accion = Convert.ToInt32(TempData["accion"]);
}
return View(Model);

}

[HttpPost]
public ActionResult guardaPlantel(Model)
{
Logica......
if(logica==exito)
return RedirectToAction("altaPlantel");
else
{
TempData["accion"] = "6";
return redirect("altaPlantel",model)
}
}
De esta manera en evito el mensaje de confirmación de envió de formulario,como estoy pasando el modelo sin ser afectado lo puedo pintar de nuevo, y el tempdata me dice que tipo de error fue.
No se cual sea tu opinión, ¿es una forma correcta?

josé M. Aguilar dijo...

Hola!

Efectivamente, la "complicación" me refería a ese tipo de soluciones. Si resuelve tu problema, es correcta ;)

Obviamente, sería bastante más costosa si tu formulario tuviera por ejemplo 20 campos de datos: tendrías que introducir en el TempData todos ellos para hacerlos persistir entre llamadas.

Como te comentaba, rara vez vale la pena ese esfuerzo.

Saludos.