Partager via


ASP.NET

Applications sur une seule page : créer des applications Web modernes et réactives avec ASP.NET

Mike Wasson

Télécharger l'exemple de code

Les applications sur une seule page sont des applications Web qui chargent une seule page HTML et la mettent à jour dynamiquement lorsque l'utilisateur interagit avec l'application.

Elles utilisent AJAX et HTML5 pour créer des applications Web fluides et réactives qui n'ont pas besoin de recharger constamment les pages. Toutefois, cela signifie que la plus grande partie du travail a lieu côté client, en JavaScript. Pour un développeur ASP.NET classique, il peut être difficile de sauter le pas. Fort heureusement, de nombreuses infrastructures JavaScript open source facilitent la création de ces applications sur une seule page.

Dans cet article, je détaillerai les étapes de la création d'une application simple sur une seule page. Je présenterai également certains concepts de base de la création des applications sur une seule page, dont les modèles MVC (Model-View-Controller) et MVVM (Model-View-ViewModel), la liaison de données et le routage.

À propos de l'exemple d'application

L'exemple d'application que j'ai créé est une simple base de données de films, illustrée à la figure 1. La colonne située totalement à gauche affiche une liste de genres. Lorsque vous cliquez sur un genre, une liste de films appartenant à ce genre s'affiche. Pour modifier une entrée, il suffit de cliquer sur le bouton Edit situé en regard de celle-ci. Une fois les modifications effectuées, vous pouvez cliquer sur Save pour envoyer la mise à jour au serveur, ou sur Cancel pour les annuler.

The Single-Page Application Movie Database App
Figure 1 Application de base de données de films sur une seule page

J'ai créé deux versions de l'application, la première à l'aide de la bibliothèque Knockout.js et la seconde, à l'aide de la bibliothèque Ember.js. Ces deux bibliothèques ont des approches différentes, il est donc instructif de les comparer. Dans les deux cas, l'application cliente comporte moins de 150 lignes de JavaScript. Côté serveur, j'ai utilisé l'API Web ASP.NET pour servir JSON au client. Vous trouverez le code source des deux versions de l'application à l'adresse github.com/MikeWasson/MoviesSPA.

Remarque : j'ai créé l'application à l'aide de la version finale (RC) de Visual Studio 2013. Il est possible que des modifications soient apportées à la version finalisée (RTM), mais cela ne devrait pas avoir d'impact sur le code.

Contexte

Dans une application Web classique, chaque fois que le serveur est appelé, il rend une nouvelle page HTML. Cela déclenche une actualisation de la page dans le navigateur. Si vous avez déjà écrit une application Web Forms ou PHP, ce cycle de vie de la page ne vous sera pas étranger.

Dans une application sur une seule page, une fois la première page chargée, toute interaction avec le serveur se déroule par l'intermédiaire des appels AJAX. Ces appels AJAX retournent des données, et non des balises, généralement au format JSON. L'application utilise des données JSON pour mettre à jour la page dynamiquement, sans avoir besoin de la recharger. La figure 2 illustre la différence entre les deux approches.

The Traditional Page Lifecycle vs. the SPA Lifecycle
Figure 2 Le cycle de vie d'une page classique comparé à celui d'une application sur une seule page

L'un des avantages d'une application sur une seule page est évident : les applications sont plus fluides et réactives, car elles ne souffrent pas de l'effet gênant provoqué par le rechargement et le nouveau rendu de la page. Un autre avantage est peut-être moins évident : il concerne l'architecture de votre application Web. L'envoi des données de l'application au format JSON crée une séparation entre la présentation (balisage HTML) et la logique de l'application (les requêtes AJAX et les réponses JSON).

Cette séparation facilite la conception et l'évolution de chaque couche. Dans une application sur une seule page bien construite, vous pouvez modifier le balisage HTML sans toucher au code qui implémente la logique de l'application (ou, tout au moins, dans un monde idéal). Vous verrez comment cela fonctionne lorsque j'aborderai la liaison de données plus loin dans cet article.

Dans une application sur une seule page pure, toute interaction avec l'interface utilisateur se produit côté client, par l'intermédiaire de JavaScript et CSS. Une fois la page initiale chargée, le serveur joue strictement un rôle de couche de service. Le client a simplement besoin de savoir quelles requêtes HTTP doivent être envoyées. Il ne se préoccupe pas de savoir comment le serveur implémente ensuite les choses à la fin.

Grâce à cette architecture, le client et le service sont indépendants. Vous pouvez remplacer tout le serveur principal qui exécute le service et tant que vous ne modifiez pas l'API, le client ne sera pas rompu. L'inverse est également vrai, à savoir que vous pouvez remplacer toute l'application cliente sans modifier la couche de service. Par exemple, vous pouvez écrire un client mobile natif qui utilise le service.

Création du projet Visual Studio

Visual Studio 2013 comporte un seul type de projet d'application Web ASP.NET. L'assistant de projet vous permet de sélectionner les composants ASP.NET à inclure dans votre projet. J'ai commencé par le modèle Vide, puis j'ai ajouté l'API Web ASP.NET au projet en sélectionnant API Web sous « Ajouter les dossiers et les références principales pour : », comme illustré à la figure 3.

Creating a New ASP.NET Project in Visual Studio 2013
Figure 3 Création d'un projet ASP.NET dans Visual Studio 2013

Le nouveau projet comporte toutes les bibliothèques nécessaires à l'API Web, ainsi que du code de configuration de l'API Web. Je n'ai pris aucune dépendance sur Web Forms ou ASP.NET MVC.

Vous remarquerez à la figure 3 que Visual Studio 2013 comprend un modèle d'application sur une seule page. Ce modèle installe la structure de base d'une application sur une seule page construite sur Knockout.js. Il prend en charge la connexion à l'aide d'une base de données des membres ou d'un fournisseur d'authentification externe. Je n'ai pas eu recours à ce modèle dans mon application car je souhaitais montrer un exemple plus simple en partant de zéro. Le modèle d'application sur une seule page est toutefois une ressource exceptionnelle, notamment si vous souhaitez ajouter l'authentification à votre application.     

Création de la couche de service

J'ai utilisé une API Web ASP.NET afin de créer une API REST simple pour l'application. Je ne m'étendrai pas sur l'API Web ici, vous trouverez de bien plus amples informations sur asp.net/web-api.

J'ai commencé par créer une classe Movie qui représente un film. Cette classe effectue deux actions :

  • Elle indique à Entity Framework (RF) comment créer les tables de la base de données afin de stocker les données de film.
  • Elle indique à l'API Web comment mettre en forme la charge utile JSON.

Vous n'avez pas besoin d'utiliser le même modèle pour les deux. Par exemple, vous pouvez opter pour un schéma de base de données différent des charges utiles JSON. Dans le cadre de cette application, j'ai misé sur la simplicité :

namespace MoviesSPA.Models
{
  public class Movie
  {
    public int ID { get; set; }
    public string Title { get; set; }
    public int Year { get; set; }
    public string Genre { get; set; }
    public string Rating { get; set; }
  }
}

J'ai ensuite utilisé la structure de Visual Studio pour créer un contrôleur d'API Web qui utilise EF comme couche de données. Pour utiliser cette structure, cliquez avec le bouton droit sur le dossier Contrôleurs dans l'Explorateur de solutions, puis sélectionnez Ajouter | Nouvel élément structuré. Dans l'assistant d'ajout de structure, sélectionnez « Contrôleur d'API Web 2 avec actions, à l'aide d'Entity Framework », comme illustré à la figure 4.

Adding a Web API Controller
Figure 4 Ajout d'un contrôleur d'API Web

La figure 5 montre l'assistant Ajouter un contrôleur. J'ai nommé le contrôleur MoviesController. Le nom est important car les URI de l'API REST reposent sur le nom du contrôleur. J'ai également activé « Utiliser les actions de contrôleur asynchrones » pour profiter de la nouvelle fonctionnalité asynchrone disponible dans EF 6. J'ai sélectionné la classe Movie pour le modèle, puis « Nouveau contexte de données » afin de créer un nouveau contexte de données EF.

The Add Controller Wizard
Figure 5 L'assistant Ajouter un contrôleur

L'assistant ajoute deux fichiers :

  • MoviesController.cs définit le contrôleur de l'API Web qui implémente l'API REST pour l'application.
  • MovieSPAContext.cs est en fait une colle EF qui fournit les méthodes permettant d'interroger la base de données sous-jacente.

La figure 6 illustre l'API REST par défaut créée par la structure.

Figure 6 API REST par défaut créée par la structure de l'API Web

Verbe HTTP URI Description
GET /api/movies Obtenir une liste de tous les films
GET /api/movies/{id} Obtenir le film dont l'ID est {id}
PUT /api/movies/{id} Mettre à jour le film dont l'ID est {id}
POST /api/movies Ajouter un nouveau film à la base de données
DELETE /api/movies/{id} Supprimer un film de la base de données

Les valeurs entre accolades sont des espaces réservés. Par exemple, pour obtenir un film dont l'ID est 5, vous utiliserez l'URI /api/movies/5.

J'ai étendu cette API en ajoutant une méthode qui recherche tous les films d'un genre spécifié :

public class MoviesController : ApiController
{
  public IQueryable<Movie> GetMoviesByGenre(string genre)
  {
    return db.Movies.Where(m =>
      m.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase));
  }
  // Other code not shown

Le client place le genre dans une chaîne de requête de l'URI. Par exemple, pour obtenir tous les films du genre Drame, le client envoie une requête GET à /api/movies?genre=drama. L'API Web lie automatiquement le paramètre de requête au paramètre de genre dans la méthode GetMoviesByGenre.

Création du client Web

Jusqu'à présent, j'ai simplement créé une API REST. Si vous envoyez une requête GET à /api/movies?genre=drama, la réponse HTTP brute ressemblera à ce qui suit :

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Date: Tue, 10 Sep 2013 15:20:59 GMT
Content-Length: 240
[{"ID":5,"Title":"Forgotten Doors","Year":2009,"Genre":"Drama","Rating":"R"}, {"ID":6,"Title":"Blue Moon June","Year":1998,"Genre":"Drama","Rating":"PG-13"},{"ID":7,"Title":"The Edge of the Sun","Year":1977,"Genre":"Drama","Rating":"PG-13"}]

Je dois maintenant écrire une application cliente qui saura utiliser ces données. Le flux de travail de base est le suivant :

  • L'interface utilisateur déclenche une requête AJAX
  • Mise à jour du HTML pour afficher la charge utile de la réponse
  • Gestion des erreurs AJAX

Vous pourriez coder tout cela manuellement. Par exemple, voici un code jQuery qui crée une liste des titres de film :

$.getJSON(url)
  .done(function (data) {
    // On success, "data" contains a list of movies
    var ul = $("<ul></ul>")
    $.each(data, function (key, item) {
      // Add a list item
      $('<li>', { text: item.Title }).appendTo(ul);
    });
  $('#movies').html(ul);
});

Ce code présente quelques problèmes. Il mélange la logique de l'application à celle de la présentation et il est étroitement lié à votre HTML. Il est en outre fastidieux à écrire. Au lieu de vous concentrer sur votre application, vous passez votre temps à écrire des gestionnaires d'événements et du code pour manipuler le DOM.

La solution consiste à créer au-dessus de l'infrastructure JavaScript. Fort heureusement, vous pouvez choisir parmi de nombreuses infrastructures JavaScript open source. Les plus populaires d'entre elles sont notamment Backbone, Angular, Ember, Knockout, Dojo et JavaScriptMVC. La plupart d'entre elles utilisent une variation des modèles MVC ou MVVM, c'est pourquoi il peut être intéressant de revoir ces derniers.

Modèles MVC et MVVM

Le modèle MVC date des années 1980 et des premières interfaces utilisateur graphiques. L'objectif de ce modèle est de factoriser le code en trois responsabilités distinctes, comme illustré à la figure 7. Voici comment les choses se passent :

  • Le modèle représente les données de domaine et la logique métier.
  • La vue affiche le modèle.
  • Le contrôleur reçoit les entrées utilisateur et met à jour le modèle.

The MVC Pattern
Figure 7 Le modèle MVC

Le modèle MVVM est une variante plus récente du MVC (voir la figure 8). Dans le cadre de MVVM :

  • Le modèle représente toujours les données de domaine.
  • Le modèle de vue est une représentation abstraite de la vue.
  • La vue affiche le modèle de vue et envoie les entrées utilisateur au modèle de vue.

The MVVM Pattern
Figure 8 Le modèle MVVM

Dans le cadre d'une infrastructure MVVM JavaScript, la vue correspond au balisage et le modèle de vue, au code.

MVC comporte de nombreuses variantes et la documentation disponible sur MVC est souvent confuse et contradictoire. Cela n'est peut-être pas surprenant pour un modèle de conception qui a commencé avec Smalltalk-76 et qui est toujours utilisé dans les applications Web actuelles. Par conséquent, bien qu'il soit intéressant de connaître la théorie, il est surtout important de comprendre l'infrastructure MVC que vous utilisez.

Création du client Web à l'aide de Knockout.js.

Pour la première version de mon application, j'ai utilisé la bibliothèque Knockout.js. Knockout suit le modèle MVVM en utilisant la liaison de données pour relier la vue au modèle de vue.

Pour créer des liaisons de données, vous ajoutez un attribut de liaison de données spécial aux éléments HTML. Par exemple, le balisage suivant lie l'élément span à une propriété nommée genre sur le modèle de vue. Dès que la valeur du genre change, Knockout met automatiquement à jour le HTML :

<h1><span data-bind="text: genre"></span></h1>

Les liaisons peuvent également fonctionner dans l'autre sens, par exemple si l'utilisateur entre du texte dans une zone de texte, Knockout met à jour la propriété correspondante dans le modèle de vue.

La liaison de données présente l'avantage d'être déclarative. Vous n'avez pas besoin de relier le modèle de vue aux éléments de la page HTML. Il suffit d'ajouter l'attribut de liaison de données et Knockout se charge du reste.

J'ai commencé par créer une page HTML avec la disposition de base, sans liaison de données, comme illustré à la figure 9.

Remarque : j'ai eu recours à la bibliothèque Bootstrap pour appliquer un style à l'application, c'est pourquoi la véritable application comporte de nombreux éléments <div> et classes CSS supplémentaires pour contrôler la mise en forme. Je ne les ai pas inclus dans les exemples de code pour plus de clarté.

Figure 9 Disposition HTML initiale

<!DOCTYPE html>
<html>
<head>
  <title>Movies SPA</title>
</head>
<body>
  <ul>
    <li><a href="#"><!-- Genre --></a></li>
  </ul>
  <table>
    <thead>
      <tr><th>Title</th><th>Year</th><th>Rating</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><!-- Title --></td>
        <td><!-- Year --></td>
        <td><!-- Rating --></td></tr>
    </tbody>
  </table>
  <p><!-- Error message --></p>
  <p>No records found.</p>
</body>
</html>

Création d'un modèle de vue

Les observables sont au cœur du système de liaison de données de Knockout. Un observable est un objet qui stocke une valeur et peut avertir les abonnés lors du changement de la valeur. Le code suivant convertit la représentation JSON d'un film en objet équivalent avec des observables :

function movie(data) {
  var self = this;
  data = data || {};
  // Data from model
  self.ID = data.ID;
  self.Title = ko.observable(data.Title);
  self.Year = ko.observable(data.Year);
  self.Rating = ko.observable(data.Rating);
  self.Genre = ko.observable(data.Genre);
};

La figure 10 représente mon implémentation initiale du modèle de vue. Cette version prend uniquement en charge l'obtention de la liste de films. J'ajouterai les fonctionnalités d'édition plus tard. Le modèle de vue contient des observables pour la liste de films, une chaîne d'erreur et le genre en cours.

Figure 10 Le modèle de vue

var ViewModel = function () {           
  var self = this;
  // View model observables
  self.movies = ko.observableArray();
  self.error = ko.observable();
  self.genre = ko.observable();  // Genre the user is currently browsing
  // Available genres
  self.genres = ['Action', 'Drama', 'Fantasy', 'Horror', 'Romantic Comedy'];
  // Adds a JSON array of movies to the view model
  function addMovies(data) {
    var mapped = ko.utils.arrayMap(data, function (item) {
      return new movie(item);
    });
    self.movies(mapped);
  }
  // Callback for error responses from the server
  function onError(error) {
    self.error('Error: ' + error.status + ' ' + error.statusText);
  }
  // Fetches a list of movies by genre and updates the view model
  self.getByGenre = function (genre) {
    self.error(''); // Clear the error
    self.genre(genre);
    app.service.byGenre(genre).then(addMovies, onError);
  };
  // Initialize the app by getting the first genre
  self.getByGenre(self.genres[0]);
}
// Create the view model instance and pass it to Knockout
ko.applyBindings(new ViewModel());

Vous remarquerez que movies est un observableArray. Comme son nom le suggère, un observableArray joue le rôle d'un tableau qui avertit les abonnés lorsque le contenu du tableau est modifié.

La fonction getByGenre effectue une requête AJAX auprès du serveur pour la liste de films, puis elle remplit le tableau self.movies avec les résultats obtenus.

Lorsque vous utilisez une API REST, l'une des parties les plus difficiles consiste à gérer la nature asynchrone du HTTP. La fonction AJAX jQuery retourne un objet qui implémente l'API Promises. Vous pouvez utiliser la méthode then d'un objet Promise pour définir un rappel appelé en cas de réussite de l'appel AJAX et un autre appelé en cas d'échec de l'appel AJAX :

app.service.byGenre(genre).then(addMovies, onError);

Liaisons de données

Maintenant que je dispose d'un modèle de vue, je peux lier le HTML à celui-ci via les données. Pour la liste de genres qui apparaît sur le côté gauche de l'écran, j'ai utilisé les liaisons de données suivantes :

<ul data-bind="foreach: genres">
  <li><a href="#"><span data-bind="text: $data"></span></a></li>
</ul>

L'attribut data-bind contient une ou plusieurs déclarations de liaison, dans lesquelles chaque liaison a la forme « binding: expression ». Dans cet exemple, la liaison foreach indique à Knockout de parcourir en boucle le contenu du tableau des genres dans le modèle de vue. Pour chaque élément du tableau, Knockout crée un nouvel élément <li>. La liaison de texte de la balise <span> définit le texte à parcourir comme étant égal à l'élément de tableau, à savoir le nom du genre dans le cas présent.

À ce stade, un clic sur les noms de genre n'aura aucun effet, c'est pourquoi j'ai ajouté une liaison de clics pour gérer les événements de clic :

<li><a href="#" data-bind="click: $parent.getByGenre">
  <span data-bind="text: $data"></span></a></li>

L'événement click est ainsi lié à la fonction getByGenre sur le modèle de vue. J'ai dû utiliser $parent ici car la liaison a lieu dans le contexte de foreach. Par défaut, les liaisons au sein de foreach font référence à l'élément actuel de la boucle.

Pour afficher la liste de films, j'ai ajouté des liaisons à la table, comme illustré à la figure 11.

Figure 11 Ajout de liaisons à la table pour afficher une liste de films

<table data-bind="visible: movies().length > 0">
  <thead>
    <tr><th>Title</th><th>Year</th><th>Rating</th><th></th></tr>
  </thead>
  <tbody data-bind="foreach: movies">
    <tr>
      <td><span data-bind="text: Title"></span></td>
      <td><span data-bind="text: Year"></span></td>
      <td><span data-bind="text: Rating"></span></td>
      <td><!-- Edit button will go here --></td>
    </tr>
  </tbody>
</table>

À la figure 11, la liaison foreach effectue une boucle sur un tableau d'objets de film. Au sein de foreach, les liaisons de texte font référence aux propriétés sur l'objet en cours.

La liaison visible sur l'élément <table> vérifie si la table est rendue. La table sera ainsi masquée si le tableau de films est vide.

Enfin, voici les liaisons du message d'erreur et le message « Aucun enregistrement trouvé » (vous remarquerez que vous pouvez insérer des expressions complexes dans une liaison) :

<p data-bind="visible: error, text: error"></p>
<p data-bind="visible: !error() && movies().length == 0">No records found.</p>

Des enregistrements modifiables

La dernière partie de cette application donne à l'utilisateur la possibilité de modifier les enregistrements d'une table. Cela implique plusieurs parties de fonctionnalité :

  • Basculement entre le mode d'affichage (texte brut) et le mode de modification (contrôles d'entrée).
  • Envoi des mises à jour au serveur.
  • Permettre à l'utilisateur d'annuler une modification et de revenir aux données initiales.

Pour assurer le suivi du mode d'affichage/de modification, j'ai ajouté un indicateur Boolean à l'objet de film, en tant qu'observable :

function movie(data) {
  // Other properties not shown
  self.editing = ko.observable(false);
};

Je souhaitais que la table des films affiche du texte lorsque la propriété de modification est false, mais passe aux contrôles d'entrée lorsque la modification est true. Pour cela, j'ai eu recours aux liaisons if et ifnot de Knockout, comme illustré à la figure 12. La syntaxe « <!-- ko --> » vous permet d'inclure des liaisons if et ifnot sans les insérer dans un élément de conteneur HTML.

Figure 12 Permettre la modification des enregistrements de film

<tr>
  <!-- ko if: editing -->
  <td><input data-bind="value: Title" /></td>
  <td><input type="number" class="input-small" data-bind="value: Year" /></td>
  <td><select class="input-small"
    data-bind="options: $parent.ratings, value: Rating"></select></td>
  <td>
    <button class="btn" data-bind="click: $parent.save">Save</button>
    <button class="btn" data-bind="click: $parent.cancel">Cancel</button>
  </td>
  <!-- /ko -->
  <!-- ko ifnot: editing -->
  <td><span data-bind="text: Title"></span></td>
  <td><span data-bind="text: Year"></span></td>
  <td><span data-bind="text: Rating"></span></td>
  <td><button class="btn" data-bind="click: $parent.edit">Edit</button></td>
  <!-- /ko -->
</tr>

La liaison de valeur définit la valeur d'un contrôle d'entrée. Il s'agit d'une liaison bidirectionnelle. Par conséquent, lorsque l'utilisateur saisit des données dans le champ de texte ou modifie la sélection de la liste déroulante, la modification est appliquée automatiquement au modèle de vue.

J'a lié les gestionnaires de clics de bouton aux fonctions nommées save, cancel et edit dans le modèle de vue.

La fonction de modification est simple. Il suffit de définir l'indicateur de modification sur true :

self.edit = function (item) {
  item.editing(true);
};

L'enregistrement et l'annulation sont légèrement plus complexes. Afin de prendre en charge l'annulation, je devais trouver un moyen de mettre en cache la valeur initiale lors de la modification. Fort heureusement, Knockout facilite l'extension du comportement des observables. Le code de la figure 13 ajoute une fonction de stockage à la classe observable. Grâce à l'appel de la fonction de stockage sur un observable, ce dernier dispose de deux nouvelles fonctions : la restauration et la validation.

Figure 13 Extension de ko.observable avec les fonctions de restauration et de validation

Je peux maintenant appeler la fonction de stockage pour ajouter cette fonctionnalité au modèle :

function movie(data) {
  // ...
  // New code:
  self.Title = ko.observable(data.Title).store();
  self.Year = ko.observable(data.Year).store();
  self.Rating = ko.observable(data.Rating).store();
  self.Genre = ko.observable(data.Genre).store();
};

La figure 14 représente les fonctions d'enregistrement et d'annulation sur le modèle de vue.

Figure 14 Ajout des fonctions d'enregistrement et d'annulation

self.cancel = function (item) {
  revertChanges(item);
  item.editing(false);
};
self.save = function (item) {
  app.service.update(item).then(
    function () {
      commitChanges(item);
    },
    function (error) {
      onError(error);
      revertChanges(item);
    }).always(function () {
      item.editing(false);
  });
}
function commitChanges(item) {
  for (var prop in item) {
    if (item.hasOwnProperty(prop) && item[prop].commit) {
      item[prop].commit();
    }
  }
}
function revertChanges(item) {
  for (var prop in item) {
    if (item.hasOwnProperty(prop) && item[prop].revert) {
      item[prop].revert();
    }
  }
}

Création du client Web à l'aide d'Ember

À titre de comparaison, j'ai écrit une autre version de mon application à l'aide de la bibliothèque Ember.js.

Une application Ember commence par une table de routage qui définit la façon dont l'utilisateur pourra parcourir l'application :

window.App = Ember.Application.create();
App.Router.map(function () {
  this.route('about');
  this.resource('genres', function () {
    this.route('movies', { path: '/:genre_name' });
  });
});

La première ligne de code crée une application Ember. L'appel de Router.map crée trois routes. Chaque route correspond à un URI ou à un modèle d'URI :

/#/about
/#/genres
/#/genres/genre_name

Pour chaque route, vous créez un modèle HTML à l'aide de la bibliothèque de modèles Handlebars.

Ember propose un modèle de niveau supérieur pour toute l'application. Ce modèle est rendu pour chaque route. La figure 15 illustre le modèle d'application de mon application. Comme vous pouvez le constater, le modèle est essentiellement constitué de HTML inséré dans une balise de script avec le type « text/x-handlebars ». Il contient une balise Handlebars spéciale insérée dans des accolades doubles. {{ }}. Cette balise a un objectif similaire à celui de l'attribut data-bind de Knockout. Par exemple, {{#linkTo}} permet de créer un lien vers une route.

Figure 15 Modèle Handlebars au niveau de l'application

ko.observable.fn.store = function () {
  var self = this;
  var oldValue = self();
  var observable = ko.computed({
    read: function () {
      return self();
    },
    write: function (value) {
      oldValue = self();
      self(value);
    }
  });
  this.revert = function () {
    self(oldValue);
  }
  this.commit = function () {
    oldValue = self();
  }
  return this;
}
<script type="text/x-handlebars" data-template-name="application">
  <div class="container">
    <div class="page-header">
      <h1>Movies</h1>
    </div>
    <div class="well">
      <div class="navbar navbar-static-top">
        <div class="navbar-inner">
          <ul class="nav nav-tabs">
            <li>{{#linkTo 'genres'}}Genres{{/linkTo}} </li>
            <li>{{#linkTo 'about'}}About{{/linkTo}} </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="container">
      <div class="row">{{outlet}}</div>
    </div>
  </div>
  <div class="container"><p>&copy;2013 Mike Wasson</p></div>
</script>

Supposons maintenant que l'utilisateur accède à /#/about. Cela permet d'appeler la route « about ». Ember commence par rendre le modèle d'application de niveau supérieur. Puis le modèle about est rendu dans {{outlet}} du modèle d'application. Voici le modèle about :

 

<script type="text/x-handlebars" data-template-name="about">
  <h2>Movies App</h2>
  <h3>About this app...</h3>
</script>

La figure 16 montre comment le modèle about est rendu dans le modèle de l'application.

Rendering the About Template
Figure 16 Rendu du modèle about

Étant donné que chaque route a son propre URI, l'historique du navigateur est préservé. L'utilisateur peut naviguer grâce au bouton Précédent. Il peut également actualiser la page sans perdre le contexte ou le signet et recharger la même page.

Contrôleurs et modèles Ember

Dans Ember, chaque route dispose d'un modèle et d'un contrôleur. Le modèle contient les données du domaine. Le contrôleur joue un rôle de proxy pour le modèle et stocke toutes les données relatives à l'état de l'application pour la vue. Cela ne correspond pas exactement à la définition classique du MVC. Sur certains points, le contrôleur est en fait plus proche d'un modèle de vue.

Voici comment j'ai défini le modèle de film :

App.Movie = DS.Model.extend({
  Title: DS.attr(),
  Genre: DS.attr(),
  Year: DS.attr(),
  Rating: DS.attr(),
});

Le contrôleur dérive d'Ember.ObjectController, comme illustré à la figure 17.

Figure 17 Le contrôleur de film dérive d'Ember.ObjectController

App.MovieController = Ember.ObjectController.extend({
  isEditing: false,
  actions: {
    edit: function () {
      this.set('isEditing', true);
    },
    save: function () {
      this.content.save();
      this.set('isEditing', false);
    },
    cancel: function () {
      this.set('isEditing', false);
      this.content.rollback();
    }
  }
});

Plusieurs choses intéressantes ont lieu ici. Tout d'abord, je n'ai pas spécifié le modèle dans la classe de contrôleur. Par défaut, la route définit automatiquement le modèle sur le contrôleur. Ensuite, les fonctions d'enregistrement et d'annulation utilisent les fonctionnalités de transaction créées dans la classe DS.Model. Pour annuler les modifications, il suffit d'appeler la fonction de restauration sur le modèle.

Ember utilise de nombreuses conventions d'affectation de noms pour connecter différents composants. Les genres acheminent les discussions vers le GenresController qui rend le modèle de genres. En fait, Ember créera automatiquement un objet GenresController si vous n'en définissez aucun. Vous pouvez toutefois remplacer les valeurs par défaut.

Dans mon application, j'ai configuré la route genres/films de façon à utiliser un contrôleur différent en implémentant le crochet renderTemplate. De cette façon, plusieurs routes peuvent partager le même contrôleur (voir la figure 18).

Figure 18 Plusieurs routes peuvent partager le même contrôleur

App.GenresMoviesRoute = Ember.Route.extend({
  serialize: function (model) {
    return { genre_name: model.get('name') };
  },
  renderTemplate: function () {
    this.render({ controller: 'movies' });
  },
  afterModel: function (genre) {
    var controller = this.controllerFor('movies');
    var store = controller.store;
    return store.findQuery('movie', { genre: genre.get('name') })
    .then(function (data) {
      controller.set('model', data);
  });
  }
});

Avec Ember, vous apprécierez de pouvoir faire des choses avec très peu de code. Mon exemple d'application comporte environ 110 lignes de JavaScript. C'est plus court que ma version Knockout et j'obtiens gratuitement un historique de navigateur. Notez cependant qu'Ember est également une infrastructure extrêmement rigide. Si vous n'écrivez pas votre code « à la façon Ember », vous risquez de rencontrer des difficultés. Lorsque vous choisissez une infrastructure, assurez-vous que l'ensemble de fonctionnalités et la conception globale de l'infrastructure correspondent à vos besoins et à votre style de codage.

En savoir plus

Au cours de cet article, j'ai montré en quoi les infrastructures JavaScript facilitent la création d'applications sur une seule page. J'ai également présenté certaines fonctionnalités courantes de ces bibliothèques, dont la liaison des données, le routage et les modèles MVC et MVVM. Pour en savoir plus sur la création d'applications sur une seule page avec ASP.NET, consultez asp.net/single-page-application.

Mike Wasson est programmeur et auteur chez Microsoft. Il a travaillé sur la documentation des API multimédia Win32 durant de nombreuses années. Il écrit désormais sur ASP.NET, en ciblant l'API Web. Vous pouvez le contacter à l'adresse mwasson@microsoft.com.

Merci à l'expert technique suivant d'avoir relu cet article : Xinyang Qiu (Microsoft)
Xinyang Qiu est ingénieur senior en conception logicielle dans le domaine des tests au sein de l'équipe ASP.NET de Microsoft. Il est également un blogueur actif pour blogs.msdn.com/b/webdev. Il est ravi de répondre aux questions ASP.NET ou de demander à des experts de répondre à vos questions. Vous pouvez le joindre à l'adresse xinqiu@microsoft.com.