Accueil > Asp.Net MVC > Unifier les remontées d’exception Ajax et non Ajax en ASP.Net MVC 3

Unifier les remontées d’exception Ajax et non Ajax en ASP.Net MVC 3

Quoi de plus énervant qu’un site qui se fige? On ne sait pas si le traitement est en cours, si une erreur s’est produite et si oui laquelle… C’est pourtant ce qui arrive fréquemment lors d’appels Ajax déclenchant une exception côté serveur. La remontée de l’erreur ne se fait alors que dans une réponse qui n’est pas affichée par défaut et le site paraît simplement mort. Il existe des mécanismes nous permettant de traiter ces erreurs et nous allons voir comment faire en MVC 3 pour unifier ces remontées d’erreur entre requête Ajax et non Ajax

En MVC, la remontée d’erreur se fait généralement via le filtre HandleError. Malheureusement, ce filtre ne gère rien de particulier pour les requêtes Ajax et se contente de renvoyer dans la réponse le contenu html de la vue spécifiée comme vue d’erreur. Cette vue est donc invisible pour l’utilisateur lambda qui ne trace pas ses requêtes avec Firebug.
2 solutions s’offrent donc à nous: Former les utilisateurs à Firebug :p ou développer notre propre remontée d’erreur qui gère correctement les requêtes Ajax. La 2ème solution paraît plus sage tout le monde en conviendra!

Le principe est de trapper toutes les exceptions, d’en extraire les infos pour alimenter un objet HandleErrorInfo, et d’appeler un nouveau contrôleur en lui passant cet objet dans le ViewData. Pour ce projet de test, nous allons afficher les exceptions dans une popup mais cette partie est totalement libre il vous suffit de changer les vues correspondantes pour avoir l’affichage souhaité.

Commençons par notre contrôleur:
Il contiendra 2 actions: une pour les appels Ajax et une autre pour les appels standards.

public class ErrorController : Controller
{
    //
    // GET: /Error/

    public ActionResult Index()
    {
        return View("Index",ViewData.Model);
    }

    public ActionResult IndexAjax()
    {
        return PartialView("Detail", ViewData.Model);
    }

}

Ces actions se contentent d’appeler une vue en lui passant leur modèle. Pour l’action ajax, une page existe déjà donc on se contente de remplir une vue partielle de détail de l’exception. Pour l’appel standard, on affiche une page d’erreur qui contiendra la même vue « Detail » que pour les appels Ajax.

Les vues sont les suivantes:

Detail

@model HandleErrorInfo
<div>
    Controller: @Model.ControllerName</div>
<div>
    Action: @Model.ActionName</div>
<div>
    Message: @Model.Exception.Message</div>

Index

@model HandleErrorInfo
@{
    ViewBag.Title = "ERROR";
}

<h2>ERROR</h2>

<div id="error" title="ERROR">
    @Html.Partial("Detail",Model)
</div>

<script>
    $(function () {
        $("#error").dialog();
    });
</script>

<a href="javascript:history.back()">Go back</a>

Mais comment le ViewData du contrôleur est-il alimenté?
Pour arriver à ça, nous allons créer un nouveau filtre qui implémente IExceptionFilter. Cette interface ne contient qu’une seule méthode OnException avec un argument ExceptionContext qui est appelée à chaque fois qu’une exception est lancée. Dans notre cas, on devra dans cette méthode effectuer plusieurs tâches:

  • Vérifier que les remontées custom sont activées dans le web.config (customErrors). L’objet HttpContext nous facilite cette tâche en exposant une propriété IsCustomErrorEnabled
  • Vérifier que l’exception n’a pas déjà été traitée par un autre filtre via la propriété ExceptionHandled de l’objet ExceptionContext et si non la marquer comme traitée par notre propre filtre
  • Mettre le StatusCode de la réponse à 500
  • Instancier et renseigner un objet HandleErrorInfo en fonction du contexte d’exception
  • Instancier notre contrôlleur
  • Invoquer l’action correcte en fonction de contexte Ajax ou non
    • Rien de bien compliqué dans les faits le code donne à peu près ça:

      public void OnException(ExceptionContext filterContext)
      {
          //run any code you need here: logging, alerts...
      
          if (filterContext.HttpContext.IsCustomErrorEnabled && !filterContext.ExceptionHandled)
          {
              filterContext.ExceptionHandled = true;
              filterContext.HttpContext.Response.StatusCode = 500;
              string controllerName = (string)filterContext.RouteData.Values["controller"];
              string actionName = (string)filterContext.RouteData.Values["action"];
              HandleErrorInfo info = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
      
              IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
              ErrorController newController = factory.CreateController(filterContext.RequestContext, "Error") as ErrorController;
              filterContext.RouteData.Values["controller"] = "Error";
              filterContext.Controller = newController;
              filterContext.Controller.ViewData = new ViewDataDictionary(info);
      
              string actionToCall = "Index";
              if (filterContext.HttpContext.Request.IsAjaxRequest())
              {
                  actionToCall = "IndexAjax";
              }
      
              filterContext.RouteData.Values["action"] = actionToCall;
              newController.ActionInvoker.InvokeAction(filterContext, actionToCall);
          }
      }
      

      On remarque tout de même que l’instanciation du contrôleur se fait via ControllerBuilder et ControllerFactory pour ne pas casser une éventuelle injection de dépendances mise en place dans votre application.

      A ce stade, il faut enregistrer le filtre dans la collection de GlobalFilters de l’application pour qu’il soit actif. Cela se fait dans le Global.asax:

      public static void RegisterGlobalFilters(GlobalFilterCollection filters)
      {
          //filters.Add(new HandleErrorAttribute());
          filters.Add(new GlobalExceptionFilter());
      }
      

      La ligne ajoutant le HandleErrorAttribute de base est commentée nous n’en avons plus besoin ce travail sera fait par notre propre filtre.

      A ce stade, la remontée d’exception non Ajax fonctionne mais pour les requêtes Ajax, rien n’est encore traité côté client pour afficher la popup en cas d’erreur. Nous allons nous aider pour cela de jQuery et de la méthode ajaxError. On ajoute dans notre vue _Layout ces quelques lignes:

      <div id="error" title="ERROR"></div>
      <script language="javascript">
          $(document).ajaxError(function (e, jqxhr, settings, exception) {
              if (jqxhr != null) {
                  $("#error").html(jqxhr.responseText);
                  $("#error").dialog();
              }
          });
      </script>
      

      A chaque fois qu’une requête Ajax reviendra avec un StatusCode à 500, cette fonction sera appelée et la paramètre jqxhr rempli avec le contenu de la réponse. Les autres paramètres contiennent d’autres infos sur l’erreur et la réponse courante. Dans notre cas, il nous suffit de remplir notre div d’erreur avec le contenu de la réponse et d’ouvrir la popup.

      Attention, si une exception est lancée dans le mécanisme d’affichage d’erreur, le filtre se réactivera et vous créerez une jolie boucle infinie…

      Vous pouvez voir le code en action ICI ou le télécharger ICI

      Enjoy…

Étiquettes : ,
  1. Aucun commentaire pour l’instant.
  1. No trackbacks yet.

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

%d blogueurs aiment cette page :