Skip to content

7. Ajaxification d'une application ASP.NET MVC

7.1. La place d'AJAX dans une application web

Pour l'instant, les exemples d'apprentissage étudiés ont l'architecture suivante :

Pour passer d'une vue [Vue1] à une vue [Vue2], le navigateur :

  • émet une requête vers l'application web ;
  • reçoit la vue [Vue2] et l'affiche à la place de la vue [Vue1].

C'est le schéma classique :

  • demande du navigateur ;
  • élaboration d'une vue en réponse au client par le serveur web ;
  • affichage de cette nouvelle vue par le navigateur.

Il existe un autre mode d'interaction entre le navigateur et le serveur web : AJAX (Asynchronous Javascript And Xml). Il s'agit en fait d'interactions entre la vue affichée par le navigateur et le serveur web. Le navigateur continue à faire ce qu'il sait faire, afficher une vue HTML mais il est désormais manipulé par du Javascript embarqué dans la vue HTML affichée. Le schéma est le suivant :

  • en [1], un événement se produit dans la page affichée dans le navigateur (clic sur un bouton, changement d'un texte, ...). Cet événement est intercepté par du Javascript (JS) embarqué dans la page ;
  • en [2], le code Javascript fait une requête HTTP comme l'aurait fait le navigateur. La requête est asynchrone : l'utilisateur peut continuer à interagir avec la page sans être bloqué par l'attente de la réponse à la requête HTTP. La requête suit le processus classique de traitement. Rien (ou peu) ne la distingue d'une requête classique ;
  • en [3], une réponse est envoyée au client JS. Plutôt qu'une vue HTML complète, c'est plutôt une vue HTML partielle, un flux XML ou JSON (JavaScript Object Notation) qui est envoyé ;
  • en [4], le Javascript récupère cette réponse et l'utilise pour mettre à jour une région de la page HTML affichée.

Pour l'utilisateur, il y a changement de vue car ce qu'il voit a changé. Il n'y a cependant pas rechargement total d'une page mais simplement modification partielle de la page affichée. Cela contribue à donner de la fluidité et de l'interactivité à la page : parce qu'il n'y a pas de rechargement total de la page, on peut se permettre de gérer des événements qu'auparavant on ne gérait pas. Par exemple, proposer à l'utilisateur une liste d'options au fur et à mesure qu'il saisit des caractères dans une boîte de saisie. A chaque nouveau caractère tapé, une requête AJAX est faite vers le serveur qui renvoie alors d'autres propositions. Sans Ajax, ce genre d'aide à la saisie était auparavant impossible. On ne pouvait pas recharger une nouvelle page à chaque caractère tapé.

7.2. Rudiments de JQuery et de Javascript

Nous avons souvent associé la bibliothèque Javascript JQuery à nos pages. On y trouve ainsi la ligne :


  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>

Note : adaptez la version de jQuery à celle de votre version de Visual Studio.

La technologie Ajax d'ASP.NET MVC utilise JQuery. Nous allons nous-mêmes écrire quelques scripts JQuery. Aussi présentons-nous maintenant les rudiments de JQuery à connaître pour comprendre les scripts de ce chapitre.

Nous créons un nouveau projet [Exemple-04] à l'intérieur de notre solution [Exemples] :

Pour utiliser Ajax avec ASP.NET MVC, une ligne doit être présente dans le fichier de configuration [Web.config] [1] :


  <appSettings>
...
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>

La ligne 3 autorise l'utilisation d'Ajax dans les vues ASP.NET. Elle est présente par défaut.

Nous créons un fichier HTML [JQuery-01.html] dans le dossier [Content] du nouveau projet [2] :

Ce fichier aura le contenu suivant :


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/Scripts/jquery-1.8.2.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>
  • ligne 6 : importation de JQuery (adaptez la version à celle de votre version de Visual Studio) ;
  • lignes 10-12 : un élément de la page d'id [element1]. Nous allons jouer avec cet élément.

On visualise ce fichier dans le navigateur Google Chrome [4] et [5] :

Avec Google Chrome faire [Ctrl-Maj-I] pour faire apparaître les outils de développement [6]. L'onglet [Console] [7] permet d'exécuter du code Javascript. Nous donnons dans ce qui suit des commandes Javascript à taper et nous en donnons une explication.

JS

résultat

$("#element1")

: rend la collection de tous les éléments d'id [element1], donc normalement une collection de 0 ou 1 élément parce qu'on ne peut avoir deux id identiques dans une page HTML.

$("#element1").text("blabla")

: affecte le texte [blabla] à tous les éléments de la collection. Ceci a pour effet de changer le contenu affiché par la page

$("#element1").hide()

cache les éléments de la collection. Le texte [blabla] n'est plus affiché.

$("#element1")

: affiche de nouveau la collection. Cela nous permet de voir que l'élément d'id [element1] a l'attribut CSS style='display : none;' qui fait que l'élément est caché.

$("#element1").show()

: affiche les éléments de la collection. Le texte [blabla] apparaît de nouveau. C'est l'attribut CSS style='display : block;' qui assure cet affichage.

$("#element1").attr('style','color: red')

: fixe un attribut à tous les éléments de la collection. L'attribut est ici [style] et sa valeur [color: red]. Le texte [blabla] passe en rouge.

Tableau
Dictionnaire

On notera que l'URL du navigateur n'a pas changé pendant toutes ces manipulations. Il n'y a pas eu d'échanges avec le serveur web. Tout se passe à l'intérieur du navigateur. Maintenant, visualisons le code source de la page :

 

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/Scripts/jquery-1.8.2.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>

C'est le texte initial. Il ne reflète en rien les manipulations que l'on a faites sur l'élément des lignes 10-12. Il est important de s'en souvenir lorsqu'on fait du débogage Javascript. Il est alors souvent inutile de visualiser le code source de la page affichée. Pour connaître le code source de la page actuellement affichée, on procèdera de la façon suivante :

 

Nous en savons assez pour comprendre les scripts JS qui vont suivre.

7.3. Mise à jour d'une page avec un flux HTML

7.3.1. Les vues

On se propose d'étudier l'application suivante :

  • en [1], l'heure de chargement de la page ;
  • en [2], on fait les quatre opérations arithmétiques sur deux nombres réels A et B ;
  • en [3], la réponse du serveur vient s'inscrire dans une région de la page ;
  • en [4], l'heure du calcul. Celle-ci est différente de l'heure de chargement de la page [5]. Cette dernière est égale à [1] montrant que la région [6] n'a pas été rechargée. Par ailleurs l'URL [7] de la page n'a pas changé.

7.3.2. Le contrôleur, les actions, le modèle, la vue

Nous créons un contrôleur nommé [Premier] :

Pour afficher la vue initiale, nous créons l'action [Action01Get] suivante :


    [HttpGet]
    public ViewResult Action01Get()
    {
      ViewModel01 modèle = new ViewModel01();
      modèle.HeureChargement = DateTime.Now.ToString("hh:mm:ss");
      return View(modèle);
}
  • ligne 4 : instanciation du modèle de la vue ;
  • ligne 5 : initialisation de l'heure de chargement de la vue ;
  • ligne 6 : affichage de la vue [Action10Get.cshtml] et de son modèle.

Le modèle [ViewModel01] est le suivant :


using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace Exemple_04.Models
{
  [Bind(Exclude = "AplusB, AmoinsB, AmultipliéparB, AdiviséparB, Erreur, HeureChargement, HeureCalcul")]
  public class ViewModel01
  {
    // formulaire
    [Required(ErrorMessage="Donnée requise")]
    [Display(Name="Valeur de A")]
    [Range(0, Double.MaxValue, ErrorMessage = "Tapez un nombre positif ou nul")]
    public double A { get; set; }
    [Required(ErrorMessage = "Donnée requise")]
    [Display(Name = "Valeur de B")]
    [Range(0, Double.MaxValue, ErrorMessage="Tapez un nombre positif ou nul")]
    public double B { get; set; }

    // résultats
    public string AplusB { get; set; }
    public string AmoinsB { get; set; }
    public string AmultipliéparB { get; set; }
    public string AdiviséparB { get; set; }
    public string Erreur { get; set; }
    public string HeureChargement { get; set; }
    public string HeureCalcul { get; set; }
  }
}
  • lignes 11-14 : la valeur A du formulaire ;
  • lignes 15-18 : la valeur B du formulaire ;
  • lignes 21-24 : les résultats des quatre opérations arithmétiques sur A et B ;
  • ligne 25 : le texte d'une éventuelle erreur ;
  • ligne 26 : l'heure de chargement de la vue dans le navigateur ;
  • ligne 27 : l'heure de calcul des champs des lignes 21-24 ;
  • ligne 7 : ce modèle de vue est également un modèle d'action. On exclut de ce dernier les champs qui ne sont pas postés par le navigateur.

La vue [Action01Get.cshtml] est la suivante :


@model Exemple_04.Models.ViewModel01
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    UpdateTargetId = "résultats",
    HttpMethod = "post",
    Url = Url.Action("Action01Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

<!DOCTYPE html>

<html lang="fr-FR">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-01</title>
  <link rel="stylesheet" href="~/Content/Site.css" />
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.en-US.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
  <script type="text/javascript" src="~/Scripts/myScripts-01.js"></script>
</head>
<body>

  <h2>Ajax - 01</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
    <table>
      <thead>
        <tr>
          <th>@Html.LabelFor(m => m.A)</th>
          <th>@Html.LabelFor(m => m.B)</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>@Html.TextBoxFor(m => m.A)</td>
          <td>@Html.TextBoxFor(m => m.B)</td>
        </tr>
        <tr>
          <td>@Html.ValidationMessageFor(m => m.A)</td>
          <td>@Html.ValidationMessageFor(m => m.B)</td>
        </tr>
      </tbody>
    </table>
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
  <hr />
  <div id="résultats" />
</body>
</html>
  • ligne 1 : la vue a pour modèle un type [ViewModel01] ;
  • ligne 21 : on a besoin de JQuery à la fois pour les validations et Ajax ;
  • lignes 22-23 : les bibliothèques de validation ;
  • lignes 24-26 : les bibliothèques de l'internationalisation ;
  • ligne 27 : la bibliothèque Ajax ;
  • ligne 28 : une bibliothèque Javascript locale ;
  • ligne 33 : affichage de l'heure de chargement de la vue ;
  • ligne 35 : un formulaire Ajax - nous reviendrons dessus ;
  • lignes 40-41 : libellés des saisies des nombres A et B ;
  • lignes 46-47 : saisies des nombres A et B ;
  • lignes 50-51 : messages d'erreurs pour les saisies des nombres A et B ;
  • ligne 56 : le bouton qui poste le formulaire. Celui-ci sera posté avec une requête Ajax ;
  • ligne 57 : une image d'attente affichée lors de la requête Ajax ;
  • ligne 58 : un lien pour poster le formulaire avec une requête Ajax ;
  • ligne 62 : une balise <div> avec l'id [résultats]. C'est là que nous placerons le flux HTML renvoyé par le serveur web.

Cette vue affiche la page suivante :

 

Examinons maintenant le code qui ajaxifie le formulaire :


...
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    UpdateTargetId = "résultats",
    HttpMethod = "post",
    Url = Url.Action("Action01Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

...
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
....
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
}
...
 <div id="résultats" />
  • ligne 15 : au lieu d'utiliser [@Html.BeginForm], on utilise [@Ajax.BeginForm]. Cette méthode admet de nombreuses surcharges. Celle utilisée a la signature suivante :
Ajax.BeginForm(string ActionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string,object> htmlAttributes)

Nous utilisons ici les paramètres effectifs suivants :

Action01Post : le nom de l'action qui va traiter le POST du formulaire,

null : il n'y a pas d'informations de route à donner,

ajaxOpts : les options de l'appel Ajax. Elles ont été définies lignes 6-10,

new { id = "formulaire" } : pour affecter l'attribut [id='formulaire'] à la balise <form> générée ;

Les options Ajax utilisées sont les suivantes :

  • ligne 8 : l'URL cible de la requête HTTP Ajax ;
  • ligne 7 : méthode de la requête HTTP Ajax ;
  • ligne 6 : id de la région de la page qui sera mise à jour par la réponse à la requête Ajax ;
  • ligne 9 : id de la région de la page qui sera affichée pendant la requête Ajax – en général une image d'attente. Ici c'est la ligne 20 qui sera affichée. Elle contient une image animée symbolisant une attente. Au départ, cette image est cachée par le style [display : none] ;
  • ligne 10 : temps d'attente en millisecondes avant que l'image animée ne soit affichée, ici 1 seconde.

Le code HTML généré par le formulaire Ajax est le suivant :


<form action="/Premier/Action01Post" data-ajax="true" data-ajax-loading="#loading" data-ajax-loading-duration="1000" data-ajax-method="post" data-ajax-mode="replace" data-ajax-update="#résultats" data-ajax-url="/Premier/Action01Post" id="formulaire" method="post">    <table>
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
</form>
<hr />
<div id="résultats" />
  • ligne 1 : la balise <form> générée. On notera les attributs [data-ajax-attr] qui reflètent les valeurs des champs de l'objet de type [AjaxOptions] qui a été associé à la requête Ajax. Ces attributs sont gérés par la bibliothèque Ajax. Sans eux, la balise <form> devient :

<form action="/Premier/Action01Post" id="formulaire" method="post">
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
</form>

On a alors affaire à un formulaire HTML classique. C'est ce code qui sera exécuté si l'utilisateur désactive le Javascript sur son navigateur. Les lignes 5-6 sont alors inutilisées.

7.3.3. L'action [Action01Post]

L'action [Action01Post] qui traite la requête HTTP Ajax est la suivante :


    [HttpPost]
    public PartialViewResult Action01Post(FormCollection postedData, SessionModel session)
    {
      // simulation attente
      Thread.Sleep(2000);
      // instanciation modèle de l'action
      ViewModel01 modèle = new ViewModel01();
      // l'heure de calcul
      modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
      // mise à jour du modèle
      TryUpdateModel(modèle, postedData);
      if (!ModelState.IsValid)
      {
        // on retourne une erreur
        modèle.Erreur = getErrorMessagesFor(ModelState);
        return PartialView("Action01Error", modèle);
      }
      // une fois sur deux, on simule une erreur
      int val = session.Randomizer.Next(2);
      if (val == 0)
      {
        modèle.Erreur = "[erreur aléatoire]";
        return PartialView("Action01Error", modèle);
      }
      // calculs
      modèle.AplusB = string.Format("{0}", modèle.A + modèle.B);
      modèle.AmoinsB = string.Format("{0}", modèle.A - modèle.B);
      modèle.AmultipliéparB = string.Format("{0}", modèle.A * modèle.B);
      modèle.AdiviséparB = string.Format("{0}", modèle.A / modèle.B);
      // vue
      return PartialView("Action01Success", modèle);
}
  • ligne 1 : l'action ne traite qu'un [POST] ;
  • ligne 2 : elle admet pour modèle d'action :
    • [FormCollection postedData] : l'ensemble des valeurs postées par la requête Ajax POST,
    • [SessionModel session] : les éléments de la session. On utilise ici une technique décrite au paragraphe 4.10 ;
  • ligne 2 : l'action va rendre un fragment HTML et non une page HTML complète ;
  • ligne 5 : artificiellement, on s'arrête deux secondes pour simuler une action Ajax longue ;
  • ligne 7 : un modèle de type [ViewModel01] est instancié ;
  • ligne 9 : l'heure de calcul est initialisée ;
  • ligne 11 : on essaie de mettre à jour le modèle de type [ViewModel01] avec les valeurs postées. On rappelle qu'il y en a deux : les valeurs des nombres A et B ;
  • ligne 12 : on vérifie le succès ou non de cette mise à jour ;
  • ligne 15 : en cas d'erreur on renseigne le champ [Erreur] du modèle ;
  • ligne 16 : on rend une vue partielle [Action01Error.cshtml] exploitant le modèle [ViewModel01] ;
  • lignes 19-24 : une fois sur deux, on simule une erreur ;
  • ligne19 : on génère un nombre entier aléatoire dans l'intervalle [0,1]. Le générateur de nombres est pris dans la session ;
  • ligne 20 : si la valeur générée est 0, on simule une erreur ;
  • ligne 22 : le message d'erreur est mis dans le modèle ;
  • ligne 23 : on rend une vue partielle [Action01Error.cshtml] exploitant le modèle [ViewModel01] ;
  • lignes 26-29 : les calculs arithmétiques sur les nombres A et B sont faits et les résultats placés dans le modèle sous forme de chaînes de caractères ;
  • ligne 31 : on rend une vue partielle [Action01Success.cshtml] exploitant le modèle [ViewModel01] ;

7.3.4. La vue [Action01Error]

La vue [Action01Error.cshtml] est la suivante :


@model Exemple_04.Models.ViewModel01
<h4>Résultats</h4>
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p style="color: red;">Une erreur s'est produite : @Model.Erreur</p>

On rappelle que ce flux HTML partiel va être envoyé en réponse à la requête HTTP Ajax de type POST et être placé dans la page dans la région d'id [résultats]. Tous ces renseignements viennent de la configuration Ajax utilisée dans la page principale [Action01Get.cshtml] :


@model Exemple_04.Models.ViewModel01
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    UpdateTargetId = "résultats",
    HttpMethod = "post",
    Url = Url.Action("Action01Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

Voici un exemple de réponse avec erreur :

 

7.3.5. La vue [Action01Success]

La vue [Action01Success.cshtml] est la suivante :


@model Exemple_04.Models.ViewModel01
<h4>Résultats</h4>
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>

Là encore, ce flux HTML partiel va être envoyé en réponse à la requête HTTP Ajax de type POST et être placé dans la page dans la région d'id [résultats] :

 

7.3.6. Gestion de la session

Nous avons vu que [Action01Post] utilisait la session. Le modèle de la session est le type [SessionModel] suivant :


using System;
namespace Exemple_03.Models
{
  public class SessionModel
  {
    public Random Randomizer { get; set; }
  }
}

La session est initialisée dans [Global.asax] :


    // Session
    protected void Session_Start()
    {
      SessionModel sessionModel=new SessionModel();
      sessionModel.Randomizer=new Random(DateTime.Now.Millisecond);
      Session["data"] = sessionModel;
}

L'association de la session à un modèle est faite dans [Application_Start] :


    protected void Application_Start()
    {
...
      // model binders
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}

La classe [SessionModelBinder] a été décrite.

7.3.7. Gestion de l'image d'attente


@model Exemple_04.Models.ViewModel01
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
...
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

...
<body>

...
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
...

Lorsque la requête Ajax démarre, la région d'id [loading] ligne 7 est affichée au bout d'une seconde [ligne 8]. Cette région est l'image de la ligne 21 initialement cachée. Cela donne l'interface suivante :

7.3.8. Gestion du lien [Calculer]

Examinons le lien [Calculer] de la page principale [Action01Get.cshtml] :


<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-01</title>
  ...
  <script type="text/javascript" src="~/Scripts/myScripts-01.js"></script>
</head>
<body>

  <h2>Ajax - 01</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
  <hr />
<div id="résultats" />
  • ligne 18 : un clic sur le lien [Calculer] provoque l'exécution de la fonction JS [postForm]. Celle-ci est définie dans le fichier [myScripts-01.js] de la ligne 5. Ce script est le suivant :

function postForm() {
  // on fait un appel Ajax à la main avec JQuery
  var loading = $("#loading");
  var formulaire = $("#formulaire");
  var résultats = $('#résultats');
  $.ajax({
    url: '/Premier/Action01Post',
    type: 'POST',
    data: formulaire.serialize(),
    dataType: 'html',
    begin: loading.show(),
    success: function (data) {
      loading.hide()
      résultats.html(data);
    }
  })
}

// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseFloat(value));
}

$.validator.methods.date = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseDate(value));
}

jQuery.extend(jQuery.validator.methods, {
  range: function (value, element, param) {
    //Use the Globalization plugin to parse the value        
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (
        val >= param[0] && val <= param[1]);
  }
});

Les fonctions des lignes 19-37 ont été déjà rencontrées au paragraphe 6.1. Elles gèrent l'internationalisation des pages. Nous ne revenons pas dessus. Lignes 1-17, nous faisons à la main l'appel Ajax qui dans le cas du bouton [Calculer] était fait par la bibliothèque Ajax associée au projet. Pour cela, nous utilisons la bibliothèque JQuery associée au projet.

  • ligne 3 : une référence sur le composant d'id [loading]. [$("#loading")] ramène la collection des éléments d'id [loading]. Il n'y en a qu'un ;
  • ligne 4 : une référence sur le composant d'id [formulaire] ;
  • ligne 5 : une référence sur le composant d'id [résultats] ;
  • ligne 6 : l'appel Ajax avec ses options ;
  • ligne 7 : l'URL cible de l'appel Ajax ;
  • ligne 8 : la méthode HTTP utilisée ;
  • ligne 9 : les données postées. [formulaire.serialize] crée la chaîne [A=val1&B=val2] du POST du formulaire d'id [formulaire] ;
  • ligne 10 : le type de données attendu en retour. On sait que le serveur va renvoyer un flux HTML ;
  • ligne 11 : la méthode à exécuter lorsque la requête démarre. Ici, on indique qu'il faut afficher le composant d'id [loading]. C'est l'image animée d'attente ;
  • ligne 12 : la méthode à exécuter en cas de succès de la requête Ajax. Le paramètre [data] est la réponse complète du serveur. On sait que c'est un flux HTML ;
  • ligne 13 : on cache le signal d'attente ;
  • ligne 14 : on met à jour le composant d'id [résultats] avec le HTML du paramètre [data].

Le lecteur est invité à tester le lien [Calculer]. Il fonctionne comme le bouton [Calculer] à une anomalie près. Une fois qu'on a utilisé ce lien, on peut alors poster des valeurs de A et B invalides :

  • en [1] et [2], on a entré des valeurs invalides. Elles sont signalées par les validateurs côté client ;
  • en [3], on a cliqué sur le lien [Calculer] ;
  • en [4], il y a eu un [POST] puisqu'on obtient la réponse [4].

Lorsque les valeurs sont invalides et qu'on clique sur le bouton [Calculer], le [POST] vers le serveur n'a pas lieu. Dans le même cas, avec le lien [Calculer] le [POST] vers le serveur a lieu. Il y a donc un comportement du bouton [Calculer] qu'on n'a pas su reproduire avec le lien [Calculer]. Plutôt que d'essayer de résoudre ce problème maintenant, nous le laissons pour un exemple ultérieur qui illustrera également un autre problème de validation côté client.

7.4. Mise à jour d'une page HTML avec un flux JSON

Dans l'exemple précédent, le serveur web répondait à la requête HTTP Ajax par un flux HTML. Dans ce flux, il y avait des données accompagnées par du formatage HTML. On se propose de reprendre l'exemple précédent avec cette fois-ci des réponses JSON (JavaScript Object Notation) ne contenant que les données. L'intérêt est qu'on transmet ainsi moins d'octets.

7.4.1. L'action [Action02Get]

L'action [Action02Get] sera le point d'entrée de la nouvelle application. Son code est le suivant :


@model Exemple_04.Models.ViewModel02
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    HttpMethod = "post",
    Url = Url.Action("Action02Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000,
    OnBegin = "OnBegin",
    OnFailure = "OnFailure",
    OnSuccess = "OnSuccess",
    OnComplete = "OnComplete"
  };    
}

<!DOCTYPE html>

<html lang="fr-FR">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-02</title>
....
  <script type="text/javascript" src="~/Scripts/myScripts-02.js"></script>
</head>
<body>
  <h2>Ajax - 02</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  @using (Ajax.BeginForm("Action02Post", null, ajaxOpts, new { id = "formulaire" }))
  {
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
  <hr />
  <div id="entete">
    <h4>Résultats</h4>
    <p><strong>Heure de calcul : <span id="heureCalcul"/></strong></p>
  </div>
  <div id="résultats">
    <p>A+B=<span id="AplusB"/></p>
    <p>A-B=<span id="AmoinsB"/></p>
    <p>A*B=<span id="AmultipliéparB"/></p>
    <p>A/B=<span id="AdiviséparB"/></p>
  </div>
  <div id="erreur">
    <p style="color: red;">Une erreur s'est produite : <span id="msg"/></p>
  </div>
</body>
</html>
  • lignes 4-14 : les options de l'appel Ajax ;
  • ligne 10 : la fonction JS à exécuter au démarrage de la requête. Cette fonction est définie dans le fichier JS référencé à la ligne 24 ;
  • ligne 11 : la fonction JS à exécuter en cas d'échec de la requête ;
  • ligne 12 : la fonction JS à exécuter en cas de réussite de la requête ;
  • ligne 13 : la fonction JS à exécuter après que la requête Ajax a obtenu son résultat (échec ou réussite) ;
  • lignes 40-43 : une région d'id [entete] ;
  • lignes 44-49 : une région d'id [résultats]. Elle affichera les résultats des quatre opérations arithmétiques ;
  • lignes 50-52 : une région d'id [erreur]. Elle affichera un éventuel message d'erreur.

7.4.2. L'action [Action02Post]

La requête Ajax est traitée par l'action [Action02Post] suivante :


[HttpPost]
    public JsonResult Action02Post(FormCollection postedData, SessionModel session)
    {
      // simulation attente
      Thread.Sleep(2000);
      // validation modèle
      ViewModel02 modèle = new ViewModel02();
      // les heures de chargement et de calcul
      string HeureChargement = DateTime.Now.ToString("hh:mm:ss");
      string HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
      // mise à jour du modèle
      TryUpdateModel(modèle, postedData);
      if (!ModelState.IsValid)
      {
        // on retourne une erreur
        return Json(new { Erreur = getErrorMessagesFor(ModelState), HeureCalcul = HeureCalcul });
      }
      // une fois sur deux, on simule une erreur
      int val = session.Randomizer.Next(2);
      if (val == 0)
      {
        // on retourne une erreur
        return Json(new { Erreur = "[erreur aléatoire]", HeureCalcul = HeureCalcul });
      }
      // calculs
      string AplusB = string.Format("{0}", modèle.A + modèle.B);
      string AmoinsB = string.Format("{0}", modèle.A - modèle.B);
      string AmultipliéparB = string.Format("{0}", modèle.A * modèle.B);
      string AdiviséparB = string.Format("{0}", modèle.A / modèle.B);
      // on retourne les résultats
      return Json(new { Erreur = "", AplusB = AplusB, AmoinsB = AmoinsB, AmultipliéparB = AmultipliéparB, AdiviséparB = AdiviséparB, HeureCalcul = HeureCalcul });
    }
  • ligne 2 : la méthode rend un type [JsonResult] ç-à-d un texte au format JSON ;
  • ligne 16 : les informations sont rendues sous la forme d'une instance de classe anonyme sérialisée en JSON. La méthode [getErrorMessagesFor] a déjà été présentée. La chaîne JSON envoyée au navigateur aura la forme suivante :
{"Erreur":"[erreur aléatoire]","HeureCalcul":"05:31:37"}
  • ligne 31 : même démarche pour les résultats arithmétiques. Cette fois, la chaîne JSON envoyée au navigateur aura la forme suivante :
{"Erreur":"","AplusB":"4","AmoinsB":"-2","AmultipliéparB":"3","AdiviséparB":"0,333333333333333","HeureCalcul":"05:52:17"}

7.4.3. Le code Javascript côté client

Rappelons la configuration de l'appel Ajax dans la page HTML envoyée au navigateur client :


  AjaxOptions ajaxOpts = new AjaxOptions
  {
    HttpMethod = "post",
    Url = Url.Action("Action02Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000,
    OnBegin = "OnBegin",
    OnFailure = "OnFailure",
    OnSuccess = "OnSuccess",
    OnComplete = "OnComplete"
};    

Les fonctions JS référencées aux lignes 7-10 (à droite du signe =) sont définies dans le fichier [myScripts-02.js] suivant :


// données globales
var entete;
var loading;
var résultats;
var erreur;
var heureCalcul;
var msg;
var AplusB;
var AmoinsB;
var AmultipliéparB;
var AdiviséparB;
var formulaire;
...
function postForm() {
...
}

// au chargement du document
$(document).ready(function () {
  formulaire = $("#formulaire");
  entete = $("#entete");
  loading = $("#loading");
  erreur = $("#erreur");
  résultats = $('#résultats');
  heureCalcul = $("#heureCalcul");
  msg = $("#msg");
  AplusB = $("#AplusB");
  AmoinsB = $("#AmoinsB");
  AmultipliéparB = $("#AmultipliéparB");
  AdiviséparB = $("#AdiviséparB");

  // on cache certains éléments de la page
  entete.hide();
  résultats.hide();
  erreur.hide();
});

// démarrage
function OnBegin() {
....
}

// fin de la requête
function OnComplete() {
...
}

// réussite
function OnSuccess(data) {
....
}

// erreur
function OnFailure(request, error) {
...
}
  • ligne 19 : la fonction JS exécutée à la fin du chargement de la page dans le navigateur ;
  • lignes 20-30 : on récupère les références de tous les composants de la page qui nous intéressent. La recherche d'un composant dans une page a un coût et il est préférable de ne la faire qu'une fois ;
  • lignes 33-35 : les composants [entete], [résultats] et [loading] sont cachés ;

Au démarrage de la requête Ajax, la fonction suivante est exécutée :


// démarrage
function OnBegin() {
  // signal d'attente allumé
  loading.show();
  // on cache certains éléments de la page
  entete.hide();
  résultats.hide();
  erreur.hide();
}
  • ligne 4 : le composant [loading] est affiché. C'est l'image animée ;
  • lignes 6-8 : les composants [entete], [résultats] et [erreur] sont cachés ;

Si la requête Ajax réussit, on exécute le code JS suivant :


// réussite
function OnSuccess(data) {
  // affichage résultats
  heureCalcul.text(data.HeureCalcul);
  entete.show();
  if (data.Erreur != '') {
    msg.text(data.Erreur);
    erreur.show();
    return;
  }
  // pas d'erreur
  AplusB.text(data.AplusB);
  AmoinsB.text(data.AmoinsB);
  AmultipliéparB.text(data.AmultipliéparB);
  AdiviséparB.text(data.AdiviséparB);
  résultats.show();
}

Pour comprendre ce code il faut se rappeler les deux textes JSON susceptibles d'être envoyés en réponse au navigateur :

{"Erreur":"[erreur aléatoire]","HeureCalcul":"05:31:37"}

en cas d'erreur sinon la chaîne :

{"Erreur":"","AplusB":"4","AmoinsB":"-2","AmultipliéparB":"3","AdiviséparB":"0,333333333333333","HeureCalcul":"05:52:17"}

Si on appelle [data] cette chaîne, la valeur du champ [Erreur] est obtenue par la notation [data.Erreur] ou [data["Erreur"]], au choix. Idem pour les autres champs de la chaîne JSON. Par ailleurs, pour affecter un texte non formaté à un composant d'id X, on écrit [X.text(chaine)]. Revenons au code de la fonction [OnSuccess] :

  • ligne 2 : [data] est la chaîne JSON reçue ;
  • ligne 4 : le composant [heureCalcul] reçoit sa valeur ;
  • ligne 5 : le composant [entete] est affiché ;
  • ligne 6 : test du champ [Erreur] de la chaîne JSON ;
  • ligne 7 : le composant [msg] reçoit sa valeur ;
  • ligne 8 : le composant [erreur] est affiché ;
  • ligne 9 : c'est terminé pour le cas d'erreur ;
  • ligne 12 : le composant [AplusB] reçoit sa valeur ;
  • ligne 13 : le composant [AmoinsB] reçoit sa valeur ;
  • ligne 14 : le composant [AmultipliéparB] reçoit sa valeur ;
  • ligne 15 : le composant [AdiviséparB] reçoit sa valeur ;
  • ligne 16 : le composant [résultats] est affiché.

La fonction [OnFailure] sera exécutée en cas d'échec de la requête HTTP Ajax. Cet échec est mesuré avec le code HTTP renvoyé par le serveur. Le code 500 [Internal Server Error] par exemple indique que le serveur n'a pu exécuter la requête. La fonction [OnFailure] est la suivante :


// erreur
function OnFailure(request, error) {
  alert("L'erreur suivante s'est produite :" + error);
}

On se contente d'afficher une boîte de dialogue avec l'erreur qui s'est produite. Dans la pratique, il faudrait être plus précis. Nous allons bientôt proposer une autre solution.

Enfin la fonction [OnComplete] est exécutée lorsque la requête est terminée que ce soit sur une réussite ou un échec.


// fin de la requête
function OnComplete() {
  // signal d'attente éteint
  loading.hide();
}

On rappelle ici que c'est la configuration de l'appel Ajax dans la vue [Action02Get.cshtml] qui fait que ces différentes fonctions sont appelées :


  AjaxOptions ajaxOpts = new AjaxOptions
  {
...
    OnBegin = "OnBegin",
    OnFailure = "OnFailure",
    OnSuccess = "OnSuccess",
    OnComplete = "OnComplete"
};

7.4.4. Le lien [Calculer]

Le code HTML du lien [Calculer] dans la vue [Action02Get.cshtml] est le suivant :


      <a href="javascript:postForm()">Calculer</a>

La fonction JS [postForm] est trouvé dans le fichier importé [myScripts-02.js] :


  <script type="text/javascript" src="~/Scripts/myScripts-02.js"></script>

Son code est le suivant :


function postForm() {
  // on fait un appel Ajax à la main avec JQuery
  $.ajax({
    url: '/Premier/Action02Post',
    type: 'POST',
    data: formulaire.serialize(),
    dataType: 'json',
    beforeSend: OnBegin,
    success: OnSuccess,
    error: OnFailure,
    complete: OnComplete
  })
}

Nous avons déjà rencontré un code similaire.

  • ligne 4 : URL cible de l'appel Ajax ;
  • ligne 5 : commande HTTP utilisée par l'appel Ajax ;
  • ligne 6 : valeurs postées. Elles sont le résultat de la sérialisation des valeurs du formulaire. Celui-ci désigné par l'id [formulaire] est référencé par la variable [formulaire]. [data] sera une chaîne de caractères de la forme [A=val1&B=val2] ;
  • ligne 7 : type de formatage de la réponse attendue. C'est une chaîne JSON ;
  • ligne 8 : fonction JS à exécuter au démarrage de l'appel Ajax ;
  • ligne 9 : fonction JS à exécuter si l'appel Ajax réussit ;
  • ligne 10 : fonction JS à exécuter si l'appel Ajax échoue ;
  • ligne 11 : fonction JS à exécuter une fois reçue la réponse du serveur, quelle que soit celle-ci, réussite ou erreur.

Revenons sur la fonction Javascript qui gère le cas où l'appel Ajax échoue (ligne 10). L'appel Ajax échoue dans diverses situations, par exemple lorsque le serveur renvoie un code d'erreur tel que [403 Forbidden], [404 Not Found], [500 Internal Server Error] [301 Moved Permanently], ...

Dans l'exemple précédent, la fonction [OnFailure] est la suivante :


// erreur
function OnFailure(request, error) {
  alert("L'erreur suivante s'est produite :" + error);
}

Généralement, l'affichage de l'objet [error] n'apporte aucune information intéressante. Si on utilise un appel Ajax fait avec JQuery, on peut utiliser la méthode [OnFailure] suivante ;


// erreur
function OnFailure(jqXHR) {
  alert("Erreur : " + jqXHR.status + " " + jqXHR.statusText);
  msg.html(jqXHR.responseText);
  erreur.show();
}

L'objet JQuery [jqXHR] a parmi ses propriétés les suivantes :

  • responseText : le texte de la réponse du serveur ;
  • status : le code d'erreur retourné par le serveur ;
  • statusText : le texte associé à ce code d'erreur.

  • ligne 3 : on affiche le code d'erreur et le libellé qui va avec ;

  • ligne 4 : on met la réponse HTML du serveur dans le composant d'id [msg] ;
  • ligne 5 : on affiche la région d'id [erreur].

Pour tester cette fonction d'erreur, nous allons créer artificiellement une exception dans l'action [Action02Post] :


    [HttpPost]
    public JsonResult Action02Post(FormCollection postedData, SessionModel session)
    {
      // une exception artificielle pour tester la fonction d'erreur de l'appel Ajax
      throw new Exception();
      // simulation attente
      Thread.Sleep(2000);
      // validation modèle
...

La ligne 5 lance une exception. Maintenant testons l'application :

On obtient la réponse suivante [1] et [2] :

La réponse du serveur nous permet de voir à quelle ligne du code serveur s'est produite l'erreur. C'est souvent une information utile à connaître. Dorénavant nous utiliserons cette technique pour gérer les erreurs des appels Ajax.

7.5. Application web à page unique

La technologie Ajax permet de construire des applications à page unique :

  • la première page est issue d'une requête classique d'un navigateur ;
  • les pages suivantes sont obtenues avec des appels Ajax. Aussi, au final le navigateur ne change jamais d'URL et ne charge jamais de nouvelle page. On appelle ce type d'application, Application à Page Unique (APU) ou en anglais Single Page Application (SPA).

Voici un exemple basique d'une telle application. La nouvelle application aura deux vues :

  • en [1], l'action [Action03Get] nous permet d'avoir la première page, la page 1 ;
  • en [2], un lien nous permet de passer à la page 2 grâce à un appel Ajax ;
  • en [3], l'URL n'a pas changé. La page présentée est la page 2 ;
  • en [4], un lien nous permet de revenir à la page 1 grâce à un appel Ajax ;
  • en [5], l'URL n'a pas changé. La page présentée est la page 1.

Le code de l'action [Action03Get] est le suivant :


    [HttpGet]
    public ViewResult Action03Get()
    {
      return View();
}
  • ligne 4 : la vue [Action03Get.cshtml] est affichée.

La vue [Action03Get.cshtml] est la suivante :


@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Action03Get</title>
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
</head>
<body>
  <h3>Ajax - 03 - Single Page Application</h3>
  <div id="content">
    @Html.Partial("Page1")
  </div>
</body>
</html>
  • lignes 16-18 : un élément d'id [content]. C'est dans cet élément que les différentes pages vont s'afficher ;
  • ligne 17 : par défaut c'est la page [Page1.cshtml] qui va d'abord s'afficher.

La page [Page1.cshtml] est la suivante :


<h4>Page 1</h4>
  <p>
    @Ajax.ActionLink("Page 2", "Action04", new { Page = 2 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>
  • ligne 1 : le titre de la page pour la différencier de la page 2 ;
  • ligne 3 : un lien Ajax avec les paramètres suivants :
    • le libellé du lien [Page 2] ;
    • l'action cible du lien [Action04] ;
    • les paramètres de l'URL demandée. Celle-ci sera [/Premier/Action04?Page=2] ;
  • les options de l'appel Ajax. Ici, seulement l'id de la région à mettre à jour avec la réponse du serveur. Pour les autres options, des valeurs par défaut sont alors utilisées lorsqu'elles existent. La méthode HTTP par défaut est GET.

Voyons ce qui se passe lorsque le lien est cliqué. L'URL [/Premier/Action04?Page=2] est demandée avec un GET. L'action [Action04] est alors exécutée :


    [HttpGet]
    public PartialViewResult Action04(string page = "1")
    {
      string vue = "Page1";
      if (page == "2")
      {
        vue = "Page2";
      }
      return PartialView(vue);
}
  • ligne 2 : l'action rend un flux HTML partiel ;
  • ligne 2 : l'action a pour modèle la chaîne [page]. Or on sait que l'URL a cette information : [/Premier/Action04?Page=2]. On rappelle que le modèle est insensible à la casse ;
  • lignes 4-8 : [vue] va recevoir la valeur [Page2] ;
  • ligne 9 : la vue partielle [Page2.cshtml] est rendue.

La vue partielle [Page2.cshtml] est la suivante :


<h4>Page 2</h4>
  <p>
    @Ajax.ActionLink("Page 1", "Action04", new { Page = 1 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>

Le serveur renvoie donc le flux HTML ci-dessus comme réponse à l'appel Ajax GET [/Premier/Action04?Page=2]. On se rappelle que cet appel Ajax utilise cette réponse pour mettre à jour la région d'id [content] (ligne 3 ci-dessous) :


<h4>Page 1</h4>
  <p>
    @Ajax.ActionLink("Page 2", "Action04", new { Page = 2 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>

Ceci provoque le nouvel affichage suivant [1] :

En suivant le même raisonnement, on voit que le clic sur le lien [Page 1] de [1] va provoquer l'affichage [2].

Revenons au schéma général d'une application ASP.NET MVC :

Grâce au Javascript embarqué dans les pages HTML et exécuté dans le navigateur, on peut déporter du code sur le navigateur et aboutir à l'architecture suivante :

  • en [1], la couche Web ASP.NET MVC est devenue une interface web d'accès aux données, généralement logées dans une base de données. Les vues délivrées ne contiennent que des données et aucun habillage HTML, par exemple des flux XML ou JSON ;
  • en [2] : le navigateur affiche des vues statiques (ç-à-d non générées dynamiquement) délivrées par un serveur web qui peut être ou non sur la même machine que le serveur [1]. Ces vues statiques sont ensuite enrichies par les données obtenues par le Javascript auprès de l'interface web [1] ;
  • le code Javascript embarqué dans les pages HTML peut être structuré en couches :
    • la couche [présentation] s'occupe des interactions avec l'utilisateur,
    • la couche [DAO] s'occupe de l'accès aux données via le serveur web [1] ,
    • la couche [métier] correspond à la couche [métier] qui auparavant était sur le serveur [1] et qui a été déportée sur le navigateur [2] ;

L'intérêt de cette architecture est qu'elle met en jeu des compétences différentes :

  • le code du serveur web [1] nécessite des compétences .NET mais pas de compétences Javascript, HTML, CSS ;
  • le code embarqué dans le navigateur [2] nécessite des compétences Javascript, HTML, CSS mais est indifférent à la technologie du serveur web [1].

Ainsi, cette architecture facilite-t-elle le travail en parallèle d'équipes aux compétences différentes. Elle s'applique particulièrement bien aux Applications à Page Unique.

7.6. Application web à page unique et validation côté client

Nous avons évoqué précédemment une anomalie sur l'exemple Ajax-01. Nous en rappelons le contexte :

  • en [1] et [2], on a entré des valeurs invalides. Elles sont signalées par les validateurs côté client ;
  • en [3], on a cliqué sur le lien [Calculer] ;
  • en [4], il y a eu un [POST] puisqu'on obtient la réponse [4].

Lorsque les valeurs sont invalides et qu'on clique sur le bouton [Calculer], le [POST] vers le serveur n'a pas lieu. Dans le même cas, avec le lien [Calculer] le [POST] vers le serveur a lieu. Il y a donc un comportement du bouton [Calculer] qu'on n'a pas su reproduire avec le lien [Calculer].

Nous allons reprendre cet exemple dans un nouveau contexte : l'application aura plusieurs vues et sera du type [Application à Page Unique] que nous venons de décrire.

7.6.1. Les vues de l'exemple

L'exemple a plusieurs vues :

  • en [1], la vue [Action05Get] ;
  • en [2], la vue partielle [Formulaire05] ;
  • en [3], la vue partielle [Failure05] ;
  • en [4], la vue partielle [Success05].

L'application est à page unique : celle-ci est chargée par le navigateur lors de la première requête. Elle est ensuite mise à jour par des appels Ajax.

Les pages précédentes sont générées par les vues [cshtml] suivantes :

La vue chargée initialement est la vue [Action05Get.cshtml] suivante :


@model Exemple_04.Models.ViewModel05
@{
  Layout = null;
}

<!DOCTYPE html>

<html lang="fr-FR">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-05</title>
  <link rel="stylesheet" href="~/Content/Site.css" />
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.en-US.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
  <script type="text/javascript" src="~/Scripts/myScripts-05.js"></script>
</head>
<body>

  <h2>Ajax - 05, Page unique - Validation formulaire côté client</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
  <div id="content">
    @Html.Partial("Formulaire05", Model)
  </div>
</body>
</html>

On notera les points suivants :

  • ligne 1 : le modèle de la vue est le type [ViewModel05] que nous allons présenter prochainement ;
  • lignes 13-19 : on retrouve les scripts Javascript nécessaires pour faire de l'Ajax et de la validation côté client ;
  • ligne 20 : nous allons ajouter nos propres fonctions Javascript dans [myScripts-05.js] ;
  • ligne 27 : l'image animée d'attente ;
  • lignes 28-30 : une balise d'id [content]. C'est dans cette balise que les vues partielles [Formulaire05, Success05, Failure05] vont venir s'insérer ;
  • ligne 29 : insertion de la vue partielle [Formulaire05].

La vue [Action05Get] est responsable de l'affichage de la partie [1] de la page initiale :

La vue partielle [Formulaire05] va générer la partie [2] ci-dessus. Son code est le suivant :


@model Exemple_04.Models.ViewModel05

@using (Html.BeginForm("Action05Post", "Premier", FormMethod.Post, new { id = "formulaire" }))
{
  <table>
    <thead>
      <tr>
        <th>@Html.LabelFor(m => m.A)</th>
        <th>@Html.LabelFor(m => m.B)</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>@Html.TextBoxFor(m => m.A)</td>
        <td>@Html.TextBoxFor(m => m.B)</td>
      </tr>
      <tr>
        <td>@Html.ValidationMessageFor(m => m.A)</td>
        <td>@Html.ValidationMessageFor(m => m.B)</td>
      </tr>
    </tbody>
  </table>
  <p>
    <table>
      <tbody>
        <tr>
          <td><a href="javascript:calculer()">Calculer</a>
          </td>
          <td style="width: 20px" />
          <td><a href="javascript:effacer()">Effacer</a>
          </td>
        </tr>
      </tbody>
    </table>
  </p>
}
  • ligne 1 : la vue partielle admet pour modèle un type [ViewModel05] ;
  • ligne 3 : le formulaire généré par la méthode [Html.BeginForm]. Parce que ce formulaire sera posté par un appel Ajax, les trois premiers paramètres de la méthode seront ignorés. Sauf si l'utilisateur a inhibé le Javascript sur son navigateur. Nous ignorons ici cette possibilité. Le quatrième paramètre est important. Le formulaire aura l'id [formulaire] ;
  • lignes 5-22 : le formulaire de saisie des nombres A et B ;
  • ligne 27 : un lien Javascript qui lance l'exécution des quatre opérations arithmétiques sur A et B ;
  • ligne 30 : un lien Javascript qui efface les saisies et les éventuels messages d'erreur qui leur sont liés.

On notera que le formulaire n'a pas de bouton de type [submit]. Nous serons amenés à faire à la main le [Post] des valeurs A et B saisies.

S'il n'y a pas d'erreurs, les résultats sont affichés :

La partie [4] ci-dessus est générée par la vue partielle [Success05.cshtml] suivante :


@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h4>Résultats</h4>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
  • ligne 1 : la vue partielle [Success05.cshtml] reçoit un modèle de type [ViewModel05] ;
  • ligne 12 : un lien Javascript pour revenir aux saisies.

En cas d'erreur, une autre vue partielle [3] est affichée :

Cette vue est générée par le code [Failure05.cshtml] suivant :


@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
  @foreach (string msg in Model.Erreurs)
  {
    <li>@msg</li>
  }
</ul>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
  • ligne 1 : la vue partielle [Failure05.cshtml] reçoit un modèle de type [ViewModel05] ;
  • ligne 14 : un lien Javascript pour revenir aux saisies.

7.6.2. Le modèle des vues

Toutes les vues précédentes partagent le même modèle [ViewModel05] :


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace Exemple_04.Models
{
  [Bind(Exclude = "AplusB, AmoinsB, AmultipliéparB, AdiviséparB, Erreurs, HeureChargement, HeureCalcul")]
  public class ViewModel05
  {
    // formulaire
    [Required(ErrorMessage="Donnée A requise")]
    [Display(Name="Valeur de A")]
    [Range(0, Double.MaxValue, ErrorMessage = "Tapez un nombre A positif ou nul")]
    public string A { get; set; }
    [Required(ErrorMessage = "Donnée B requise")]
    [Display(Name = "Valeur de B")]
    [Range(0, Double.MaxValue, ErrorMessage="Tapez un nombre B positif ou nul")]
    public string B { get; set; }

    // résultats
    public string AplusB { get; set; }
    public string AmoinsB { get; set; }
    public string AmultipliéparB { get; set; }
    public string AdiviséparB { get; set; }
    public List<string> Erreurs { get; set; }
    public string HeureChargement { get; set; }
    public string HeureCalcul { get; set; }
  }
}

C'est le modèle [ViewModel01] déjà présenté à quelques détails près :

  • lignes 15 et 19 : les champs A et B sont désormais de type [string] afin d'afficher des champs de saisie vides plutôt que des champs avec la valeur 0, lors de l'affichage initial du formulaire de saisie ;
  • lignes 14 et 18 : cela n'empêche pas de vérifier la valeur saisie avec un validateur [Range] ;
  • ligne 26 : une liste de messages d'erreurs affichée par la vue [Failure05].

7.6.3. Les données de portée [Session]

Au paragraphe 7.3.6, nous avons vu que les données de la session étaient encapsulées dans le modèle [SessionModel] suivant :


using System;
namespace Exemple_03.Models
{
  public class SessionModel
  {
    // le générateur de nombres aléatoires
    public Random Randomizer { get; set; }
  }
}

Ce modèle de session est élargi pour intégrer les valeurs de A et B :


using System;
namespace Exemple_03.Models
{
  public class SessionModel
  {
    // le générateur de nombres aléatoires
    public Random Randomizer { get; set; }
    // les valeurs de A et B
    public string A { get; set; }
    public string B { get; set; }
  }
}

Il est en effet nécessaire de mémoriser les valeurs de A et B dans la session comme le montre la séquence suivante :

Requête 1

Requête 2

En [4], on retrouve les saisies faites en [1]. Or il y a deux requêtes HTTP distinctes. On sait que la mémoire entre deux requêtes HTTP est la session. Pour que la seconde puisse retrouver les valeurs postées par la première, il faut que ces dernières soient mises en session.

7.6.4. L'action serveur [Action05Get]

L'action [Action05Get] est l'action qui fait afficher la page unique initiale. Son code est le suivant :


    [HttpGet]
    public ViewResult Action05Get()
    {
      ViewModel05 modèle = new ViewModel05();
      modèle.HeureChargement = DateTime.Now.ToString("hh:mm:ss");
      return View(modèle);
}
  • ligne 6 : la vue [Action05Get.cshtml] déjà étudiée est affichée avec un modèle de type [ViewModel05] ;

7.6.5. L'action client [Calculer]

Examinons les interactions de l'utilisateur avec les vues :

Le lien [1] est un lien Javascript :


<a href="javascript:calculer()">Calculer</a>

La fonction Javascript [calculer] est trouvée dans le fichier [myScripts-05.js] :


  <script type="text/javascript" src="~/Scripts/myScripts-05.js"></script>

Le code de la fonction Javascript [calculer] est le suivant :


// données globales
var content;
var loading;

function calculer() {
  // d'abord les références sur le DOM
  var formulaire = $("#formulaire");
  // ensuite validation du formulaire
  if (!formulaire.validate().form()) {
    // formulaire invalide - terminé
    return;
  }
  // on fait un appel Ajax à la main
  $.ajax({
    url: '/Premier/Action05FaireCalcul',
    type: 'POST',
    data: formulaire.serialize(),
    dataType: 'html',
    beforeSend: function () {
      loading.show();
    },
    success: function (data) {
      content.html(data);
    },
    complete: function () {
      loading.hide();
    },
    error: function (jqXHR) {
      // affichage réponse serveur
      content.html(jqXHR.responseText);
    }
  })
}

function retourSaisies() {
 ...
}

function effacer() {
  ...
}

// au chargement du document
$(document).ready(function () {
  // on récupère les références des différents composants de la page
  loading = $("#loading");
  content = $("#content");
  // on cache l'image animée
  loading.hide();
});
  • on rappelle que le code Javascript est toujours exécuté côté client, dans le navigateur ;
  • ligne 44 : la fonction JS exécutée lorsque le chargement initial de la page unique est terminé ;
  • ligne 46 : référence sur l'image animée d'id [loading] ;
  • ligne 47 : référence sur la région d'id [content]. C'est cette région qui reçoit les vues partielles [Formulaire05, Success05, Failure05] ;
  • lignes 2-3 : les variables des lignes 46-47 sont déclarées globales afin que les autres fonctions y aient accès. Il y a un coût à rechercher des éléments dans une page (lignes 46-47). Il n'y a pas lieu de répéter cette recherche si on peut l'éviter ;
  • ligne 5 : la fonction [calculer] ;
  • ligne 7 : on récupère une référence sur le formulaire. La vue partielle [Formulaire05] lui a donné l'id [formulaire] ;
  • ligne 9 : cette instruction exécute les validateurs du formulaire côté client. C'est ce qui manquait dans l'anomalie constatée page 183. Cette méthode est fournie par la bibliothèque [jquery.unobstrusive-ajax] utilisée par la page unique :

  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>

L'instruction rend [false] si le formulaire est déclaré invalide ;

  • ligne 11 : l'appel Ajax au serveur n'est pas fait si le formulaire est invalide ;
  • lignes 14-32 : l'appel Ajax fait au serveur ;
  • ligne 15 : l'URL cible est l'action serveur [Action05FaireCalcul] ;
  • ligne 16 : elle est demandée via un [POST] ;
  • ligne 17 : les valeurs postées. Ce sont les saisies du formulaire, ici les valeurs de A et B ;
  • lignes 22-24 : en cas de succès de l'appel Ajax, la fonction [calculer] met à jour la région d'id [content] avec le flux HTML envoyé par le serveur.

Ce flux HTML est celui envoyé par l'action [Action05FaireCalcul] ciblée par l'appel Ajax. Le code de cette action côté serveur est le suivant :


    [HttpPost]
    public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
    {
      // modèle
      ViewModel05 modèle = new ViewModel05();
      // l'heure de calcul
      modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
      // mise à jour du modèle
      TryUpdateModel(modèle, postedData);
      if (!ModelState.IsValid)
      {
        // on retourne une erreur
        modèle.Erreurs = getListOfMessagesFor(ModelState);
        return PartialView("Failure05", modèle);
      }
...
}
  • ligne 1 : l'action n'accepte qu'un [post] ;
  • ligne 2 : elle rend une vue partielle ;
  • ligne 2 : elle reçoit en paramètres, les valeurs postées (postedData) et le modèle de la session (session) ;
  • ligne 5 : le modèle de la vue partielle est créé ;
  • ligne 7 : il est mis à jour avec l'heure de calcul ;
  • ligne 9 : on essaie d'appliquer les valeurs postées au modèle. Les validateurs de celui-ci vont alors être exécutés. On peut se demander pourquoi on prend cette peine alors que les validateurs côté client empêchent le POST si les données saisies sont invalides. En fait, on n'est pas sûr de la provenance du POST. Il a peut être été fait par un code qui n'est pas le nôtre. Aussi doit-on toujours faire les vérifications côté serveur ;
  • ligne 10 : on teste si les validateurs ont réussi ;
  • ligne 13 : si le modèle est invalide, on le met à jour avec une liste d'erreurs. On ne détaillera pas la méthode interne [getListOfMessagesFor] analogue à la méthode [GetErrorMessagesFor] décrite page 65 ;
  • ligne 14 : la vue partielle [Failure05] est affichée avec son modèle. On rappelle le code de cette vue ;

@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
  @foreach (string msg in Model.Erreurs)
  {
    <li>@msg</li>
  }
</ul>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
  • lignes 7-12 : la liste des erreurs du modèle est affichée à l'aide d'une balise <ul>.

On se rappelle que la fonction JS [calculer] à l'origine du [Post] à l'action serveur [Action05FaireCalcul] va mettre ce flux HTML dans la région d'id [content]. Cela donne quelque chose comme ceci :

Continuons l'étude du code de l'action [Action05FaireCalcul] :


    [HttpPost]
    public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
    {
      // modèle
      ViewModel05 modèle = new ViewModel05();
...
      // on met les valeurs de A et B en session
      session.A = modèle.A;
      session.B = modèle.B;
      // pas d'erreurs pour l'instant
      List<string> erreurs = new List<string>();
      // une fois sur deux, on simule une erreur
      int val = session.Randomizer.Next(2);
      if (val == 0)
      {
        erreurs.Add("[erreur aléatoire]");
      }
      if (erreurs.Count != 0)
      {
        modèle.Erreurs = erreurs;
        return PartialView("Failure05", modèle);
      }
      // calculs
      double A = double.Parse(modèle.A);
      double B = double.Parse(modèle.B);
      modèle.AplusB = string.Format("{0}", A + B);
      modèle.AmoinsB = string.Format("{0}", A - B);
      modèle.AmultipliéparB = string.Format("{0}", A * B);
      modèle.AdiviséparB = string.Format("{0}", A / B);
      // vue
      return PartialView("Success05", modèle);
}
  • ligne 7 : le modèle a été déclaré valide ;
  • lignes 8-9 : on met en session les valeurs saisies A et B. On veut pouvoir les retrouver dans la requête qui va suivre ;
  • lignes 11-22 : on génère de façon aléatoire une erreur une fois sur deux ;
  • lignes 24-29 : on fait les quatre opérations arithmétiques sur les nombres réels saisis ;
  • ligne 31 : on retourne la vue partielle [Success05] avec son modèle. Cette vue partielle est la suivante :

@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h4>Résultats</h4>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>

On se rappelle que la fonction JS [calculer] à l'origine du [Post] à l'action serveur [Action05FaireCalcul] va mettre ce flux HTML dans la région d'id [content]. Cela donne quelque chose comme ceci :

7.6.6. L'action client [Effacer]

Le lien Javascript [Effacer] permet de remettre le formulaire dans son état initial :

Dans le formulaire, le lien JS [Effacer] est défini comme suit :


<a href="javascript:effacer()">Effacer</a>

La fonction JS [effacer] est définie dans le fichier [myScripts-05.js] de la façon suivante :


// données globales
var content;
var loading;

function calculer() {
...
}

function retourSaisies() {
...
}

function effacer() {
  // d'abord les références sur le DOM
  var formulaire = $("#formulaire");
  var A = $("#A");
  var B = $("#B");
  // on affecte des valeurs valides aux saisies
  A.val("0");
  B.val("0");
  // puis on valide le formulaire pour faire disparaître
  // les éventuels msg d'erreur
  formulaire.validate().form();
  // puis on affecte des chaînes vides aux champs de saisie
  A.val("");
  B.val("");
}

// au chargement du document
$(document).ready(function () {
  // on récupère les références des différents composants de la page
  loading = $("#loading");
  content = $("#content");
  // on cache l'image animée
  loading.hide();
});
  • lignes 15-17 : on récupère des références sur divers éléments du DOM (Document Object Model) ;
  • lignes 19-20 : on met des valeurs valides dans les champs de saisie des nombres A et B ;
  • ligne 23 : on exécute les validateurs côté client. Comme les valeurs de A et B sont valides, cela va avoir pour effet de faire disparaître d'éventuels messages d'erreur qui pourraient être affichés ;
  • lignes 25-26 : on met des chaînes vides dans les champs de saisie des nombres A et B ;

7.6.7. L'action client [Retour aux Saisies]

Le lien Javascript [Retour aux Saisies] permet de revenir au formulaire après avoir obtenu des résultats :

Dans le formulaire, le lien JS [Retour aux Saisies] est défini comme suit :


  <a href="javascript:retourSaisies()">Retour aux saisies</a>

La fonction JS [retourSaisies] est définie dans le fichier [myScripts-05.js] de la façon suivante :


// données globales
var content;
var loading;

function calculer() {
...
}

function retourSaisies() {
  // on fait un appel Ajax à la main
  $.ajax({
    url: '/Premier/Action05RetourSaisies',
    type: 'POST',
    dataType: 'html',
    beforeSend: function () {
      loading.show();
    },
    success: function (data) {
      content.html(data);
    },
    complete: function () {
      loading.hide();
      // IMPORTANT !! validation
      $.validator.unobtrusive.parse($("#formulaire"));
    },
    error: function (jqXHR) {
      content.html(jqXHR.responseText);
    }
  })
}

function effacer() {
...
}

// au chargement du document
$(document).ready(function () {
  // on récupère les références des différents composants de la page
  loading = $("#loading");
  content = $("#content");
  // on cache l'image animée
  loading.hide();
});
  • lignes 11-29 : un appel Ajax ;
  • ligne 12 : l'URL cible ;
  • ligne 13 : elle sera demandée par une commande HTTP POST. C'est un POST sans paramètres postés. C'est pourquoi, on ne trouve pas une ligne du type :
    data: formulaire.serialize(),

dans l'appel Ajax ;

  • ligne 14 : le flux attendu du serveur est un flux HTML ;
  • lignes 18-20 : ce flux HTML servira à mettre à jour la région d'id [content] ;

L'action serveur [Action05RetourSaisies] est la suivante :


    [HttpPost]
    public PartialViewResult Action05RetourSaisies(SessionModel session)
    {
      // vue
      return PartialView("Formulaire05", new ViewModel05() { A = session.A, B = session.B });
}
  • ligne 2 : l'action reçoit pour paramètre le modèle de la session dans lequel nous avons mémorisé précédemment les valeurs de A et B saisies ;
  • ligne 5 : on retourne la vue partielle [Formulaire05] avec un modèle de type [ViewModel05] dans lequel on prend soin d'initialiser les champs A et B avec les valeurs de A et B prises dans la session ;

Maintenant revenons au code de la fonction Javascript [retourSaisies] :


function retourSaisies() {
  // on fait un appel Ajax à la main
  $.ajax({
    url: '/Premier/Action05RetourSaisies',
    type: 'POST',
    dataType: 'html',
    beforeSend: function () {
      loading.show();
    },
    success: function (data) {
      content.html(data);
    },
    complete: function () {
      loading.hide();
      // IMPORTANT !! validation
      $.validator.unobtrusive.parse($("#formulaire"));
    },
    error: function (jqXHR) {
      content.html(jqXHR.responseText);
    }
  })
}
  • ligne 13 : la méthode exécutée lorsque l'appel Ajax est terminé ;
  • ligne 14 : l'image animée d'attente est cachée ;
  • ligne 16 : une instruction un peu hermétique pour moi trouvée sur le net pour résoudre le problème suivant : dans le formulaire affiché par le lien [Retour aux saisies], les validateurs côté client ne fonctionnaient plus. En cherchant des informations sur la bibliothèque JS [jquery.unobtrusive-ajax], j'ai trouvé la solution de la ligne 16. Elle parse le formulaire, peut-être pour activer les validateurs côté client.

7.7. Rendre accessible sur Internet une application ASP.NET

Voir le paragraphe 9.26.

7.8. Génération d'une application native pour Android à partir d'une application à page unique APU

Voir le paragraphe 9.27.