blog.pagesd.info

Aller au contenu | Aller au menu | Aller à la recherche

jeudi 17 novembre 2011

MVC Scaffolding, Eager loading et ViewModel

Pour tenter de mettre au point une méthode simple(iste) pour construire une application avec ASP.NET MVC, j'essaie de développer une petite application destinées à générer une brochure de voyages . Pour cela, je dois gérer une base de données avec une table "Voyages", une table "Tarifs" pour enregistrer les chaque tarif du voyage et une table "Parties" qui sert à décrire les différentes étapes du voyage.

Eager loading pour les actions Index

Pour l'instant, j'ai plus ou moins laissé MVC Scaffolding écrire mon code pour la classe Voyage associée à la table Voyages. Par défaut, il a généré le code C# suivant pour l'action Index du contrôleur VoyagesController afin retrouver tous les voyages à afficher :

var voyages = voyageRepository
    .AllIncluding(voyage => voyage.Parties, voyage => voyage.Tarifs)
    .OrderBy(voyage => voyage.Position)
    .ThenBy(voyage => voyage.Title);

Ce qui correspond à quelque chose dans ce genre si on ne passe pas par un Repository :

var voyages = context
    .Voyages
    .Include(voyage => voyage.Parties)
    .Include(voyage => voyage.Tarifs)
    .OrderBy(voyage => voyage.Position)
    .ThenBy(voyage => voyage.Title);

Ces deux codes C# ont tous les deux pour effet de générer une (grosse) requête SQL qui charge à la fois le contenu de la table Voyages et par des jointures celui des tables Parties et Tarifs :

SELECT 
[UnionAll1].[VoyageID] AS [C1], 
[UnionAll1].[VoyageID1] AS [C2], 
[UnionAll1].[Position1] AS [C3], 
[UnionAll1].[Title1] AS [C4], 
[UnionAll1].[VoyageType] AS [C5], 
[UnionAll1].[Notes] AS [C6], 
[UnionAll1].[C1] AS [C7], 
[UnionAll1].[PartieID] AS [C8], 
[UnionAll1].[VoyageID2] AS [C9], 
[UnionAll1].[Position2] AS [C10], 
[UnionAll1].[PartieType] AS [C11], 
[UnionAll1].[Content] AS [C12], 
[UnionAll1].[C2] AS [C13], 
[UnionAll1].[C3] AS [C14], 
[UnionAll1].[C4] AS [C15], 
[UnionAll1].[C5] AS [C16], 
[UnionAll1].[C6] AS [C17], 
[UnionAll1].[C7] AS [C18], 
[UnionAll1].[C8] AS [C19], 
[UnionAll1].[C9] AS [C20], 
[UnionAll1].[C10] AS [C21], 
[UnionAll1].[C11] AS [C22]
FROM  (SELECT
        CASE WHEN ([Extent2].[PartieID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1],
        [Extent1].[Position] AS [Position], 
        [Extent1].[Title] AS [Title], 
        [Extent1].[VoyageID] AS [VoyageID], 
        [Extent1].[VoyageID] AS [VoyageID1], 
        [Extent1].[Position] AS [Position1], 
        [Extent1].[Title] AS [Title1], 
        [Extent1].[VoyageType] AS [VoyageType], 
        [Extent1].[Notes] AS [Notes], 
        [Extent2].[PartieID] AS [PartieID], 
        [Extent2].[VoyageID] AS [VoyageID2], 
        [Extent2].[Position] AS [Position2], 
        [Extent2].[PartieType] AS [PartieType], 
        [Extent2].[Content] AS [Content], 
        CAST(NULL AS int) AS [C2], 
        CAST(NULL AS int) AS [C3], 
        CAST(NULL AS nvarchar(1)) AS [C4], 
        CAST(NULL AS nvarchar(1)) AS [C5], 
        CAST(NULL AS real) AS [C6], 
        CAST(NULL AS real) AS [C7], 
        CAST(NULL AS real) AS [C8], 
        CAST(NULL AS real) AS [C9], 
        CAST(NULL AS real) AS [C10], 
        CAST(NULL AS nvarchar(1)) AS [C11] 
        FROM  [Voyages] AS [Extent1] 
        LEFT OUTER JOIN [Parties] AS [Extent2] ON [Extent1].[VoyageID] = [Extent2].[VoyageID]
UNION ALL
        SELECT
        2 AS [C1], 
        [Extent3].[Position] AS [Position], 
        [Extent3].[Title] AS [Title], 
        [Extent3].[VoyageID] AS [VoyageID], 
        [Extent3].[VoyageID] AS [VoyageID1], 
        [Extent3].[Position] AS [Position1], 
        [Extent3].[Title] AS [Title1], 
        [Extent3].[VoyageType] AS [VoyageType], 
        [Extent3].[Notes] AS [Notes], 
        CAST(NULL AS int) AS [C2], 
        CAST(NULL AS int) AS [C3], 
        CAST(NULL AS int) AS [C4], 
        CAST(NULL AS int) AS [C5], 
        CAST(NULL AS nvarchar(1)) AS [C6], 
        [Extent4].[TarifID] AS [TarifID], 
        [Extent4].[VoyageID] AS [VoyageID2], 
        [Extent4].[Title] AS [Title2], 
        [Extent4].[Year] AS [Year], 
        [Extent4].[Prix1] AS [Prix1], 
        [Extent4].[Prix2] AS [Prix2], 
        [Extent4].[Prix3] AS [Prix3], 
        [Extent4].[Prix4] AS [Prix4], 
        [Extent4].[Prix5] AS [Prix5], 
        [Extent4].[Notes] AS [Notes1]
        FROM  [Voyages] AS [Extent3]
        INNER JOIN [Tarifs] AS [Extent4] ON [Extent3].[VoyageID] = [Extent4].[VoyageID]) AS [UnionAll1]
ORDER BY [UnionAll1].[Position] ASC, [UnionAll1].[Title] ASC, [UnionAll1].[VoyageID1] ASC, [UnionAll1].[C1] ASC  

Puis dans la vue Index.cshtml, on utilise le IEnumerable<Voyage> obtenu pour afficher une liste des voyages, avec pour chacun d'entre eux le nombre de parties qui composent sa description détaillée et le nombre de tarifs définis pour ce voyage.

@foreach (var item in Model) {
  <tr>
    <td>
      @Html.ActionLink("Edit", "Edit", new { id = item.VoyageID })
    </td>
    <td>
      @item.Position
    </td>
    <td>
      @Html.ActionLink(@item.Title, "Details", new { id = item.VoyageID })
    </td>
    <td>
      @Html.DisplayTextFor(_ => item.TypeVoyage).ToString()
    </td>
    <td>
      @(item.Parties == null ? "" : item.Parties.Count.ToString())
    </td>
    <td>
      @(item.Tarifs == null ? "" : item.Tarifs.Count.ToString())
    </td>
    <td>
      @Html.ActionLink("Delete", "Delete", new { id = item.VoyageID })
    </td>
  </tr>
}

Ce qui est intéressant, c'est que MVC Scaffolding fait automatiquement du "Eager Loading" pour que les instructions item.Parties.Count.ToString() et item.Tarifs.Count.ToString() en boucle ne provoquent pas autant d'accès à la base de données qu'il y a de voyage à afficher.

Lazy loading et SELECT N+1

En effet, si par défaut, MVC Scaffolding n'avait pas fait le lien avec les parties et les tarifs et qu'il se soit contenté de générer le code C# suivant :

var voyages = voyageRepository
    .All
    .OrderBy(voyage => voyage.Position)
    .ThenBy(voyage => voyage.Title);

Ou sans le Repository :

var voyages = context
    .Voyages
    .OrderBy(voyage => voyage.Position)
    .ThenBy(voyage => voyage.Title);

Alors la consultation de la page Index n'aurait pas généré 1 seule requête, mais 71 !!!

Soit 1 première requête toute simple pour charger la liste des 35 voyages :

SELECT 
[Extent1].[VoyageID] AS [VoyageID], 
[Extent1].[Position] AS [Position], 
[Extent1].[Title] AS [Title], 
[Extent1].[VoyageType] AS [VoyageType], 
[Extent1].[Notes] AS [Notes]
FROM [Voyages] AS [Extent1]
ORDER BY [Extent1].[Position] ASC, [Extent1].[Title] ASC 

Puis 35 requêtes pour charger successivement les parties de chacun des 35 voyages :

SELECT 
[Extent1].[PartieID] AS [PartieID], 
[Extent1].[VoyageID] AS [VoyageID], 
[Extent1].[Position] AS [Position], 
[Extent1].[PartieType] AS [PartieType], 
[Extent1].[Content] AS [Content]
FROM [Parties] AS [Extent1]
WHERE [Extent1].[VoyageID] = @EntityKeyValue1  

Et encore 35 requêtes pour charger successivement les tarifs de chacun des 35 voyages :

SELECT 
[Extent1].[TarifID] AS [TarifID], 
[Extent1].[VoyageID] AS [VoyageID], 
[Extent1].[Title] AS [Title], 
[Extent1].[Year] AS [Year], 
[Extent1].[Prix1] AS [Prix1], 
[Extent1].[Prix2] AS [Prix2], 
[Extent1].[Prix3] AS [Prix3], 
[Extent1].[Prix4] AS [Prix4], 
[Extent1].[Prix5] AS [Prix5], 
[Extent1].[Notes] AS [Notes]
FROM [Tarifs] AS [Extent1]
WHERE [Extent1].[VoyageID] = @EntityKeyValue1   

Par rapport à l'eager loading, ici on a fait du "lazy loading" : les entités enfants ne sont chargées que lorsque on y accède. Des fois c'est bien. Des fois c'est mal. Mais heureusement, MVC Scaffolding qui a pensé à tout est est assez malin pour nous éviter l'écueil du SELECT N+1 quand on génère l'action Index (merci Scott).

Lazy loading et virtual

Pour être complet, c'est parce que les propriétés Parties et Tarifs de la classe Voyage sont déclarées en tant que propriétés virtuelles, que Entity Framework a fait du "lazy loading".

Ainsi, si au lieu d'utiliser virtual pour les déclarer :

public virtual ICollection<Partie> Parties { get; set; }
public virtual ICollection<Tarif> Tarifs { get; set; }

On s'était contenté de :

public ICollection<Partie> Parties { get; set; }
public ICollection<Tarif> Tarifs { get; set; }

Alors la consultation de la page Index n'aurait plus généré qu'une seule requête pour charger uniquement les données de la table des voyages (pas de eager loading puisque on n'a pas d'Include) :

SELECT 
[Extent1].[VoyageID] AS [VoyageID], 
[Extent1].[Position] AS [Position], 
[Extent1].[Title] AS [Title], 
[Extent1].[VoyageType] AS [VoyageType], 
[Extent1].[Notes] AS [Notes]
FROM [Voyages] AS [Extent1]
ORDER BY [Extent1].[Position] ASC, [Extent1].[Title] ASC 

Et puis c'est tout ! Comme on n'a pas de virtual, Entity Framework ne fait pas non plus de lazy loading et ne cherche donc pas à charger les parties et les tarifs.

C'est pour cela que les colonnes nombre de parties et nombre de tarifs restent vides puisque les instructions item.Parties == null ? "" : item.Xxxxxx.Count.ToString() sont là pour gérer le fait que les collections voyage.Parties et voyage.Tarifs sont nulles.

Par contre, même sans le virtual, si on demande à faire de l'eager loading à grand coups de Include, on retombe bien sur la grosse requête qui accède par jointure aux tables Voyages, Parties et Tarifs.

Action Index et ViewModel

A titre personnel, je trouve malgré tout que la requête SQL générée par Entity Framework est un peu lourde pour ce qu'on en fait. Alors que le but est simplement d'afficher le nombre de parties et de tarifs d'un voyage, on se retrouve quand même à charger toutes les parties et tous les tarifs de tous les voyages, ce qui revient donc à charger l'intégralité des tables Parties et Tarifs.

Note : Si on avait une pagination par paquet de 20 voyages, ça serait un peu mieux puisque cela reviendrait "seulement" à charger toutes les parties et tous les tarifs correspondant à 20 voyages uniquement.

Dans l'idéal, il faudrait donc récupérer les informations de base de chaque voyage sans ses propriétés voyage.Parties et voyage.Tarifs, plus le nombre de ses parties et le nombre de ses tarifs. Ce qui pourrait être représenté par la classe ViewModel suivante :

public class VoyageViewModel
{
    public int VoyageID { get; set; }
    public int Position { get; set; }
    public string Title { get; set; }
    public int VoyageType { get; set; }
    public int TarifsCount { get; set; }
    public int PartiesCount { get; set; }
}

Le fait d'utiliser un ViewModel est d'autant plus intéressant qu'une liste n'est pas là pour afficher la totalité des propriétés d'une entité, mais seulement quelques informations essentielles pour remplir son rôle, à savoir permettre de retrouver d'un coup d'oeil les informations qui comptent et éventuellement accéder au détail de la fiche pour consulter l'ensemble de ses données.

Par exemple, si j'ai une table Contacts avec civilité, nom, prénom, téléphone, portable, fax, email, adresse, code postal, ville, pays, format pour les emails, date de naissance, etc... je ne veux pas d'une liste qui fassent 50 colonnes pour afficher tout ça. Je préfère sacrifier quelques détails et me contenter d'une liste édulcorée mais lisible.

Pour résumer, je pense que dans le cas d'une liste (au moins), il est préférable d'employer un objet ViewModel spécifique pour collecter les données importantes puis de transmettre cet objet à la vue pour qu'elle présente ces informations à l'utilisateur.

Cela implique donc de créer une classe ViewModel "VoyagesIndex" (nommée d'après le nom du contrôleur (VoyagesController) et de l'action (Index) qui va l'utiliser) :

public class VoyagesIndex
{
    public int VoyageID { get; set; }
    public int Position { get; set; }
    public string Title { get; set; }
    public int VoyageType { get; set; }
    public int TarifsCount { get; set; }
    public int PartiesCount { get; set; }

    public VoyageType TypeVoyage
    {
        get { return (VoyageType)VoyageType; }
        set { VoyageType = (int)value; }
    }
}

Note : public VoyageType TypeVoyage est une bidouille qui n'a rien à voir avec le sujet et que j'essairai d'expliquer plus tard si je persiste à passer par elle.

Il faut ensuite modifier le code de l'action Index pour qu'elle récupère une collection de VoyagesIndex et plus une collection d'objets Voyage :

var voyages = voyageRepository
    .All
    .OrderBy(voyage => voyage.Position)
    .ThenBy(voyage => voyage.Title)
    .Select(voyage => new VoyagesIndex
    {
        VoyageID = voyage.VoyageID
        Position = voyage.Position,
        Title = voyage.Title,
        VoyageType = voyage.VoyageType,
        TarifsCount = voyage.Tarifs.Count(),
        PartiesCount = voyage.Parties.Count()
    }).ToList();

Et pour finir modifier la vue Index.cshtml pour qu'elle utilise désormais un IEnumerable<VoyagesIndex> pour afficher son contenu :

@foreach (var item in Model) {
  <tr>
    <td>
      @Html.ActionLink("Edit", "Edit", new { id = item.VoyageID })
    </td>
    <td>
      @item.Position
    </td>
    <td>
      @Html.ActionLink(@item.Title, "Details", new { id = item.VoyageID })
    </td>
    <td>
      @item.TypeVoyage.ToString()
    </td>
    <td>
      @item.PartiesCount
    </td>
    <td>
      @item.TarifsCount
    </td>
    <td>
      @Html.ActionLink("Delete", "Delete", new { id = item.VoyageID })
    </td>
  </tr>
}

Grâce à quoi la requête SQL générée quand on consulte la page Index est un peu plus simple à lire, et normalement plus efficace puisqu'elle ne cherche pas à charger les tables Parties et Tarifs.

SELECT 
[Project2].[VoyageID] AS [VoyageID], 
[Project2].[Position] AS [Position], 
[Project2].[Title] AS [Title], 
[Project2].[VoyageType] AS [VoyageType], 
[Project2].[C1] AS [C1], 
[Project2].[C2] AS [C2]
FROM ( SELECT 
        [Project1].[VoyageID] AS [VoyageID], 
        [Project1].[Position] AS [Position], 
        [Project1].[Title] AS [Title], 
        [Project1].[VoyageType] AS [VoyageType], 
        [Project1].[C1] AS [C1], 
        [SSQTAB1].[A1] AS [C2] 
        FROM ( SELECT 
                [Extent1].[VoyageID] AS [VoyageID], 
                [Extent1].[Position] AS [Position], 
                [Extent1].[Title] AS [Title], 
                [Extent1].[VoyageType] AS [VoyageType], 
                [SSQTAB1].[A1] AS [C1] 
                FROM [Voyages] AS [Extent1] 
                 OUTER APPLY 
                (SELECT 
                        COUNT(1) AS [A1] 
                        FROM [Tarifs] AS [Extent2] 
                        WHERE [Extent1].[VoyageID] = [Extent2].[VoyageID]) AS [SSQTAB1]
        )  AS [Project1]
         OUTER APPLY
        (SELECT
                COUNT(1) AS [A1]
                FROM [Parties] AS [Extent3]
                WHERE [Project1].[VoyageID] = [Extent3].[VoyageID]) AS [SSQTAB1]
)  AS [Project2]
ORDER BY [Project2].[Position] ASC, [Project2].[Title] ASC   

lundi 17 octobre 2011

Personnalisation de la CSS pour ASP.NET MVC

Ce billet est avant tout un pense-bête personnel pour noter les quelques modifications que j'apporte assez régulièrement à la feuille de style fournie par défaut quand on génère un nouveau projet ASP.NET MVC3 dans Visual Studio.

En général, lorsque je débute un nouveau projet, j'ai tendance à conserver la feuille de style telle que Visual Studio l'a créée. Puis quand je bloque sur des fonctionnalités ou des problèmes techniques, je m'accorde un petit moment de détente en adaptant « Site.css » pour essayer d'embellir mon projet avant de repartir de plus belle.

mysite01.png

Normalize

Avant toute chose, et pour partir du bon pied, je démarre par un petit reset.css, ou ces derniers temps un normalize.css dans la vue _Layout.cshtml :

<head>
  ...
  <link href="@Url.Content("~/Content/normalize.css")" rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
  ...
</head>

Undo

Comme j'aime très moyennement les coins arrondis, je commence par m'en débarrasser. Ca m'amuse toujours de commencer de façon radicale en lançant un rechercher / remplacer de -radius par -no-radius. Cela me permet de venir à bout de border-radius, -moz-border-radius et autre -webkit-border-radius. Mais au final, je fais ça de façon plus civilisée en redéfinissant tous les styles « xxxxx-border-radius » à 0.

/* Suppression des coinzarrondis */
#main,
footer, 
#footer,
ul#menu li a
{
  border-radius: 0;
  -webkit-border-radius: 0;
  -moz-border-radius: 0;
}

Il y a aussi l'effet d'ombre 3D sur le titre « My MVC Application » que je trouve un peu lourd. Là encore, un rechercher / remplacer à l'arrache de -shadow par -no-shadow ne fait pas de mal. Ou plus proprement je redéfini le style « text-shadow » de « header h1 ».

/* Suppression de l'effet 3D sur le titre */
header h1,
#header h1
{
  text-shadow: 0 0 0 transparent;
}

Font

Une fois défoulé, j'apporte encore quelques modifications encore plus personnelles pour obtenir un résultat un peu plus à mon goût.

Comme je suis toujours dans ma période Century Gothic is Beautiful, c'est elle qui gagne par rapport à la police « Trebuchet MS » utilisée par défaut.

/* Utilisation de ma police de prédilection */
body
{
  font-family: "Century Gothic", "Trebuchet MS", Verdana, Helvetica, Sans-Serif;
}

Color

Puis, même si j'aime beaucoup le bleu assez soutenu utilisé comme couleur de fond, je le change pour une couleur plus claire et neutre.

Ensuite, je change les couleurs de titres au gré de l'humeur du jour ou des « Oh, c'est quoi cette joli couleur » trouvés sur d'autres sites. Ou plus simplement, c'est le titre général en chocolat, le titre de page en gris clair et les sous-titre en vert.

Après, j'ai encore un problème avec le menu qui est bleuté par défaut parce que je n'ai rien trouvé qui me plaise vraiment. Pour l'instant, je me contente de le faire ressortir en le passant en blanc sur fond vert avec un effet chocolat quand on passe la souris dessus.

/* Changement du jeu de couleurs */
body { background-color: #e0e0e0; } /* Un fond de page gris clair */
header h1, #header h1, h1 { color: #d2691e; } /* chocolat */
h2 { color: #888888; } /* gris */
h3 { color: #39b449; } /* vert */
ul#menu li a { color: #fff; background-color: #39b449; } /* Menu blanc/vert */
ul#menu li a:hover { background-color: #d2691e; } /* Et survolé en chocolat */
ul#menu { border-bottom: 0; } /* Tant qu'à faire */

Table

Une fois zen, je pars à l'assaut des tables. Déjà c'est du n'importe quoi. Pour éviter de m'emporter, je commence par un table { width : 100% } pour qu'elles s'étalent par défaut sur toute la largeur de la page.

/* Par défaut les tables occupent toute la largeur de la page */
table
{
  width: 100%;
}

Puis j'aère les cellules parce que ça sert à rien de se payer des 21 pouces si c'est pour rester tout riquiqui et tassé.

/* Aération des cellules */
thead th,
tbody td,
tfoot th
{
  border: 1px dotted #aaa;
  padding: 0.5em;
  text-align: left;
  vertical-align: top;
}

En parlant de thead et autres, je me plonge dans toutes les vues d'ores et déjà générées par défaut pour rajouter les balises <thead> et <tbody> que Visual Studio s'acharne à oublier. Y m'énerve ! C'est pourtant pas compliqué de comprendre que :

  • C'est quand même beaucoup plus simple à styler
  • C'est bien plus pratique pour utiliser la plupart des plugins jQuery

Après ça, je peux facilement colorer les en-têtes et pieds de tables en reprenant la couleur du menu d'onglets :

/* Redéfini les couleurs de l'en-tête et du pied des tables */
thead tr th,
tfoot tr th
{
  background-color: #3399ff;
  color: #fff;
}

Puis là, comme j'en ai rien à faire des vieux navigateurs, j'alterne la couleur de fond des lignes avec du CSS3 et un fond moins blanc que blanc.

/* Alterne la couleur de fond des lignes d'une table */
tbody tr:nth-child(odd)
{
  background-color: #f0f0f0;
}

Pour continuer à animer et égayer un peu les tableaux, je fais mon petit effet au passage de la souris en changeant la couleur de fond de la ligne pour que ça pète un peu plus. Là aussi, la couleur dépend des jours et de l'inspiration. Et aujourd'hui c'est jaune clair parce qu'il a l'avantage d'avoir un petit effet orangé sur mon portable.

/* Ajoute un effet lors du survol d'une ligne */
tbody tr:hover
{
  background-color: #ffff80;
}

Form

Là aussi, il y aurait pas mal à dire du choix du balisage. Le problème, c'est que c'est tout un ensemble entre les helpers et le système de validation alors c'est un peu compliqué et que je résiste encore pour conserver tel quel le html généré par Visual Studio. Ce qui fait que je n'arrive pas à me fixer sur quelque chose de « définitif ». Enfin bref, y'a rien qui me plait pour l'instant. A suivre donc…

mysite02.png

jeudi 6 octobre 2011

Générer des URLs en minuscules avec ASP.NET MVC

Un des trucs bien de ASP.NET MVC c'est que son système de routes sert aussi pour générer automatiquement les URLs correctes pour les actions des contrôleurs à partir de méthodes telles que Html.ActionLink(), Url.Action()… Un des trucs crispant de ASP.NET MVC, c'est qu'il génère ces URLs en conservant les noms des contrôleurs et des actions tels quels.

Ainsi, si on a une action "Update" dans un contrôleur "ContactsController", cela génèrera l'URL "/Contacts/Update" alors que cela aurait été tellement plus joli de générer une URL "/contacts/update" !

Ce qui laisse 2 solutions :

  • Attendre le jour où ASP.NET MVC gèrera ça de lui-même
  • Se dire que quelques majuscules par ci par là c'est pas si moche que ça
  • N'utiliser que des minuscules pour les noms de contrôleurs et d'actions
  • Améliorer le système de routes pour minusculiser automatiquement les noms des contrôleurs et des actions

Du temps de MVC 1, j'avais déjà fait ça pour certains projets, en reprenant le code proposé par Graham O'Neale dans son billet Lowercase Route URL's in ASP.NET MVC.

Mais je trouve que c'est typiquement le genre de petits trucs sur lequel il ne faut pas perdre de temps et qu'il ne faut pas chercher à gérer soi-même. Et dans ce cas là, l'utilisation d'un package NuGet tout prêt est la solution idéale.

Ce qui tombe bien, puisque Lee Dumond a justement créé un package LowercaseRouteMVC exprès pour ça. Son utilisation est très simple puisqu'il suffit de référencer sa DLL puis de légèrement modifier la façon de déclarer les routes dans le Globas.asax.

Installer

  • Faire un clic droit sur la branche "Référence" du projet et sélectionner le sous-menu "Manage NuGet Packages…".
  • Rechercher "lowercase"
  • Demander à installer le package "LowercaseRouteMVC"
  • Lire et accepter la licence

Modifier

  • Ouvrir le fichier "Global.asax"
  • Ajouter un using LowercaseRouteMVC; au bon endroit
  • Modifier la procédure "RegisterRoutes" pour remplacer route.MapRoute par route.MapRouteLowercase
using LowercaseRoutesMVC;
  ...
public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
  
  routes.MapRouteLowercase(
    "Default", // Route name
    "{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  );

Terminé

  • Lancer l'exécution par F5
  • Les URLs générées par l'application ne contiennent plus que des noms de contrôleurs et d'actions en minuscules :)

jeudi 16 juin 2011

Utiliser jQuery UI Datepicker avec ASP.NET MVC

Normalement, je n'aime pas trop proposer un calendrier pour permettre la saisie de dates dans un formulaire. Mais comme le widget Datepicker de jQuery UI n'est pas trop envahissant ou irritant, j'ai malgré tout décidé de lui laisser une chance. Et surtout, je vois ça comme un bon moyen d'aborder les templates Editor et Display de ASP.NET MVC et d'étudier un peu plus correctement comment les utiliser.

Un projet MVC juste pour tester

Pour commencer, j'ai échafaudé vite fait une application de gestion de livres à partir de la classe suivante :

public class Livre
{
    public int ID { get; set; }
    public string Titre { get; set; }
    public DateTime Edition { get; set; }
}

Pour faire vite, j'ai simplement aménagé l'exemple EF Code First and Data Scaffolding with the ASP.NET MVC 3 Tools Update de Scott Guthrie. Après avoir bricolé un lien vers l'action "Index" de mon nouveau contrôleur "Livres" dans le fichier "_Layout.cshtml", j'ai pu accéder à mon écran de création d'un nouveau livre :

jquery-datepicker-mvc-01.jpg

Deux templates DateTime.cshtml

J'ai alors pu passer à l'exemple Create user friendly date fields with ASP.NET MVC EditorTemplates & jQueryUI donné par Rachel Appel pour créer les deux templates "DateTime.cshtml" dans les sous-répertoires "DisplayTemplates" et "EditorTemplates".

\Views\Shared\DisplayTemplates\DateTime.cshtml

@model DateTime   
@String.Format("{0:d}", Model.Date)

\Views\Shared\EditorTemplates\DateTime.cshtml

@model DateTime
@Html.TextBox("", String.Format("{0:d}", Model.Date.ToShortDateString()))

Ce qui m'a permi d'avoir des dates "propres" (sans avoir l'heure à 00:00:00) que ce soit en affichage ou en saisie :

jquery-datepicker-mvc-02.jpg

L'avantage, c'est que maintenant je n'ai rien d'autre à faire pour que toutes les zones de dates existantes ou à venir dans mon application Livres bénéficient de la même présentation.

Ajout de jQuery UI Datepicker

Je continue pas à pas le tutoriel de Rachel Appel en référençant la librairie jQuery UI (et sa CSS) dans mon fichier "_Layout.cshtml" :

<link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.min.js")" type="text/javascript"></script>

Puis je fais évoluer mon EditorTemplate pour ajouter une classe CSS afin que jQuery puisse identifier les zones de saisie de date :

@model DateTime
@Html.TextBox("", String.Format("{0:d}", Model.Date.ToShortDateString()), new { @class = "datefield" })

Et je n'ai plus qu'à utiliser la fonction jQuery ready pour indiquer que tous les éléments qui ont la classe CSS "datefield" doivent être complété d'un calendrier :

<script type="text/javascript">
$(function () {
    $(".datefield").datepicker();
});
</script>

Personnellement, j'ai placé ce script dans mon fichier "_Layout.cshtml" plutôt que dans le template "DateTime.cshtml" pour éviter qu'il soit répété (et donc ré-exécuté) autant de fois qu'il y a de date dans mon formulaire de saisie.

Et maintenant, quand je suis en saisie d'une date, j'ai le calendrier de jQuery UI qui apparait :

jquery-datepicker-mvc-03.jpg

Un calendrier en français

C'est pas mal, mais c'est tout en anglais :) Heureusement, il y a moyen d'avoir une version traduite en français très facilement. Il suffit de récupérer le fichier "jquery.ui.datepicker-fr.js" dans le référentiel Git de jQuery UI : https://github.com/jquery/jquery-ui/blob/master/ui/i18n/ puis de l'enregistrer dans le répertoire "Scripts" de la solution (et de penser à l'inclure dans le projet).

Il ne reste alors plus qu'à référencer ce script (après le script pour jQuery UI ?) dans le layout :

<script src="@Url.Content("~/Scripts/jquery.ui.datepicker-fr.js")" type="text/javascript"></script>

Et cerise sur le gâteau, le calendrier est maintenant en mesure de reconnaitre la date en cours et de s'y positionner correctement :

jquery-datepicker-mvc-04.jpg

Le cas des dates nullables

En creusant un peu sur différents exemples d'utilisation du Datepicker de jQuery UI avec ASP.NET MVC, je suis tombé sur des démos qui allaient un peu plus loin et qui prenaient en compte le cas où la date était nulle.

Dans ce cas là, il faut que les templates "Datetime.cshtml" héritent de l'objet DateTime? et plus de l'objet DateTime. Et donc modifier le code pour gérer le fait qu'on a à faire à un objet nullable, ce qui au final donne les templates suivants :

\Views\Shared\DisplayTemplates\DateTime.cshtml

@model System.DateTime?
@(Model.HasValue ? Model.Value.Date.ToShortDateString() : string.Empty)

\Views\Shared\EditorTemplates\DateTime.cshtml

@model System.DateTime?
@Html.TextBox("", Model.HasValue ? Model.Value.Date.ToShortDateString() : string.Empty, new { @class = "datefield" })

Pour tester que ça marchait, j'ai dû ajouter un seul "?" à ma classe "Livre" :

public class Livre
{
    public int ID { get; set; }
    public string Titre { get; set; }
    public DateTime? Edition { get; set; }
}

Et après ça j'ai dû supprimer ma base de données (le fichier App_Data\Livres.sdf dans mon cas) puis qu'elle avait changé. Chercher "Changing our Model and Database Schema" sur le billet VS 2010 SP1 and SQL CE de Scott Guthrie pour plus d'explications.

Note : j'ai vu des exemples qui initialisent une valeur par défaut lorsque la date est nulle (genre Model.HasValue ? Model.Value.Date.To...() : DateTime.Today.To...()). Mais selon moi, ce n'est pas quelque chose qui doit être décidé et accompli au niveau d'un template. Il est préférable de prévoir ce genre d'initialisation dans une classe ViewModel.

Moderniser le code HTML

Le déclenchement du calendrier est basé sur la présence de la classe CSS "datefield" (ce que fait $(".datefield").datepicker();). Mais c'est quasiment la préhistoire du Javascript non intrusif. Je pense qu'actuellement, il vaut bien mieux se baser sur l'attribut type qui est justement prévu pour définir une saisie de date en HTML5.

Et donc, plutôt que d'ajouter une classe CSS "datefield", le template Editor va directement ajouter un attribut type="date" :

@model System.DateTime?
@Html.TextBox("", Model.HasValue ? Model.Value.Date.ToShortDateString() : string.Empty, new { @type = "date" })

Il faut alors revoir la fonction jQuery ready pour que désormais elle prenne en compte les éléments ayant ce type :

<script type="text/javascript">
$(function () {
    $("input[type=date]").datepicker();
});
</script>

Et on va même plus loin en utilisant la librairie Modernizr promue par ASP.NET MVC pour appliquer le calendrier de jQuery UI uniquement lorsque le navigateur ne prend pas en charge la saisie des dates :

<script type="text/javascript">
$(function () {
    if (!Modernizr.inputtypes.date) {
        $("input[type=date]").datepicker();
    }
});
</script>

Conclusion

Finalement, c'est pas hyper compliqué de faire des templates. Et en y réfléchissant un peu mieux, je pense que c'est une solution qui devrait me plaire parce qu'elle est totalement "discrète" :

  • côté client, le calendrier est appliqué de façon non intrusive : le code de la balise input n'a pas été affublé d'un onclick pour lui attacher un calendrier.
  • côté serveur, le calendrier est généré de façon non intrusive : le formulaire a conservé @Html.EditorFor(model => model.Edition) sans qu'on ait à le défigurer avec un helper spécifique genre @Html.DateTimeFor(model => model.Edition).

vendredi 8 avril 2011

Gérer les virgules avec les Data Annotations

Après avoir "réparé" mon Visual Studio 2010, j'ai pu me remettre à la version 2 du tutoriel MVC Music Store pour me confronter aux dernières (pour moi) technologies ASP.NET MVC 3 et entre autre Razor et les Data Annotations.

Et justement, j'ai été un peu embarrassé par le fonctionnement de la validation via les Data Annotations dès lors qu'on n'est pas des yankees pure souche.

Par exemple, si je veux passer le prix d'un album de 8,99 à 8,90 j'obtiens l'erreur "The field Prix must be a number." de la part du plugin jQuery Validation.

Et si j'essaie de contourner en saisissant 8.90 (avec un point au lieu de la virgule), c'est l'erreur "The value '8.90' is not valid for Prix." qui prend le relai. Mais dans ce cas, cette erreur n'est pas renvoyée par jQuery Validation mais par la méthode TryUpdateModel() dans le contrôleur : mon PC étant en français, le .NET exige une virgule comme séparateur décimal.

Zut ! Déjà les messages en anglais c'est pas tip-top. Mais que ça m'affiche des valeurs numériques avec des virgules et que ça me gueule dessus quand j'essaie de saisir c'est un peu pénible quand même.

Jusqu'à présent, plutôt que de chercher à gérer le problème virgule, je me contentais de bidouiller la section «globalization» dans le fichier web.config pour que ASP.NET prenne lui aussi le "." comme séparateur décimal :

<configuration>
   <system.web>
      <globalization culture="en-US" />
   </system.web>
</configuration>

Mais cette fois-ci, je me suis dit que j'allai creuser un peu plus sinon ça enlève pas mal d'intérêt aux Data Annotations.

Localiser les messages du plugin jQuery Validation

Déjà, quand on fait des recherches sur la localisation de jQuery Validation, on se rend compte que c'est un problème général qui semble avoir été un peu laissé de côté...

Par contre, si on regarde dans le repository du plugin, il existe un répertoire localization qui contient un fichier messages_fr.js avec les messages d'erreur en français.

Super ! Mais ça ne sert pas à grand chose parce que les messages d'erreurs sont déjà initialisés directement par ASP.NET MVC :(

<input data-val="true" 
       data-val-number="The field Prix must be a number."
       data-val-range="Le champ Prix doit &amp;#234;tre compris entre 0,01 et 100."
       data-val-range-max="100" 
       data-val-range-min="0.01" 
       data-val-required="Le champ Prix est requis." 
       id="Price" name="Price" type="text" value="8,99" />

C'est malin ça de définir les messages d'erreur alors que le plugin jQuery Validation les initialise déjà de son côté. Et c'est encore plus rigolo d'en mettre certains en français et d'autres en anglais :)

Localiser "The field Xxxxx must be a number."

Première méthode pour traduire ce message : supprimer l'attribut "data-val-number" de toutes les zones de saisie et inclure le fichier messages_fr.js (attention, l'ordre des scripts est important) :

<script type="text/javascript">
    $(document).ready(function () {
        $(":input[data-val-number]").attr("data-val-number", "");
    });
</script>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script type="text/javascript">

    /* ----- messages_fr.js ----- */
    /*
    * Translated default messages for the jQuery validation plugin.
    * Locale: FR
    */
    jQuery.extend(jQuery.validator.messages, {
        required: "Ce champ est requis.",
        remote: "Veuillez remplir ce champ pour continuer.",
        email: "Veuillez entrer une adresse email valide.",
        url: "Veuillez entrer une URL valide.",
        date: "Veuillez entrer une date valide.",
        dateISO: "Veuillez entrer une date valide (ISO).",
        number: "Veuillez entrer un nombre valide.",
...

Après ça, la saisie d'une valeur incorrecte dans la zone prix n'affiche plus l'erreur "The field Prix must be a number." mais "Veuillez entrer un nombre valide.".

Deuxième méthode : si on veut continuer à indiquer le nom du champ qui pose problème dans le message d'erreur, il faut sortir l'artillerie lourde et utiliser les expressions régulières (l'ordre des scripts est toujours important) :

<script type="text/javascript">
    $(document).ready(function () {
        var reg_us = /The field (.+) must be a number\./gi;
        var msg_fr = "Le champ $1 doit être un nombre.";
        $(":input[data-val-number]").each(function () {
            var message = $(this).attr("data-val-number");
            message = message.replace(reg_us, msg_fr);
            $(this).attr("data-val-number", message);
        });
    });
</script>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script type="text/javascript">

    /* ----- messages_fr.js ----- */
    /*
    * Translated default messages for the jQuery validation plugin.
    * Locale: FR
    */
    jQuery.extend(jQuery.validator.messages, {
        required: "Ce champ est requis.",
...

Dans ce cas, la saisie d'une valeur incorrecte dans le prix affiche le message "Le champ Prix doit être un nombre.".

Faire accepter les nombres à virgule à jQuery Validation

C'est bien beau de parler à l'utilisateur en français, mais c'est quand même plus important de lui permettre de pouvoir saisir le prix qu'il veut sans avoir à abandonner les valeurs décimales.

Et là, le plugin jQuery Validation a tout prévu puisque le répertoire localisation contient également des fichiers methods_de.js, methods_nl.js et methods_pt.js. Mais malheureusement pour moi, pas de methods_fr.js en vue :(

En y regardant de plus près, le fichier methods_de.js devrait faire l'affaire. Ce qui se confirme sur le forum developpez.com.

Ni une, ni deux, il suffit de compléter les scripts de la façon suivante (l'ordre des scripts est important) :

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<script type="text/javascript">
jQuery.extend(jQuery.validator.methods, {
    date: function(value, element) {
        return this.optional(element) || /^\d\d?\.\d\d?\.\d\d\d?\d?$/.test(value);
    },
    number: function(value, element) {
        return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:\.\d{3})+)(?:,\d+)?$/.test(value);
    }
});
</script>

Et maintenant si je change mon prix en 8,90, je n'ai plus le message "Le champ Prix doit être un nombre.". Non. Maintenant ça me dit que "Le champ Prix doit être compris entre 0,01 et 100.".

C'est des coriaces !

Gérer les virgules dans jQuery Validation

Si c'est ça, je regarde dans le source de jquery.validate.js. Ah ben bien sûr, y'a un problème avec le "range" :

range: function( value, element, param ) {
    return this.optional(element) || ( value >= param[0] && value <= param[1] );
},

Il compare value (le 8,90 que j'ai saisi) avec param[0] (la valeur minimum de 0.01) et param[1] (la valeur maximum de 100). Personne lui a jamais dit qu'on ne peut pas comparer les points et les virgules ?

Et le pire, c'est qu'il fait pareil avec les fonctions "min" et max" le bougre ! Ca va pas se passer comme ça :

/* ----- methods_fr.js ----- */
/*
* Localized default methods for the jQuery validation plugin.
* Locale: FR
*/
jQuery.extend(jQuery.validator.methods, {
    min: function (value, element, param) {
        return this.optional(element) || replaceComma(value) >= replaceComma(param);
    },
    max: function (value, element, param) {
        return this.optional(element) || replaceComma(value) <= replaceComma(param);
    },
    range: function (value, element, param) {
        value = replaceComma(value);
        return this.optional(element) || (value >= replaceComma(param[0]) && value <= replaceComma(param[1]));
    },
    date: function (value, element) {
        return this.optional(element) || /^\d\d?\.\d\d?\.\d\d\d?\d?$/.test(value);
    },
    number: function (value, element) {
        return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:\.\d{3})+)(?:,\d+)?$/.test(value);
    }
});

function replaceComma(value) {
    // Quick & Dirty replace "," by "." as decimal separators
    return value.replace(",", ".");
}

Et après ça, 8,90 passe enfin alors que 100,01 est bien refusé !

Ouf ! Ca devrait faire l'affaire jusqu'à la sortie de jQuery Validation 2.0.

Mise à jour : si j'avais cherché mieux, j'aurais pu trouver le billet Using MVC 3 with non-English Locales de Rick Anderson, le co-auteur du tutoriel Getting Started With MVC3.

mercredi 8 septembre 2010

ASP.NET MVC 2 : Nouvelle syntaxe <%: %> pour encoder la sortie HTML

Ceci est la traduction du billet "New <%: %> Syntax for HTML Encoding Output in ASP.NET 4 (and ASP.NET MVC 2)" de Scott Guthrie.

Le billet d'aujourd'hui va couvrir une petite (mais malgré tout très utile) nouveauté syntaxique qui est apparue avec ASP.NET 4. Il s'agit de la possibilité d'encoder automatiquement le code HTML inséré à l'intérieur des pages. Cela contribue à protéger vos applications et vos sites contre le Cross-site scripting (XSS) et les attaques par injection HTML, et cela grâce à une nouvelle syntaxe à la fois élégante et concise.

Encoder le HTML

Le Cross-site scripting (XSS) et les attaques par injection HTML représentent deux des principales failles de sécurité qui gangrènent les sites internet et les applications. Elles surviennent lorsque des hackers découvrent un moyen pour injecter un script côté client ou du code HTML dans des pages web qui seront ensuite consultées par d'autres visiteurs du site. Cela peut aussi bien servir à vandaliser le site qu'à permettre aux hackers de lancer des scripts côté client pour voler les informations des cookies ou bien usurper l'identité d'un utilisateur sur le site pour réaliser des actions malveillantes.

Une des solutions pour parvenir à réduire ce risque est de s'efforcer à ce que le contenu HTML renvoyé au navigateur client soit toujours encodé. Cela permet de s'assurer que toute information qui aurait pû être créée ou modifiée par un utilisateur final ne pourra jamais être renvoyée vers une page côté client en contenant des balises telles que <script> ou <img>.

Comment encodait-on le HTML jusqu'à présent ?

Les applications ASP.NET (et en particulier celles qui utilisent ASP.NET MVC) s'appuient très souvent sur la syntaxe <%= %> pour générer certains éléments. Actuellement, les développeurs peuvent utiliser les méthodes Server.HtmlEncode() ou HttpUtility.Encode() à l'intérieur de ce bloc pour y encoder le HTML avant qu'il ne soit renvoyé côté client. C'est ce qui est fait avec un code tel que celui-ci :

Bien que cela fonctionne parfaitement, cela présente deux inconvénients :

  1. C'est assez verbeux
  2. Les développeurs oublient souvent d'appeler la méthode Server.HtmlEncode() – et il n'existe pas de moyen facile pour vérifier qu'elle est bien utilisée tout au long de l'application.

Encoder avec la nouvelle syntaxe <%: %>

Avec ASP.NET 4, nous avons introduit une nouvelle syntaxe <%: %> pour générer des éléments comme le fait un bloc <%= %>, mais qui en plus l'encode automatiquement avant de le générer. Cela évite d'avoir à convertir explicitement le contenu HTML comme nous l'avons fait dans l'exemple ci-dessus. Au lieu de ça, vous pouvez maintenant vous contenter d'écrire le code ci-dessous pour faire exactement la même chose :

Nous avons opté pour la syntaxe <%: %> afin qu'il soit facile et rapide de remplacer les morceaux de code <%= %> existants. Cela vous permet également de rechercher facilement les éléments <%= %> dans votre code et de contrôler les cas où vous n'utilisez pas l'encodage HTML dans votre application pour vous assurer que c'est bien ce que vous souhaitiez faire.

Eviter le double encodage

Bien que l'encodage HTML soit souvent une bonne pratique, il existe quelques cas où le contenu que vous renvoyez doit être du HTML ou qu'il est déjà encodé, auquel cas vous ne souhaitez pas l'encoder à nouveau.

ASP.NET 4 comporte une nouvelle interface IHtmlString (ainsi que son implémentation concrète : HtmlString) que vous pouvez implémenter sur vos types pour indiquer que leur valeur est déjà correctement encodée (ou tout au moins contrôlée) pour pouvoir être affichée en tant que HTML, et que par conséquent cette valeur ne doit pas être (ré)encodée. Le bloc <%: %> teste la présence de cette interface IHtmlString dans les expressions qu'il contient et le cas échéant ne vas pas faire d'encodage HTML pour les valeurs qui implémentent cette interface. Cela évite aux développeurs d'avoir à se poser la question au cas par cas pour savoir s'il faut utiliser la syntaxe <%: %> ou non. Vous pouvez vous contenter de toujours utiliser des blocs <%: %> et faire en sorte que toutes les propriétés ou types qui sont déja encodées implémentent bien l'interface IHtmlString.

Utiliser les helpers ASP.NET MVC avec <%: %>

Prenons le cas où vous utilisez des méthodes helpers en ASP.NET MVC pour avoir un exemple concret qui montre dans quel cas ce mécanisme d'encodage HTML s'avère utile. Les méthodes helper renvoient habituellement du HTML. A titre d'exemple, la méthode helper Html.TextBox() renvoie une balise du style <input type="text"/>. Avec ASP.NET MVC 2, ces méthodes helper renvoient maintenant des types HtmlString par défaut, de façon à indiquer que l'affichage du contenu renvoyé est sûr et qu'il n'a pas à être encodé par le bloc <%: %>.

Cela vous permet d'utiliser ces méthodes aussi bien avec des blocs <%= %> :

Qu'à l'intérieur des nouveaux blocs <%: %> :

Dans les deux cas ci-dessus, le contenu HTML renvoyé par la méthode helper sera envoyé vers le client en tant que HTML et le bloc <%: %> ne cherchera pas à faire un double encodage.

Cela vous permet de toujours utiliser la syntaxe <%: %> par défaut plutôt que des blocs <%= %> dans vos applications. Si vous êtes du genre intraitable, vous pouvez même créer une règle de build dont le but sera de traquer l'utilisation de blocs <%= %> dans votre application pour que vous soyez certain que rien n'échappe à l'encodage HTML

Génération des vues ASP.NET MVC 2

Lorsque vous utilisez VS 2010 (ou sa version gratuite Visual Web Developer 2010 Express) pour construire des applications ASP.NET MVC 2, vous pouvez voir que les vues qui sont générées par l'intermédiaire de la boite de dialogue "Add View" utilisent désormais des blocs <%: %> par défaut quand il s'agit de renvoyer du contenu. Dans l'exemple ci-dessous, j'ai demandé à générer une simple vue "Edit" pour un objet Article. Vous pouvez voir que la syntaxe <%: %> est employée trois fois pour le label, la textbox et le message de validation (tous trois obtenus grâce à des méthodes helper) :

Résumé

La nouvelle syntaxe <%: %> offre une méthode concise pour encoder automatiquement le contenu puis le renvoyer vers la sortie. Cela vous permet de rendre votre code un peu moins verbeux et d'assez facilement vérifier / contrôler que vous faites toujours de l'encodage HTML dans tout votre site. Ce qui participe à la protection de votre application contre les attaques de type Cross-site scripting (XSS) et injection HTML.

Ceci est la traduction du billet "New <%: %> Syntax for HTML Encoding Output in ASP.NET 4 (and ASP.NET MVC 2)" de Scott Guthrie.

vendredi 6 août 2010

ASP.NET MVC 2 : Validation basée sur le modèle

Ceci est la traduction du billet "ASP.NET MVC 2: Model Validation" de Scott Guthrie.

Ceci est le deuxième billet d'une série consacrée à la prochaine sortie de ASP.NET MVC 2. Ce billet aborde les améliorations apportées à la validation dans la future version de ASP.NET MVC 2.

La validation dans ASP.NET MVC 2

Valider les saisies utilisateurs et appliquer des règles métiers est un besoin élémentaire dans la plupart des applications web. ASP.NET MVC 2 propose un tas de nouvelles fonctionnalités qui simplifient de façon significative la validation de la saisie utilisateur et l'application de règles de validation sur les objets Model ou ViewModel. Ces fonctionnalités sont conçues de façon à ce que la logique de validation soit toujours appliquée côté serveur et qu'en option elle puisse aussi être applicable côté client via du code Javascript. L'infrastructure et les fonctionnalités de validation d'ASP.NET MVC 2 sont conçues avec deux objectifs :

  1. Les développeurs peuvent facilement employer les DataAnnotations pour gérer la validation. Basées sur de simples déclarations, les DataAnnotations donnent la possibilité d'ajouter des règles de validation aux objets et aux propriétés avec un minimum de code.
  2. S'ils le souhaitent, les développeurs peuvent intégrer leur propre système de validation ou employer un framework de validation existant, tel que Castle Validator ou EntLib Validation Library. Les fonctionnalités de validation de ASP.NET MVC 2 sont conçues pour faciliter l'incorporation de tout type d'architecture de validation - tout en conservant les avantages de la nouvelle infrastructure de validation de ASP.NET MVC 2 (comprenant la validation côté client, la validation du binding avec le modèle, etc...)

Grâce à tout cela, la validation est vraiment très simple dans la majorité des cas, tout en restant suffisamment flexible pour les scénarios d'utilisation les plus évolués.

Utiliser les DataAnnotations pour la validation en ASP.NET MVC 2

Pour apprendre à tirer parti du support des DataAnnotations pour valider les données, nous allons étudier un exemple de scénario CRUD tout simple. Et nous allons plus précisément développer un formulaire "Create" qui permettra à l'utilisateur de saisir les coordonnées de ses amis :

Notre objectif est de vérifier que les informations saisies sont valides avant de les enregistrer dans la base de données et si ce n'est pas le cas afficher les messages d'erreurs correspondant :

Nous souhaitons que cette validation s'effectue à la fois côté serveur et côté client (via du Javascript). Et nous voulons que notre code respecte l'approche DRY ("Don't Repeat Yourself") - ce qui implique que nous devons appliquer les règles de validation en un seul endroit et qu'elles soient respectées par les contrôleurs, les actions et les vues.

Je vais utiliser VS 2010 et ASP.NET MVC 2 pour mettre en oeuvre le scénario décrit ci-dessus. Mais vous pouvez tout aussi bien employer VS 2008 et ASP.NET MVC 2 pour arriver au même résultat.

Etape 1: Implémenter FriendsController (sans validation pour commencer)

Nous commencerons par ajouter une classe "Person" toute simple à notre nouveau projet ASP.NET MVC 2 :

Celle-ci contient 4 propriétés, implémentées en utilisant les propriétés automatiques de C# (un truc que VB gère aussi dans VS 2010 - Youhou !).

Nous allons ensuite ajouter une classe contrôleur "FriendsController" à notre projet qui exposera deux méthodes d'action "Create". La première action est appelée lorsque une requête HTTP-GET est faite sur l'URL /Friends/Create. Cela affichera un formulaire vide pour y saisir les informations d'une personnes. La seconde action est appelée lorsque une requête HTTP-POST est postée vers l'URL /Friends/Create. Elle fait correspondre les zones de saisie du formulaire avec un objet Person, contrôle qu'il n'y a pas d'erreur de correspondance, et si c'est correct va éventuellement l'enregistrer dans la base de données (nous verrons cet aspect du travail plus loin dans ce tutoriel). Si les données envoyées par le formulaire sont incorrectes, la méthode d'action ré-affiche le formulaire de saisi avec les messages d'erreur :

Une fois que nous avons programmé notre contrôleur, nous pouvons faire un clic-droit à l'intérieur d'une de ces méthodes d'action et sélectionner la commande "Add View" de Visual Studio - ce qui affiche la boite de dialogue "Add View". Nous demandons à générer une vue de type "Create" basée sur un objet Person :

Visual Studio va alors générer automatiquement un fichier Create.aspx dans le répertoire \Views\Friends de notre projet. Vous pouvez pouvoir voir ci-dessous comment il tire parti des nouveaux helpers HTML fortement typés gérés par ASP.NET MVC 2 (ce qui offre un contrôle à la compilation et une meilleure intellisense).

Et maintenant, lorsque nous lançons l'application et appelons l'URL /Friends/Create nous arrivons sur un formulaire vide dans lequel nous pouvons effectuer notre saisie :

Cependant, étant donné que nous n'avons pas encore implémenté l'aspect validation, rien ne nous empêche de saisir des informations erronées dans ce formulaire et de les publier vers le serveur.

Etape 2: Activer la Validation avec les DataAnnotations

Nous allons maintenant mettre à jour notre application pour qu'elle respecte quelques règles de validation de base. Nous implémenterons ces règles sur notre objet "Person" - et pas dans le contrôleur "FriendsController" ou la vue "Create". L'avantage d'implémenter ces règles au niveau de l'objet "Person" nous assure que ces règles seront suivies dans tous les cas où notre application utilisera l'objet "Person" (pour gérer l'action "Edit" par exemple). Cela nous aide à éviter de répéter nos règles de validation plusieurs fois dans notre application et donc à respecter la philosophie DRY.

ASP.NET MVC 2 permet aux développeurs d'ajouter facilement des attributs de validation sous forme de déclaration aux différentes classes Model ou ViewModel et ensuite que ces règles de validation soient automatiquement appliquées chaque fois que l'application fera du binding de données pour ces classes. Pour mettre cela en application, nous allons modifier notre classe Person pour y définir quelques attributs de validation. Pour cela, nous commençons par faire un "using" de l'espace de noms "System.ComponentModel.DataAnnotations" dans l'en-tête du fichier source - puis préfixer les propriétés de l'objet Person avec les attributs de validation [Required], [StringLength], [Range], et [RegularExpression] (qui sont tous disponible via cet espace de nom) :

Remarque: Nous avons défini les messages d'erreurs sous forme de chaines. Il est aussi possible de les définir dans des fichiers de ressources puis de les adapter en fonction de la langue des visiteurs. Vous avez plus d'informations sur la façon de localiser les messages d'erreur dans le billet «Localizing ASP.NET MVC Validation».

Maintenant que nous avons ajoutés des attributs de validation à notre classe Person, nous pouvons relancer l'application et regarder ce qui se passe quand nous saisissons des données incorrectes puis que nous les envoyons au serveur :

Vous pouvez constater que notre application offre désormais une gestion des erreurs tout à fait satisfaisante. Les zones de saisies avec des données incorrectes sont surlignées en rouge et les messages d'erreur de validation que nous avons définis sont portés à la connaissance de l'utilisateur. D'autre part, le formulaire a conservé le contenu des éléments saisis - si bien que l'utilisateur n'a qu'à les corriger et pas à les saisir à nouveau.

Vous vous demandez sans doute comment ça marche ?

Pour bien comprendre ce comportement, regardons de plus près la méthode d'action Create qui gère la requête POST envoyée par le formulaire :

Quand notre formulaire HTML est envoyé vers le serveur, la méthode ci-dessus est appelée. Etant donnée que cette méthode d'action attend un objet "Person" en paramètre, ASP.NET MVC va créer un objet "Person" et automatiquement initialiser ses propriétés à partir des informations du formulaire. Au cours de cette phase, il va aussi contrôler si les attributs de validation DataAnnotation sont valides pour cet objet Person. Si tout est correct, alors la propriété ModelState.IsValid renverra vrai et dans ce cas nous sauvegarderons (bientôt) cette personne dans la base de données puis nous ferons une redirection vers la page d'accueil.

S'il y a la moindre erreur de validation pour notre objet "Person", la méthode d'action va ré-afficher le formulaire avec les données saisies. C'est ce dont se charge la dernière ligne de code dans l'exemple ci-dessus.

Les messages d'erreurs sont alors affichés dans la vue grâce au fait que notre formulaire Create contient des méthodes helper <%= Html.ValidationMessageFor() %> en face de chaque helper <%= Html.TextBoxFor() %>. Ces helpers vont afficher le message d'erreur correspondant pour chaque propriété incorrecte qui aura été passée à à vue :

Le plus sympa avec cette approche, c'est qu'elle est carrément simple à mettre en œuvre et qu'elle nous permet très facilement d'ajouter ou de modifier des règles de validation à notre classe Person sans avoir à mettre à jour le moindre code dans nos vues ou nos contrôleurs. Cette capacité de définir les règles de validation à un seul endroit pour qu'elles soient prises en compte et respectées partout ailleurs nous permet de faire évoluer notre application et ses règles extrêmement rapidement avec un minimum de travail et en suivant au mieux la philosophie DRY.

Etape 3: Activer la validation côté client

Actuellement, la validation pour notre application ne s'exécute que côté serveur, ce qui signifie que nos utilisateurs devront valider le formulaire pour l'envoyer vers le serveur afin de savoir si leur saisie est correcte.

Un des aspects intéressant de l'architecture de validation de ASP.NET MVC 2 c'est qu'elle gère à la fois le côté serveur et le côté client. Tout ce que nous avons à faire pour que cela fonctionne, c'est de référencer deux Javascripts dans notre vue et d'ajouter une ligne de code :

Une fois que nous avons ajouté ces trois lignes, ASP.NET MVC 2 va utiliser les méta-données de validation que nous avons ajouté à notre classe Person et s'occupera pour nous de tous les traitements de validation en Javascript côté client. Cela signifie que les utilisateurs auront un retour immédiat sur les erreurs de validation dès qu'ils quitteront une zone de saisie.

Pour voir ce que donne cette prise en charge Javascript côté client dans le cas de notre application, nous n'avons qu'à relancer l'application et remplir les trois premières zones de saisie avec des valeurs correctes avant d'essayer de cliquer sur le bouton "Create". Comme vous pouvez le voir ci-dessous, nous obtenons aussitôt un message d'erreur sans que le formulaire ait besoin d'être envoyé vers le serveur :

Si nous saisissons alors n'importe quoi d'incorrect, le message d'erreur passe immédiatement de "Email Required" à "Not a valid email" (tout deux correspondant aux messages d'erreurs que nous avons définis quand nous avons ajouté les règles de validation à la classe Person) :

Le message d'erreur disparait dès que nous saisissons une adresse email valide et le fond de la zone de saisie revient à sa couleur normale :

Ce qu'il y a de bien, c'est que nous n'avons pas eu à écrire nous même le moindre morceau de JavaScript pour que cela fonctionne. Et notre code pour la validation reste fidèle au principe DRY - nous pouvons définir les règles à un seul endroit et elles s'appliquent dans toute l'application - aussi bien côté serveur que côté client.

Ce qu'il faut savoir, c'est que pour des raisons de sécurité, les règles de validations seront toujours appliquées côté serveur, même dans le cas où vous les avez déjà activées côté client. De cette façon, vous être protégé des pirates qui pourraient essayer de contourner les règles de validation côté client pour tromper votre serveur.

Avec ASP.NET MVC 2, le support de la validation côté client en JavaScript peut fonctionner avec n'importe quel framework ou système de validation que vous pouvez utiliser. Cela ne nécessite pas d'avoir choisi l'approche DataAnnotation - l'ensemble du système fonctionne de façon totalement indépendante des DataAnnotations et peut aussi bien être employée avec Castle Validator, EntLib Validation Block ou tout autre méthode de validation personnalisée que vous pouvez décider de mettre en place.

Si vous ne souhaitez pas utiliser nos fichiers JavaScript côté client, vous pouvez également les remplacer par un plugin de validation en jQuery et employer cette librairie à la place. Le téléchargement de ASP.NET MVC Futures intègrera la possibilité d'utiliser une validation jQuery en complément de la validation ASP.NET MVC 2 côté serveur.

Etape 4: Créer un attribut de validation personnalisé

L'espace de nom System.ComponentModel.DataAnnotations du framework .NET contient un certain nombre d'attributs de validation tous prêts que vous pouvez utiliser. Nous en avons déjà vu 4 jusqu'à présent : [Required], [StringLength], [Required], et [RegularExpression].

Vous avez aussi la possibilité de définir et d'employer vos propres attributs de validations personnalisés. Vous pouvez développer des attributs spécialisés à partir de zéro en utilisant la classe de base ValidationAttribute disponible dans l'espace de noms System.ComponentModel.DataAnnotations. Une autre solution consiste à hériter d'un des attributs de validation déjà existant et à simplement étendre certaine de ses fonctionnalités.

Par exemple, pour arriver à nettoyer un peu le code de la classe Person, nous pourrions créer un nouvel attribut de validation [Email] qui utiliserait l'expression régulière servant à vérifier les adresses email. Pour cela, il nous suffit d'hériter de la classe d'attribut RegularExpression et de simplement appeler le constructeur de l'objet de base avec la bonne expression régulière :

Nous pouvons alors mettre à jour la classe Person pour qu'elle utilise notre nouvel attribut [Email] au lieu de l'expression régulière que nous utilisions jusqu'ici - ce qui rend notre code beaucoup plus clair et concis :

Lors de la création d'attributs de validation personnalisés, vous pouvez spécifier une logique de validation qui s'exécute à la fois sur le serveur et sur le client via JavaScript.

Etape 5: Sauvegarder dans la base de données

Nous allons maintenant développer les traitements nécessaires pour enregistrer nos amis dans la base de données :

Pour l'instant, nous travaillons seulement avec une bonne vieille classe C# (aussi connue sous le nom de classe "POCO" pour "Plain Old CLR (ou C#) Object"). Nous aurions pu écrire un morceau de code supplémentaire pour assurer la persistance en faisant correspondre les éléments de cette classe à la base de données. Les solutions de mapping objet-relationnel (ORM) telles que NHibernate gèrent désormais très bien ce genre de mapping POCO / PI (Persistence Ignorant). la nouvelle version de ADO.NET Entity Framework (EF) qui va être livrée avec .NET 4 supportera elle aussi le mapping POCO / PI et comme NHibernate, elle permettra également de spécifier le mapping pour la persistance directement au niveau du code (sans qu'il soit besoin de passer par des fichiers de configuration ou des concepteurs visuels).

Si notre objet Person avait été mappé à la base de données d'une de ces façons, nous n'aurions rien eu d'autre à faire, ni au niveau de la classe Person, ni au niveau des règles de validation : tout aurait continué à fonctionner parfaitement.

Mais qu'est-ce qui se passe dans le cas où nous utilisons un outil graphique pour effectuer notre mapping objet-relationnel ?

De nos jours, la plupart des développeurs qui utilisent Visual Studio n'écrivent pas eux même le code dont ils ont besoin pour le mapping et la persistance. Ils font confiance pour cela au concepteurs graphiques intégrés de Visual Studio qui s'en chargent pour eux.

Une question qui revient souvent quand on utilise les DataAnnotations (ou tout autre forme de validation basée sur des attributs) est : "Comment faire pour les appliquer lorsque le modèle d'objet avec lequel on travaille est créé / mis à jour via un concepteur visuel ?". Par exemple, que se passerait-il si au lieu d'avoir une classe Person de type POCO comme celle que nous utilisons depuis le début, nous avions mis au point cette classe Person avec un outil graphique tel que le concepteur LINQ to SQL ou celui de ADO.NET EF ?

La copie d'écran ci-dessus montre une classe Person crée à l'aide du concepteur ADO.NET EF dans VS 2010. La fenêtre du haut défini la classe Person et la fenêtre du bas montre l'éditeur de mapping qui sert à assurer la correspondance entre ses propriétés et une table "People" dans la base de données. Quand on clique sur "Save", le concepteur génère automatiquement une classe Person à votre place dans le projet. C'est très bien, sauf qu'à chaque fois que vous changez quelque chose et re-cliquez sur "Save", il va re-générer cette classe. Et ainsi tous les attributs de validation que vous auriez pu y définir seraient perdu.

Une méthode qui s'offre à nous pour ajouter des méta-données sous forme d'attribut (tels que les attributs de validations) à une classe qui est générée et maintenue par un concepteur de Visual Studio est d'employer la technique de la "buddy class". Cela consiste à créer une autre classe avec nos attributs de validation et autres méta-données puis à la relier à la classe qui a été généré par Visual Studio en appliquant un attribut "MetadataType" à une troisième classe partielle qui sera combinée avec la classe générée lors de la compilation. Prenons l'exemple des règles de validation que nous avons utilisées jusqu'à maintenant. Si nous voulons les appliquer à une classe Person qui provient des concepteurs LINQ to SQL ou ADO.NET EF, nous n'avons qu'à placer ce code de validation dans une classe à part nommée "Person_Validation" puis à faire le lien avec la classe "Person" générée par VS en utilisant le code ci-dessous :

L'approche ci-dessus n'est sans doute pas aussi élégante qu'une approche purement POCO mais présente l'avantage de fonctionner plutôt bien avec n'importe quel code généré par un outil ou un concepteur de Visual Studio.

Dernière étape – Enregistrer en base de données

Notre dernière étape - que nous utilisions une classe "Person" de type POCO ou générée automatiquement - sera de sauvegarder nos amis convenables dans la base de données.

Faire cela demande juste de remplacer le "Todo" laissé dans la classe FriendsController par 3 lignes de code qui enregistrent notre nouvel ami dans la base de données. Le code ci-dessous présente la totalité de la classe FriendsController dans le cas où on passe par ADO.NET EF pour gérer la persistance des données.

Et maintenant, quand nous allons sur l'URL /Friends/Create nous pouvons facilement ajouter des personnes à la base de données de nos amis :

La validation de toutes ces données est appliquée aussi bien par le navigateur client que par le serveur. Nous pouvons donc facilement ajouter, modifier ou supprimer des règles de validation à un seul endroit et avoir la certitude qu'elles seront prises en compte par tous les contrôleurs et toutes les vues de notre application.

Conclusion

ASP.NET MVC 2 simplifie énormément la gestion de la validation dans les applications web. Il favorise une approche DRY avec une validation qui s'appuie sur le modèle ce qui permet de s'assurer que les règles de validation seront appliquées de façon identique dans toute l'application. Avec la prise en charge des DataAnnotations par ASP.NET MVC 2, la mise en oeuvre de la plupart des scénarios classiques de validation devient un véritable jeu d'enfant. Et les possibilités d'extension au niveau de l'infrastructure de validation de ASP.NET MVC 2 vous permettent de gérer un grande variété de scénarios de validation plus poussés - et d'intégrer n'importe quel framework ou système de validation existant ou personnalisé.

Ceci est la traduction du billet "ASP.NET MVC 2: Model Validation" de Scott Guthrie.

mardi 3 août 2010

End of Tutorial

You’ve now seen how ASP.NET MVC can be used to create a realistic e-commerce application. This extended example demonstrated many of the framework’s features (controllers, actions, routing, views and partials, model binding, metadata, validation, master pages, and Forms Authentication) and related technologies (LINQ to SQL, Ninject for DI, and NUnit and Moq for unit testing). You’ve made use of clean, component-oriented architecture to separate out the application’s concerns, keeping it simple to understand and maintain.

Steven Sanderson - Pro Asp.net Mvc V2 Framework

samedi 31 juillet 2010

ASP.NET MVC 2 : Des helpers HTML fortement typés

Ceci est la traduction du billet "ASP.NET MVC 2: Strongly Typed Html Helpers" de Scott Guthrie.

Ceci est le premier billet d'une série consacrée à la prochaine sortie de ASP.NET MVC 2. Ce billet présente les nouveaux helpers fortement typés qui ont fait leur apparition dans ASP.NET MVC 2.

Les helpers HTML actuels

ASP.NET MVC 1 est fourni avec un certain nombre de helpers HTML que vous pouvez utiliser dans les vues pour simplifier la génération de l'interface utilisateur en HTML. Par exemple, pour afficher une textbox, vous pouvez utiliser le helper Html.TextBox() de la façon suivante dans une de vos vues :

Le premier paramètre de la méthode helper ci-dessus fournit le nom et l'id pour la textbox et le second paramètre défini la valeur qu'elle doit contenir. Ce helper aura comme effet de produire le code HTML suivant :

Les nouveaux helpers HTML fortement typés

Une des fonctionnalités qui nous a le plus été demandée était de gérer des helpers HTML fortement typés basés sur des expressions lambda pour référencer les objets Model ou ViewModel passés aux vues. Cela permet une meilleure vérification des vues au moment de la compilation (et donc de découvrir les bugs lors de la programmation et pas de l'exécution) et aussi la possibilité de profiter de l'intellisense dans les vues.

Les nouveaux helpers HTML fortement typés font désormais parti de ASP.NET MVC 2. Ces méthodes utilisent la convention de nommage "Html.HelperNameFor()", comme par exemple : Html.TextBoxFor(), Html.CheckBoxFor(), Html.TextAreaFor()... Ils autorisent l'utilisation d'une expression lambda pour définir en une fois les attributs id/name et la valeur du contrôle à générer.

Par exemple, avec ASP.NET MVC 2, nous pouvons maintenant utiliser le nouveau helper Html.TextBoxFor() en plus du Html.TextBox() classique :

Vous remarquerez dans le code ci-dessus que nous n'avons plus besoin d'utiliser la chaine "ProductName". Les expressions lambda sont suffisamment souples pour permettre de retrouver le nom de la propriété ou du champ dans notre modèle d'objets en plus de sa valeur.

Etant donné que les helpers HTML sont fortement typés, cela nous permet d'avoir accès à l'intellisense de Visual Studio lorsque nous écrivons l'expression lambda :

Le code HTML généré est exactement le même que pour la version présentée dans le premier exemple :

Les helpers HTML fortement typés fournis avec ASP.NET MVC 2

ASP.NET MVC 2 intègre d'ores et déjà les helpers HTML fortement typés suivants :

Eléments HTML

  • Html.TextBoxFor()
  • Html.TextAreaFor()
  • Html.DropDownListFor()
  • Html.CheckboxFor()
  • Html.RadioButtonFor()
  • Html.ListBoxFor()
  • Html.PasswordFor()
  • Html.HiddenFor()
  • Html.LabelFor()

Autres helpers

  • Html.EditorFor()
  • Html.DisplayFor()
  • Html.DisplayTextFor()
  • Html.ValidationMessageFor()

Je reviendrai sur les nouvelles méthodes helpers Html.EditorFor() et Html.DisplayFor() dans un autre billet de cette série quand j'aborderai les évolutions apportées à la fonctionalité d'auto-scaffold en ASP.NET MVC 2. Nous verrons aussi le helper Html.ValidationMessageFor() dans le prochain billet de cette série qui présentera les nouveautés en matière de validation apportées par ASP.NET MVC 2.

Les helpers HTML fortement typés et le Scaffolding

VS 2008 et VS 2010 utilisent désormais tous les deux les nouveaux helpers fortement typés pour auto-générer les vues fortement typées à partir de la commande "Add View".

Par exemple, supposons que nous ayons une classe "ProductsController" toute simple comme ci-dessous avec une action Edit() qui renvoie un formulaire de modification pour un objet "Product" :

Dans Visual Studio, nous pouvons faire un clic-droit à l'intérieur de l'action Edit() et choisir la commande "Add View" dans le menu contextul pour créer une nouvelle vue. Nous choisirons de créer une vue selon le modèle "Edit" qui sera basée sur un objet de type "Product" :

Avec ASP.NET MVC 2, la vue qui a été créée par défaut utilise désormais les nouveaux helpers HTML fortement typés pour faire référence à l'objet Product :

Conclusion

Les helpers HTML fortement typés intégrés avec ASP.NET MVC 2 fournissent une méthode pratique pour obtenir un contrôle du type de données à l'intérieur de nos vues. Ils permettent un meilleur contrôle de nos vues dès la compilation (ce qui vous permet de détecter les erreurs à la compilation et pas à l'utilisation) et ils offrent un support de l'intellisense plus puissant lorsque vous codez vos vues depuis Visual Studio.

Ceci est la traduction du billet "ASP.NET MVC 2: Strongly Typed Html Helpers" de Scott Guthrie.

mercredi 28 juillet 2010

Création d'un premier projet avec Sinatra

Ceci est la traduction du tutoriel Sinatra "Project 1: Reverse" de Darren Jones.

Après avoir brillamment suivi pas à pas mon premier tutoriel pour installer Ruby et Sinatra sur mon PC Windows 7, je continue sur ma lancée avec la réalisation du deuxième tutoriel proposé par Darren Jones.

Le but de ce tutoriel est de programmer une première application très très simple qui va se contenter d'afficher un texte à l'envers, d'où son nom : Projet 1 : Reverse. A travers cette mini-application, on peut déjà apprendre quelques trucs sur le fonctionnement de Sinatra, voire sur Ruby si on débute comme moi.

C'est parti

Pour commencer, j'ai créé un répertoire C:\Ruby\projets\reverse puis un fichier main.rb à l'intérieur :

require 'rubygems'
require 'sinatra'

get '/' do
  "I did it my way!"
end

Toutes les applications Sinatra ont besoin des deux premières lignes. Il y a ensuite une ligne blanche pour faire plus joli puis 3 lignes de code où se situe toute l'action :

  • «get» indique quelle méthode HTTP on souhaite gérer : un GET dans le cas présent,
  • «'/'» correspond à la route à gérer, soit la racine de l'application dans ce cas,
  • «do ... end» est un bloc de code pour définir ce qui se passe quand quelqu'un demande la racine du site.

La dernière ligne à l'intérieur du bloc «do ... end» contient toujours (je pense) ce qui sera affiché dans la page, soit "I did it my way!" dans ce premier exemple.

Pour tester ce code, on peut directement double-cliquer sur le fichier main.rb dans le répertoire C:\Ruby\projets\reverse ou faire ça à la main dans une invite de commande :

C:\Ruby\projets\reverse>ruby main.rb

Et Sinatra entre en scène :

== Sinatra/1.0 has taken the stage on 4567 for development with backup from WEBrick
[2010-07-27 21:44:57] INFO  WEBrick 1.3.1
[2010-07-27 21:44:57] INFO  ruby 1.9.1 (2010-07-02) [i386-mingw32]
[2010-07-27 21:44:57] INFO  WEBrick::HTTPServer#start: pid=3604 port=4567

On peut alors lancer un navigateur pour appeler l'URL http://localhost:4567/ :

reverse-1.png

Tant qu'on est là, si on essaie d'aller sur une URL qui n'existe pas, comme http://localhost:4567/reverse, on obtient alors la page d'erreur 404 de Sinatra pour indiquer qu'il ne connait pas ce morceau :

reverse-2.png

Cette page d'erreur nous conseille même sur la route ajouter dans notre fichier pour que cela fonctionne, en l'occurrence :

get '/reverse' do
  "Hello World"
end

Avec Sinatra, c'est pas plus compliqué que ça pour créer des actions correspondant à différentes routes.

Ajouter une vue

Pour l'instant, on va rester sur notre route «'/'» et essayer de faire un peu mieux que de seulement renvoyer une ligne de texte. Pour cela, il faut créer une vue en modifiant le fichier main.rb de la façon suivante :

require 'rubygems'
require 'sinatra'

get '/' do
  erb :home
end

__END__

@@ home

<h1>Reverse</h1>

<p>Welcome to the home page of my very first Sinatra app.</p>

Ce coup-ci, au lieu d'utiliser la dernière ligne du bloc pour dire à Sinatra ce qu'il doit afficher, nous lui avons demandé d'utiliser la vue "home" que nous avons codé en erb (embedded ruby). Cette vue est enregistrée à la fin du fichier, après la ligne __END__ et elle est repéré par le code @@ home.

Pour voir ce que donne cette vue, il faut revenir à l'invite de commande et arrêter le serveur s'il est toujours en cours d'exécution. Ctrl-C =>Sinatra has ended his set (crowd applauds). Puis on relance le serveur avec ruby main.rb et on réaffiche la page http://localhost:4567 :

reverse-3.png

Créer une vue externe

En fait, on n'est pas obligé de stocker les vues dans le même fichier. Il est bien plus pratique de les enregistrer dans un sous-répertoire "views" de notre projet (soit C:\Ruby\projets\reverse\views dans mon cas).

Là, il suffit de créer le fichier "home.erb" avec le code ci-dessous :

<h1>Reverse</h1>

<p>Welcome to the home page of my very first Sinatra app.</p>

Il est alors possible de simplifier le fichier "main.rb" de la façon suivante :

require 'rubygems'
require 'sinatra'

get '/' do
  erb :home
end

Il ne reste plus qu'à contrôler que tout est ok : Ctrl-C, ruby main.rb, rafraichir la page et vérifier que rien n'a changé.

Créer un layout

D'un point de vue visuel, on peut faire encore mieux en définissant un "layout" qui servira de gabarit pour englober toutes les vues. Cela permet d'éviter de répéter le même code dans toutes les vues de l'application.

Pour cela, on doit juste créer un fichier "layout.erb" dans le sous-répertoire "views" et y saisir le code html ci-dessous :

<!DOCTYPE html>
<html lang="en">
<head>
<title>Reverse!</title>
<meta charset=utf-8 />
</head>
<body>
<h1>Reverse</h1>

<%= yield %>

<p>The first Sinatra project for I Did It My Way</p>
</body>
</html>

Tout ce code html sera toujours affiché à chaque fois qu'une vue sera affichée, à part la ligne <%= yield %> qui sera remplacée par le contenu spécifique de la vue.

Ainsi, si on modifie légèrement le code de la vue "home.erb" :

<h2>Home</h2>
<p>Welcome to the home page. This app is going to be amazing....</p>

Le fait de relancer le serveur et de ré-afficher l'URL http://localhost:4567 doit donner le résultat suivant :

reverse-4.png

Comme vous pouvez le constater, le sous-titre "Home" et le message "Welcome..." en provenance de la vue "home.erb" apparaissent entre le titre "Reverse" et le paragraphe "The first app...", soit exactement là où se situait la ligne <%= yield %> dans le fichier "layout.erb".

ERB

La balise <%= yield %> est un exemple d'embedded ruby (ou erb en abrégé). On peut ainsi ajouter du code ruby dans les fichiers html en l'insérant à l'intérieur de blocs <% ... %>. C'est très utile dans le cas de conditions if :

<% if something_happens %>
<h1>Something happened</h1>
<% else %>
<h1>Nothing happend</h1>
<% end %>

Si le code ruby est placé dans un bloc <%= ... %>, alors ce code est évalué et son résultat est affiché. Comme par exemple dans le code suivant :

<% title = "Reverse" %>
<h1>
<%= title %>
</h1>

Le premier bloc de code <% ... %> défini une variable appelée "title" et le second bloc de code <%= ... %> évalue cette variable et affiche sa valeur. Même si cet exemple est ultra simple, dans la vrai vie on peut faire des tas de chose très utile grâce à l'embedded ruby.

Définir une variable

Plus concrètement, nous allons utiliser embeded ruby pour définir le titre de notre page. Dans un premier temps, on met à jour le code de "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Home"
  erb :home
end

Celui-ci initialise une variable session nommée @title (c'est une variable session parce que son nom débute par un @). Les variables session sont disponibles dans les autres parties du code, y compris dans les vues. On peut donc maintenant faire référence à notre variable session @title dans notre vue ou même notre layout.

Par conséquent, nous pouvons modifier "layout.erb" pour qu'il utilise notre variable session :

<!DOCTYPE html>
<html lang="en">
<head>
<title>Reverse!</title>
<meta charset=utf-8 />
</head>
<body>
<h1>Reverse</h1>
<h2><%= @title %></h2>

<%= yield %>

<p>The first Sinatra project for I Did It My Way</p>
</body>
</html>

Puis nous pouvons alors supprimer le titre qui était en dur dans la vue "home.erb" :

<p>Welcome to the home page. This app is going to be amazing....</p>

Par acquit de conscience, on peut redémarrer le serveur et contrôler que rien n'a changé, ce qui signifie que notre variable session a bien été prise en compte et que nous pouvons maintenant définir le titre de la page au niveau de l'action.

On va vérifier cela en créant une nouvelle route dans le source "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Home"
  erb :home
end

get '/frank' do
  @title = "My Way"
  erb :home
end

Après avoir encore une fois redémarré le serveur, on peut aller à la page http://localhost:4567/frank pour constater que le sous-titre n'est plus "Home" mais "My Way" :

reverse-5.png

Mais on continue à voir presque la même chose, étant donné qu'on utilise la même vue dans les deux cas. On va donc créer une vue différente qui s'affichera pour la route "/frank". Pour cela, on saisi le code suivant dans un fichier "frank.erb" à créer dans le sous-répertoire "views" :

<p>
And now, the end is here
And so I face the final curtain
My friend, I'll say it clear
I'll state my case, of which I'm certain
I've lived a life that's full
I traveled each and ev'ry highway
And more, much more than this, I did it my way
</p>

Il faut aussi changer la ligne erb:home par erb:frank dans le cas de la route "/frank" avant de redémarrer le serveur et de ré-afficher la page http://localhost:4567/frank pour voir ce que cela donne.

Poster quelque chose

Jusqu'à présent on n'a pas fait grand chose d'autre que des pages statiques. Mais quand on crée une application web, c'est quand même pour avoir un peu d'interaction avec le visiteur. On va donc rendre les choses un peu plus intéressante en commençant par créer un formulaire dans notre page d'accueil. Pour cela, on remplace tout le code de "home.erb" par le code suivant :

<form action="/reverse" method ="post" accept-charset="utf-8">
<input type="text" id="phrase" name="phrase" value="Write something...">
<input type="submit" value="...and reverse it!">
</form>

Une fois que c'est fait, on a aussi besoin d'une nouvelle action pour prendre en compte le formulaire lorsqu'il est envoyé. Si vous observez le code html de "home.erb", vous pouvez voir que le formulaire va être posté à l'URL "/reverse" avec une méthode POST.

Normalement, c'est le moment où vous devriez vous rendre compte que la syntaxe de Sinatra est bien faite. Et vous devriez même avoir une idée ce que nous allons ajouter dans le fichier "main.erb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Home"
  erb :home
end

post '/reverse' do
  params.inspect
end

L'avant-avant dernière ligne (l'antépénultième pour les érudits) défini la nouvelle action qui va gérer la route "/reverse". Cette action est définie pour une route de type POST, ce qui signifie qu'elle ne sera activé que pour une requête POST (ce qui correspond à l'envoi d'un formulaire). Ca c'est bon ? Mais par contre, c'est quoi cette ligne params.inspect ? "params" est une collection qui contient toutes les informations qui ont été envoyées en tant que paramètre (aussi bien à travers un formulaire que via l'URL) Par conséquent, params.inspect affiche les paires clé/valeur correspondantes à tous ces paramètres.

Allez. On relance le serveur, on accède à la page http://localhost:4567/, on saisi une phrase au hasard et on clique sur le bouton [... and reverse it] :

reverse-6.png

Cela signifie que la clé "phrase" contient la valeur "I Did It My Way". Et nous avons une clé "phrase" parce que le formulaire dans "home.erb" contient une balise input dont l'attribut name est "phrase". Il est possible d'accéder à n'importe quel paramètre stocké dans la collection "params" en employant la syntaxe params[:key]. Par exemple, params[:phrase] renverra "I Did It My Way".

Supposons que l'on ait le formulaire suivant :

<form action="/reverse" method ="post" accept-charset="utf-8">
<input type="text" name="name">
<input type="text" name="email">
<input type="text" name="password">
<input type="submit" value="submit">
</form>

On va pouvoir accéder aux valeurs de ce formulaire en utilisant params[:name], params[:email] et params[:password].

Maintenant que nous savons comment accéder aux données d'un formulaire, nous allons pouvoir faire quelque chose du texte saisi dans notre vue. Pour cela, nous ajoutons le code ci-dessous au fichier "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/reverse' do
  @title = "Here's Your Reversed Text:"
  params[:phrase].reverse
end

On re-démarre le serveur, on ré-accède à la page http://localhost:4567/, on re-saisi une phrase au hasard et on re-clique sur le bouton [... and reverse it]. Cette fois-ci, on doit voir la phrase saisie affichée à l'envers, de la droite vers la gauche :

reverse-7.png

On obtient ce résultat parce que la dernière ligne de la méthode est params[:phrase].reverse. Et si vous vous souvenez bien, la dernière ligne d'une méthode est ce qui est renvoyé pour l'URL demandée. params[:phrase] correspond au texte entré dans le formulaire et le .reverse à sa suite correspond à la méthode "reverse" pour les chaines de caractères. Et cette méthode fait exactement ce que son nom laisse supposer, soit inverser l'ordre des caractères d'une phrase. Coup de bol, c'est aussi ce à quoi notre application était destinée !

Faire un postback

On peut pousser le bouchon encore plus loin et utiliser la même URL pour nos deux pages (l'affichage du formulaire et l'affichage du résultat de notre application). C'est possible parce que pour afficher le formulaire on fait une requête GET et que pour inverser le texte on envoie le formulaire avec une requête POST. Par conséquent, on peut donc gérer ces deux actions avec la même route, mais deux actions différentes.

On peut donc réécrire "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/' do
  @title = "Here's Your Reversed Text:"
  params[:phrase].reverse
end

Cette fois-ci, l'URL de la route est toujours la même, mais la méthode HTTP est différente (soit un GET, soit un POST). Le fait de poster vers soit-même s'appelle un "postback". En plus du GET et du POST, Sinatra gère les deux autres méthodes HTTP, à savoir PUT et DELETE. Nous n'en avons pas besoin pour cette application, mais nous aurons l'occasion d'y revenir dans un autre projet.

Pour que le postback fonctionne, il faut aussi penser à modifier l'attribut action du formulaire au niveau de la vue "home.erb" pour qu'il pointe vers la même URL :

<form action="/" method ="post" accept-charset="utf-8">
<input type="text" id="phrase" name="phrase" value="Write something...">
 <input type="submit" value="...and reverse it!">
</form>

Et comme pour l'instant le résultat de notre application se présente seulement sous la forme d'une ligne de texte, on va enjoliver ça en créant une nouvelle vue "reverse.erb" qui va nous permettre d'afficher ce résultat de façon un peu plus élégante :

<h3>Here is your reversed text......</h3>
<p><strong><%= @reversed_text %></strong></p>

Il suffit alors de référencer cette vue dans le fichier "main.rb", dans lequel nous initialisons la variable session "@reversed_text" que nous avons utilisée dans notre nouvelle vue :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/' do
  @title = "Here's Your Reversed Text:"
  @reversed_text = params[:phrase].reverse
  erb :reverse
end

Si on relance tout et que l'on fait tout bien comme il faut, on arrive sur l'écran suivant :

reverse-8.png

Les paramètres nommés

On pourrait en rester là puisque l'application fait ce qui était prévu. Mais on peut faire mieux, comme par exemple permettre à l'utilisateur d'indiquer la phrase qu'il veut inverser directement au niveau de l'URL. OK, mais comment faire pour retrouver le texte qui a été proposé ? Pas compliqué, il faut juste ajouter une route avec un paramètre nommé :

get '/:phrase' do

Cela va ajouter automatiquement un paramètre "phrase" à la collection "params". On retrouvera donc tout ce que l'utilisateur aura indiqué dans l'URL en utilisant params[:phrase]. Par exemple, pour l'URL http://localhost:4567/frank on aura params[:phrase] égal à "frank" et pour l'URL http://localhost:4567/sinatra on aura params[:phrase] égal à "sinatra" .

On va donc compléter notre fichier "main.rb" pour lui ajouter du code destiné à gérer cette route :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/' do
  @title = "Here's Your Reversed Text:"
  @reversed_text = params[:phrase].reverse
  erb :reverse
end

get '/:phrase' do
  @title = "Here's Your Reversed Text:"
  @reversed_text = params[:phrase].reverse
  erb :reverse
end

Voyons voir si ça marche. Tout ce qu'il y a à faire (après avoir relancé le serveur pour la dernière fois), c'est d'appeler l'URL http://localhost:4567/ suivi d'une phrase de votre choix et de vérifier que ce texte s'affiche bien à l'envers, comme dans la copie d'écran ci-dessous :

reverse-9.png

Voilà c'est fini

Et ainsi se termine le tutoriel consacré à la première application Sinatra proposée par Darren Jones (@daz4126) dont ce billet constitue une traduction très très libre.

lundi 26 juillet 2010

Et de deux

You’ve virtually completed the public-facing portion of SportsStore. (...) The well-separated architecture means you can easily change the behavior of any application piece (e.g., what happens when an order is submitted, or the definition of a valid shipping address) in one obvious place without worrying about inconsistencies or subtle, indirect consequences. You could easily change your database schema without having to change the rest of the application (just change the LINQ to SQL mappings).

Steven Sanderson - Pro Asp.net Mvc V2 Framework

vendredi 23 juillet 2010

Tout vient à point à qui peut attendre

In this chapter, you built most of the core infrastructure needed for the SportsStore application. It doesn’t yet have many features you could show off to your boss or client, but behind the scenes you’ve got the beginnings of a domain model, with a product repository backed by a SQL Server database. There’s a single MVC controller, ProductsController, that can produce a paged list of products, and there’s a DI container that coordinates the dependencies between all these pieces. Plus, there’s a clean custom URL schema, and you’re now starting to build the application code on a solid foundation of unit tests.

Steven Sanderson - Pro Asp.net Mvc V2 Framework

jeudi 22 juillet 2010

Installer Sinatra sous Windows 7

Ceci est une adaptation en français du tutoriel "Installing Sinatra" de Darren Jones.

Je suis tombé sur une super série de tutoriels pour apprendre à utiliser Sinatra. C'est le résultat de la bonne résolution de Daz (Darren Jones) pour l'année 2010 et ça s'appelle I Dit It My Way ou "The Sinatra Songbook Project".

C'est l'occasion pour se mettre à Sinatra et je me suis donc lancé dans l'installation des outils nécessaires à son utilisation, en suivant un des premiers tutoriels : Installing Sinatra.

Installer Ruby sur Windows

il faut commencer par installer Ruby étant donné que ce n’est pas quelque chose de déjà présent sur mon PC. C’est pas compliqué du tout. Il suffit d’utiliser RubyInstaller pour Windows (la façon la plus simple d’installer Ruby sous Windows) qui va s’occuper de tout.

  • Cliquer sur le gros bouton rouge « Download » sur la page d’accueil
  • Sélectionner la version la plus récente de RubyInstallers (soit Ruby 1.9.1-p429 à ce jour)
  • Enregistrer le fichier sur le disque dur (soit dans le répertoire C:\Temp dans mon cas)
  • Double-cliquer sur C:\Temp\rubyinstaller-1.9.1-p429.exe pour lancer l’installation
  • Le seul truc un peu important, c’est de demander à faire l’installation dans C:\Ruby (et pas C:\Ruby191 comme le propose l’installeur) et de cocher les choix « Add Ruby executables to your PATH » et « Associate .rb and .rbw files with this Ruby installation ».

L’installation est très rapide et au final on se retrouve avec un répertoire « C:\Ruby » d’une cinquantaine de méga.

Installer Sinatra sur Windows

On entre ensuite dans le vif du sujet qui consiste à installer Sinatra. Pour cela, il faut ouvrir une « Invite de commandes » et aller dans le répertoire « C:\Ruby » et lancer la commande suivante :

C:\Ruby>gem install sinatra

Ce qui donne presque aussitôt :

Successfully installed rack-1.2.1
Successfully installed sinatra-1.0
2 gems installed
Installing ri documentation for rack-1.2.1...
Installing ri documentation for sinatra-1.0...
Updating class cache with 0 classes...
Installing RDoc documentation for rack-1.2.1...
Installing RDoc documentation for sinatra-1.0...

C:\Ruby>

Ca y est, mon PC devrait contenir Ruby + Sinatra !

Créer une première application Sinatra

On va alors pouvoir tester que ça marche en codant notre toute première application Sinatra. Pour cela, il suffit d'ouvrir Notepad pour créer le fichier C:\Ruby\test.rb avec le code suivant :

require 'rubygems'
require 'sinatra'
get '/hi' do
  "I Did It My Way!"
end

Puis on demande à Ruby d’exécuter ce programme :

C:\Ruby>ruby test.rb

Ce qui donne :

== Sinatra/1.0 has taken the stage on 4567 for development with backup from WEBrick
[2010-07-22 12:07:23] INFO  WEBrick 1.3.1
[2010-07-22 12:07:23] INFO  ruby 1.9.1 (2010-07-02) [i386-mingw32]
[2010-07-22 12:07:23] INFO  WEBrick::HTTPServer#start: pid=952 port=4567

Ca marche ! Il ne reste plus qu’à lancer un des navigateurs installé sur le PC pour aller voir ce que donne l’adresse « http://localhost:4567/hi ». Et là, je retrouve bien le message attendu, à savoir « I Dit It My Way ! ».

C’est magnifique ! Tout fonctionne comme prévu. Il n'y a qu'à arrêter l'exécution du programme test.rb par un simple Ctrl-C et se préparer pour la suite des tutoriels...

== Sinatra has ended his set (crowd applauds)
[2010-07-22 12:10:43] INFO  going to shutdown ...
[2010-07-22 12:10:43] INFO  WEBrick::HTTPServer#start done.

vendredi 2 juillet 2010

Data annotation belong to View models

Personally, my recommendation for people who want strict SoC (like I do) is to use ViewModels and only place the annotations on the view model.

ASP.NET MVC 2 Templates, Part 4: Custom Object Templates, via The Big Boy MVC Series

View models belong to Web-project

In an ASP.NET MVC application the view models belong to the Web-project, they are not part of the domain model. Usually a view model belongs to a single controller, and its views.

Using AutoMapper to map view models in ASP.NET MVC, via The Big Boy MVC Series

mardi 1 juin 2010

MVC Music Store / Raven DB : StoreManagerController (2° partie)

Ceci est la traduction du billet "Porting MVC Music Store to Raven: StoreManagerController, part 2", le dernier de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Le contrôleur StoreManagerController contient encore deux méthodes que nous n'avons pas étudiées. Nous allons devoir les aborder de façon un peu différente.

Est-ce que vous devinez pourquoi ?

C'est parce qu'au départ nous étions d'accord sur le fait qu'il n'y avait aucune raison valable de gérer les artistes en tant que document spécifique. Après tout, il ne s'agit que de données de référence. Sauf que maintenant nous devons y faire référence.

C'est sûr qu'on pourrait créer une série de documents artistes, ce qui faciliterait énormément la migration du code :

Mais je continue à penser que les artistes n'existent pas réellement en tant qu'entité indépendante dans notre modèle. Par conséquent, au lieu de suivre cette voie nous allons plutôt faire une projection.

Nous commençons par définir un index "Artists" à l'aide des requêtes linq map / reduce suivantes :

// map 
from album in docs.Albums
select new { album.Artist.Id, album.Artist.Name }

// reduce 
from artist in results
group artist by new { artist.Id, artist.Name } into g
select new { g.Key.Id, g.Key.Name }

Si vous regardez attentivement ce code, vous pouvez voir que sa fonctionnalité principale est de faire un distinct sur l'ensemble des artistes de tous les albums.

Si bien que maintenant nous pouvons coder nos deux dernières méthodes comme ceci :

Il faut bien comprendre un truc : ça ne coûte rien de faire des requêtes avec Raven, parce que Raven ne permet les requêtes que sur des index et que ces index sont créés en tache de fond, ce qui contribue à rendre les requêtes très rapides.

Et cela remet en cause la façon dont vous allez concevoir votre système et de votre modèle de données. Avec Raven, vous cherchez à axer la plupart de vos traitements sur des index et interroger ces index, parce que c'est ce qui est le moins gourmand.

lundi 31 mai 2010

MVC Music Store / Raven DB : StoreManagerController

Ceci est la traduction du billet "Porting MVC Music Store to Raven: StoreManagerController", le douzième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

La dernière partie du portage de MVC Music Store sous Raven concerne toute la partie administration, implémentée au niveau du contrôleur StoreManagerController. Je vais commencer par une rapide comparaison de toutes les méthodes où le passage sous Raven n'apporte rien de nouveau puis je mettrai l'accent sur une différence de conception plutôt intéressante entre les deux implémentations.

Code d'origine

Portage sous Raven

Le code pour Raven est beaucoup plus court pour la bonne et simple raison que j'ai fait disparaitre tout la pseudo gestion d'erreur qu'il contenait.

Code d'origine

Portage sous Raven

Là encore, le fait de faire disparaitre une gestion d'erreur qui n'est là que pour la gallerie a un impact plus que certain sur la taille du code.

Code d'origine

Portage sous Raven

Ici nous avons une différence plus intéressante. Le code d'origine a pour effet d'effacer les commandes qui contiennent l'album supprimé. Ce que ne fait pas le code porté sous Raven.

La notion d'intégrité référentielle n'existe pas sous Raven (ou de façon plus générale sous les bases de données document). Cela peut être un avantage ou un inconvénient. Mais dans ce cas précis, cela s'avère être un avantage puisque nous pouvons supprimer un album sans perdre de commandes. Je ne sais pas pour vous, mais en ce qui me concerne ça ne me déplait pas de conserver toutes mes commandes :)

Avec Raven, les documents sont indépendants les uns des autres. Par conséquent, le fait de modifier un document n'a aucun impact sur les autres documents.

Il reste encore deux méthodes à étudier en ce qui concerne le contrôleur StoreManagerController, mais je verrai ça dans mon prochain billet.

dimanche 30 mai 2010

MVC Music Store / Raven DB : Gérer le règlement

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Porting the checkout process", le onzième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Dans MVC Music Store, la gestion du règlement est composée de deux parties : renseigner l'adresse et le mode de paiement puis terminer la commande.

Le code pour gérer l'adresse et le mode de paiement dans la version d'origine :

Et ce même code correspondant à la version portée sous Raven :

Comme vous pouvez le constater, ils sont presque identiques. Mais le code pour Raven n'est pas tout à fait complet.

Si vous vous souvenez, nous avions décidé de stocker une propriété CountSold dans le document Album, pour pouvoir plus facilement faire un classement en fonction de ce compteur. Il nous reste donc à réaliser l'incrémentation de ce compteur, ce que j'ai codé immédiatement après l'appel à CreateOrder :

Le truc essentiel dans ce code, c'est que nous avons chargé tous les documents albums en une seule requête. Et lorsque nous sauvegardons, Raven va effectuer un seul appel (batch) au serveur.

Et maintenant, pour être tout à fait complet, voyons ce que donnait la méthode Complete() :

Et dans la nouvelle version sous Raven :

Je pense qu'arrivé à ce point, vous êtes en mesure de comprendre comment fonctionne les deux versions.

Mon prochain billet concernera la partie administration de l'application.

vendredi 28 mai 2010

MVC Music Store / Raven DB : ShoppingCartController

Ceci est la traduction du billet "Porting MVC Music Store to Raven: ShoppingCart", le dixième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Le contrôleur ShoppingCartController est considérablement impacté par tous les changements que nous avons apportés à la classe ShoppingCart. Penchons-nous sur ces modifications, en commençant par l'action Index() dans le code d'origine :

Celui-ci exécute deux requêtes différentes pour gérer la commande alors que la version pour Raven exécute seulement une requête au niveau de la méthode FindShoppingCart () :

Ce code implémente seulement la logique pour charger le panier depuis Raven ou créer un nouveau panier (pour l'identifiant spécifié). Notez-bien que nous n'enregistrons pas le nouveau panier dans la base de données, mais associons seulement ce nouveau panier avec la session. Nous n'avons pas besoin de sauvegarder étant donné que pour l'instant il ne contient rien de significatif. Lorsque nous appellerons SaveChanges(), le nouveau panier sera envoyé vers Raven pour stockage.

Maintenant, voyons-voir l'action AddToChart d'origine :

Et la version portée sous Raven :

Elles sont très similaires, si ce n'est que dans le cas de Raven, la méthode AddToCart() de la classe ShoppingCart est uniquement concerné par l'ajout d'un nouvel article au panier ou par la mise à jour de la quantité d'un article existant. Ainsi, il n'y a absolument aucun accès à la base de données dans la version pour Raven de la méthode ShoppingCart.AddToCart().

La différence c'est donc que dans l'approche pour Raven, nous appelons la méthode session.SaveChanges() au niveau de l'action. Pour la bonne et simple raison que c'est le bon endroit où faire cela étant donné que le code appelant est en charge de l'environnent, y compris la sauvegarde lorsque cela est nécessaire.

Je pense que le code pour Raven est plutôt facile à suivre. Il y a juste un truc un peu étrange à faire remarquer au niveau de la dernière ligne : id.Split(). Pourquoi diable fait-on ça ?

Et bien c'est parce que Raven utilise des identifiants de la forme "albums/616" et que la valeur DeleteId sera utilisé par le code Javascript appelant pour retrouver un élément à partir de son identifiant. Et comme l'identifiant d'un élément HTML ne peut pas contenir de "/", nous ne renvoyons que la partie numérique de l'identifiant. Ce n'est pas un problème puisque dans ce cas précis nous ne gérons que des albums.

Encore une fois, on ne peut pas faire plus simple. Par conséquent, je ferai seulement remarquer qu'avec l'approche suivie par Raven on peut profiter du cache de l'unit of work alors qu'avec le code d'origine non.

Dans mon prochain billet, je m'occuperai de la gestion de la commande.

jeudi 27 mai 2010

MVC Music Store / Raven DB : ShoppingCart

Ceci est la traduction du billet "Porting MVC Music Store to Raven: ShoppingCart", le neuvième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

ShoppingCart

La classe ShoppingCart de MVC Music Store est ma tête à claque du moment. C'est plus fort que moi.

Vous pouvez admirer à quoi elle ressemble dans l'illustration de droite. Le souci avec elle, c'est que c'est le genre de code qui fait l'amalgame entre deux responsabilités différentes :

  • Les opérations au sujet du panier
    • GetCart
    • GetCartId
    • GetCartItems
    • GetCount
  • Les traitements du contenu du panier
    • AddToCart
    • CreateOrder
    • EmptyCart
    • MigrateCart
    • RemoveFromCart

Vous avez sans doute remarqué que toutes les opérations au sujet du panier correspondent à des fonctions get. Tous les traitements du contenu du panier sont relatifs aux articles du panier, ils représentent la logique métier du panier et sa raison d'être. Les opérations Get ne sont pas rattachées aux articles du panier, elles dépendent d'un autre genre d'objet qui gère les instances de paniers.

Dans la plupart des applications, cet objet s'appellerait un Repository. Je ne suis pas certain que cela nous soit utile dans le cas présent. Si on étudie les méthodes Get de plus près, on se rend compte que la seule justification de leur existence vient du fait que l'on a choisi de stocker uniquement les articles du panier. Comme il n'existe pas d'entité panier, nous sommes obligé d'effectuer des requêtes explicites pour obtenir ces données.

Avec Raven, nous procèderons différemment, ce qui fait que la seule chose dont nous devrions avoir besoin est GetCart() et peut-être GetCartId().

Voici donc à quoi un document panier ressemblera :

Ce qui donnera en tant qu'entité :

La méthode GetTotal a été remplacée par une propriété Total. Contrairement à la méthode GetTotal qui va générer un accès à la base de données, cette propriété fonctionne uniquement à partir des données en mémoire. Cela constitue une autre différence majeure de Raven par rapport à une autre solution ORM : on ne va pas faire de lazy loading. C'est quelque chose d'inhérent aux bases de données documents : les données du modèle ont rarement besoin de parcourir d'autres données en dehors de leur propre document. Parcourir le document avec Raven ne risque pas de provoquer du lazy loading ou de nous entrainer dans de redoutables problèmes de type SELECT N+1.

Et maintenant, occupons nous de gérer les opérations au sujet du panier. Les plus importantes sont GetCartId et GetCart. Je considère que ces méthodes n'ont rien à faire là. J'ai donc créé une nouvelle classe ShoppingCartFinder qui ressemble à ceci :

Vous pouvez voir que nous n'exposons plus la méthode GetCartId. Il s'agit d'un élément interne qui n'a pas à être connu des clients de cette classe. La méthode SetShoppingCartId() est là parce que nous devons gérer l'initialisation de l'identifiant d'un panier étant donné que nous voulons gérer le transfert d'un panier (lorsqu'un utilisateur anonyme se connecte). Comme nous n'avons pas besoin des autres méthodes, je les ai supprimées.

Continuons avec les traitements du contenu du panier. Pour mémoire, voici la méthode AddToCart() d'origine :

Et voici cette méthode portée sous Raven :

Le code pour Raven fonctionne entièrement en mémoire et sans se soucier de tout l'aspect persistance. Le code d'origine se charge explicitement de tout ce qui est persistance. Ce n'est pas un problème en soi, mais ce n'est pas le bon endroit pour s'occuper de persistance.

Passons à RemoveFromCart() :

Vous pouvez voir que sa taille diminue de façon très significative et qu'une fois encore, il ne s'agit plus que d'un traitement en mémoire. La méthode EmptyCart() n'est pas implémentée puisqu'avec Raven cela correspond juste à un Lines.Clear().

Un truc intéressant à voir, c'est que l'ancienne implémentation de EmptyCart() aurait généré N requêtes (N étant le nombre d'articles dans le panier) alors qu'avec Raven cela engendre une seule requête.

La méthode CreateOrder() d'origine :

Et celle pour Raven pour laquelle il n'y a pas grand chose à dire si ce n'est que l'ancien code exacuterait N * 2 requêtes là où le code pour Raven continuera de se contenter d'une seule requête :-)

MigrateCart() est plus intéressant parce que son implémentation est complètement différente. Dans le code d'origine, on met à jour tous les articles du panier un par un :

Avec Raven, nous allons faire quelque chose de radicalement différent :

L'identifiant du panier sert pour définir la clé du document et donc, en initialisant cet identifiant (soit avec le nom de l'utilisateur soit avec une valeur stockée en session), nous pouvons charger le panier à l'aide d'une méthode Load (sur la clé primaire pour faire une comparaison avec le monde des bases relationnelles). Le transfert du panier est alors une opération toute simple. Tout ce que vous avez à faire, c'est de changer sa clé. Etant donné que Raven ne permet pas de la renommer, nous allons faire une suppression puis une insertion qui s'exécuteront dans la même transaction.

Le code pour appeler la méthode MigrateCart() est le suivant :

Etant donné que SaveChanges est atomique et transactionnel, cela a le même effet que de faire un Rename.

Et c'est tout pour le panier. Je consacrerai mon prochain billet au contrôleur ShoppingCartController qui utilise cette classe.

- page 1 de 4