Exposer des pages web avec Nancy

Exposer des pages web avec Nancy

Durant les deux premiers articles de cette série sur Nancy, nous avons vu comment démarrer un projet et ensuite comment exposer des web services REST. Dans ce 3e article, nous allons voir comment créer un vrai site web MVC avec la stack open source.

Nous allons utiliser Bootstrap pour faire quelque chose de pas trop vilain. Et comme les packages NuGet ne sont jamais à jour, nous allons les chercher à la main et ajouter les répertoires css, fonts et js à notre projet dans un répertoire Content (c’est là que Nancy va chercher tout le contenu statique) :

image

Nous sommes maintenant prêts à créer nos pages web. Dans le premier article de cette série, souvenez-vous, nous avions choisi un template Nancy avec Razor. Nous allons donc créer des vues Razor. Mais où ? Et comment ?

Créer notre première page

Comme pour les web services, ce sont les modules qui pilotent les pages Nancy. Avant de commencer, nous allons rapidement modifier le SampleModule pour qu’il n’écoute plus que les requêtes du type http://localhost:7844/sample :

public SampleModule() : base("/sample")

La voie est maintenant libre pour créer une page d’accueil qui écoutera la racine de l’application. Pour commencer, nous allons créer un HomeModule, qui sera gèrera les pages de base de l’application :

using Nancy;
namespace NancyDemo
{
    public class HomeModule : NancyModule
    {
        public HomeModule()
        {
            Get["/"] = parameters => { return View["index"]; };
        }
    }
}

Nous venons de définir une route qui écoute l’URL http://localhost:7844/ et qui rend la vue “index”. Si nous lançons notre application, nous obtenons d’abord une bonne grosse 404 :

image

En effet, nous avons défini la route, mais pas encore créé la vue. Attardons-nous pour le moment sur la définition de la route :

Get["/"] = parameters => { return View["index"]; };

La classe de base NancyModule, dont hérite notre HomeModule expose un dictionnaire View, qui sert à accéder comme son nom l’indique à l’ensemble de nos vues. L’accesseur permet de chercher une vue par :

  • nom [string] (comme dans notre exemple)
  • modèle [dynamic]
  • couple {nom, modèle}

Nous reparlerons plus tard du modèle. Mais à aucun moment, nous n’avons défini où se trouvait physiquement la vue. Pour cela, Nancy procède par convention. Il existe plusieurs conventions par défaut. Si dans notre module HomeModule, notre route est “/mon/long/chemin/vers/ma/vue”, nous pouvons placer notre fichier vue.cshtml dans les répertoires suivants :

  • la racine de l’application
  • /Views
  • /Home
  • /Views/Home

Par défaut, Nancy nous permet déjà d’organiser notre application de plusieurs façons. Mais il est également possible de créer ses propres conventions en créant un Boostrapper custom de la façon suivante :

public class CustomConventionsBootstrapper : DefaultNancyBootstrapper
{
    protected override void ApplicationStartup(TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
    {
        this.Conventions.ViewLocationConventions.Add((viewName, model, context) =>
        {
            return string.Concat("custom/", viewName);
        });
    }
}

(source)

Maintenant que nous savons où placer notre vue, ajoutons le fichier Index.cshtml par exemple dans le répertoire Views/Home :

image

… et dont le contenu est le suivant :

<html>
<head>
   <meta name="viewport" content="width=device-width" />
   <title></title>
</head>
<body>
    <div>This is my home page !</div>
</body>
</html>

Et le résultat est (sous un tonnerre d’applaudissements) le suivant :

image

En rendant la chose un peu plus jolie avec Bootstrap, nous avons maintenant la vue suivante :

image

Nous avons simplement ajouté un fichier _Layout.cshtml dans le répertoire Views/Shared. Il contient le menu avec les liens Home, About et Contact. La vue Index.cshtml est maintenant réduite au code suivant :

@{
    Layout ="Shared/_Layout.cshtml";
}
<h1>
    This is my home page !
</h1>

Nancy nous permet donc de bénéficier de Razor et de Bootstrap sans difficulté. Mais nos pages sont un peu… statiques. Nous allons les rendre un peu plus dynamiques.

Injecter un modèle dans une vue

La page About va nous permettre d’afficher du contenu dynamique, que nous allons tirer d’une classe SiteInformation, que  nous allons placer dans un nouveau répertoire Models (mais qui pourrait être n’importe où ailleurs) :

using System;

namespace NancyDemo.Models
{
    public class SiteInformation
    {
        public string Name { get; set; }
        public string Owner { get; set; }
        public string Description { get; set; }
        public DateTime CreationDate { get; set; }
    }
}

Nous allons maintenant ajouter une nouvelle vue About dans le répertoire Views/Home. Avantage sympa : on peut parfaitement utiliser les facilités de scaffolding d’ASP.NET MVC pour créer nos vues :

image

image

image

Ce qui génère le fichier About.cshtml suivant dans le répertoire voulu :

@{ Layout = "~/Views/Shared/_Layout.cshtml"; }

Attention à bien corriger le chemin avec “Views/Shared/_Layout.cshtml” en supprimant le “~/” au début, sinon Nancy se perd. C’est dommage, mais on fera avec.

Nous allons le compléter de façon à afficher les propriétés de la classe SiteInformation. Et tout comme avec ASP.NET MVC nous pouvions typer fortement nos modèles dans la vue de façon à bénéficier de l'IntelliSense, nous allons le faire avec Nancy. La directive est simplement un peu différente. Là où avec ASP.NET MVC, nous aurions écrit :

@model NancyDemo.Models.SiteInformation

Nous allons plutôt écrire :

@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<NancyDemo.Models.SiteInformation>

Et à partir de là, c’est facile, la magie d'IntelliSense fait le reste :

image

Il ne nous reste plus qu’à modifier notre HomeModule pour lui ajouter une nouvelle route et lui injecter notre modèle :

public HomeModule()
{
    Get["/"] = parameters => { return View["index"]; };
    Get["/about"] = parameters =>
    {
        var siteInformation = new Models.SiteInformation
        {
            Name = "My Nancy Demo",
            Owner = "Nicholas Suter",
            Description = "How to build fancy websites using Nancy ! ",
            CreationDate = new System.DateTime(2014, 11, 16)
        }
        return View["about", siteInformation];
    };
}

Le résultat est le suivant :

image

Nous venons donc de voir comment alimenter une vue depuis un modèle. Mais il faut évidemment pouvoir faire le chemin inverse. Là encore, Nancy va nous faciliter le travail.

Mettre à jour le modèle depuis la vue

La page contact va permettre à l’utilisateur de remonter des informations au gestionnaire du site. Pour cela, nous allons lui exposer un formulaire :


@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<NancyDemo.Models.ContactRequest>
@{
Layout = "Views/Shared/_Layout.cshtml";
}

<h1>Contact</h1>
<div class="container">
<form role="form" class="form-horizontal" action="/contact" method="post">
    <div class="form-group">
        <label for="Email">Email address</label>
        <div class="input-group">
            <div class="input-group-addon">@@</div>
            <input type="email" class="form-control" id="Email" name="Email" placeholder="Enter email">
        </div>
    </div>
    <div class="form-group">
        <label for="Details">Details</label>
        <input type="text" class="form-control" id="Details" name="Details" placeholder="Details">
    </div>

    <div class="form-group">
        <label for="Reason">Reason</label>
        <select class="form-control" id="Reason" name="Reason">
            <option>Question</option>
            <option>Request</option>
        </select>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>
</div>

Côté serveur, nous devrons récupérer le résultat de ce formulaire et en faire quelque chose. Dans notre cas, nous allons nous contenter de rediriger vers la Home avec un paramètre d'URL particulier. Nous allons donc compléter le HomeModule avec deux nouvelles méthodes : un Get pour afficher le formulaire et un Post pour traiter son contenu :


Get["/contact"] = parameters => { return View["contact", contactRequest]; };

Post["/contact"] = parameters =>
{
contactRequest = this.Bind<ContactRequest>();

return Response.AsRedirect("/?status=added&title=" + contactRequest.Email);
};

Notons comment Nancy expose des helpers pour faire le binding entre le contenu du Form et notre modèle objet. Tous les détails sont ici.

Conclusion

Voilà, nous venons de voir succintement comment créer des pages web avec Nancy. J'aime beaucoup ce framework, très léger, qui permet de bootstrapper rapidement et efficacement des services REST et des pages dynamiques. Fouillez la documentation, Nancy est vraiment modulaire et souple et mérite réellement que l'on s'y intéresse de près.