MSDN Magazine > Home > Issues > 2008 > March >  ASP.NET MVC: Création d'applications Web s...
ASP.NET MVC
Création d'applications Web sans Web Forms
Chris Tavares

Cet article s'appuie sur une version préliminaire de l'infrastructure ASP.NET MVC. Il est possible que certains détails soient modifiés ultérieurement.
Cet article aborde les sujets suivants:
  • Modèle MVC (Model View Controller)
  • Création de contrôleurs et de vues
  • Création et publication de formulaires
  • Fabriques de contrôleurs et autres points d'extensibilité
Cet article utilise les technologies suivantes:
ASP.NET
Téléchargement du code disponible sur: MVCFramework2008_03.exe (189 KB)
Browse the Code Online
Je suis développeur professionnel depuis à peu près 15 ans maintenant et j'ai pratiqué cette activité en amateur au moins 10 ans avant cela. Comme la plupart des gens de ma génération, j'ai démarré sur des machines 8 bits, puis je suis passé à la plate-forme PC. Alors que je travaillais avec des machines de plus en plus complexes, je me suis mis à écrire des applications allant de petits jeux à la gestion de données personnelles et au contrôle du matériel externe.
Pendant la première moitié de ma carrière, cependant, tous les logiciels que j'ai écrits avaient une chose en commun : il s'agissait toujours d'applications locales s'exécutant sur l'ordinateur de bureau d'un utilisateur. Au début des années 90, j'ai commencé à entendre parler de cette nouvelle chose appelée le Web. J'y ai vu une opportunité de créer une application Web qui me permette d'entrer mes informations de feuille de présence sans devoir quitter mon site de travail pour retourner à mon bureau.
L'expérience fut, en un mot, confondante. Être confronté au Web sans état ne s'accordait pas avec mon esprit orienté bureautique. Ajoutez à cela un débogage miteux, un serveur UNIX auquel je n'avais pas d'accès racine et cet étrange crochet, et mon ancien moi s'est honteusement remis au développement bureautique pendant encore quelques années.
Je suis resté à l'écart du développement Web. À l'évidence c'était quelque chose d'important, mais je ne comprenais pas vraiment le modèle de programmation. Et puis Microsoft publia ® .NET Framework et ASP.NET. Enfin une infrastructure qui me permettait de travailler sur des applications Web ! Pourtant c'était presque comme programmer des applications de bureau. Je pouvais générer des fenêtres (pages), accrocher des contrôles à des événements et le concepteur m'avait épargné l'utilisation de ces fichus crochets. Et mieux encore, ASP.NET traitait automatiquement pour moi la nature sans état du Web avec un état d'affichage ! J'étais de nouveau un programmeur heureux...au moins pour un temps.
Au fur et à mesure que mon expérience progressait, il en fut de même pour mes choix en conception. J'avais appris quelques astuces que j'ai appliquées dans mon travail sur mes applications de bureau, pour n'en citer que deux :
  • la séparation des intérêts : ne pas mélanger la logique de l'interface utilisateur avec le comportement sous-jacent.
  • le test unitaire automatisé : écrire des tests automatisés qui vérifient si votre code fait ce que vous pensez qu'il fait.
Les principes sous-jacents ici s'appliquent quelle que soit la technologie. La séparation des intérêts est un principe fondamental qui vous permet de gérer la complexité. Le mélange de différentes responsabilités dans le même objet (comme le calcul des heures de travail restantes, la mise en forme des données et la création d'un graphique) revient à parler de problèmes de maintenance. Et le test automatisé est essentiel pour obtenir un code de qualité pour la production tout en préservant votre santé mentale, en particulier quand vous mettez à jour un projet existant.
ASP.NET Web Forms permettait de démarrer très facilement, mais, d'un autre côté, essayer d'appliquer mes principes de conception à des applications Web demandait une lutte acharnée. Web Forms est résolument orienté sur l'interface utilisateur : la page en est le noyau. Vous commencez par concevoir votre interface utilisateur et à y faire glisser des contrôles. Il est très agréable de simplement commencer à lancer votre logique d'application dans les gestionnaires d'événements de la page (tout comme Visual Basic® autorisait les applications Windows®).
De plus, le test unitaire des pages est souvent difficile. Vous ne pouvez pas exécuter un objet Page tout au long de son cycle de vie sans faire le tour d'ASP.NET. Bien qu'il soit possible de tester des applications Web en envoyant des requêtes HTTP à un serveur ou en automatisant un navigateur, ce genre de test est fragile (modifiez un ID de contrôle et le test échoue), difficile à paramétrer (vous devez configurer exactement de la même façon le serveur sur chaque ordinateur de développeur) et lent d'exécution.
Quand j'ai commencé à créer des applications Web plus sophistiquées, les abstractions offertes par Web Forms, comme les contrôles, l'état d'affichage et le cycle de vie de page, ont commencé à m'irriter au lieu de m'aider. Je passais de plus en plus de temps à configurer les liaisons de données (et à écrire des tonnes de gestionnaires d'événements pour obtenir une configuration correcte). Je devais deviner comment réduire la taille d'état d'affichage pour obtenir un chargement plus rapide de mes pages. Web Forms exige la présence d'un fichier physique à chaque URL, ce que les sites dynamiques (comme un wiki, par exemple) rend difficile. Et écrire avec succès un WebControl personnalisé est un processus remarquablement complexe qui nécessite de parfaitement comprendre le cycle de vie des pages et le concepteur Visual Studio®.
Comme je travaillais chez Microsoft, j'ai eu l'opportunité de partager mon expérience des divers points douloureux de .NET et j'espère pouvoir atténuer en partie cette douleur. Une telle opportunité s'est récemment présentée grâce à ma participation comme développeur au projet Web Client Software Factory de Microsoft patterns & practices (codeplex. com/websf). L'un des éléments que l'équipe Microsoft patterns & practices intègre à ses produits finis est le test unitaire automatisé. Dans Web Client Software Factory, nous avons proposé l'utilisation du motif MVP (Model View Presenter) pour générer des formulaires Web pouvant être testés.
En résumé, au lieu de mettre votre logique dans la page, MVP vous fait créer vos pages de sorte que la page (Vue) effectue simplement des appels dans un objet séparé, le Présentateur. L'objet Présentateur exécute alors toute la logique nécessaire pour répondre à l'activité sur la vue, généralement en utilisant d'autres objets (le Modèle) pour accéder à des bases de données, exécuter la logique métier, etc. Une fois ces étapes terminées, le Présentateur met à jour l'affichage. Cette approche vous offre une capacité de test parce que le présentateur est isolé du pipeline ASP.NET. Il communique avec la vue par une interface et peut être testé en étant isolé de la page.
MVP fonctionne, mais la mise en œuvre peut être un peu compliquée. Vous avez besoin d'une interface d'affichage séparée et vous devez écrire de nombreuses fonctions de transfert d'événement dans vos fichiers code-behind. Mais si vous voulez une interface utilisateur testable dans vos applications Web Forms, c'est ce que vous aurez de mieux. Toute amélioration nécessiterait une modification dans la plate-forme sous-jacente.

Modèle Model View Controller (MVC)
Heureusement, l'équipe ASP.NET a écouté les développeurs comme moi et a commencé à développer une nouvelle infrastructure d'application Web qui collabore avec les formulaires Web que vous connaissez et affectionnez tant, mais dont les objectifs de conception sont très différents :
  • Utilisation du HTTP et du HTML.
  • Testabilité intégrée dès le départ.
  • Extensibilité presque sur tous les points.
  • Contrôle total sur votre production.
Cette nouvelle infrastructure est basée sur le modèle MVC (Model View Controller), d'où le nom, ASP.NET MVC. Le motif MVC a été inventé dans les années 70 dans le cadre de Smalltalk. Comme je le montrerai dans cet article, il s'ajuste en fait très bien à la nature du Web. MVC divise votre interface utilisateur en trois objets distincts : Le contrôleur, qui reçoit et contrôle les entrées ; le modèle, qui contient votre logique de domaine et l'affichage, qui génère votre sortie. Dans le contexte du Web, les entrées sont une requête HTTP et le flux de requête s'apparente à la figure 1.
Figure 1 Flux de requêtes du modèle MVC (Cliquer sur l'image pour l'agrandir)
Ceci est en fait tout à fait différent du processus dans Web Forms. Dans le modèle Web Forms, l'entrée passe dans la page (vue) et la vue est responsable à la fois du traitement de l'entrée et de la génération de la sortie. Quand il s'agit de MVC, à l'inverse, les responsabilités sont séparées.
Donc, maintenant vous pensez probablement soit : « Hé, c'est génial. Comment je l'utilise ? », soit « Pourquoi dois-je écrire trois objets alors que je ne devais en écrire qu'un avant » ? Ce sont de très bonnes questions et le mieux est de les expliquer en étudiant un exemple. Donc je vais écrire une petite application Web en utilisant l'infrastructure MVC pour démontrer ses avantages.

Création d'un contrôleur
Pour suivre cette explication, vous devez installer Visual Studio 2008 et vous procurer une copie de l'infrastructure MVC. Au moment où j'écris cet article, elle est disponible dans le CTP (Community Technology Preview) de décembre 2007 sur les Extensions ASP.NET (asp.net/downloads/3.5-extensions). Vous devrez vous procurer les extensions CTP et la Boîte à outils MVC qui inclut quelques objets d'aide très utiles. Une fois que vous avez téléchargé et installé le CTP, vous obtiendrez un nouveau type de projet dans votre boîte de dialogue Nouveau projet appelé Application Web ASP.NET.
En sélectionnant le projet d'application Web MVC, vous obtenez une solution paraissant légèrement différente du site ou de l'application Web habituel. Le modèle de solution crée une application Web avec quelques nouveaux répertoires (comme à la figure 2). En particulier, le répertoire Controllers contient les classes de contrôleur et le répertoire Views (et tous ses sous-répertoires) contient les vues.
Figure 2 La structure de projet MVC 
Je vais écrire un contrôleur très simple qui retourne un nom transmis sur l'URL. En cliquant avec le bouton droit sur le dossier Controllers et en choisissant Ajouter un élément, la boîte de dialogue Ajouter un nouvel élément s'affiche avec quelques nouveaux ajouts, notamment une classe de contrôleur MVC et plusieurs composants d'affichage MVC. Ici, j'ajoute une classe au nom très original HelloController :
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Controllers
{
    public class HelloController : Controller
    {
        [ControllerAction]
        public void Index()
        {
            ...
        }
    }
}
Une classe de contrôleur est beaucoup plus légère qu'une page. À vrai dire, les seules opérations véritablement nécessaires sont dériver de System.Web.Mvc.Controller et placer l'attribut [ControllerAction] sur vos méthodes d'action. Une action est une méthode appelée en réponse à une requête envoyée à une URL particulière. Les actions sont responsables de tout le traitement nécessaire puis d'afficher une vue. Je commence par écrire une action simple qui transmet le nom à l'affichage, comme vous pouvez le voir ici :
[ControllerAction]
 public void HiThere(string id)
 {
     ViewData["Name"] = id;
     RenderView("HiThere");
 }
La méthode d'action reçoit le nom de l'URL via le paramètre id (nous en parlerons dans un moment), elle le stocke dans la collection ViewData et affiche alors une vue nommée HiThere.
Avant d'expliquer comment cette méthode est appelée ou à quoi ressemble l'affichage, j'aimerais parler de la testabilité. Vous vous rappelez de ce que j'ai dit précédemment sur la difficulté de tester les classes Web Forms ? En fait, les contrôleurs sont beaucoup plus faciles à tester. En fait, un contrôleur peut être directement instancié, et les méthodes d'action appelées, sans infrastructure supplémentaire. Vous n'avez pas besoin d'un contexte HTTP, ni d'un serveur, juste d'un atelier de test. À titre d'exemple, j'ai inclus un test unitaire Visual Studio Team System (VSTS) pour cette classe dans la figure 3.
namespace HelloFromMVC.Tests
{
    [TestClass]
    public class HelloControllerFixture
    {
        [TestMethod]
        public void HiThereShouldRenderCorrectView()
        {
            TestableHelloController controller = new 
              TestableHelloController();
            controller.HiThere("Chris");

            Assert.AreEqual("Chris", controller.Name);
            Assert.AreEqual("HiThere", controller.ViewName);
        }

    }

    class TestableHelloController : HelloController
    {
        public string Name;
        public string ViewName;

        protected override void RenderView(
            string viewName, string master, object data)
        {
            this.ViewName = viewName;
            this.Name = (string)ViewData["Name"];
        }
    }

}

Il se passe plusieurs choses ici. Le test en lui-même est très simple : instanciez le contrôleur, appelez la méthode avec les données prévues, puis vérifiez que la bonne vue est affichée. J'effectue la vérification en créant une sous-classe spécifique au test qui remplace la méthode RenderView. Ceci me permet d'éviter la création de code HTML. Ce qui m'importe, c'est que les bonnes données ont été envoyées à la vue et que la bonne vue est affichée. Je ne me préoccupe pas des détails sous-jacents de l'affichage lui-même pour ce test.

Création d'une vue
Bien sûr, il faut enfin que je produise du code HTML, donc créons cette vue HiThere. Pour ce faire, je crée d'abord un nouveau dossier dans la solution nommée Hello sous le dossier des vues. Par défaut, le contrôleur cherchera une vue dans le dossier Views\<ControllerPrefix> (le préfixe de contrôleur est le nom de la classe de contrôleur moins le mot « Controller »). Donc pour les vues affichées par le contrôleur HelloController, il cherche dans Views\Hello. À la fin, la solution ressemble à la figure 4.
Figure 4 Ajout d'une vue au projet (Cliquer sur l'image pour l'agrandir)
Le code HTML pour la vue apparaît comme ceci :
<html  >
<head runat="server">
    <title>Hi There!</title>
</head>
<body>
    <div>
        <h1>Hello, <%= ViewData["Name"] %></h1>
    </div>
</body>
</html>
Plusieurs choses doivent vous sauter aux yeux. Il n'y a pas de balises runat ="server". Il n'y a pas de balise form. Il n'y a pas de déclaration de contrôle. En fait, ceci ressemble beaucoup plus à de l'ASP classique qu'à de l'ASP.NET. Notez que les affichages MVC ont seulement pour rôle de générer une sortie, donc ils n'ont pas besoin de la gestion des événements ou des contrôles complexes qu'assurent les pages Web Forms.
L'infrastructure MVC emprunte le format de fichier .aspx comme langage de modèle de texte utile. Vous pouvez même utiliser du code-behind si vous le voulez, mais par défaut le fichier code-behind apparaît comme ceci :
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Views.Hello
{
    public partial class HiThere : ViewPage
    {
    }
}
Pas de méthodes page Init ou load, pas de gestionnaires d'événements, rien sauf la déclaration de la classe de base, qui n'est pas Page mais ViewPage. Ceci est tout ce dont vous avez besoin pour une vue MVC. Exécutez l'application, accédez au dossier http//localhost:<port>/Hello/HiThere/Chris et vous verrez quelque chose similaire à la figure 5.
Figure 5 Vue MVC réussie (Cliquer sur l'image pour l'agrandir)
Si, au lieu de la figure 5, vous voyez une vilaine exception, ne vous affolez pas. Si le fichier HiThere.aspx est défini comme document actif dans Visual Studio lorsque vous appuyez sur F5, Visual Studio tentera d'accéder au fichier .aspx directement. Puisque les affichages MVC nécessitent que le contrôleur se soit d'abord exécuté, il est inutile d'essayer d'accéder directement à la page. Modifiez juste l'URL pour qu'elle corresponde à ce que vous voyez dans la figure 5 et tout devrait bien se passer.
Comment l'infrastructure MVC saurait-elle appeler ma méthode d'action ? Il n'y avait même pas d'extension de fichier pour cette URL. La réponse est le routage d'URL. Si vous regardez dans le fichier global.asax.cs, vous verrez le morceau de code à la figure 6. Le RouteTable global enregistre une collection d'objets Route. Chaque Route décrit un formulaire URL et quoi en faire. Par défaut, deux routages sont ajoutés à la table. Le premier est celui par qui la magie se produit. Il dit que pour chaque URL constitué de trois parties après le nom du serveur, la première partie doit être prise comme nom de contrôleur, la seconde comme nom d'action et la troisième comme paramètre ID :
public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        // Change Url= to Url="[controller].mvc/[action]/[id]" 
        // to enable automatic support on IIS6 

        RouteTable.Routes.Add(new Route
        {
            Url = "[controller]/[action]/[id]",
            Defaults = new { action = "Index", id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });

        RouteTable.Routes.Add(new Route
        {
            Url = "Default.aspx",
            Defaults = new { 
                controller = "Home", 
                action = "Index", 
                id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });
    }
}

Url = "[controller]/[action]/[id]"
Ce routage par défaut est ce qui a permis l'invocation de ma méthode HiThere. Vous rappelez-vous de cette URL : http://localhost/Hello/HiThere/Chris ? Ce routage mappait Hello au contrôleur, HiThere à l'action et Chris à l'ID. L'infrastructure MVC a alors créé une instance HelloController, appelé la méthode HiThere et passé Chris comme valeur du paramètre ID.
Ce routage par défaut vous apporte beaucoup, mais vous pouvez aussi ajouter vos propres routages. Par exemple, je veux un site vraiment convivial où les gens n'ont qu'à entrer leur nom pour recevoir une formule de salutation personnalisée. Si j'ajoute ce routage en haut de la table de routage
  RouteTable.Routes.Add(new Route
  {
    Url = "[id]",
    Defaults = new { 
        controller = "Hello", 
        action = "HiThere" },
    RouteHandler = typeof(MvcRouteHandler)
  });
je peux simplement aller à http://localhost/Chris et mon action est toujours invoquée et je vois ma salutation habituelle.
Comment le système a-t-il su quel contrôleur et quelle action invoquer ? La réponse est dans le paramètre Defaults. Il utilise la nouvelle syntaxe de type anonyme C# 3.0 pour créer un pseudo-dictionnaire. L'objet Defaults sur le routage peut contenir des informations supplémentaires arbitraires, mais pour MVC il peut également contenir certaines entrées connues : contrôleur et action. S'il n'y a pas de contrôleur ou d'action spécifié dans l'URL, il utilisera alors le nom dans Defaults. C'est pourquoi je peux les laisser en dehors de l'URL et ma requête sera toujours mappée au bon contrôleur et à la bonne action.
Une chose supplémentaire à noter : vous souvenez-vous que j'ai dit « ajoute en haut de la table » ? Si vous le mettez en bas, vous obtiendrez une erreur. Le routage fonctionne sur une base premier venu, premier servi. Lorsqu'il traite les URL, le système de routage parcourt la table du haut en bas et le premier routage correspondant l'emporte. Dans ce cas, l'itinéraire par défaut « [controller]/[action]/[id] » correspond parce qu'il y a des valeurs par défaut pour l'action et l'ID. Ainsi, il recherche ChrisController, et puisque je n'ai pas de contrôleur je reçois une erreur.

Un exemple plus large
Maintenant que j'ai démontré les principes fondamentaux de l'infrastructure MVC, j'aimerais vous présenter un exemple plus large qui va plus loin que le simple affichage d'une chaîne. Un wiki est un site Web qui peut être modifié dans le navigateur. Il est facile d'ajouter ou de modifier des pages. J'ai écrit un petit exemple de wiki en utilisant l'infrastructure MVC. L'écran « Modifier cette page » est illustré à la figure 7.
Figure 7 Modification de la page d'accueil (Cliquer sur l'image pour l'agrandir)
Vous pouvez observer le téléchargement de code pour cet article pour voir comment la logique de wiki sous-jacente est implémentée. Maintenant je veux me concentrer sur la façon dont l'infrastructure MVC facilite la mise en place du wiki sur le Web. J'ai commencé par concevoir ma structure d'URL. Je voulais ce qui suit :
  • /[pagename] affiche la page avec ce nom.
  • /[pagename]?version=n affiche la version demandée de la page, où 0 = la version actuelle, 1 = la précédente et ainsi de suite.
  • /Edit/[pagename] ouvre l'écran de modification pour cette page.
  • /CreateNewVersion/[pagename] est l'URL publié pour soumettre une modification.
Démarrons l'affichage de base d'une page wiki. Pour ce faire, j'ai créé une nouvelle classe nommée WikiPageController. J'ai ensuite ajouté une action nommée ShowPage. Le WikiPageController a alors commencé à ressembler à la figure 8. La méthode ShowPage est assez simple. Les classes WikiSpace et WikiPage représentent respectivement une série de pages wiki et une page spécifique (et ses révisions). Cette action charge le modèle et appelle RenderView. Mais qu'est-ce que cette ligne « new WikiPageViewData » ici ?
public class WikiPageController : Controller 
{
  ISpaceRepository repository;

  public ISpaceRepository Repository 
  {
    get {
      if (repository == null) 
      {
        repository = new FileBasedSpaceRepository(
            Request.MapPath("~/WikiPages"));
      }
      return repository;
    }

    set { repository = value; }
  }

  [ControllerAction]
  public void ShowPage(string pageName, int? version) 
  {
    WikiSpace space = new WikiSpace(Repository);
    WikiPage page = space.GetPage(pageName);

    RenderView("showpage", 
      new WikiPageViewData 
      { 
        Name = pageName,
        Page = page,
        Version = version ?? 0 
      });
  }
}

Mon exemple précédent a démontré une méthode pour passer des données du contrôleur à la vue : le dictionnaire ViewData. Les dictionnaires sont commodes, mais ils sont également dangereux. Ils peuvent contenir absolument n'importe quoi, vous ne recevez aucune information IntelliSense® sur le contenu et, parce que le dictionnaire ViewData est de type Dictionnaire<chaîne, objet>, vous devez tout lancer pour convertir son contenu.
Lorsque vous savez de quelles données vous aurez besoin dans la vue, vous pouvez plutôt passer l'objet ViewData fortement typé. Dans mon cas, j'ai créé un objet simple, WikiPageViewData, comme illustré à la figure 9. Cet objet apporte les informations de page wiki à la vue avec quelques méthodes d'utilitaire pour effectuer des opérations comme la version HTML du balisage wiki.
public class WikiPageViewData {

    public string Name { get; set; }
    public WikiPage Page { get; set; }
    public int Version { get; set; }

    public WikiPageViewData() {
        Version = 0;
    }

    public string NewVersionUrl {
        get {
            return string.Format("/CreateNewVersion/{0}", Name);
        }
    }

    public string Body {
        get { return Page.Versions[Version].Body; }
    }

    public string HtmlBody {
        get { return Page.Versions[Version].BodyAsHtml(); }
    }

    public string Creator {
        get { return Page.Versions[Version].Creator; }
    }

    public string Tags {
        get { return string.Join(",", Page.Versions[Version].Tags); }
    }
}

Maintenant que les données de vue sont définies, comment je les utilise ? Dans ShowPage.aspx.cs vous verrez ceci :
namespace MiniWiki.Views.WikiPage {
    public partial class ShowPage : ViewPage<WikiPageViewData>
    {
    }
}
Notez que j'ai défini la classe de base pour être de type ViewPage<WikiPageViewData>. Ceci signifie que la propriété ViewData de la page est de type WikiPageViewData et pas un dictionnaire comme dans l'exemple précédent.
Le balisage dans le fichier .aspx est assez simple :
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content 
  ID="Content1"  
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <h1><%= ViewData.Name %></h1>
  <div id="content" class="wikiContent">
    <%= ViewData.HtmlBody %>
  </div>
</asp:Content>
Notez que je n'utilise pas l'opérateur d'indexation [] lorsque je fais référence à ViewData. Au lieu de cela, puisque j'ai maintenant un ViewData fortement typé, je peux accéder à la propriété directement. Aucune conversion n'est nécessaire et Visual Studio vous fournit IntelliSense.
L'observateur astucieux aura remarqué la balise <asp:Content> dans ce fichier. Oui, les pages maîtres fonctionnent avec les vues MVC. Et elles peuvent être également des vues. Regardons le code-behind de la Page maître :
namespace MiniWiki.Views.Layouts
{
    public partial class Site :  
        System.Web.Mvc.ViewMasterPage<WikiPageViewData>
    {
    }
}
La balise associée est illustrée à la figure 10. Maintenant, la Page maître reçoit exactement le même objet ViewData que la vue. J'ai déclaré la classe de base de ma Page maître pour être ViewMasterPage<WikiPageViewData> pour avoir le bon type ViewData. De là, je configure les diverses balises DIV pour faire la mise en page de ma page, remplir la liste de version et apporter les finitions avec l'espace réservé de contenu habituel.
<%@ Master Language="C#" 
  AutoEventWireup="true" 
  CodeBehind="Site.master.cs" 
  Inherits="MiniWiki.Views.Layouts.Site" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<%@ Import Namespace="MiniWiki.DomainModel" %>
<%@ Import Namespace="System.Web.Mvc" %>
<html >
<head runat="server">
  <title><%= ViewData.Name %></title>
  <link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="inner">
    <div id="top">
      <div id="header">
        <h1><%= ViewData.Name %></h1>
      </div>
      <div id="menu">
        <ul>
          <li><a href="http://Home">Home</a></li>
          <li>
            <%= Html.ActionLink("Edit this page", 
                  new { controller = "WikiPage", 
                        action = "EditPage", 
                        pageName = ViewData.Name })%>
        </ul>
      </div>
    </div>
    <div id="main">
      <div id="revisions">
        Revision history:
        <ul>
          <% 
            int i = 0;
            foreach (WikiPageVersion version in ViewData.Page.Versions)
            { %>
              <li>
                <a href="http://<%= ViewData.Name %>?version=<%= i %>">
                  <%= version.CreatedOn %>
                  by
                  <%= version.Creator %>
                </a>
              </li>
          <%  ++i;
          } %>
        </ul>
      </div>
      <div id="maincontent">
        <asp:ContentPlaceHolder 
          ID="MainContentPlaceHolder" 
          runat="server">
        </asp:ContentPlaceHolder>
      </div>
    </div>
  </div>
</body>
</html>

Autre élément notable : l'appel à Html.ActionLink. Ceci est un exemple d'une assistance d'affichage. Les diverses classes d'affichage ont deux propriétés, Html et Url. Chacune possède des méthodes utiles pour produire des morceaux de code HTML. Dans ce cas, Html.ActionLink prend un objet (ici d'un type anonyme) et le refait passer par le système de routage. Ceci produit une URL qui dirigera vers le contrôleur et l'action que j'ai spécifiés. De cette façon, qu'importe la manière dont je modifie mes routages, le lien « Modifier cette page » pointera toujours au bon endroit.
Vous aurez peut-être noté également que j'ai aussi dû créer manuellement un lien (les liens vers les versions de page précédentes). Malheureusement, le système actuel de routage ne fonctionne pas si bien pour produire des URL impliquant des chaînes de requête. Ceci devrait être résolu dans les versions ultérieures de l'infrastructure.

Création et publication de formulaires
Observons maintenant l'action EditPage sur le contrôleur :
[ControllerAction]
public void EditPage(string pageName)
{
  WikiSpace space = new WikiSpace(Repository);
  WikiPage page = space.GetPage(pageName);

  RenderView("editpage", 
    new WikiPageViewData { 
      Name = pageName, 
      Page = page });
}
Encore une fois, l'action ne fait pas grand-chose : elle affiche juste la vue avec la page donnée. Les choses deviennent plus intéressantes dans la vue illustrée à la figure 11. Ce fichier génère un formulaire HTML, mais vous ne voyez pas de Runat="server". L'assistance Url.Action est utilisée pour produire l'URL auquel renvoie le formulaire. Il existe également plusieurs usages des diverses assistances HTML comme TextBox, TextArea et SubmitButton. Elles font à peu près ce que vous pouvez prévoir : produire du code HTML pour les divers champs d'entrée.
<%@ Page Language="C#" 
  MasterPageFile="~/Views/Shared/Site.Master" 
  AutoEventWireup="true" 
  CodeBehind="EditPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.EditPage" %>
<%@ Import Namespace="System.Web.Mvc" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<asp:Content ID="Content1" 
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <form action="<%= Url.Action(
    new { controller = "WikiPage", 
    action = "NewVersion", 
    pageName = ViewData.Name })%>" method=post>
    <%
      if (ViewContext.TempData.ContainsKey("errors"))
      {
    %>
    <div id="errorlist">
      <ul>
      <%
        foreach (string error in 
          (string[])ViewContext.TempData["errors"])
        {
      %>
        <li><%= error%></li>
      <% } %>
      </ul>
    </div>
    <% } %>
    Your name: <%= Html.TextBox("Creator",
                   ViewContext.TempData.ContainsKey("creator") ? 
                   (string)ViewContext.TempData["creator"] : 
                   ViewData.Creator)%>
    <br />
    Please enter your updates here:<br />
    <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? 
        (string)ViewContext.TempData["body"] : 
        ViewData.Body, 30, 65)%>
    <br />
    Tags: <%= Html.TextBox(
              "Tags", ViewContext.TempData.ContainsKey("tags") ? 
              (string)ViewContext.TempData["tags"] : 
              ViewData.Tags)%>
    <br />
    <%= Html.SubmitButton("SubmitAction", "OK")%>
    <%= Html.SubmitButton("SubmitAction", "Cancel")%>
  </form>
</asp:Content>

En programmation Web, le traitement des erreurs sur un formulaire fait partie des tâches les plus ennuyeuses. En particulier lorsque vous voulez afficher des messages d'erreur mais également garder les données précédemment entrées. Nous avons tous déjà commis une erreur sur un formulaire de 35 champs et obtenu un paquet de messages d'erreur et un formulaire vide. L'infrastructure MVC offre TempData comme emplacement pour enregistrer les informations précédemment entrées pour permettre le repeuplement du formulaire. ViewState facilitait grandement cette opération dans Web Forms, puisque l'enregistrement du contenu des contrôles était pour ainsi dire automatique.
J'aimerais le faire aussi dans MVC, et c'est là qu'intervient TempData, un dictionnaire très similaire au ViewData sans type. Cependant, le contenu de TempData n'existe que le temps d'une requête unique, puis il est supprimé. Pour voir comment ceci est utilisé, observez l'action NewVersion à la figure 12.
[ControllerAction]
public void NewVersion(string pageName) {
  NewVersionPostData postData = new NewVersionPostData();
  postData.UpdateFrom(Request.Form);

  if (postData.SubmitAction == "OK") {
    if (postData.Errors.Length == 0) {
      WikiSpace space = new WikiSpace(Repository);
      WikiPage page = space.GetPage(pageName);
      WikiPageVersion newVersion = new WikiPageVersion(
        postData.Body, postData.Creator, postData.TagList);
      page.Add(newVersion);
    } else {
      TempData["creator"] = postData.Creator;
      TempData["body"] = postData.Body;
      TempData["tags"] = postData.Tags;
      TempData["errors"] = postData.Errors;

      RedirectToAction(new { 
        controller = "WikiPage", 
        action = "EditPage", 
        pageName = pageName });
      return;
    }
  }

  RedirectToAction(new { 
    controller = "WikiPage",
    action = "ShowPage", 
    pageName = pageName });
}

D'abord, elle crée un objet NewVersionPostData. Il s'agit d'un autre objet d'assistance qui possède des propriétés et des méthodes qui enregistrent le contenu de la publication et de la validation. Pour charger l'objet postData, j'utilise une fonction d'aide de la Boîte à outils MVC. UpdateFrom est en fait une méthode d'extension fournie par la boîte à outils et elle utilise la réflexion pour mettre en correspondance les noms de champs de formulaire et les noms de propriétés sur mon objet. Le résultat est que toutes les valeurs de champ sont chargées dans mon objet postData. L'utilisation de UpdateFrom a le désavantage, cependant, de récupérer les données de formulaire directement de HttpRequest, ce qui rend le test unitaire plus difficile.
La première chose que vérifie NewVersion est SubmitAction. Ce sera bon si l'utilisateur a cliqué sur le bouton OK et souhaite effectivement publier la page modifiée. S'il existe une autre valeur, l'action finit par rediriger vers ShowPage qui affichera la page d'origine.
Si l'utilisateur a cliqué sur OK, je vérifie la propriété postData.Errors. Cette opération exécute quelques validations simples sur le contenu de publication. S'il n'y a pas d'erreur, j'opère le traitement pour écrire la nouvelle version de page de nouveau dans le wiki. Cependant, s'il y a des erreurs, les choses deviennent intéressantes.
En cas d'erreurs, je définis les divers champs du dictionnaire TempData de sorte qu'il incorpore le contenu du PostData. Puis, je renvoie à la page de modification. Maintenant, puisque le TempData est déterminé, la page s'affichera de nouveau avec le formulaire initialisé avec les valeurs que l'utilisateur a publiées la dernière fois.
Ce processus de gestion des publications, de la validation et de TempData est un peu rébarbatif et nécessite un peu plus de travail manuel qu'il n'est vraiment nécessaire. Les versions futures doivent inclure des méthodes d'aide qui automatisent au moins une partie de la vérification de TempData. Une dernière remarque sur TempData : le contenu de TempData est enregistré dans la session côté serveur de l'utilisateur. Si vous arrêtez la session, TempData ne fonctionnera pas.

Création de contrôleur
Les bases du wiki sont maintenant opérationnelles, mais il y a quelques points de difficulté dans la mise en œuvre que j'aimerais éclaircir avant d'aller plus loin. Par exemple, la propriété Repository est utilisée pour détacher la logique du wiki du stockage physique. Vous pouvez fournir des référentiels enregistrant le contenu sur le système de fichiers (comme je l'ai fait ici), une base de données ou à l'emplacement que vous aurez choisi. Malheureusement, j'ai dû faire face à deux problèmes.
Premièrement, ma classe de contrôleur est solidement couplée à la classe FileBasedSpaceRepository concrète. J'ai besoin d'avoir une valeur par défaut, de sorte que si la propriété n'est pas définie, il me reste quelque chose de raisonnable à utiliser. Pire encore, le chemin aux fichiers sur le disque est codé en dur ici aussi. Ce truc vient peut-être de la configuration.
Deuxièmement, la propriété Repository est vraiment une dépendance obligatoire, mon objet ne s'exécutera pas sans elle. Une bonne conception indique que le référentiel doit vraiment être un paramètre de constructeur, pas une propriété. Mais je ne peux pas l'ajouter au constructeur parce que l'infrastructure MVC nécessite un constructeur sans argument sur les contrôleurs.
Heureusement, il existe un point d'extensibilité qui peut me sortir de ce pétrin : la fabrique de contrôleurs. Une fabrique de contrôleurs fait ce que son nom indique : elle crée des instances de contrôleur. Vous avez juste besoin de créer une classe qui implémente l'interface IControllerFactory et de l'enregistrer auprès du système MVC. Vous pouvez enregistrer des fabriques de contrôleurs pour tous les contrôleurs ou juste pour des types spécifiques. La figure 13 présente une fabrique de contrôleurs pour WikiPageController, qui passe maintenant le référentiel en tant que paramètre de constructeur.
public class WikiPageControllerFactory : IControllerFactory {

  public IController CreateController(RequestContext context, 
    Type controllerType)
  {
    return new WikiPageController(
      GetConfiguredRepository(context.HttpContext.Request));
  }

  private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
  {
    return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
  }
}

Dans ce cas, la mise en œuvre est simple, mais ceci peut permettre de créer des contrôleurs qui utilisent des outils beaucoup plus puissants (en particulier des conteneurs d'injection de dépendance). En tout cas, j'ai maintenant tous les détails nécessaires pour séparer les dépendances pour le contrôleur dans un objet plus facile gérer et à maintenir.
La dernière étape pour que tout cela fonctionne consiste à enregistrer la fabrique avec l'infrastructure. J'utilise pour cela la classe ControllerBuilder en ajoutant la ligne suivante à Global.asax.cs dans la méthode Application_Start (avant ou après les routages) :
ControllerBuilder.Current.SetControllerFactory(
  typeof(WikiPageController), typeof(WiliPageControllerFactory));
ceci enregistrera une fabrique pour WikiPageController. Si j'avais d'autres contrôleurs dans ce projet, ils n'utiliseraient pas cette fabrique, puisqu'elle est enregistrée seulement pour le type WikiPageController. Vous pouvez appeler également SetDefaultControllerFactory si vous voulez définir l'utilisation d'une fabrique pour chaque contrôleur.

Autres points d'extensibilité
La fabrique de contrôleurs n'est que le point de départ de l'extensibilité d'infrastructure. Je n'ai pas assez de place dans cet article pour les détailler tous, donc je me contenterai des grandes lignes. Premièrement, si vous voulez produire autre chose que du code HTML ou si vous voulez utiliser un moteur de modélisation autre que les formulaires Web, vous pouvez déterminer le ViewFactory du contrôleur sur un autre paramètre. Vous pouvez implémenter l'interface IViewFactory, vous aurez alors un contrôle intégral sur le mode de génération de la sortie. Ceci est utile pour produire des flux RSS, du code XML ou même des graphiques.
Le système de routage est relativement flexible, comme vous avez pu le voir. Mais il n'y a rien dans le système de routage qui soit spécifique à MVC. Chaque routage possède une propriété RouteHandler que j'ai, jusqu'ici, toujours définie sur MvcRouteHandler. Mais il est possible d'implémenter l'interface IRouteHandler et de lier le système de routage à d'autres technologies Web. Une future version de l'infrastructure comportera un WebFormsRouteHandler et d'autres technologies bénéficieront du système de routage générique à l'avenir.
Les contrôleurs n'ont pas besoin de dériver de System.Web.Mvc.Controller. Tout ce qu'un contrôleur doit faire est implémenter l'interface IController, qui ne compte qu'une seule méthode appelée Execute. De là vous pouvez faire ce que vous voulez. D'un autre côté, si vous voulez seulement exploiter quelques comportements de la classe Controller de base, Controller compte beaucoup de fonctions virtuelles que vous pouvez remplacer :
  • OnPreAction, OnPostAction et OnError vous permettent d'accrocher des éléments avant et après traitement à chaque action exécutée. OnError vous offre un mécanisme de gestion des erreurs à l'échelle du contrôleur.
  • HandleUnknownAction est appelé lorsqu'une URL est routée au contrôleur mais que ce contrôleur n'implémente pas l'action demandée dans le routage. Par défaut, cette méthode émet une exception, mais vous pouvez l'ignorer pour faire ce que vous voulez.
  • InvokeAction est la méthode qui détermine la méthode d'action à appeler et qui l'appelle. Si vous souhaitez personnaliser le processus (par exemple, pour vous débarrasser de l'exigence relative aux attributs [ControllerAction]), vous êtes au bon endroit.
Il existe plusieurs méthodes plus virtuelles sur Controller, mais elles sont principalement là comme points de test et non comme des points d'extension. Par exemple, RedirectToAction est virtuel, vous pouvez donc créer une classe dérivée qui n'opère en fait pas de redirection. Ceci vous permet de tester des actions de redirection sans avoir besoin d'exécuter un serveur Web complet.

Adieu Web Forms ?
À ce stade vous pouvez vous demander, « Qu'advient-il de Web Forms ? MVC le remplace-t-il » ? La réponse est non ! Web Forms est une technologie connue et Microsoft continuera à la prendre en charge et à l'améliorer. Web Forms fonctionne très bien dans de nombreuses applications. L'application de rapport de base de données d'intranet typique, par exemple, peut être créée à l'aide de formulaires Web en bien moins de temps qu'il n'en faudrait pour l'écrire dans MVC. De plus, Web Forms prend en charge un vaste ensemble de contrôles, dont beaucoup sont extrêmement sophistiqués et vous épargnent des masses de travail.
Alors quand choisir MVC par rapport à Web Forms ? Cela dépend surtout de vos contraintes et de vos préférences. Avez-vous du mal à former vos URL comme vous le souhaitez ? Voulez-vous effectuer des tests unitaires sur votre interface utilisateur ? Ces scénarios pencheraient en faveur de MVC. D'un autre côté, affichez-vous beaucoup de données avec des grilles modifiables et de jolis contrôles TreeView ? Alors vous vous en sortirez probablement mieux avec Web Forms pour l'instant.
Progressivement, l'infrastructure MVC accèdera probablement au département de contrôle de l'interface utilisateur, mais il est très probable qu'il ne sera jamais aussi simple au premier abord que Web Forms dans lequel un grand nombre de fonctionnalités sont accessibles par un simple glisser-déplacer. Mais dans le même temps, l'infrastructure ASP.NET MVC offre aux développeurs Web une nouvelle méthode pour créer des applications Web dans Microsoft .NET Framework. L'infrastructure est conçue pour offrir une capacité de test, utilise le HTTP au lieu d'essayer d'en faire abstraction et est extensible dans à peu près tous ses aspects. C'est un complément séduisant à Web Forms pour les développeurs qui veulent un contrôle intégral sur leurs applications Web.

Chris Tavares est développeur dans l'équipe Microsoft patterns & practices, où il aide la communauté des développeurs à comprendre les méthodes recommandées pour créer des systèmes sur plates-formes Microsoft. Il est également un membre virtuel de l'équipe ASP.NET MVC et contribue à la conception de la nouvelle infrastructure. Vous pouvez contacter Chris à l'adresse cct@tavaresstudios.com.

Page view tracker