blog.pagesd.info

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

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.

mercredi 26 mai 2010

MVC Music Store / Raven DB : StoreController

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

Commençons par la méthode Index() :

Il y a un truc dans ce code qui me gène, c'est qu'il va exécuter deux requêtes sur la base de données. Mais je ne vais pas m'appesantir étant donné que nous allons modifier tout ça.

Et voici ma version portée sous Raven :

Comme vous pouvez le constater, c'est à peut près pareil et donc pas très intéressant. Voyons voir ce que nous avons d'autre :

Ce qu'il faut bien voir, c'est que ce code cherche à faire une recherche sur le libellé d'un genre. Le problème c'est que le libellé du genre n'est pas la clé primaire, et que pour couronner le tout, il n'y a même pas d'index sur cette colonne libellé. Bon, c'est vrai que la table genre ne contient que 10 lignes, mais c'est une question de principe (si vous êtes très sympa, vous n'aurez droit qu'à un sermon du DBA pour avoir osé faire une requête sans index sur la base de production).

Avec Raven, il nous serait donc très simple d'implémenter ça en suivant la même approche, mais je ne vois pas d'excuse pour faire ça. Le genre que nous récupérons dans la méthode Browse() dépend des données que nous avons renvoyées avec la méthode Index(). Il n'y a donc pas de raison pour ne pas faire passer directement l'identifiant du genre. J'ai donc modifié l'action Index() pour renvoyer l'objet genre complet et pas seulement son libellé et par la suite renvoyer l'identifiant à l'action Browse() au lieu du libellé.

J'avais donc commencé à implémenter ça mais je me suis retrouvé coincé par l'association entre les albums et les genres.

Normalement, les bases de données documents n'ont pas d'associations et pas de jointures non plus. Alors, comment gérer ça ?

Depuis le temps vous devez commencer à vous douter de la réponse : en créant un un index :)

// AlbumsByGenre
from album in docs.Albums
where album.Genre != null
select new { Genre = album.Genre.Id }

Et cet index nous permet d'écrire le code suivant :

Et pour finir, il nous reste l'action GenreMenu :

Que nous pouvons facilement porter de la façon suivante :

Et nous en avons terminé avec StoreController.

MVC Music Store / Raven DB : Faire une migration plus poussée

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

Je me suis rendu compte qu'à cause d'une faute de frappe lorsque j'avais fait la reprise des données, les informations sur l'artiste étaient enregistrées en tant que "Arist" au lieu de "Artist". Cela va me donner l'occasion de montrer comment faire une migration du modèle de données un peu plus sophistiquée.

using (var documentStore = new DocumentStore { Url = "http://localhost:8080" })
{
    documentStore.Initialise();

    var count = 0;

    do
    {
        var queryResult = documentStore.DatabaseCommands.Query("Raven/DocumentsByEntityName", new IndexQuery
        {
            Query = "Tag:`Albums`",
            PageSize = 128,
            Start = count
        });


        if (queryResult.Results.Length == 0)
            break;

        count += queryResult.Results.Length;
        var cmds = new List<ICommandData>();
        foreach (var result in queryResult.Results)
        {
            var arist = result.Value<JObject>("Arist");
            if(arist == null)
                continue;
                        
            result["Artist"] = arist;
            result.Remove("Arist");

            cmds.Add(new PutCommandData
            {
                Document = result,
                Metadata = result.Value<JObject>("@metadata"),
                Key = result.Value<JObject>("@metadata").Value<string>("@id"),
            });
        }

        documentStore.DatabaseCommands.Batch(cmds.ToArray());

    } while (true);
    
}

Je ne pense pas que le code soit très compliqué à suivre. Vous pouvez voir comment on peut manipuler les documents en travaillant directement au niveau du document JSON plutôt que de passer par une couche objet.

mardi 25 mai 2010

MVC Music Store / Raven DB : Faire évoluer le modèle de données

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

Dans mon dernier billet, j'ai indiqué que nous devions ajouter une propriété CountSold à tous les albums, généralement quelque chose d'assez pénible à faire dans l'univers des bases de données SQL. La commande pour ajouter une colonne est toute simple, mais c'est une vrai galère d'en venir à bout, de la déployer et de la versionner. Avec Raven, quand vous ajoutez une propriété, elle sera automatiquement ajoutée à votre document la prochaine fois que vous le sauvegarderez. Vous n'avez rien d'autre à faire. Et même, c'est pareil si vous décidez de supprimer une propriété. Raven s'occupera de fera le ménage après vous.

Mais comment faire quand on veut initialiser cette propriété avec une valeur définie, et pas se contenter de la valeur par défaut ? Dans ce cas là, il faut un peu mettre la main à la pâte, mais ça reste très simple :

using (var documentStore = new DocumentStore { Url = "http://localhost:8080" })
{
    documentStore.Initialise();
    using (var session = documentStore.OpenSession())
    {
        IDictionary<string,int> albumToSoldCount = new Dictionary<string, int>();
        int count = 0;

        do
        {
            var results = session.Query<SoldAlbum>("SoldAlbums")
                .Take(128)
                .Skip(count)
                .ToArray();

            if (results.Length == 0)
                break;
            count += results.Length;
            foreach (var soldAlbum in results)
            {
                albumToSoldCount[soldAlbum.Album] = soldAlbum.Quantity;
            }
        } while (true);

        count = 0;
        do
        {
            var albums = session.Query<Album>()
                .Skip(count)
                .Take(128)
                .ToArray();
            if (albums.Length == 0)
                break;

            foreach (var album in albums)
            {
                int value;
                albumToSoldCount.TryGetValue(album.Id, out value);

                album.CountSold = value;
            }

            count += albums.Length;

            session.SaveChanges();
            session.Clear();
        } while (true);
    }
}

Pour ceux d'entre-vous qui n'ont pas pris la peine de lire le code, cette fonction parcours l'index SoldAlbums que nous avons créé auparavant et mémorise ses valeurs. Puis nous parcourons les albums par lot de 128 et nous mettons à jour leur compteur CountSold. L'un dans l'autre, c'est plutôt facile.

Un des autres avantages de ce script, c'est que vous pouvez l'exécuter autant de fois que vous le voulez sans que cela fausse vos données.

MVC Music Store / Raven DB : Refaire HomeController, la bonne méthode

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

Comme je l'ai indiqué dans le billet précédent, nous pouvons solutionner le problème de la méthode GetTopSellingAlbums() grâce au map/reduce, mais cela n'est pas vraiment la bonne façon de faire les choses. Le problème en procédant de la sorte (en plus des regards effrayés et des cris de détresse que vous suscitez dès que vous mentionnez cette solution), c'est qu'on essaie de résoudre le problème selon une logique relationnelle. Et d'ailleurs, la solution précédente est quasiment identique à la façon dont une base de données relationnelle pourrait traiter ce genre de requête. Voyons plutôt quelle serait l'approche d'une base de données documents pour résoudre ce genre de problème.

La réponse est évidente à trouver : rappelez-vous que les documents sont indépendants et réfléchissez à nouveau à la question. Ce que nous cherchons à savoir, c'est quels sont les albums les plus vendus. Si nous ajoutions une propriété CountSold à l'album, cela deviendrait immédiatement bien plus simple de répondre à cette question. Et pour cela, il nous suffit de mettre à jour les différents albums qui font parti de la commande lorsque celle-ci est validée. C'est quelque chose de tout à fait acceptable et ce genre d'opération est couramment effectuée, y compris avec des bases de données SQL.

Pour l'instant, laissons de côté la façon de créer la propriété CountSold et de l'initialiser avec les bonnes valeurs (je verrai ça dans mon prochain billet). Nous supposerons donc que c'est déjà fait et qu'il ne nous reste plus qu'à trouver comment résoudre le problème de notre méthode GetTopSellingAlbums().

Et bien, c'est plutôt simple. Tout ce que nous avons à faire, c'est de définir un index sur CountSold.

// AlbumsByCountSold
from album in docs.Albums
select new { album.CountSold };

Avec ça, nous pouvons implémenter la fonction GetTopSellingAlbums() de la façon suivante :

C'est fait : simple, efficace et même élégant (même si c'est moi qui le dit).

vendredi 21 mai 2010

MVC Music Store / Raven DB : Refaire HomeController, méthode map/reduce

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

Actuellement, le contrôleur HomeController contient le code suivant :

Je n'aime vraiment pas quand un contrôleur se charge de faire des requêtes, mais ça n'est pas le sujet pour l'instant.

Grâce à EF Prof, on peut voir à quoi ressemble cette requête :

Et là on se trouve face à un problème très intéressant : il ne nous est pas possible de reproduire cette requête. En effet, cette requête porte sur plusieurs tables que dans notre modèle nous avons réparties dans différents documents.

Il existe plusieurs méthodes pour résoudre cela. Une des façon de faire serait de définir un index map / reduce au niveau des documents orders.

Note: Oui, je sais ce que vous allez dire.

La méthode que je suis sur le point de vous montrer n'est pas celle que je conseillerais dans la réalité. Mais je veux malgré tout vous la présenter. Dans mon prochain billet, je vous expliquerai la façon dont Raven permet de gérer ça dans les formes.

Avec Raven, le map / reduce consiste simplement en quelques requêtes Linq. Il n'y a donc pas de raison de s'affoler. Pour mémoire, nous avons défini les documents suivant dans notre base de données :

Nous créons l'index "SoldAlbums" à l'aide des requêtes suivantes :

// map
from order in docs.Orders
from line in order.Lines
select new{ line.Album, line.Quantity }

// reduce
from result in results
group result by result.Album into g
select new{ Album = g.Key, Quantity = g.Sum(x=>x.Quantity) }

Comme vous pouvez le voir, il s'agit de deux requêtes Linq toute simples.

Leur résultat devrait être le suivant :

Dès lors que nous avons cela, c'est un jeu d'enfant d'en faire découler GetTopSellingAlbums. En fait, la fonction ci-dessous implémente exactement la même logique et renvoie le même résultat que l'implémentation d'origine :

La façon dont elle fonctionne est très simple. Nous sélectionnons les albums les plus vendus (en triant les quantités par ordre décroissant), puis nous les chargeons depuis la base de données. Et dans le cas où nous aurions moins d'albums vendus que ce que nous comptons afficher, nous complétons avec d'autres albums normaux.

Au final ce code exécute 2 ou 3 requêtes. Je n'aime vraiment pas ça, mais sur ma machine, cela prend environ moins de 10 ms pour faire ces trois requêtes, ce qui est tout à fait supportable.

Je vous ai présenté cette solution parce que je voulais vous montrer que c'était une approche du problème, mais pas la solution recommandée pour le résoudre. Nous verrons une meilleure approche dans le billet suivant.

jeudi 20 mai 2010

MVC Music Store / Raven DB : Migrer les données

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

Voici le code nécessaire pour lire les données dans la base de données de MVC Music Store et les transformer en documents comme attendu par Raven :

using (var documentStore = new DocumentStore { Url = "http://localhost:8080" })
{
    documentStore.Initialise();
    using (var session = documentStore.OpenSession())
    {
        foreach (var album in storeDB.Albums.Include("Artist").Include("Genre"))
        {
            session.Store(new
            {
                Id = "albums/" + album.AlbumId,
                album.AlbumArtUrl,
                Arist = new { album.Artist.Name, Id = "artists/" + album.Artist.ArtistId },
                Genre = new { album.Genre.Name, Id = "genres/" + album.Genre.GenreId },
                album.Price,
                album.Title,
            });
        }
        foreach (var genre in storeDB.Genres)
        {
            session.Store(new
            {
                genre.Description,
                genre.Name,
                Id = "genres/" + genre.GenreId
            });
        }
        session.SaveChanges();
    }
}

Comme vous pouvez le constater, c'est plutôt simple. Et même si c'est moi qui le dit, plutôt bien foutu.

J'ai utilisé des types anonymes parce que je me contente de migrer les données. Je ne m'occupe pas vraiment de savoir comment gérer les types pour l'instant.

mercredi 19 mai 2010

MVC Music Store / Raven DB : Configurer l'application

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

Juste quelques mots sur la façon de configurer Raven pour l'utiliser avec l'application MVC Music Store avant de me lancer dans le développement du reste du code.

  • Le fonctionnement retenu est (délibérément) très proche de celui employé avec NHibernate. Nous initialisons un objet DocumentStore au démarrage de l'application.
  • Puis nous gérons l'ouverture / fermeture des sessions dans le cadre de la requête HTTP, complété par une méthode CurrentSession() pour accéder à la session en cours.
  • Si l'application avait employé un conteneur, j'aurais fait en sorte que les contrôleurs récupèrent une instance de la session par son intermédiaire. Mais comme il n'y en a pas, je m'en tiens à une méthode statique.
    • Si cela ne vous plait vraiment pas, n'hésitez pas à proposer autre chose.
public class MvcApplication : System.Web.HttpApplication
{
    private const string RavenSessionKey = "Raven.Session";
    private static DocumentStore _documentStore;

    protected void Application_Start()
    {
        _documentStore = new DocumentStore { Url = "http://localhost:8080/" };
        _documentStore.Initialise();

        AreaRegistration.RegisterAllAreas();

        RegisterRoutes(RouteTable.Routes);
    }

    public MvcApplication()
    {
        BeginRequest += (sender, args) => HttpContext.Current.Items[RavenSessionKey] = _documentStore.OpenSession();
        EndRequest += (o, eventArgs) =>
        {
            var disposable = HttpContext.Current.Items[RavenSessionKey] as IDisposable;
            if (disposable != null)
                disposable.Dispose();
        };
    }

    public static IDocumentSession CurrentSession
    {
        get { return (IDocumentSession)HttpContext.Current.Items[RavenSessionKey]; }
    }
}

Et c'est à peu près tout, du moins en ce qui concerne l'initialisation de Raven.

mardi 18 mai 2010

MVC Music Store / Raven DB : Modèle de données

Ceci est la traduction du billet "Porting MVC Music Store to Raven: The data model", le premier d'une série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Le tutoriel "MVC Music Store" est venu à point nommé pour moi. Je souhaitais faire une application de démonstration pour Raven DB et le fait que quelqu'un d'autre ait déjà fait tout le travail ingrat (l'interface utilisateur :) à ma place et que je n'ai plus qu'à refaire l'accès aux données est une situation rêvée. Mon objectif est de ne rien toucher du tout au Javascript ou au code HTML et de me contenter de remplacer les contrôleurs. Ca devrait être intéressant de voir si je peux y arriver.

Le modèle de base de données dans le tutoriel d'origine est le suivant :

On peut déjà remarquer deux ou trois choses intéressantes dans ce schéma :

  • Il serait plus correct que la table Cart soit nommée CartLineItem puisqu'elle stocke une ligne par article dans le panier
    • CartId n'est pas une clé étrangère, mais référence le nom de l'utilisateur ou l'identifiant de la session
  • La table Artist gère uniquement le nom de l'artiste et rien d'autre.

A partir de ces informations, je considère que le modèle de données suivant devrait convenir.

Albums

  • Le document Album contient à la fois une référence pour le Genre et pour le libellé du genre. Cela nous permet d'afficher l'album sans avoir à référencer le document Genre.
  • Et pour les mêmes raisons, le document Album contient aussi le nom et l'identifiant de l'artiste.
  • Il n'y a pas un ensemble de documents Artists dans la base de données. Nous ne gérons aucune information sur les artistes, si ce n'est leur nom, et je ne vois donc pas de raison pour définir un document Artist pour l'instant.

Genre

Le document Genre est la réplique exacte de la table Genre, rien d'extraordinaire à ce niveau.

Cart

Le document Cart suit un format de document plutôt classique. Nous avons un simple document qui contient un tableau d'éléments là où le modèle de données relationnel contient un ensemble de lignes. Vous pouvez voir que le UserIdentifier nous sert pour stocker l'identifiant de l'utilisateur ou celui de la session pour le panier.

Orders

Order constitue une autre document plutôt standard. Nous regroupons toutes les information de la commande dans un simple document et nous stockons les informations liées à celle-ci (Address) dans un noeud spécifique.

Artist

Il n'y a pas de document Artist.

Pourquoi un document pour Genre et pas pour Artist ?

Pour la bonne raison que l'application va faire quelque chose avec les Genres (en plus d'afficher leur description) alors que la seule chose que l'on fait avec les Artistes est d'afficher leur nom dans la cadre d'un album. Pour l'instant, je considère que Artist fait partie intégrante de Album et qu'il n'y a donc pas à définir un document spécifique pour lui. La seule raison au fait qu'il existe un identifiant artiste en plus du nom est que je suppose (d'après les données) que la source pour les artistes est un système externe qui fait quelque chose d'un peu plus utile que simplement stocker le nom de l'artiste.

Nous n'avons fait que la moitié du travail à faire. Nous avons défini le modèle de données, maintenant nous devons étudier comment il sera utilisé dans un contexte plus large et lui ajouter le modèle de requête en utilisant des index. Nous verrons cela dans un prochain billet.

mercredi 28 avril 2010

C'est Quoi cette Expression Lambda ?

Ceci est la traduction du billet "What on Earth is a Lambda Expression?" de Simon Ince. Bien qu'il s'agisse d'une traduction, celle-ci s'inscrit dans une série de tutoriels dont le but est de me familiariser avec les nouveautés de C# 2, 3 et 4 :

  1. Les propriétés automatiques du c#
  2. Les types nullables en c#
  3. Les types implicites en c#

Ces derniers temps, j’ai eu à faire à quelques clients qui se demandaient ce qu’était une Expression Lambda, ce qui n’a pas manqué de me surprendre. Il semblerait donc qu’une seconde vague de développeurs se mette à utiliser les Lambdas (sans doute ceux qui n’étaient pas passé à C# 3.0 dès sa sortie) et qu’ils ont besoin de quelques pistes. C’est pourquoi ce billet est destiné à vous aider à comprendre ce que représentent les expressions lambdas.

Je ne vais pas chercher à répondre au comment, au pourquoi, au quand ou à quoi que ce soit dans ce genre. Il existe déjà de bien meilleurs billets sur le sujet. Non, je vais juste vous dire « bon sang, mais ça veut dire quoi une syntaxe pareille ? ».

Prenons un exemple concret

Je vais utiliser une situation classique auquel tout développeur est confronté de nos jours : filtrer une liste de Lamas en tenant compte de leur taille de leur propension à ronchonner (Ouais, je sais que c’est plus typique du pays de Candy que de celui de l’informatique, mais le vendredi après-midi c’est permis).

Supposons que nous ayons une liste de nos Lamas préférés :

private static List<Lama> Lamas = new List<Lama>()
{
    new Lama { Nom = "Larry", Taille = 10, EstRonchon = true },
    new Lama { Nom = "Loulou", Taille = 12, EstRonchon = false },
    new Lama { Nom = "Lara", Taille = 8, EstRonchon = true },
    new Lama { Nom = "Lorry", Taille = 4, EstRonchon = true },
    new Lama { Nom = "Laurel", Taille = 20, EstRonchon = false },
    new Lama { Nom = "Louise", Taille = 17, EstRonchon = true }
};

Maintenant, imaginez que je veuille obtenir une liste de tous les Lamas qui sont à la fois grands et du genre ronchonneur. On pourrait y arriver de la façon suivante :

var results = new List<Lama>();
foreach (var lama in Lamas)
{
    bool include = lama.EstRonchon && lama.Taille > 9;
    if (include)
        results.Add(lama);
}

Refactoriser en Lambda

Le problème c’est que ça fait un paquet de code pour appliquer un filtre tout bête… Alors que nous savons bien qu’il existe de supers méthodes d’extensions en LINQ qui permettent d’exécuter une commande Where sur une collection d’objets.

On va modifier un peu notre syntaxe de départ pour nous orienter dans la bonne direction. Notez bien que la plupart du code C# contenu dans le reste de ce billet est délibérément faux, étant donné que je cherche à vous conduire vers la solution. Je vous préviendrai la prochaine fois que vous aurez à faire à une syntaxe correcte !

Imaginons que Where prenne comme paramètre le nom d’une méthode qui réalise le filtrage de la liste. Notre code pourrait alors se présenter comme ceci :

var results = Lamas.Where(Filter);

... avec une méthode helper qu'on appellerait Filter :

private bool Filter(Lama lama)
{
    return lama.EstRonchon && lama.Taille > 9;
}

C’est pas complètement idiot ? La méthode Where appelle la méthode Filter en lui passant chaque Lama un par un pour vérifier s’il doit faire parti des résultats ou non.

Méthode anonyme en ligne

Ouais mais quand même : notre méthode Filter ne sert qu’à un seul endroit pour filtrer nos Lamas. On pourrait donc se simplifier la vie et éviter d’avoir à la déclarer en faisant une méthode en ligne à la place. Pourquoi pas quelque chose comme ci-dessous (encore une fois, c’est une syntaxe fictive comme la plupart du code dans ce billet) :

var results = Lamas.Where(
    bool Filter(Lama lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Ca c’est fait. Et en plus on s’est débarrassé du mot clé private puisque la méthode n’est plus un membre de la classe. Mais alors, à quoi ça sert qu’elle ait encore un nom ? Y’a qu’à le virer :

var results = Lamas.Where(
    bool (Lama lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Ca c’est déjà plus concis. Suivez-bien et je vous traduis ce que ça veut dire : « cette méthode renvoie un Booléen, et attend un Lama en entrée », suivi du code pour le corps de la méthode.

Types implicites

Attendez-voir. Le compilateur C# est quand même vachement intelligent, pas vrai ? Alors pourquoi est-ce que je me décarcasse à lui dire que la méthode renvoie un Booléen puisqu’il sait bien que la méthode Where a besoin d’un Booléen et qu’il est assez grand peut se rendre compte que la commande « EstRonchon && Taille > 9 » est une expression de type Booléen ? Tchao Tchao le Booléen :

var results = Lamas.Where(
    (Lama lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Et on sait bien que la méthode Where s’applique à une List<Lama>, ce qui fait que le seul argument possible pour cette méthode est de type Lama… Alors arrêtons d’écrire des trucs inutiles dans notre code :

var results = Lamas.Where(
    (lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Constructeurs inutiles

Le truc c’est que notre méthode n’est rien de plus qu’une seule et ridiculement simple ligne d’expression Booléenne. Alors pourquoi avoir encore besoin du mot clé return ? Ou du point-virgule pour terminer la ligne ? On sait très bien ce qu’elle fait. Et on n’a quand même pas besoin des accolades pour une expression d’une seule ligne, pas vrai ?

var results = Lamas.Where(
    (lama)
        lama.EstRonchon && lama.Taille > 9
    );

Mais ça se complique un peu si on se met à supprimer les espaces inutiles :

var results = Lamas.Where( (lama) lama.EstRonchon && lama.Taille > 9 );

Là ça devient un peu plus coton à lire. Il nous faudrait trouver une autre façon de séparer les paramètres en entrée du corps de notre expression. Et avec C#, c’est justement à ça que sert l’opérateur « => » :

var results = Lamas.Where( (lama) => lama.EstRonchon && lama.Taille > 9 );

Et on n’a pas non plus besoin des parenthèses autour du paramètre en entrée puisqu’on en a un seul :

var results = Lamas.Where( lama => lama.EstRonchon && lama.Taille > 9 );

Et pourquoi diable gaspiller toutes ces lettres pour écrire « lama » à chaque fois alors qu’on pourrait très bien se contenter d’un simple « l » ?

var results = Lamas.Where( l => l.EstRonchon && l.Taille > 9 );

Résumé

Il s’avère que les trois dernières commandes ci-dessus sont des Expressions Lambdas valides qui filtrent une liste de Lamas pour nous. L’objectif de ce code n’a pas varié d’un iota et il continue à avoir la même signification :

Tiens. C'est une méthode qui prend un paramètre nommé « l » et renvoie un résultat Booléen en appliquant l’expression suivante au paramètre en entrée. T"as qu'à t'en servir pour filtrer les Lamas, steuplé !

Et maintenant, à vous de découvrir ce que la syntaxe ci-dessous peut bien vouloir dire :

grandRonchonLamas.ForEach(l => Console.WriteLine(l.Nom));

Je souhaite de tout cœur que cette approche un peu décalée a réussi à vous expliquer comment utiliser les Expressions Lambdas. Maintenant, vous n’avez plus qu’à approfondir tout ça et à vous documenter un peu pour comprendre des trucs comme Expression<>, Func<>, Action<>, etc…

Amusez-vous bien !

Ceci est la traduction du billet "What on Earth is a Lambda Expression?" de Simon Ince.

vendredi 2 avril 2010

Faire fonctionner ASP.NET MVC sur IIS 6

Voici la façon la plus simple possible qui soit pour qu'une application ASP.NET MVC puisse être installée et fonctionner sur un serveur IIS 6 qui n'y est pas préparé. Personnellement, j'ai dû refaire ça hier et j'avais presque oublié comment m'y prendre. Donc, pour ne pas prendre de risque, je préfère noter tout ça pour la prochaine fois où je pourrais en avoir besoin.

Concrètement, je n'ai rien inventé et tout vient du billet de Phil Haack : ASP.NET MVC on IIS 6 Walkthrough.

Ajouter l'extension .aspx aux routes MVC

Pour que MVC fonctionne sur un IIS 6 où ASP.NET s'attend à avoir des URLs avec des extensions ".aspx", il faut commencer par mettre à jour les routes définies dans le fichier Global.asax.cs pour qu'elles prennent en compte cette extension.

Comment faire

Pour cela, il faut modifier la méthode RegisterRoutes comme ci-dessous :

public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  routes.MapRoute(
    "Default",
    "{controller}.aspx/{action}/{id}",
    new { action = "Index", id = "" }
  );

  routes.MapRoute(
    "Root",
    "",
    new { controller = "Home", action = "Index", id = "" }
  );
}

La première route (nommée "Default") fait qu'on aura plus des URLs du style "http://www.example.com/Controller/Action/Id" mais "http://www.example.com/Action.aspx/Action/Id" que IIS 6 sait très bien gérer.

La seconde route (nommée "Root") est nécessaire pour gérer l'URL "http://www.example.com/" que la route "Default" ne sait plus prendre en compte puisqu'elle attend une extension ".aspx".

Remarque

Pour que l'URL "http://www.example.com/" fonctionne, il faut également penser à configurer IIS pour que le fichier Default.aspx soit un des documents par défaut du site (à la place ou en plus des fichiers Index.aspx, Index.htm...).

Attention

Etant donné que nos URLs prennent des ".aspx", il faut aussi modifier le fichier Web.Config pour mettre à jour l'URL qui y est paramétrée pour effectuer l'authentification. Rechercher "loginUrl=" et remplacer :

<forms loginUrl="~/Account/LogOn" ...

Par :

<forms loginUrl="~/Account.aspx/LogOn" ...

Après ce gros morceau, il ne devrait pas y avoir d'autre problème avec les URLs. Sauf si on a codé en dur des liens au lieu d'utiliser les helpers Html.ActionLink() ou Url.Action() qui vont bien... Mais ça, c'est le métier qui rentre.

Installer ASP.NET MVC sur le serveur IIS 6

Je rigole. C'est beaucoup trop compliqué à faire.

Par contre, ce qui est beaucoup plus fastoche, c'est de copier System.Web.Mvc.dll dans le répertoire "bin" de l'application !

Un dernier truc

Tant qu'à faire de modifier le Web.Config, autant en profiter pour lui ajouter la ligne :

<customErrors mode="Off" />

Au moins, avec ça, si jamais il y a un problème on est sûr d'avoir un bel écran jaune avec des messages d'erreur en pagaille.

mercredi 24 mars 2010

Contact Manager avec MongoDB et NoRM

Maintenant que j'ai enfin expédié mon billet consacré au portage hyper passionnant de l'application ContactManager sous NHibernate, je peux enfin revenir à des trucs beaucoup plus amusants.

Ca faisait un petit moment que j'avais envie de tester pour de bon une de ces nouvelles bases de données NoSQL dont on nous rabat les oreilles ces derniers temps.

Mais bon, ça demande du temps et il faut choisir quelle NoBase de données employer et il faut qu'il y ait des pilotes pour C# et il y a plein d'autres trucs à faire... Et un soir, Rob Conery sort un billet où non seulement il explique comment utiliser une base de données MongoDB avec LINQ mais en plus il présente le "provider" NoRM sur lequel il est en train de travailler avec quelques autres.

C'est trop beau pour être vrai => je télécharge, je trifouille et j'arrive à peu près à faire marcher ses bouts d'exemples.

C'est pas compliqué ! J'ai plus qu'à transformer Contact Manager pour qu'il fonctionne avec MongoDB et NoRM !!!

Oulla! Ne nous emballons pas. NoRM est en cours de développement donc y'a pas forcément tout ce qu'il faut et surtout j'ai pas encore tout assimilé en matière de base de données documents.

C'est pas compliqué ! J'ai qu'à refaire seulement la toute première étape !!!

Le faire, c'est bien et ça va même assez vite. Le dire, c'est mieux mais c'est un peu plus long.

Et donc, après quelques heures d'efforts, voici mon tutorial sur le développement d'une application de gestion de contacts avec ASP.NET MVC, MongoDB et NoRM qui reprend quasiment mot à mot la première étape du tutoriel de Microsoft. Comme l'original, ce tutoriel présente donc comment créer une application de gestion de contact de la façon la plus simple qui soit. Tout au long de celui-ci, vous pourrez voir comment mettre en place le support d'opérations CRUD classiques vers une base de données MongoDB : création, lecture, mise à jour et suppression d'enregistrements.

mardi 23 mars 2010

Porter Contact Manager sous NHibernate

Après avoir fait quelques essais très basiques avec NHibernate, j'ai voulu essayer d'être plus concret et d'aller un peu plus loin en tentant de porter l'application Contact Manager sous NHibernate. A priori, ça ne devait pas être trop compliqué dans la mesure où il y a quelques temps j'avais déjà remplacé Entity Framework par LINQ to SQL et que ça ne s'était pas trop mal passé. Sans compter, que je souhaitais juste faire en sorte que ça marche à peu près tel quel, sans trop chercher à découvrir ou à mettre au point la "bonne" façon d'utiliser NHibernate avec ASP.NET MVC.

Dans la pratique, ça m'a pris un peu plus de temps que ce que j'avais pensé y consacrer. En fait, j'aurais pu faire ce billet depuis un petit moment, mais avant il fallait que j'arrive à mettre au propre la solution que j'avais suivie. Parce que c'est là que le bât blesse, c'est qu'avec NHibernate il y a tout plein de façons de faire et que je n'ai pas réussi à trouver une documentation "officielle" (et récente) qui décrive clairement quel est l'état de l'art pour utiliser NHibernate dans le cadre d'une application ASP.NET.

Voici donc parmi tous mes essais (avec des réussites, des ratés et des abandons) une des façons de faire à laquelle je suis arrivé et qui est assez "simple" à expliquer. Ca marche correctement, mais je ne peux absolument pas affirmer que c'est la méthode à suivre.

Etape 1 : Installer NHibernate

Pour faire simple, le plus pratique est d'aller sur le site de Castle ActiveRecord et d'y télécharger la toute dernière version d'Active Record. Une fois l'archive décompressée, il suffit de créer un répertoire "lib" dans le répertoire racine de "ContactManager" et d'y copier les quelques fichiers suivants :

  • Iesi.Collections.dll
  • Iesi.Collections.xml
  • LinFu.DynamicProxy.dll
  • log4net.dll
  • log4net.xml
  • nhibernate-configuration.xsd
  • nhibernate-mapping.xsd
  • NHibernate.ByteCode.LinFu.dll
  • NHibernate.ByteCode.LinFu.xml
  • NHibernate.dll
  • NHibernate.Linq.dll
  • NHibernate.xml

En réalité, je n'utilise pas du tout l'approche Active Record. Mais l'avantage de passer par leur site c'est qu'on est certain d'avoir DLLs récentes avec des versions compatibles entre elles. Au cours de mes différents essais, j'avais récupéré des DLLs au fur et à mesure de mes besoins et d'un petit peu tous les côtés et ça m'avait joué quelques mauvais tours.

Une fois ces différentes librairies mises en place, il suffit de référencer les 4 éléments suivants dans l'application ContactManager existante :

  • log4net.dll
  • NHibernate.dll
  • NHibernate.ByteCode.LinFu.dll
  • NHibernate.Linq.dll

Et pour bien faire les choses, il faut aussi prendre le temps de dé-référencer les éléments qui correspondent à Entity Framework et par conséquent de supprimer les fichiers liés à EF dans le sous-répertoire Models.

Etape 2 : Configurer NHibernate

NHibernate est installé et référencé au niveau du projet. Il faut maintenant le configurer pour lui expliquer qu'on aimerait bien qu'il utilise notre base de données SQL Server habituelle. Et là commence la galère puisqu'il existe apparement des tonnes de façon différentes pour faire ça. J'en ai essayé quelques unes et je n'ai pas vraiment très bien compris quels sont les avantages et les inconvénients de telle ou telle façon de faire (mais c'est vrai que j'ai pas trop passé de temps dessus).

Par conséquent, je vais aller au plus simple et donner directement la méthode que j'ai repérée sur la vidéo de présentation de NHibernate proposée par TekPub. C'est juste un extrait, mais ça m'a suffi pour emprunter le source à l'application Kona de Rob Conery. Cela consiste à créer un fichier "nhibernate.config" à la racine de l'application ASP.NET MVC pour y définir tous les éléments nécessaires pour configurer NHibernate :

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">

  <session-factory name="ContactManager">
    <property name="dialect">NHibernate.Dialect.MsSql2008Dialect</property>
    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
    <property name="connection.connection_string">server=.\SQLExpress;database=ContactManagerDB;Integrated Security=true;</property>
    <property name="show_sql">true</property>
    <property name="cache.use_query_cache">false</property>
    <property name="adonet.batch_size">16</property>
    <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
    <mapping assembly="ContactManager" />
  </session-factory>
  
</hibernate-configuration>

Par rapport à l'écriture de ces informations dans le Web.config, il me semble que c'est un peu plus clair d'isoler ça dans un fichier à part (sans compter que ça évite de complexitrifier le fichier Web.config inutilement). Sinon, il aurait aussi été possible d'utiliser directement le fichier "hibernate.cfg.xml" standard mais je trouve qu'un fichier ".config" c'est plus propre (et plus sûr ?) qu'un fichier ".xml".

Etape 3 : Gérer une session NHibernate

Dans la version Entity Framework de l'application Contact Manager, on utilise un objet ContactManagerDBEntities généré automatiquement par ADO.NET EF pour communiquer avec la base de données SQL Server. Dans la version portée sous LINQ to SQL, c'est un objet ContactManagerModelDataContext qui joue ce rôle.

En ce qui concerne NHibernate, les communications avec la base de données se font par l'intermédiaire d'un objet Session. Et là aussi, pour gérer / instancier cet objet Session, il existe tout plein de façons de faire.

Pour l'instant, je reprend de façon ultra simplifiée la méthode proposée par Rob Conery parce qu'il est possible de la mettre en oeuvre en très très peu de lignes au niveau du fichier global.asax.cs :

using System;
using System.IO;
using System.Web.Mvc;
using System.Web.Routing;
using NHibernate;
using NHibernate.Cfg;

namespace ContactManager
{

    public class MvcApplication : System.Web.HttpApplication
    {

        public static ISessionFactory SessionFactory = CreateSessionFactory();

        private static ISessionFactory CreateSessionFactory()
        {
            var cfg = new Configuration().Configure(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nhibernate.config"));
            return cfg.BuildSessionFactory();
        }

Grâce à ça, il devient tout simple de remplacer la ligne :

private ContactManagerDBEntities _entities = new ContactManagerDBEntities();

par l'instruction suivante :

private ISession _session = MvcApplication.SessionFactory.GetCurrentSession();

Etape 4 : Faire le mapping entre les tables et les objets

Avec NHibernate, les classes Contact et Group ne sont pas générées automatiquement comme avec Entity Framework ou LINQ to SQL. Il faut donc créer à la main des classes POCO (c'est pas le plus compliqué) et il faut également créer un fichier de mapping XML pour associer chaque classe à la table correspondante dans la base de données (c'est déjà un peu plus mariole).

Je ne vais pas expliquer comment faire ni entrer dans les détails de comment ça marche (en plus je n'ai pas encore suffisament bien compris moi-même), mais voici ce à quoi je suis arrivé après quelques essais.

Models\Contact.cs

namespace ContactManager.Models
{
    public partial class Contact
    {
        public Contact()
        {
            Group = new Group();
        }
        public virtual int Id { get; set; }
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual string Phone { get; set; }
        public virtual string Email { get; set; }
        public virtual Group Group { get; set; }

        /// <summary>
        /// Create a new Contact object.
        /// </summary>
        /// <param name="id">Initial value of Id.</param>
        /// <param name="firstName">Initial value of FirstName.</param>
        /// <param name="lastName">Initial value of LastName.</param>
        /// <param name="phone">Initial value of Phone.</param>
        /// <param name="email">Initial value of Email.</param>
        public static Contact CreateContact(int id, string firstName, string lastName, string phone, string email)
        {
            Contact contact = new Contact();
            contact.Id = id;
            contact.FirstName = firstName;
            contact.LastName = lastName;
            contact.Phone = phone;
            contact.Email = email;
            return contact;
        }
    }

}

Mappings\Contact.hbm.xml

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">

  <class name="Contact" table="Contacts" dynamic-update="true">

    <cache usage="read-write" />

    <id name="Id" column="Id" type="integer">
      <generator class="identity" />
    </id>
    <property name="FirstName" type="string" />
    <property name="LastName" type="string" />
    <property name="Phone" type="string" />
    <property name="Email" type="string" />
    <many-to-one name="Group" column="groupId" not-null="true" />

  </class>

</hibernate-mapping>

Models\Group.cs

using System.Collections.Generic;

namespace ContactManager.Models
{
    public class Group
    {
        public Group()
        {
            Contacts = new List<Contact>();
        }
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual IList<Contact> Contacts { get; set; }
    }
}

Mappings\Group.hbm.xml

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">

  <class name="Group" table="Groups" dynamic-update="true">

    <cache usage="read-write" />

    <id name="Id" column="Id" type="integer">
      <generator class="identity" />
    </id>
    <property name="Name" type="string" />
    <bag
        name="Contacts"
        inverse="true"
        lazy="true">
      <key column="groupId" />
      <one-to-many class="Contact" />
    </bag>

  </class>

</hibernate-mapping>

En ce qui concerne les deux fichiers Contact.hbm.xml et Group.hbm.xml, il faut impérativement modifier leur propriété "Action de génération" au niveau de l'explorateur de projet pour la définir à "Ressource incorporée" sans quoi le mapping NHibernate ne fonctionne pas. C'est un problème très facile à identifier parce qu'on obtient une erreur "Association references unmapped class: ContactManager.Models.Contact" dès le lancement de l'application.

Redévelopper le repository avec NHibernate

Après ces 4 premières étapes, tout est enfin prêt pour pouvoir modifier la couche repository de l'application afin de remplacer Entity Framework par NHibernate :

  • NHibernate est installé et référencé
  • NHibernate est configuré pour accéder à la base de données SQL Server
  • Un objet NHibernate Session a été défini pour communiquer avec la base de données
  • Les objets données et les fichiers de mapping NHibernate sont en place

Par conséquent, il ne reste plus qu'à implémenter l'interface IContactManagerRepository en utilisant NHibernate et LINQ to NHibernate pour remplacer le fichier EntityContactManagerRepository.cs qui l'implémentait pour Entity Framework.

Models\NHContactManagerRepository.cs

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;

namespace ContactManager.Models
{
    public class NHContactManagerRepository : IContactManagerRepository
    {
        private ISession _session = ContactManager.MvcApplication.SessionFactory.OpenSession();

        public Contact GetContact(int id)
        {
            return (from c in _session.Linq<Contact>()
                    where c.Id == id
                    select c).FirstOrDefault();
        }

        public Contact CreateContact(int groupId, Contact contactToCreate)
        {
            // Associate group with contact
            contactToCreate.Group = GetGroup(groupId);

            // Save new contact
            _session.SaveOrUpdate(contactToCreate);
            return contactToCreate;
        }

        public Contact EditContact(int groupId, Contact contactToEdit)
        {
            // Associate with new group
            contactToEdit.Group = GetGroup(groupId);

            // Save changes
            _session.SaveOrUpdate(contactToEdit);
            _session.Flush();
            return contactToEdit;
        }

        public void DeleteContact(Contact contactToDelete)
        {
            _session.Delete(contactToDelete);
            _session.Flush();
        }

        public Group CreateGroup(Group groupToCreate)
        {
            _session.SaveOrUpdate(groupToCreate);
            return groupToCreate;
        }

        public IEnumerable<Group> ListGroups()
        {
            return _session.Linq<Group>().OrderBy(o => o.Name).ToList();
        }

        public Group GetFirstGroup()
        {
            return _session.Linq<Group>().FirstOrDefault();
        }

        public Group GetGroup(int id)
        {
            return (from g in _session.Linq<Group>()
                    where g.Id == id
                    select g).FirstOrDefault();
        }

        public void DeleteGroup(Group groupToDelete)
        {
            _session.Delete(groupToDelete);
            _session.Flush();
        }

    }
}

Une fois encore, je n'ai pas cherché à faire "bien" mais simplement à ce que ça marche. Dans la pratique, il vaudrait mieux éviter tous ces "_session.Flush()" et passer par des transactions et des "using" mais pour l'instant, ce n'était absolument pas mon objectif.

Pour pouvoir compiler et vérifier que tout fonctionne comme prévu, il reste encore à modifier le constructeur ContactManagerService pour qu'il utilise ce repository en lieu et place de celui d'Entity Framework.

public ContactManagerService(IValidationDictionary validationDictionary)
    : this(validationDictionary, new NHContactManagerRepository())
{
}

Une fois cette ultime modification réalisée, la solution compile sans aucune erreur et elle m'offre bien les mêmes fonctionnalités que les versions avec Entity Framework ou LINQ to SQL :

  • lister les contacts rattachés à un groupe,
  • ajouter, modifier ou supprimer un contact,
  • ajouter ou supprimer un groupe de contacts.

Et juste pour le plaisir, je peux lancer les quelques tests unitaires de ContactManager.Test et tout va bien !

Conclusion

Par rapport au passage sous LINQ to SQL, ça a été beaucoup plus laborieux. Le problème, c'est qu'il y a à la fois beaucoup (trop) de façon de faire et beaucoup (trop) d'informations disponibles ce qui fait qu'il n'est vraiment pas évident de savoir dans quelle direction partir.

Mais malgré tout, c'est quand même un succès. Il a suffi de changer le repository (et de recréer les classes Contact et Group générées automatiquement) pour que l'application ContactManager de base fonctionne correctement avec NHibernate. C'est donc un nouveau bon point pour le pattern Repository (zut! juste au moment où j'avais presque envie de me (ré)orienter vers Active Record). Et avec la prochaine version de Entity Framework qui devrait être POCO friendly, on peut même supposer que ce sera de plus en plus facile de passer d'un ORM à l'autre.

Peut-être que plus tard (c'est à dire une fois que je saurai faire), j'écrirai un autre billet pour présenter une façon d'utiliser Nibernate avec ASPNET MVC qui soit un peu plus orthodoxe. Mais déjà, j'aimerai très vite essayer de voir ce que ça donne en changeant la base de données et en testant cette version de Contactmanager NHibernate avec MySQL ou Oracle.

jeudi 11 mars 2010

Faire une liste Html.RadioButton

Un bouton radio ne s'utilisant jamais seul, on doit donc coder plusieurs helpers Html.RadioButton() quand on veut permettre à l'utilisateur de faire un choix entre plusieurs options :

<% using (Html.BeginForm()) { %>
   <fieldset>
     <p>
        Couleur ?
        <%= Html.RadioButton("Couleur", "R", false) %>Rouge
        <%= Html.RadioButton("Couleur", "V", true) %>Vert
        <%= Html.RadioButton("Couleur", "B", false) %>Bleu
     </p>
     <p>
        <input type="submit" value="Save" />
     </p>
   </fieldset>
<% } %>

Ce code ASP.NET permet d'afficher la page ci-dessous :

radio-button-helper.jpg

Si on consulte le code source de cette page, on trouve le code HTML suivant :

<form action="/Home/Edit" method="post">
   <fieldset>
     <p>
        Couleur ?
        <input id="Couleur" name="Couleur" type="radio" value="R" />Rouge
        <input checked="checked" id="Couleur" name="Couleur" type="radio" value="V" />Vert
        <input id="Couleur" name="Couleur" type="radio" value="B" />Bleu
     </p>
     <p>
        <input type="submit" value="Save" />
     </p>
   </fieldset>
</form>

Il faut cliquer sur un des 3 boutons radio pour sélectionner une option. Une fois qu'une option a été sélectionnée, le fait de cliquer sur un autre bouton radio va sélectionner une nouvelle option et automatiquement dé-sélectionner l'option qui était sélectionnée auparavant. Les 3 boutons radio sont liés entre eux et on ne peut sélectionner qu'une seule option à la fois parce qu'au niveau du code HTML, les 3 <input type="radio" ...> ont le même attribut name.

Pour l'instant, si on veut sélectionner une option, il est obligatoire de bien viser et de cliquer pile sur le petit rond du bouton radio. Dans la pratique, il faudrait faire en sorte que quand on clique sur le libellé de l'option, cela permette aussi de sélectionner cette option.

Pour faire ça, il faut associer le libellé au bouton radio. En HTML, on fait ça en utilisant une balise "label" pour définir le libellé et on relie ce label au bouton radio avec un attribut "for" qui pointe vers l'identifiant du bouton radio :

<input id="Couleur" name="Couleur" type="radio" value="R" /><label for="Couleur">Rouge</label>

Avec le helper Html.RadioButton() de ASP.NET MVC, il suffit donc de faire ça, mais le problème c'est que ça n'est pas si facile que ça...

Pour commencer, si on re-regarde le code HTML généré, on s'aperçoit que les 3 boutons radio ont le même identifiant ! Outre le fait que c'est pas normal du tout, ça nous complique la vie pour associer chaque libellé au bon bouton radio (puisque l'attribut "for" a besoin de l'identifiant du contrôle associé). Il va donc falloir trouver un moyen pour définir un identifiant unique pour chaque bouton radio. Heureusement, c'est prévu : le helper Html.RadioButton() accepte un objet htmlAttributes qui va nous permettre de faire ça.

Couleur ?
<%= Html.RadioButton("Couleur", "R", false, new { @id = "Red" })%><label for="Red">Rouge</label>
<%= Html.RadioButton("Couleur", "V", true, new { @id = "Green" })%><label for="Green">Vert</label>
<%= Html.RadioButton("Couleur", "B", false, new { @id = "Blue" })%><label for="Blue">Bleu</label>

Et ce coup-ci, on peut constater au niveau du code HTML généré que les 3 boutons radio n'ont plus à partager le même identifiant :

Couleur ?
<input id="Red" name="Couleur" type="radio" value="R" /><label for="Red">Rouge</label>
<input checked="checked" id="Green" name="Couleur" type="radio" value="V" /><label for="Green">Vert</label>
<input id="Blue" name="Couleur" type="radio" value="B" /><label for="Blue">Bleu</label>
 

Et maintenant, il est possible de cliquer sur le nom d'une couleur pour automatiquement sélectionner le bouton radio qui lui est associé.

mercredi 3 mars 2010

ASP.NET MVC c'est pas forcément LINQ to SQL

Ceci est la traduction du billet "ASP.NET MVC is not all about Linq to SQL" de Mike Brind.

La plupart des exemples destinés à illustrer l'utilisation de ASP.NET MVC sont tous basés sur l'utilisation de LINQ to SQL ou d'Entity Framework pour gérer l'accès aux données. Les forums sur www.asp.net regorgent de questions pour savoir s'il existe des alternatives et en effet, il est possible de faire autrement. Cet article va montrer comment utiliser une couche d'accès aux données en ADO.NET pur afin de gérer le contenu dynamique d'une application de type CRUD toute simple.

Pour les besoins de cette démonstration, je vais énormément emprunter à la série d'articles qu'Imar Spaanjaar a consacré à la réalisation d'applications web multi-couches en ASP.NET 2.0. Je vous recommande fortement de lire ces articles ou au minimum les deux premiers afin de vous familiariser avec les rudiments d'une approche multi-couches lors de la conception d'une application ASP.NET. Je vais reprendre quasiment telles quelles 3 des principales couches de son application : Business Objects, Business Logic et Data Access dont la construction est expliquée de façon très détaillée dans les articles d'Imar. Par conséquent, cet article se contentera d'étudier à quoi servent ces couches sans chercher à entrer dans le détail de leur code.

Pour commencer, nous allons jeter un coup d'œil à l'application présentée par Imar. Il s'agit d'une application toute simple où on retrouve les opérations CRUD typiques. Elle permet aux utilisateurs de gérer des contacts, avec leurs adresses, numéros de téléphone et adresses e-mail, et leur offre la possibilité de créer, lire, mettre à jour et supprimer l'une ou l'autre de ces entités.

Les entités gérées par l'application sont ContactPersons, PhoneNumbers, Addresses and EmailAddresses. Elles sont toutes définies dans la couche des objets métiers (BO = Business Objects) de l'application. Dans l'exemple d'origine, chacune de ces classe contient uniquement des propriétés publiques (avec leurs méthodes get et set) et elles ne gèrent absolument aucun traitement. Ceux-ci sont gérés au sein de la couche des règles métiers (BLL = Business Logic Layer) dans des classes NomEntitéManager. Il existe une correspondance une-à-une entre une entité et sa classe Manager associée. Chacune des classes Manager contient des méthodes pour retrouver une instance de l'entité ou une collection d'entités, pour enregistrer une entité (ajout ou modification) et pour supprimer une entité. Cette partie de l'application peut aussi être utilisée pour gérer la validation, les autorisations, etc... Mais il n'y en a pas dans l'exemple pour qu'il reste suffisamment compréhensible. Si vous souhaitez voir comment mettre en œuvre des règles de validation dans la couche métier, vous pouvez vous reporter à la seconde série d'articles dans laquelle Imar présente les évolutions apportées à son application, y compris sa migration sous ASP.NET 3.5.

La dernière couche est la couche d'accès aux données (DAL = Data Access Layer). Cette couche comprend elle aussi des classes qui ont une correspondance une-à-une avec les classes Manager dans la couche BLL. En pratique, les méthodes de la couche BLL appellent les méthodes correspondantes dans la couche DAL. Les méthodes de la DAL sont les seules de l'application qui ont à connaitre le mécanisme utilisé pour conserver (stocker) les entités. Pour notre exemple, il s'agit d'une base de données SQL Server Express. Par conséquent, les classes de la DAL utilisent ADO.NET et les classes SqlClient. Le principe derrière cette approche est que si jamais vous souhaitez changer le mécanisme employé pour le stockage (pour utiliser XML, Oracle, un Web service ou même LINQ to SQL ou tout autre ORM), vous n'aurez qu'à remplacer cette couche DAL. Du moment que votre nouvelle DAL exposera des méthodes avec les signatures attendues par la couche BLL, tout devrait continuer à fonctionner sans qu'il soit nécessaire de faire la moindre modification dans le reste de l'application. Pour être sûr que la nouvelle DAL respecte bien les mêmes signatures de méthode que la DAL actuelle, le plus simple est de définir des Interfaces (ce qui pourrait faire un très bon sujet d'article).

Architecture MVC

Il existe tout un tas de très bons articles qui expliquent ce qu'est l'architecture MVC, c'est pourquoi ce billet ne va pas entrer trop dans le détail. Pour une présentation plus détaillée, je vous recommande de consulter directement les tutoriels sur ASP.NET MVC sur le site de Microsoft (ou leurs traductions en français sur le site Developpez). Très succinctement, le M est pour le Modèle, dans lequel on va retrouver les couches BO, BLL et DAL. Le V est pour les Vues, qui correspond à toute la partie interface utilisateur (ce que nos utilisateurs verront). Et le C est pour les Contrôleurs. Le rôle des contrôleurs est de coordonner les réponses de l'application aux demandes faites par les utilisateurs. Si un utilisateur clique sur un bouton qui pointe vers une URL spécifique, cette demande est mappée à une action du contrôleur (c'est à dire une méthode dans la classe contrôleur) qui a alors la responsabilité de gérer tous les traitements nécessaires pour répondre à cette demande et renvoyer une réponse à l'utilisateur, généralement sous la forme d'une nouvelle Vue ou d'une mise à jour de la vue en cours.

Après avoir créé une nouvelle application MVC dans Visual Studio puis supprimé les vues et les contrôleurs créés par défaut, la première chose que j'ai faite a été de copier les fichiers correspondant aux couches BO, BLL et DAL depuis l'application d'Imar dans le dossier Models de ma nouvelle application. J'ai également copié la base de données SQL Server du répertoire App_Data depuis l'application d'origine dans le dossier App_Data de l'application MVC puis j'ai fait de même pour le fichier Style.ccs que j'ai placé dans le dossier Content.

J'ai aussi changé quelques petits trucs. Il faut ajouter la chaine de connexion à la base de données dans le fichier Web.config de l'application MVC. Même si ce n'était pas vraiment indispensable, j'ai aussi modifié les namespaces au niveau des fichiers copiés puis un peu mis à jour quelques morceaux du code de la DAL pour utiliser des fonctionnalités du C# 3.0. Une fois tout ça fini, j'ai fait un rapide Ctrl + Maj + F5 pour vérifier que le projet compilait. Par la suite, je n'aurait pas à revenir sur ces fichiers, à part pour quelques méthodes de la DAL et les méthodes correspondantes dans la BLL, comme nous le verrons par la suite.

Les Contrôleurs

Ayant supprimé les contrôleurs créés par défaut par Visual Studio, j'ai ajouté quatre contrôleurs, un pour chaque entité. ce qui me donne donc au final les contrôleurs suivants : ContactController, PhoneController, AddressController et EmailController.

Chacun de ces contrôleurs sera chargé de coordoner 4 actions : List, Add, Edit et Delete. Par conséquent, la première chose à faire est de définir la route pour ces différentes actions au niveau du fichier Global.asax.cs :

public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  routes.MapRoute(
      "Default",                                              
      "{controller}/{action}/{id}",                           
      new { controller = "Contact", action = "List", id = "" }  
  );
}

La page par défaut de l'application sera chargée d'afficher la liste des contacts. Les données sur les contacts sont obtenues grâce à la méthode GetList() dans la classe ContactPersonManager de la BLL. Par conséquent, le code source pour l'action List() du contrôleur est le suivant :

public ActionResult List()
{
  var model = ContactPersonManager.GetList();
  return View(model);
}

Les vues fortement typées

Je vais utiliser des vues fortement typées tout au long de l'application. D'une part parce que cela permet de profiter de l'intellisense dans le code source des vues et d'autre part parce qu'elles ne dépendent pas du ViewData qui est source d'erreur à cause de l'utilisation de chaines pour indexer les différentes valeurs. Pour faire un peu de ménage, j'ai ajouté quelques namespaces de type ContactManagerMVC.Xxxxx dans la section <namespaces> du fichier Web.Config. Voici donc les namespaces que j'utilise par rapport à ceux définis dans le code d'Imar :

<namespaces>
<add namespace="System.Web.Mvc"/>
<add namespace="System.Web.Mvc.Ajax"/>
<add namespace="System.Web.Mvc.Html"/>
<add namespace="System.Web.Routing"/>
<add namespace="System.Linq"/>
<add namespace="System.Collections.Generic"/>
<add namespace="ContactManagerMVC.Views.ViewModels"/>
<add namespace="ContactManagerMVC.Models.BusinessObject"/>
<add namespace="ContactManagerMVC.Models.BusinessObject.Collections"/>
<add namespace="ContactManagerMVC.Models.BusinessLogic"/>
<add namespace="ContactManagerMVC.Models.DataAccess"/>
<add namespace="ContactManagerMVC.Models.Enums"/>
</namespaces>

Cela a pour effet de les rendre disponibles dans toute l'application et d'éviter d'avoir à saisir les types complets à l'intérieur des vues. L'objet renvoyé par la méthode GetList() est de type ContactPersonList qui est défini dans le dossier Collections à l'intérieur de la couche BO. Il s'agit tout simplement d'une collection d'objets ContactPerson. La déclaration de page en première ligne de la vues List est la suivante :

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
    Inherits="System.Web.Mvc.ViewPage<ContactPersonList>" %>

Vous pouvez aussi voir que j'utilise une page maître. Dans celle-ci, j'ai fait référence au fichier css récupéré du code d'Imar. Le code html qui gère l'affichage des objets ContactPerson de la collection est le suivant :

<table class="table">
    <tr>
      <th scope="col">Id</th>
      <th scope="col">Full Name</th>
      <th scope="col">Date of Birth</th>
      <th scope="col">Type</th>
      <th scope="col">&nbsp;</th>
      <th scope="col">&nbsp;</th>
      <th scope="col">&nbsp;</th>
      <th scope="col">&nbsp;</th>
      <th scope="col">&nbsp;</th>  
    </tr>
    <%
      if (Model != null)
      {
        foreach (var person in Model)
        {%>
    <tr>
      <td><%= person.Id %></td>
      <td><%= person.FullName %></td>
      <td><%= person.DateOfBirth.ToString("d") %></td>
      <td><%= person.Type %></td>
      <td title="address/list" class="link">Addresses</td>
      <td title="email/list" class="link">Email</td>
      <td title="phone/list" class="link">Phone Numbers</td>
      <td title="contact/edit" class="link">Edit</td>
      <td title="contact/delete" class="link">Delete</td>
    </tr>
    <%
        }
      }else{%>
    <tr>
      <td colspan="9">No Contacts Yet</td>
    </tr>  
     <% }%>
  </table>

Vous pouvez constater au premier coup d'oeil l'avantage d'avoir des vues fortement typées. L'objet Model est du type ContactPersonList, ce qui fait que chaque élément est du type ContactPerson et que ses propriétés sont disponibles sans qu'il soit nécessaire de caster le Model en ContactPersonList. Si on avait dû faire un cast et qu'on ait fait une erreur, cette erreur de casting n'aurait été détectée qu'au moment de l'exécution, ce qui est pour le moins embêtant.

J'ai un petit peu triché pour le code html. J'aurai pu demander à générer un contenu de type "List" quand j'ai créé la vue, ce qui m'aurait permis d'avoir un squelette de page approprié pour les listes. Mais je ne l'ai pas fait. Je voulais obtenir quelque chose qui puisse fonctionner plus simplement avec la css d'Imar. J'ai donc lancé son application sur mon PC et une fois qu'elle s'est affichée dans mon navigateur, j'ai affiché la source et copié le html à partir de celle-ci. Imar utilise des GridViews dans son application webforms et par conséquent il y a pas mal de css et autre qui est automatiquement inséré dans le html généré. J'ai donc nettoyé un peu cette soupe et défini les styles correspondant à la table dans le fichier Site.css. J'en ai également profité pour ajouter un peu de css pour les éléments <th> et <td> comme vous le verrez si vous téléchargez mon application.

J'ai aussi ajouté des attributs "title" aux cellules de la table qui contiennent les liens vers les autres actions dans l'application d'origine. Je ne souhaitais pas que toute la page soit repostée pour afficher les adresses ou les numéros de téléphone ou lorsque l'utilisateur veut modifier ou supprimer un enregistrement existant, mais que le site utilise de l'Ajax pour tout cela. Ces attributs "title" vont jouer un rôle clé dans la façon dont je compte gérer Ajax dans mon application. Et pour finir, la classe css "link" me sert pour que le texte agisse comme un lien avec du souligné et un pointeur de type main lorsque la souris passe dessus.

Utiliser jQuery pour la partie Ajax

Avant de jeter un coup d'œil au gros morceau de script qui gère les fonctionnalités Ajax de l'application, voici trois lignes supplémentaires de html que j'ai ajouté en bas de la vue List :

<input type="button" id="addContact" name="addContact" value="Add Contact" />
<div id="details"></div>
<div id="dialog" title="Confirmation Required">Are you sure about this?</div>

La première ligne affiche le bouton qui permet aux utilisateurs de créer un nouveau contact. La deuxième ligne contient une balise <div> vide qui sert de conteneur pour du contenu à venir. La troisième ligne est destinée à la boite de confirmation en jQuery qui servira pour demander aux utilisateurs de valider le fait qu'ils souhaitent supprimer un contact.

Pour que tout cela puisse fonctionner, il faut encore ajouter 3 fichiers dans la page maître : un pour insérer la librairie jQuery en elle-même et les deux autres pour certaines fonctionnalités de la librairie jQuery.UI (pour gérer une boite de dialogue modale et saisir des dates à l'aide d'un calendrier) :

<script src="../../Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>
<script src="../../Scripts/ui.core.min.js" type="text/javascript"></script>
<script src="../../Scripts/jquery-ui.min.js" type="text/javascript"></script>

Voici ce que donne la vue List quand on y accède :

Et voici le source jQuery complet nécessaire pour gérer entièrement cette vue List :

<script type="text/javascript">
  $(function() {
    // row colours
    $('tr:even').css('background-color', '#EFF3FB');
    $('tr:odd').css('background-color', '#FFFFFF');
    // selected row managment
    $('tr').click(function() {
      $('tr').each(function() {
        $(this).removeClass('SelectedRowStyle');
      });
      $(this).addClass('SelectedRowStyle');
    }); 
    // hide the dialog div
    $('#dialog').hide();
    // set up ajax to prevent caching of results in IE
    $.ajaxSetup({ cache: false });
    // add an onclick handler to items with the "link" css class
    $('.link').live('click', function(event) {
      var id = $.trim($('td:first', $(this).parents('tr')).text());
      var loc = $(this).attr('title');
      // check to ensure the link is not a delete link
      if (loc.lastIndexOf('delete') == -1) {
        $.get(loc + '/' + id, function(data) {
          $('#details').html(data);
        });
      // if it is, show the modal dialog   
      } else {
        $('#dialog').dialog({
          buttons: {
            'Confirm': function() {
              window.location.href = loc + '/' + id;
            },
            'Cancel': function() {
              $(this).dialog('close');
            }
          }
        }); 
        $('#dialog').dialog('open');
        }
      }); 
      // add an onclick event handler to the add contact button
      $('#addContact').click(function() {
        $.get('Contact/Add', function(data) {
          $('#details').html(data);
        });
      }); 
    });
</script>

Cela peut paraitre un peu compliqué, mais comme pour tout avec jQuery c'est en fait assez simple. J'ai séparé du mieux possible les différentes parties du code à l'aide de commentaires pour essayer de faciliter sa compréhension :

  • La première chose que ce script effectue, c'est de remplacer la fonctionnalité AlternatingRowColor qui existe au niveau des contrôles serveurs quand on utilise les webforms. Au lieu de cela, le script applique un style css aux lignes de la table une fois que la table a été affichée.
  • Puis j'ai ajouté un peu de code pour mettre en évidence la ligne en cours de sélection, comme Imar l'avait fait dans l'application d'origine.
  • La <div> créée pour gérer le message de confirmation de la boite de dialogue est ensuite masquée pour qu'elle ne s'affiche pas sous la liste
  • La ligne $.ajaxSetup({ cache: false }); sert à éviter que IE mette en cache les informations. Si vous ne faites pas cela, vous allez vous creuser les méninges en vous demandant pourquoi vos mises à jours et vos suppressions ne sont pas répercutées à l'affichage alors qu'elles ont bien été prises en compte dans la base de données.

Le dernier gros bloc de code est assez intéressant. Il utilise la méthode .live() qui fait en sorte que les gestionnaires d'évènements sont bien attachés à tous les éléments correspondants, qu'ils existent déjà à cet instant précis ou pas encore. Par exemple, lorsque l'utilisateur clique sur le lien "Adresses", le résultat est l'affichage d'une autre table qui liste les numéros de téléphone correspondant :

Vous pouvez voir que cette table contient des liens "Edit" et "Delete". Si je n'avais pas utilisé la méthode .live(), les gestionnaires d'évènements n'auraient pas été attachés à ces deux liens. Le gestionnaire d'évènement est attaché aux cellules ayant la classe css "link". Il commence par récupérer la valeur de l'identifiant pour l'enregistrement. Dans le cas de la table des contacts, il s'agira du champ ContactPersonId (puisque c'est ce que contient la première cellule de la ligne). Pour les sous-listes, il s'agira de l'identifiant du numéro de téléphone ou de l'adresse email. Nous avons besoin de ces identifiants pour les faire passer aux actions du contrôleur chargées de gérer la modification, la suppression ou l'affichage des sous-listes. Nous verrons cela un peu plus loin.

Vous devez maintenant commencer à comprendre pourquoi j'ai ajouté des attributs "title" aux cellules de la table. Ils contiennent la route qu'il faut appeler après avoir construit l'url complète en lui ajoutant l'identifiant. Ensuite, j'effectue un contrôle pour voir si la route contient le mot "delete". Si ce n'est pas le cas, la requête est lancée et son résultat affiché dans la balise <div> masquée. S'il s'agit d'un lien pour supprimer un enregistrement, la boite de confirmation modale est affichée avant de supprimer l'enregistrement, ce qui donne ainsi la possibilité à l'utilisateur de changer d'avis.

Et pour finir, j'attache un gestionnaire d'évènement au clic sur le bouton "Add Contact". Mais nous verrons ça dans la partie suivante.

Vous voyez ? Je vous avais bien dit que c'était simple !

Ajouter un contact

Quand on ajoute des enregistrements dans une application ASP.NET, la pratique habituelle est de proposer à l'utilisateur un formulaire contenant une série de zones de saisies dans lesquelles il peut entrer des données. La plupart des zones de saisie pour un objet ContactPerson sont basiques : nom, prénom, date de naissance. Mais une d'entre elle n'est pas si évidente : le type. Cette valeur doit être obtenue à partir de l'énumération (ami, collègue, etc...) définie au niveau de la classe PersonType.cs dans le dossier Models\Enums. Nous devons donc faire en sorte de proposer à l'utilisateur un choix restreint de valeurs possibles. Pour cela, une DropDownList fera parfaitement l'affaire. Cependant, cette série de valeurs ne fait pas parti de l'objet ContactPerson existant. Il va donc falloir présenter une version personnalisée de cet objet ContactPerson à la vue pour pouvoir gérer cela. Et c'est là que les ViewModels personnalisés entrent en jeu.

J'ai vu qu'il y avait débat pour savoir où est-ce qu'il fallait placer les ViewModels dans l'application. Certains pensent qu'ils font parti du modèle. A mon avis, ils sont plutôt liés aux vues. Ils n'ont vraiment de sens que dans le cadre des applications MVC et ils ne sont pas vraiment réutilisables, et donc ils ne devraient pas se retrouver dans le modèle. C'est pourquoi j'ai choisi de créer un répertoire ViewModels à l'intérieur du répertoire Views. Puis j'ai créé le fichier ContactPersonViewModel.cs en saisissant le code source suivant :

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

namespace ContactManagerMVC.Views.ViewModels
{
  public class ContactPersonViewModel
  {
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public IEnumerable<SelectListItem> Type { get; set; }
  }
}

Si vous regardez la dernière propriété de cette classe, vous pouvez constater que j'ai défini Type comme étant une collection de IEnumerable<SelectListItem>. C'est ce qui sera rattaché à la liste déroulante au niveau de la vue.

Au niveau du contrôleur, il y a besoin de deux actions pour gérer l'ajout. La première action est décoré avec l'attribut [AcceptVerbs(HttpVerbs.Get)] et la seconde avec l'attribut [AcceptVerbs(HttpVerbs.Post)]. Le but de la première action est de renvoyer le formulaire destiné à saisir le contact alors que la seconde doit gérer les valeurs qui sont envoyées lorsque le formulaire est posté :

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Add()
{
  var personTypes = Enum.GetValues(typeof (PersonType))
    .Cast<PersonType>()
    .Select(p => new
                   {
                     ID = p, Name = p.ToString()
                   });
  var model = new ContactPersonViewModel
                {
                  Type = new SelectList(personTypes, "ID", "Name")
                };
  return PartialView(model);
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Add(ContactPerson person)
{
  ContactPersonManager.Save(person);
  return RedirectToAction("List");
}

Les premières lignes dans la première action sont chargées de transférer les valeurs de l'énumération ContactType dans un tableau, chaque élément étant ensuite casté en objet anonyme avec une propriété ID et une propriété Name. ID est la valeur d'énumération, et Name la valeur constante qui va de pair avec l'énumération. Un objet ContactPersonViewModel est ensuite instancié et sa propriété Type est initialisé avec un objet SelectList auquel on transmet un IEnumerable des objets anonymes obtenus précédemment et on indique quels champs utiliser pour la valeur et pour le libellé.

Pour ajouter un contact, j'ai créé une vue partielle fortement typée et choisi le type ContactPersonViewModel. Le code pour la vue partielle est le suivant :

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ContactPersonViewModel>" %>

<script type="text/javascript">
  $(function() {
  $('#DateOfBirth').datepicker({ dateFormat: 'yy/mm/dd' });
  });
</script>

<% using (Html.BeginForm("Add", "Contact", FormMethod.Post)) {%>
      <table>
        <tr>
          <td class="LabelCell">Name</td>
          <td><%= Html.TextBox("FirstName") %></td>
        </tr>
        <tr>
          <td class="LabelCell">Middle Name</td>
          <td><%= Html.TextBox("MiddleName") %></td>
        </tr>
        <tr>v
          <td class="LabelCell">Last Name</td>
          <td><%= Html.TextBox("LastName") %></td>
        </tr>
        <tr>
          <td class="LabelCell">Date of Birth</td>
          <td><%= Html.TextBox("DateOfBirth", String.Empty)%></td>
        </tr>
        <tr>
          <td class="LabelCell">Type</td>
          <td><%=Html.DropDownList("Type")%>
          </td>
        </tr>
        <tr>
          <td class="LabelCell"></td>
          <td><input type="submit" name="submit" id="submit" value="Save" /></td>
        </tr>
      </table>
<% } %>

Le jQuery au début de cette vue sert à associer un datepicker jQuery.UI à la zone de saisie DateOfBirth. Au niveau du html helper qui défini la zone de saisie DateOfBirth, le second paramètre nous assure que par défaut, sa valeur sera vide. Et pour le reste, toutes les zone de saisie prennent le même nom que la propriété correspondante de ContactPerson que nous voulons saisir. Cela nous permet d'être sûr qu'au moment du post, la correspondance entre les zones de saisie et le modèle fonctionnera de façon correcte. L'enum pour le ContactType est lui aussi automatiquement lié pour nous par MVC :

La méthode action qui répond à la requête POST est alors capable de faire correspondre les valeurs du formulaire aux propriété d'un objet ContactPerson avant d'appeler la méthode Save() de la BLL en trois fois rien de code avant de renvoyer l'utilisateur vers l'action List.

Modifier un contact

Cette fois encore, il existe deux actions dans le contrôleur pour gérer les modifications : une pour la requête GET initiale et une autre pour la requête POST lorsque le formulaire est validé :

[AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Edit(int id)
    {
      var personTypes = Enum.GetValues(typeof (PersonType))
        .Cast<PersonType>()
        .Select(p => new { ID = p, Name = p.ToString() });

      var contactPerson = ContactPersonManager.GetItem(id);
      var model = new ContactPersonViewModel
                    { 
                      Id = id,
                      FirstName = contactPerson.FirstName,
                      MiddleName = contactPerson.MiddleName,
                      LastName = contactPerson.LastName,
                      DateOfBirth = contactPerson.DateOfBirth,
                      Type = new SelectList(personTypes, "ID", "Name", contactPerson.Type)
                    };
      return PartialView(model);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(ContactPerson person)
    {
      ContactPersonManager.Save(person);
      return RedirectToAction("List");
    }

Nous avons déjà vu comment jQuery est utilisé pour appeler l'action Edit en lui faisant passer l'ID du contact à modifier. Cet identifiant est utilisé pour retrouver les informations sur ce contact dans la base de données en passant comme d'habitude par la BLL qui fait ensuite appel à la DAL. L'objet ContactPersonViewModel est construit à partir des informations du contact et complété par une SelectList comme pour l'action Add. Mais cette fois, le constructeur de SelectList utilise un quatrième paramètre pour définir la valeur actuelle de la propriété Type pour le contact à modifier. Ce dernier paramètre correspond à la valeur qui sera pré-sélectionné dans la liste déroulante.

Le code de la vue partielle Edit est lui aussi quasiment identique à celui de la vue Add :

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ContactPersonViewModel>" %>

<script type="text/javascript">
  $(function() {
    $('#DateOfBirth').datepicker({dateFormat: 'yy/mm/dd'});
  });
</script>

<% using (Html.BeginForm("Edit", "Contact", FormMethod.Post)) {%> 
     <table>
        <tr>
          <td class="LabelCell">Name</td>
          <td><%= Html.TextBox("FirstName") %></td>
        </tr>
        <tr>
          <td class="LabelCell">Middle Name</td>
          <td><%= Html.TextBox("MiddleName") %></td>
        </tr>
        <tr>
          <td class="LabelCell">Last Name</td>
          <td><%= Html.TextBox("LastName") %></td>
        </tr>
        <tr>
          <td class="LabelCell">Date of Birth</td>
          <td><%= Html.TextBox("DateOfBirth", Model.DateOfBirth.ToString("yyyy/MM/dd")) %></td>
        </tr>
        <tr>
          <td class="LabelCell">Type</td>
          <td><%= Html.DropDownList("Type")%></td>
        </tr>
        <tr>
          <td class="LabelCell"><%= Html.Hidden("Id") %></td>
          <td><input type="submit" name="submit" id="submit" value="Save" /></td>
        </tr>
      </table>
<% } %>

Les principales différences viennent du fait que la zone DateOfBirth contient une chaîne pour formater la date et l'afficher de façon plus conviviale. Et juste avant le bouton de validation du formulaire, il y a maintenant un helper Html.Hidden() qui sert pour conserver l'identifiant du contact en cours de modification. Et bien sûr, le post du formulaire pointe vers une action différente du contrôleur. Il y aurait sans doute des avantages à combiner les formulaires (et les actions) Add et Edit et à se baser sur un drapeau pour savoir dans quel mode on se trouve (Add ou Edit) afin de pouvoir présenter tel ou tel code html au niveau de la vue et effectuer tel ou tel traitement au niveau de l'action. Cela permettrait d'éviter pas mal de répétitions. Mais j'ai préféré les garder séparés pour que l'application de démonstration reste le plus claire possible.

Supprimer un contact

L'action de suppression est assez simple et elle n'a pas besoin d'avoir une vue correspondante. Une fois la suppression réalisée, elle se contente de rediriger l'utilisateur sur l'action List :

public ActionResult Delete(int id)
{
  ContactPersonManager.Delete(id);
  return RedirectToAction("List");
}

J'ai fait quelques modifications par rapport au code d'origine des couches BLL et DAL. Au départ, la méthode ContactPersonManager.Delete() utilisait une instance du contact à supprimer. Et au niveau de la méthode Delete() dans la DAL, il n'y avait que l'identifiant de ce contact qui était utilisé (et donc nécessaire). Comme je ne vois pas l'intérêt de passer un objet complet quand on n'a besoin que de son identifiant, j'ai modifié ces deux méthodes pour qu'elles fonctionnent avec un entier. L'autre avantage, c'est que cela simplifie le code puisqu'avant il fallait instancier un objet ContactPerson juste pour pouvoir le supprimer.

Quand on clique sur un lien "Delete", le code jQuery déclenche l'affichage de la boite de dialogue pour confirmer la suppression.

Si l'utilisateur clique sur le bouton "Cancel", rien ne se passe (à part la fermeture de la boite de dialogue). Par contre, s'il clique sur le bouton "Confirm", l'url qui a été construite par le jQuery est appelée afin de pointer sur l'action Delete du contrôleur.

Gérer les collections

Toutes les collections (PhoneNumberList, EmailAddressList et AddressList) sont gérées exactement de la même manière. Par conséquence, je n'ai qu'à en choisir une (EmailAddressList en l'occurrence) pour illustrer leur fonctionnement. Vous pourrez aussi consulter les sources de l'application pour voir exactement comment les autres fonctionnent.

Pour commencer, nous allons voir comment afficher les adresses email associées au contact sélectionné. Pour cela, il y a besoin d'avoir une action List au niveau du contrôleur :

public ActionResult List(int id)
{
  var model = new EmailAddressListViewModel
                {
                  EmailAddresses = EmailAddressManager.GetList(id),
                  ContactPersonId = id
                };
  return PartialView(model);
}

Cette méthode action prend l'identifiant du contact (souvenez-vous, il est obtenu à partir de la première cellule dans la ligne qui a été cliquée) puis renvoie un autre ViewModel personnalisé (EmailAddressListViewModel). C'est grâce à ça que l'identifiant du contact peut être transmis à la vue :

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<EmailAddressListViewModel>" %>
<script type="text/javascript">
  $(function() {
    $('#add').click(function() {
      $.get('Email/Add/<%= Model.ContactPersonId %>', function(data) {
        $('#details').html(data);
      });
    });
  });
</script>
<table class="table">
   <tr>
     <th scope="col">Contact Person Id</th>
     <th scope="col">Email</th>
     <th scope="col">Type</th>
     <th scope="col">&nbsp;</th>
     <th scope="col">&nbsp;</th>
   </tr>
   <%if(Model.EmailAddresses != null)
     {foreach (var email in Model.EmailAddresses) {%>
   <tr>
     <td><%= email.Id %></td>
     <td><%= email.Email %></td>
     <td><%= email.Type %></td>
     <td title="email/edit" class="link">Edit</td>
     <td title="email/delete" class="link">Delete</td>
   </tr>
        <%}
    }else
 {%>
   <tr>
     <td colspan="9">No email addresses for this contact</td>
   </tr>
 <%}%>
</table>
<input type="button" name="add" value="Add Email" id="add" />

Vous pouvez voir que la propriété ContactPersonId est nécessaire pour utiliser la méthode d'action Add. Nous devons être certain que nous allons ajouter le nouvel élément à la collection en le reliant au bon contact. En ce qui concerne les méthodes d'action Edit et Delete, elles fonctionnent exactement de la même façon que pour l'objet ContactPerson : l'identifiant de l'élément qui doit être mis à jour ou supprimé est passé via l'URL et les cellules de la table sont complétées par un attribut "title" ce qui leur permet d'être prises en compte par la méthode jQuery .live() qu'on avait mis en place au niveau de la vue List pour les contacts.

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Add(int id)
{
  var contactTypes = Enum.GetValues(typeof(ContactType))
    .Cast<ContactType>()
    .Select(c => new
    {
      Id = c,
      Name = c.ToString()
    });
  var model = new EmailAddressViewModel
                {
                  ContactPersonId = id,
                  Type = new SelectList(contactTypes, "ID", "Name")
                };
  return PartialView("Add", model);
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Add(EmailAddress emailAddress)
{
  emailAddress.Id = -1;
  EmailAddressManager.Save(emailAddress);
  return RedirectToAction("List", new {id = emailAddress.ContactPersonId});
}

Une ViewModel personnalisée a été créé exprès pour afficher les objets EmailAddress existants dans le but de les modifier ou d'en ajouter. Celle-ci possède le même type de propriété pour associer la collection IEnumerable<SelectListItem> à la liste déroulante des types de contacts. Ces méthodes se distinguent de leur équivalent dans ContactController par ce qu'elles renvoient. La première renvoie le formulaire Add sous forme de vue partielle alors que la seconde redirige vers l'action List du contrôleur, ce qui a pour effet de rafraichir l'affichage de la collection une fois qu'elle a été mise à jour (et c'est aussi la raison pour laquelle nous avions spécifié l'option "cache" à faux dans le script jQuery).

En ce qui concerne l'enregistrement d'un élément de la collection, nous devons définir sa propriété Id à -1 avant d'effectuer le Save. Ceci est nécessaire pour que la procédure stockée "Upsert" soit correctement capable de déterminer s'il faut ajouter ou modifier l'élément. En effet, la propriété Id de l'élément est initialisée par défaut par le système de routage ce qui fait que dans la pratique elle va contenir la valeur de ContactPerson.Id. Par conséquent, si nous ne l'initialisions pas explicitement à -1, la procédure "Upsert" ne chercherait pas à créer un nouvel enregistrement pour l'objet EmailAddress, mais plutôt à modifier l'élément EmailAddress dont l'identifiant serait en fait l'identifiant du contact en cours. Ceci est dû au fait qu'Imar a choisi d'utiliser une approche de type "Upsert" pour modifier (UPdate) et ajouter (inSERT) des données avec une seule procédure.

Quoiqu'il en soit, voici le code de la vue partielle destinée à ajouter une adresse email :

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<EmailAddressViewModel>" %>

<script type="text/javascript">
  $(function() {
    $('#save').click(function() {
      $.ajax({
        type: "POST",
        url: $("#AddEmail").attr('action'),
        data: $("#AddEmail").serialize(),
        dataType: "text/plain",
        success: function(response) {
          $("#details").html(response);
        }
      });
    });
  });
</script>

<% using(Html.BeginForm("Add", "Email", FormMethod.Post, new { id = "AddEmail" })) {%>
<table class="table">
<tr>
  <td>Email:</td>
  <td><%= Html.TextBox("Email")%></td>
</tr>
<tr>
  <td>Type:</td>
  <td><%= Html.DropDownList("Type") %></td>
</tr>
<tr>
  <td><%= Html.Hidden("ContactPersonId") %></td>
  <td><input type="button" name="save" id="save" value="Save" /></td>
</tr>
</table>
<% } %>

Dans ce cas particulier, le code jQuery est chargé d'envoyer le formulaire par Ajax. Si vous faites attention, vous pouvez constater que ce code est rattaché à un bouton html et pas à un input type="submit". Il va d'abord sérialiser le contenu des champs du formulaire puis faire une requête pour atteindre l'action Add() décoré avec l'attribut [AcceptVerbs(HttpVerbs.Post)].

Modifier et supprimer des adresses email

Pour la modification des objets EmailAddress nous avons besoin du même genre d'actions et de vues que celles auxquelles nous avons eu à faire jusqu'à présent. Il faut ajouter deux actions au niveau du contrôleur, une pour le GET et l'autre pour le POST :

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Edit(int id)
{
  var emailAddress = EmailAddressManager.GetItem(id);
  var contactTypes = Enum.GetValues(typeof(ContactType))
    .Cast<ContactType>()
    .Select(c => new
    {
      Id = c,
      Name = c.ToString()
    });
  var model = new EmailAddressViewModel
  {
    Type = new SelectList(contactTypes, "ID", "Name", emailAddress.Type),
    Email = emailAddress.Email,
    ContactPersonId = emailAddress.ContactPersonId,
    Id = emailAddress.Id
  };
  return View(model);
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(EmailAddress emailAddress)
{
  EmailAddressManager.Save(emailAddress);
  return RedirectToAction("List", "Email", new { id = emailAddress.ContactPersonId });
}

Puis la vue partielle pour la modification qui doit commencer à devenir familière :

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<EmailAddressViewModel>" %>

<script type="text/javascript">
  $(function() {
    $('#save').click(function() {
      $.ajax({
        type: "POST",
        url: $("#EditEmail").attr('action'),
        data: $("#EditEmail").serialize(),
        dataType: "text/plain",
        success: function(response) {
          $("#details").html(response);
        }
      });
    });
  });
</script>

<% using(Html.BeginForm("Edit", "Email", FormMethod.Post, new { id = "EditEmail" })) {%>
<table class="table">
<tr>
  <td>Email:</td>
  <td><%= Html.TextBox("Email")%></td>
</tr>
<tr>
  <td>Type:</td>
  <td><%= Html.DropDownList("Type") %></td>
</tr>
<tr>
  <td><%= Html.Hidden("ContactPersonId") %><%= Html.Hidden("Id") %></td>
  <td><input type="button" name="save" id="save" value="Save" /></td>
</tr>
</table>
<% } %>

Une fois encore, c'est presque le même chose que pour la vue Add, si ce n'est la présence d'un champ caché pour stocker la valeur EmailAddress.Id afin de pouvoir ensuite mettre à jour la bonne adresse email.

En ce qui concerne l'action Delete, il n'y a pas vraiment besoin d'explications supplémentaires :

public ActionResult Delete(int id)
{
  EmailAddressManager.Delete(id);
  return RedirectToAction("List", "Contact");
}

Conclusion

Le but de ce petit exercice était de démontrer qu'il est parfaitement possible de réaliser des applications MVC sans recourir à LINQ to SQL ou Entity Framework. Je suis parti d'une application de type Web Forms déjà développée en ASP.NET 2.0. Le fait qu'elle soit dès le départ très bien structurée en couches m'a permis de réutiliser ses couches Business Objects, Business Logic et Data access avec peu ou pas de modification. La couche DAL d'accès aux données DAL utilise toujours ADO.NET et fait appel à des procédures stockées de base de données SQL Server.

Tout au long de cet exercice, j'ai présenté comment utiliser des vues fortement typées basées sur des ViewModels personnalisés et comment une petite dose de jQuery peut grandement améliorer l'interface utilisateur. Bien entendu, cette application n'est en aucun cas l'application idéale et elle est loin d'être prête pour une utilisation réelle. Il resterait beaucoup de choses à faire pour l'améliorer, comme par exemple refactoriser les vues et les actions liées aux opérations d'ajout et de modification. D'autre part, elle est totalement dépourvue de toute forme de validation. Les suppressions renvoient toujours à la page d'accueil, alors qu'il serait plus pratique pour l'utilisateur qu'après avoir supprimé un téléphone, un email ou une adresse on ré-affiche directement la sous-liste correspondante mise à jour. Cela impliquerait de faire passer l'identifiant ContactPersonId à l'action Delete() et devrait être relativement facile à réaliser.

Il est possible de télécharger le code source de cette application à la fin du billet "ASP.NET MVC is not all about Linq to SQL" de Mike Brind.

mercredi 24 février 2010

Convertir SQL en LINQ : DISTINCT, WHERE et ORDER BY

Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.

Ce troisième billet de la série continue la description des clauses spécifiques et il va porter sur la façon de transformer les clauses DISTINCT, WHERE et ORDER BY en clauses LINQ.

La clause DISTINCT

Les commandes SQL SELECT peuvent contenir un mot-clé DISTINCT qui sert à supprimer tous les doublons dans les résultats renvoyés. Avec le langage LINQ, il n'existe pas de mot-clé "distinct" qui pourrait compléter la clause select ou être inséré au niveau de la requête LINQ. Pour éviter d'avoir des doublons dans les résultats obtenus, il faut employer la méthode Distinct(). Cette méthode renvoie des éléments distincts à partir de la source sur laquelle on l'applique. Les deux exemples ci-dessous ont le même effet :

SQL :

SELECT DISTINCT Region
FROM   ClientTable

LINQ :

var ListeRegions = (from Contact in ClientListe
                   select Contact.Region).Distinct();

La clause WHERE

Comme pour une requête SQL, une expression LINQ permet elle aussi de filtrer les résultats renvoyés en ajoutant une clause "where". Cette clause peut contenir n’importe quelle expression C# booléenne.

SQL :

SELECT *
FROM   ClientTable
WHERE  Region = 'PACA'

LINQ :

from Contact in ClientListe
where Contact.Region == "PACA"
select Contact

Utiliser un opérateur

Avec SQL, les clauses WHERE contiennent très souvent d’autres opérateurs comme AND par exemple. Il existe généralement un opérateur équivalent en C# qui peut être employé au niveau de la clause where de la requête LINQ pour obtenir le même genre de résultat.

SQL :

SELECT *
FROM   ClientTable
WHERE  Region = 'PACA'
AND    CodePostal = '06570'

LINQ :

from Contact in ClientListe
where Contact.Region == "PACA"
&& Contact.CodePostal == "06570"
select Contact

Même lorsqu’il n’existe pas vraiment d’équivalent en C#, il est généralement possible de reproduire n’importe quelle expression SQL sous forme d’expression C#. Par exemple, il n’existe pas de mot-clé en C# qui corresponde au mot-clé "BETWEEN" du SQL. Mais on peut facilement obtenir un résultat similaire en C#.

SQL :

SELECT *
FROM   CommandeTable
WHERE  DateCommande BETWEEN '2010-01-01' AND '2010-12-31'

LINQ :

from Colis in CommandeListe
where Colis.DateCommande >= "2010-01-01"
&& Colis.DateCommande <= "2010-12-31"
select Colis

Dans le cas de l’opérateur "IN" du SQL, il est possible d’utiliser la méthode Contains() pour arriver au même résultat :

SQL :

SELECT *
FROM   ClientTable
WHERE  Region IN ('IDF', 'PACA')

LINQ :

string[] regions = { "RA", "PACA" };
var ContactsSud = from Contact in ClientListe
                  where regions.Contains(Contact.Region)
                  select Contact

La clause ORDER BY

La clause ORDER BY du SQL peut elle aussi être représentée par une expression LINQ. La cause "orderby" du langage LINQ sert à indiquer comment les résultats doivent être classés en définissant une liste d’expressions séparées par des virgules. Il est possible d’employer n’importe quelle expression C#, sans qu’il soit nécessaire que cette expression fasse parti des éléments sélectionnés dans la requête LINQ.

SQL :

SELECT *
FROM   ClientTable
ORDER BY Telephone

LINQ :

from Contact in ClientListe
orderby Contact.Telephone
select Contact

Définir l’ordre du tri

La clause SQL ORDER BY peut aussi contenir les mots-clés ASC ou DESC pour préciser sir le tri doit de faire de façon croissante ou décroissante. Avec LINQ, il faut utiliser les mots-clés "ascending" ou "descending" pour obtenir le même résultat. Quand aucun de ces mots-clés n’est indiqué, le tri est réalisé de façon croissante par défaut.

SQL :

SELECT * FROM ClientTable
ORDER BY Telephone ASC, Nom DESC

LINQ :

from Contact in ClientListe
orderby Contact.Telephone ascending, Contact.Nom descending
select Contact

Conclusion

Avec ce nouveau billet et les deux billets précédents, il vous est possible de convertir des requêtes SQL basiques en requêtes LINQ. Dans le prochain billet, j’aborderai la façon de traduire les fonctions scalaires ou les fonctions d’agrégations du SQL en LINQ

mardi 23 février 2010

Convertir SQL en LINQ : FROM et SELECT

Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.

Après un premier billet pour comparer SQL et LINQ, ce deuxième billet de la série va présenter comment gérer des clauses plus spécifiques du SQL, en commençant par deux des clauses les plus importantes : FROM et SELECT.

La clause FROM

Une commande SQL SELECT débute toujours par une clause SELECT suivie d’une clause FROM. Dans le cas de LINQ, une expression commence toujours par une clause « from » (ou « aggregate » comme nous le verrons par la suite). La clause SQL FROM de base indique la table sur laquelle va porter la requête, et de façon similaire, la clause LINQ from indique l’objet sur lequel va porter la requête (ClientListe dans notre exemple).

Cet objet peut représenter des données en mémoire, des données d’une table SQL ou des données XML. Dans les exemples à venir, j’utilise des données « en mémoire » pour que le code soit suffisamment simple. En plus de cet objet sur lequel porte la requête, la clause LINQ from contient toujours un identifiant pour la « ligne » courante (Contact dans notre exemple) qui sert d’alias pour représenter cette ligne.

Avec SQL, il suffit de faire « SELECT * » pour sélectionner toutes les colonnes de la table. En ce qui concerne LINQ, il suffit d’utiliser la clause « select alias » pour renvoyer tous les membres de l’objet requêté :

SQL :

SELECT *
FROM   ClientTable

LINQ :

from Contact in ClientListe
select Contact

Utiliser un alias dans le FROM

En SQL, il est possible de définir un alias sur une table au niveau de la partie FROM de la requête. Cela permet ensuite de faire référence aux colonnes de la table source par l’intermédiaire de cet alias. Comme nous l’avons vu dans le paragraphe précédent, l’identifiant qui est défini au niveau de la clause from en LINQ sert lui aussi essentiellement à cela :

SQL :

SELECT Contact.ClientID, Contact.Telephone
FROM   ClientTable Contact

LINQ :

from Contact in ClientListe
select new { Contact.ClientID, Contact.Telephone }

La clause SELECT

En SQL, les commandes SELECT contiennent une liste des valeurs à sélectionner parmi les informations disponibles (Nom, Telephone, Ville…). De la même façon, le langage LINQ vous permet lui aussi de sélectionner certains des membres pour renvoyer un objet anonyme constitué à partir de cette sélection.

Il n’est absolument pas nécessaire que les membres que vous définissez correspondent à l’objet que vous avez spécifié au niveau de la clause from. Il peut en effet s’agir de n’importe quelle expression C# valide (comme par exemple « 3 + 4 »). S’il n’est pas possible de déduire le nom d’un membre que vous avez sélectionné, vous devrez utiliser un alias (voir le paragraphe suivant).

SQL :

SELECT Nom, ClientID
FROM   ClientTable Contact

LINQ :

from Contact in ClientListe
select new { Contact.Nom, Contact.ClientID }

Utiliser un alias dans le SELECT

Toujours en SQL, il est possible de définir des alias pour les différents éléments que vous sélectionnez au niveau de la clause SELECT, ce qui permet ensuite d’utiliser ces alias pour faire référence à ces éléments. De la même façon, LINQ vous permet de donner un nom aux éléments que vous sélectionnez, ce qui vous permettra par la suite d’utiliser ce nom faire référence à ces éléments lorsque vous exploiterez le résultat de la requête LINQ.

SQL :

SELECT Nom NomContact, ClientID ContactID
FROM   ClientTable Contact

LINQ :

from Contact in ClientListe
select new { NomContact = Contact.Nom, ContactID = Contact.ClientID }

Dans le prochain billet, je présenterai les équivalents des clauses DISTINCT, WHERE et ORDER BY pour le langage LINQ.

lundi 22 février 2010

Convertir SQL en LINQ : Introduction

Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.

Comme vous le savez sans doute déjà, les commandes LINQ permettent d’écrire des requêtes en C# en utilisant une syntaxe inspirée de SQL. Cependant, la syntaxe de LINQ ne correspond pas exactement à la syntaxe du SQL. Par conséquent, si vous avez déjà travaillé en SQL ou que vous êtes habitués à écrire des requêtes SQL, vous allez sans doute chercher à transformer vos requêtes SQL en requêtes LINQ.

Ce billet va être le premier d’une série consacrée à la façon de traduire du SQL en LINQ. Dans cette introduction, je souhaite présenter ce qui différencie LINQ de SQL puis j’aborderai les particularités de chaque syntaxe dans la suite de la série.

Pré-requis

Les exemples de code SQL que je vais utiliser seront basés sur une table ClientTable et une table CommandeTable. Pour les exemples C#, je m’appuierai sur des objets ClientListe et les objets CommandeListe, tous deux de type IEnumerable. Et j’utiliserai deux classes, Client et Commande qui sont définies de la façon suivante :

class Client
{
    public int ClientID;
    public string Nom;
    public string Telephone;
    public string Adresse;
    public string Ville;
    public string CodePostal;
    public string Region;
}

class Commande
{
    public int CommandeID;
    public int ClientID;
    public Single Cout;
    public string Telephone;
    public DateTime DateCommande;
    public string Livraison;
    public string NomArticle;
}

Syntaxe de base

LINQ gère l’équivalent des commandes SQL SELECT, mais pas les autres types de commandes SQL telles que CREATE, INSERT, UPDATE ou DELETE. On peut considérer que la syntaxe de base d’une requête SQL SELECT est constituée d’une série de « clauses » débutant par une clause SELECT :

sqlSelectClause [ sqlClause1 [ sqlClause2 [ ... ] ] ]

Il peut exister un certain nombre de différences en fonction des nombreuses versions de SQL, mais on a toujours plus ou moins la même syntaxe générale :

SELECT Nom NomContact, ClientID ContactID
FROM   ClientTable
ORDER BY Nom

La syntaxe de base pour une expression LINQ consiste également en une série de « clauses » qui débute par la clause « from » (ou éventuellement une clause « aggregate » comme nous le verrons plus tard) :

linqFromClause [ linqClause1 [ linqClause2 [ ... ] ] ]

Ce qui donne par exemple :

from Contact in ClientListe
orderby Contact.Nom
select new { NomContact = Contact.Nom, ContactID = Contact.ClientID }

J’ai employé le terme « expression LINQ » ci-dessus parce qu’à proprement parler les requêtes LINQ ne sont pas des commandes complètes. Alors qu’une requête SQL se suffit à elle-même, si on se place d’un point de vue syntaxique, une requête LINQ n’est que l’équivalent d’une expression telle que « 3 * 4 ». Cela ne constitue pas une commande complète et il faut donc « faire » quelque chose avec. Une requête LINQ peut apparaitre dans du code C# tel que celui-ci :

var ContactsTries = from Contact in ClientListe
                    orderby Contact.Nom
                    select new { NomContact = Contact.Nom, ContactID = Contact.ClientID };

Chaque clause d’une requête LINQ porte sur un objet de type IEnumerable<T> et renvoie un nouvel objet de type IEnumerable<T> (sans que le type T retourné soit nécessairement du même type que le T initial). En général, les clauses de la requête sont analogues aux clauses SQL (SELECT ou ORDER BY par exemple) et vous pouvez habituellement traduire votre requête SQL en LINQ en faisant du « clause à clause ». Dans les exemples ci-dessus, même si les clauses apparaissent dans un ordre légèrement différent, vous pouvez malgré tout voir qu’elles sont assez semblables.

Conclusion

Après cette introduction assez généraliste, j’aborderai des aspects plus spécifiques au cours des billets suivants. Mon objectif tout au long de ces petits tutoriels consacrés à la traduction du SQL en LINQ est de présenter les points suivants :

lundi 15 février 2010

NerdDinner (en français) : Fin !

Ca y est ! Ce week-end j'ai complètement terminé la traduction du tutoriel NerdDinner en français. Et finalement, j'ai réussi à tout finir beaucoup plus vite que prévu et surtout j'ai même pris de l'avance par rapport à la réalisation du tutoriel.

Vu que la partie consacrée à l'ajout de la carte ne m'emballait pas des masses, j'ai préféré m'atteler à sa traduction. Ca m'a remotivé et j'ai alors pu finir la réalisation de cette partie en me basant directement sur ma traduction plutôt que sur le tutoriel en VO.

Pour la dernière partie qui concerne les tests unitaires, j'ai donc décidé de fonctionner de la même façon et même d'aller encore plus loin en choisissant de traduire entièrement cette partie avant de passer à sa réalisation. Donc maintenant, il ne me reste plus qu'à suivre mon tutoriel en français pour terminer le projet NerdDinner et disposer d'une application totalement finie.

Pour conclure, je suis vraiment très content d'avoir réussi le défi que je m'étais fixé et d'être arrivé à traduire les presque 200 pages du tutoriel (184 en fait) sans jamais me décourager.

Même si le fait d'avoir tout traduit et aussi d'avoir réellement réécrit moi-même la totalité du code (à part les expressions régulières et les procédures SQL Server) m'a demandé beaucoup de temps, je suis convaincu que ça m'a permis de bien mieux comprendre comment utiliser ASP.NET MVC, en tout cas beaucoup mieux que si je m'étais contenté de lire vite fait le tutoriel et de faire un peu de copier / coller pour le réaliser.

L'autre avantage c'est que j'ai fait pas mal de progrès en matière de traduction et que vers la fin j'ai atteint un rythme tout à fait honorable. Au début, j'ai beaucoup utilisé Google Traduction pour dégrossir le plus gros du boulot et disposer d'un point de départ rassurant. Mais dans les derniers temps, je comptais beaucoup plus sur l'excellent site WordReference pour m'aider à comprendre certaines expressions ou à trouver la traduction la plus adaptée.

- page 2 de 4 -