Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Le pattern PRG

Poster un commentaire

Supposons que l’on écrive le code suivant:


@model WebApplication12.Models.Location
<form method="post">
    @Html.LabelFor(m=>m.Pays)
    @Html.TextBoxFor(m=>m.Pays,new {placeholder="Entrer un pays"})
    <br/>
    @Html.LabelFor(m => m.Ville)
    @Html.TextBoxFor(m => m.Ville, new { placeholder = "Entrer une ville" })

    <br/>
    <button type="submit">Sauver</button>
</form>

Côté contrôleur:


public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Index(Location location)
    {
        return View();
    }
}

Vous reconstituerez facilement le modèle.

  •  Lancez l’application
  • Faites une saisie
  • Soumettez le formulaire
  • Faites F5 pour le reposter

Un formulaire similaire à celui-ci devrait s’afficher:

2014-07-07_09-36-37

Outre que ce message est incompréhensible pour un utilisateur lambda il pose plusieurs problèmes:

  • On s’attend à rafraîchir la page, en réalité on répète l’action du POST ce qui n’est pas tout à fait la même chose. Des effets indésirables peuvent en résulter
  • Une réponse POST ne peut pas facilement être mise dans les favoris. C’est aussi potentiellement une source de problèmes.
  • Si vous faites un back dans le navigateur, au lieu de réafficher la page, vous allez en plus réitérer l’action du POST.
    Là aussi des effets indésirables peuvent en résulter

 

C’est pour essayer de contrôler ce problème que certains développeurs utilisent le modèle PRG (POST/Redirect/GET).

Note: l’expression employée dans la littérature est PRG pattern.
Je ne suis pas convaincu de voir là un pattern justement. D’abord parce qu’il est loin de résoudre entièrement le problème et de plus il présente des effets de bords pas forcément souhaitables. Il me semble préférable de dire qu’il s’agit d’un choix d’implémentation à discuter avec votre architecte ou votre lead technique.

Concrètement, nous devons modifier le code de la façon suivante (et au passage industrialisons le un peu mieux!):

[HttpPost]
public ActionResult Index(Location location)
{
    if (ModelState.IsValid)
    {
        // sauvegarde du modèle par le repository
        // ...
        //

        return RedirectToAction("Index");
    }

    return View();
}

 

Note: Il y a quelques adaptations à prévoir selon la nature de votre besoin. Par exemple vous pourriez rediriger vers une autre vue que Index. Ce qui est important c’est le principe. Le sens, pas le verbe!

 

Discutons un peu la ligne suivante qui peut surprendre:


return View();

 

Pourquoi pas un redirect afin d’éviter le problème du repost?

PRG n’impose en fait pas de redirect dans ce cas de figure. L’objectif de PRG n’est pas de gérer l’affichage de la popup du navigateur, mais de faire en sorte que le rafraîchissement de la page soit sur.

Dans notre cas effectivement nous verrons la popup s’afficher dans le navigateur, mais côté code on ne fera que réenclencher l’erreur de validation et recharger la page. Aucune sauvegarde ou autre traitement n’aura été effectué puisque l’on entre pas dans le if.
Après à vous de voir si vous souhaitez ou non que cette popup apparaisse dans certains cas. C’est une affaire de goûts et de couleurs.

 

Le code lance une redirection 302. C’est fonctionnement par défaut sur une redirection lorsque l’on ne sait pas quoi mettre d’autre.
Dans ce cas précis le choix le plus approprié est la redirection 303. Seulement ce n’est pas possible nativement dans MVC.

https://connect.microsoft.com/VisualStudio/feedback/details/706961/asp-net-mvc-controller-redirecttoaction-method-should-return-http-303-response

C’est la raison pour laquelle la plupart du temps on laisse une redirection 302.

 

On pourrait aussi créer un ActionResult personnalisé:

public class PrgActionResult : RedirectToRouteResult
{
    public PrgActionResult(RouteValueDictionary routeValues)
    : base(routeValues)
    {
    }

    public PrgActionResult(string routeName, RouteValueDictionary routeValues)
    : base(routeName, routeValues)
    {
    }

    public PrgActionResult(string routeName, RouteValueDictionary routeValues, bool permanent)
    : base(routeName, routeValues, permanent)
    {
    }

    public override void ExecuteResult(ControllerContext context)
    {
        base.ExecuteResult(context);

        context.HttpContext.Response.Status = "303 See Other";
        context.HttpContext.Response.StatusCode = 303;
    }
}

 

Puis on fait un appel du genre:


return new PrgActionResult("Default", null);

 

Curieusement ce point est très peu abordé dans la communauté.

Quelques difficultés

Il reste tout de même quelques points à développer.

  • Les exemples précédents sont donnés en MVC. Mais peut être travaillez vous avec des WebForms? Le modèle d’interaction des WebForms s’appuie intrinsèquement sur des POST. Un postback, c’est un POST.

Rien ne vous empêche bien sûr de faire une redirection sur le gestionnaire du Button.Click pour revenir à PRG, mais vous déviez de la philosophie de WebForm et en particulier le ViewState qui sera perdu. C’est ou non un problème, tout dépend comment le code a été fait.

Personnellement je prêche fortement pour l’abandon du modèle WebForm dans tous les nouveaux projets.

 

  • PRG multiplie les aller-retour entre client et serveur. Cela peut ou non être un problème. A vous de l’évaluer

 

Pour finir ayez également en tête les scénarios que PRG ne résout pas:

  1. l’utilisateur clique frénétiquement sur le bouton de soumission du formulaire
  2. l’utilisateur rafraîchit la page avant que la soumission initiale soit achevée

 

Bibliographie

  1. http://www.kirit.com/Response.Redirect%20and%20encoded%20URIs
  2. http://blog.andreloker.de/post/2008/06/Post-Redirect-Get.aspx

 

 

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s