blog.pagesd.info

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

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.

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 :

  • FROM et SELECT
  • DISTINCT, WHERE et ORDER BY
  • Les fonctions (scalaires et agrégations)
  • GROUP BY et HAVING
  • Les jointures
  • UNION, TOP et sous-requêtes

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.

vendredi 29 janvier 2010

Utiliser ASP.NET MVC et NHibernate (3° partie)

Ceci est la traduction du billet "Using ASP.NET MVC and NHibernate (Part 3)" de César Intriago.

Voici le troisième et dernier billet d'une série d'articles consacré à l'utilisation de NHibernate pour développer des applications ASP.NET MVC. Les liens vers les deux premières parties de ce tutoriel sont disponibles ci-dessous :

Dans cet article, nous allons créer notre application ASP.NET MVC pour gérer les Posts et les Categories. Notre projet aura la structure suivante :

Le contrôleur CategoriesController va nous permettre de retrouver, créer, modifier et supprimer des catégories de notre repository.

Le contrôleur PostController va nous permettre de faire la même chose pour les Posts. En ce qui concerne les vues pour les Posts, je vais utiliser une ViewModel afin de représenter la combinaison des Posts et des Categories.

Dans notre projet ASP.NET MVC nous allons ajouter des références vers nos projets Core et Infrastructure afin de pouvoir utiliser les composants NHibernate pour la persistance que nous avons développé dans les deux premières parties.

Nous utiliserons des vues fortement typées basées sur notre modèle et ViewModel afin que Visual Studio puisse scaffolder les vues en s'appuyant sur ces types d'objet.

J'ai apporté quelques modification au code par rapport à la deuxième partie car j'ai détecté quelques bugs grâce aux test unitaires. Par conséquent, il est nécessaire de télécharger la version complète du projet.

ViewModel

Les vues pour les Posts ont besoin des objets Post et Category pour fonctionner correctement. Pour cela, un pattern très répandu est de passer par un modèle de vue qui sert de conteneur pour ces deux objets afin de faciliter leur utilisation au niveau de la vue.

Par conséquent, dans le cas des vues destinées aux Posts, je n'ai pas utilisé directement les objets du modèle (c'est à dire les classes Post et Category). A la place, j'ai créé une classe PostViewModel qui contient ces deux objets du modèle. De cette façon, je peux simplifier la manipulation des Posts et des Categories auxquelles ils sont rattachés comme s'il s'agissait d'une seule entité.

Conclusion

Configurer NHibernate pour la première fois peut demander un petit effort (ou un effort certain), mais il vous fera gagner énormément de temps pour réaliser la couche données de votre application. Et combiné à un framework tel que ASP.NET MVC, vous parvenez à une franche séparation des préoccupations (separation of concerns en anglais) qui vous aidera à conserver un projet bien organisé, facile à comprendre et à tester. Je suis persuadé qu'en suivant cette méthode vous obtiendrez un projet avec un code et une structure tirés à quatre épingle, d'autant plus que les modèles de Visual Studio pour ASP.NET MVC rendent ce travail encore plus facile étant donné que vous pouvez automatiser la création des vues.

C'est extraordinaire tellement c'est facile et rapide de créer une application avec ASP.NET MVC. J'attends avec impatience la sortie de la version 2.0 (en fait je teste d'ores et déjà sa Preview). C'est vraiment le moment idéal pour apprendre ASP.NET MVC et NHibernate.

Ceci est la traduction du billet "Using ASP.NET MVC and NHibernate (Part 3)" de César Intriago.

Utiliser ASP.NET MVC et NHibernate (2° partie)

Ceci est la traduction du billet "Using ASP.NET MVC and NHibernate (Part 2)" de César Intriago.

Et voici la deuxième partie d'une série d'article consacré à l'utilisation de NHibernate dans une application ASP.NET MVC. Vous pouvez lire la première partie de ce tutoriel ici.

Représenter une relation many-to-many

Nous allons continuer en créant un fichier de mapping pour la classe Post. Celui-ci est légèrement différent de celui pour la classe Category étant donné que nous devons représenter une relation plusieurs-à-plusieurs entre les tables Posts et Categories, ce que nous réaliserons en utilisant un élément BAG dans le fichier de mapping. Voici le code de notre fichier Post.hbm.xml :

IMPORTANT : Vous devez définir l'action de génération de tous les fichiers de mapping à "Ressource incorporée" afin que NHibernate puisse trouver le bon fichier dans l'assembly.

Post.hbm.xml

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

  <class name="Post" table="Posts" dynamic-update="true">
    <cache usage="read-write"/>
    <id name="Id" column="Id" type="Guid">
      <generator class="guid"/>
    </id>
    <property name="Title" length="100"/>
    <property name="Body"/>
    <property name="CreationDate" type="datetime"/>
    <property name="IsPublic" type="bool"/>

    <bag name="Categories" table="PostCategory" lazy="false" >
      <key column="idPost" ></key>
      <many-to-many class="Category" column="idCategory" ></many-to-many>
    </bag>

  </class>
</hibernate-mapping>

Voici quelques explications sur ce que nous avons fait au niveau de l'élément <bag> :

  • L'attribut name défini le nom de la propriété de la classe Post où nous stockerons la collection des catégories.
  • L'attribut table correspond au nom de la table dans la base de données qui relie les tables Posts et Categories.
  • L'attribut key.column est le nom de l'identifiant de la table Posts.
  • L'attribut class représente le nom de la classe Category dans le modèle d'objet.
  • L'attribut many-to-many.column est le nom de l'identifiant de la table Categories.

Pour vous aider à mieux comprendre ce fichier de mapping, voici tout d'abord à quoi ressemble le diagramme de classe :

puis le schéma de la base de données correspondante :

Comme vous pouvez le constater dans le diagramme de classe, nous n'avons pas besoin de créer une classe pour la table PostCategory.

Test unitaire

Nous allons maintenant tester nos repositorys et vérifier que nous pouvons créer des billets à l'aide des tests unitaires suivants :

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Core.Domain.Model;
using Core.Domain.Repositories;
using Core;

namespace NHibernate101.Tests
{
    [TestClass]
    public class RepositoriesTest
    {
        IRepository<Category> categoriesRepository;
        IRepository<Post> postsRepository;
        Post testPost;
        Category testCategory1;
        Category testCategory2;

        public RepositoriesTest()
        {
        }

        private TestContext testContextInstance;

        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        [TestInitialize()]
        public void CreateRepositories()
        {
            categoriesRepository = new CategoryRepository();
            postsRepository = new PostRepository();
        }

        [TestMethod]
        [DeploymentItem("hibernate.cfg.xml")]
        public void CanCreateCategory()
        {
            testCategory1 = new Category() { Name = "ASP.NET" };
            categoriesRepository.Save(testCategory1);
        }

        [TestMethod]
        [DeploymentItem("hibernate.cfg.xml")]
        public void CanCreatePost()
        {
            testPost = new Post();
            testPost.Title = "ASP.NET MVC and NHibernate";
            testPost.Body = "In this article I’m going to cover how to install and configure NHibernate and use it in a ASP.NET MVC application.";
            testPost.CreationDate = DateTime.Now;
            testPost.IsPublic = true;

            testCategory2 = new Category() { Name= "ASP.NET MVC"};

            categoriesRepository.Save(testCategory2);
            testPost.Categories.Add(testCategory2);

            postsRepository.Save(testPost);
        }
    }
}

Lancer le test et s'il réussi, nous devrions retrouver le nouveau billet et la nouvelle catégorie dans la base de données :

Il s'agit là des rudiments de NHibernate. Je vous encourage à vous documenter plus en détail sur le site de la communauté NHibernate. Dans la prochaine partie de ce tutoriel nous commencerons à développer notre application ASP.NET MVC pour gérer les billets et les catégories !

Ceci est la traduction du billet "Using ASP.NET MVC and NHibernate (Part 2)" de César Intriago.

Utiliser ASP.NET MVC et NHibernate (1° partie)

Ceci est la traduction du billet "Using ASP.NET MVC and NHibernate (Part 1)" de César Intriago.

Dans cet article je vais expliquer comment installer et configurer NHibernate pour l'utiliser dans une application ASP.NET MVC. Ceci est le premier article d'une série dans laquelle je montrerai comment installer NHibernate pour une application .Net.

Utiliser ASP.NET MVC et NHibernate

NHibernate c'est quoi ?

NHibernate est un outil de mapping Objet-Relationnel (object-relational mapping ou ORM en anglais) pour .Net qui permet de faire correspondre des modèles orientés objets avec une base de données. Comme vous le verrez dans cet article, NHibernate s'occupera de la plupart des tâches relatives à la couche de persistance. Vous pouvez avoir plus d'informations au sujet de NHibernate à partir du site consacré à la communauté NHibernate.

Vous pouvez télécharger la dernière version de NHibernate sur SourceForge. Les exemples de code dans cet article sont basés sur NHibernate 2.1.1 (la version la plus récente disponible au moment de la rédaction de cet article).

Installer NHibernate

Téléchargez et dézippez NHibernate sur votre ordinateur. Ca y est, NHibernate est installé.

Créer le projet ASP.NET MVC

Créez un nouveau projet ASP.NET MVC et n'oubliez pas de créer un projet de test.

Ajoutez deux nouveaux projets de type "Bibliothèque de classe" à votre solution : Infrastructure et Core (pour que tout soit structuré du mieux possible).

Dans le projet Core, ajoutez une référence à l'assemblie NHibernate.

La base de données

Pour notre exemple, nous allons créer un modèle contenant des Posts (des billets en français) et des Categories. C'est un modèle très simple qui nous aidera à bien comprendre comment NHibernate fonctionne : un billet (ou post) peut appartenir à une ou plusieurs catégories et une catégorie peut contenir zéro ou plus de zéro billets :

Nous allons utiliser SQL Server 2008 Express Edition pour créer notre base de données, une autre solution étant de créer notre modèle dans Visual Studio et de demander à NHibernate de créer la base de données pour nous. Mais dans notre cas, nous allons créer manuellement la base de données.

Créer le Modèle

L'étape suivante va consister à créer notre modèle qui est une représentation orientée objet de notre base de données. Pour cela nous allons utiliser le concepteur de classes de Visual Studio :

Les Repository

Un Repository nous permet de créer, sélectionner, modifier et supprimer nos objets et il est indépendant de la base de données. Pour ce tutoriel, nous avons besoin de créer deux repositorys : PostRepository et CategoryRepository. Tout deux vont implémenter la même interface présentée ci-dessous :

IRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Core
{
    public interface IRepository<T>
    {
        void Save(T entity);
        void Update(T entity);
        void Delete(Guid id);
        T GetById(Guid id);
        T GetAll();
    }
}

Pour créer nous deux repositorys, nous avons d'abord besoin d'une une classe helper pour créer une session NHibernate sur notre base de données :

NHibernateHelper.cs

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

namespace Core.Domain.Repositories
{

    public class NHibernateHelper
    {
        private static ISessionFactory _sessionFactory;

        private static ISessionFactory SessionFactory
        {
            get
            {
                if (_sessionFactory == null)
                {
                    var configuration = new Configuration();
                    configuration.Configure();
                    _sessionFactory = configuration.BuildSessionFactory();
                }
                return _sessionFactory;
            }
        }

        public static ISession OpenSession()
        {
            return SessionFactory.OpenSession();
        }
    }
}

Ensuite, nous pouvons créer les repositorys :

PostRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Core.Domain.Model;
using NHibernate;
using NHibernate.Criterion;
 
namespace Core.Domain.Repositories
{
    public class PostRepository: IRepository<Post>
    {
        #region IRepository<Post> Members
 
        void IRepository<Post>.Save(Post entity)
        {
            using (ISession session = NHibernateHelper.OpenSession())
            {
                using (ITransaction transaction = session.BeginTransaction())
                {
                    session.Save(entity);
                    transaction.Commit();
                }
            }
        }

        void IRepository<Post>.Update(Post entity)
        {
            using (ISession session = NHibernateHelper.OpenSession())
            {
                using (ITransaction transaction = session.BeginTransaction())
                {
                    session.Update(entity);
                    transaction.Commit();
                }
            }
        }

        void IRepository<Post>.Delete(Guid id)
        {
            using (ISession session = NHibernateHelper.OpenSession())
            {
                using (ITransaction transaction = session.BeginTransaction())
                {
                    session.Delete(id);
                    transaction.Commit();
                }
            }
        }

        Post IRepository<Post>.GetById(Guid id)
        {
            using (ISession session = NHibernateHelper.OpenSession())
                return session.CreateCriteria<Post>().Add(Restrictions.Eq("Id", id)).UniqueResult<Post>();
        }

        Post IRepository<Post>.GetAll()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

CategoryRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Core.Domain.Model;
using NHibernate;
using NHibernate.Criterion;

namespace Core.Domain.Repositories
{
    public class CategoryRepository: IRepository<Category>
    {
        #region IRepository<Category> Members

        void IRepository<Category>.Save(Category entity)
        {
            using (ISession session = NHibernateHelper.OpenSession())
            {
                using (ITransaction transaction = session.BeginTransaction())
                {
                    session.Save(entity);
                    transaction.Commit();
                }
            }
        }
 
        void IRepository<Category>.Update(Category entity)
        {
            using (ISession session = NHibernateHelper.OpenSession())
            {
                using (ITransaction transaction = session.BeginTransaction())
                {
                    session.Update(entity);
                    transaction.Commit();
                }
            }
        }

        void IRepository<Category>.Delete(Guid id)
        {
            using (ISession session = NHibernateHelper.OpenSession())
            {
                using (ITransaction transaction = session.BeginTransaction())
                {
                    session.Delete(id);
                    transaction.Commit();
                }
            }
        }

        Category IRepository<Category>.GetById(Guid id)
        {
            using (ISession session = NHibernateHelper.OpenSession())
                return session.CreateCriteria<Category>().Add(Restrictions.Eq("Id", id)).UniqueResult<Category>();
        }

        Category IRepository<Category>.GetAll()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

Comme vous pouvez le constater, c'est dans les repositorys que nous plaçons le code pour appeler les méthodes NHibernate, ce que nous faisons en créant un objet Session en premier.

Où est-ce que nous en sommes...

Avant de continuer, revoyons un peu ce que nous avons déjà fait :

  1. Nous avons créé un projet Core qui référence la librairie NHibernadte.dll
  2. Dans ce projet Core, nous avons créé deux classes qui représentent notre modèle : "Post.cs" et "Category.cs", la classe Post ayant une collection de catégories.
  3. Nous avons cré deux repositorys pour enregistrer, modifier, supprimer et retrouver les objets de notre modèle.

Les Mappings

Il est temps de faire quelque chose dans notre projet Infrastructure où nous mapperont notre modèle à notre base de données, ce qui se fait via des fichiers XML avec NHibernate. Nous allons suivre la convention de nommage qui existe au sujet des fichiers maps : [ClassName].hbm.xml.

Nous allons créer deux nouveaux fichiers : Category.hbm.xml et Post.hbm.xml. Le contenu de chacun de ces fichiers fait correspondre une classe à une table, une propriété à une colonne, et indique aussi le type de données. Pour cette première partie du tutoriel, nous avons besoin de créer le fichier Category.hbm.xml :

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

  <class name="Category" table="Categories" dynamic-update="true">
    <cache usage="read-write"/>
    <id name="Id" column="Id" type="Guid">
      <generator class="guid"/>
    </id>
    <property name="Name" length="100"/>
  </class>
</hibernate-mapping>

IMPORTANT : Vous devez définir l'action de génération de ces deux fichiers à "Ressource incorporée" afin que NHibernate puisse trouver le bon fichier dans l'assembly.

Configurer NHibernate

Nous sommes presque prêts. La prochaine étape va consister définir la chaine de connexion à la base de données et quelques paramètres pour NHibernate. Cette configuration doit être stockée dans un fichier "hibernate.cfg.xml" (définissez l'action de génération de ce fichier à "Ressource incorporée"). NHibernate dispose d'une fonctionnalité spéciale de "Lazy-Loading" pour dynamic proxy systems, et nous devons ajouter les références suivantes à notre projet Infrastructure pour qu'il puisse l'utiliser :

  • Castle.Core
  • Castle.DynamicProxy2
  • NHibernate.ByteCode.Castle.dll

Nous pouvons alors créer le fichier de configuration XML suivant :

hibernate.cfg.xml

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <session-factory>
    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
    <property name="connection.connection_string">server=.\SQLExpress;database=NHibernate101;Integrated Security=true;</property>
    <property name="show_sql">true</property>
    <property name="dialect">NHibernate.Dialect.MsSql2008Dialect</property>
    <property name="cache.use_query_cache">false</property>
    <property name="adonet.batch_size">100</property>
    <property name="proxyfactory.factory_class">NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle</property>
    <mapping assembly="Infrastructure" />
  </session-factory>
</hibernate-configuration>

La ligne <property name="proxyfactory.factory_class">... utilise la fonctionalité de "lazy loading" au sujet de laquelle vous trouverez plus d'informations sur le site d'Hibernate.

Test unitaire de NHibernate

Il est enfin temps de tester notre configuration de NHibernate ! Nous allons ajouter quelques données à notre table "Categories". Pour cela nous avons besoin que notre projet de test référence l'assembly NHibernate et d'y copier le fichier hibernate.cfg.xml.

Ajouter les références suivantes :

  • Castle.Core
  • Castle.DynamicProxy2
  • Infrastructure
  • NHibernate
  • NHibernate.ByteCode.Castle

Puis ajouter la méthode de test suivante :

[TestMethod]
        [DeploymentItem("hibernate.cfg.xml")]
        public void CanCreateCategory()
        {
            IRepository<Category> repo = new CategoryRepository();
            Category category = new Category();
            category.Name = "ASP.NET";

            repo.Save(category);

        }

Lancer le projet de test et notre méthode de test CanCreateCategory devrait réussir le test (avec un peu de chance) :

Nous pouvons alors vérifier dans la base de données que la nouvelle catégorie a bien été ajoutée :

Dans la prochaine partie de ce tutoriel, nous complèterons le modèle et les tests unitaires puis nous attaquerons la création de notre application ASP.NET MVC.

Ceci est la traduction du billet "Using ASP.NET MVC and NHibernate (Part 1)" de César Intriago.

jeudi 28 janvier 2010

Encore plus de traduction de NerdDinner

Ca va faire un bon moment que je n'ai pas publié de billet sur l'avancement de ma traduction du tutoriel NerdDinner en français, mais heureusement, ça ne veut absolument pas dire que je n'ai pas progressé depuis décembre !

En presque un mois, j'ai même ajouté 5 parties supplémentaires à la liste des traductions. A fin décembre, j'avais réussi à traduire près de la moitié du tutoriel ce qui n'était déjà pas si mal. Et maintenant, j'en suis arrivé à un peu plus des trois quarts du tutoriel traduit à ce jour :

J'ai d'autant plus été motivé pour y travailler ce mois-ci que je me suis rendu compte de façon presque fortuite que ça avait au moins été utile à quelques personnes.

Et comme je suis prévoyant et que j'ai d'ores et déjà traduit la dernière page de conclusion du tutoriel, il ne me reste plus que 2 parties à traiter :

Malgré tout, je vais sans doute un peu lever le pied sur ce travail de traduction parce que :

  • A titre personnel je suis en plein dans la partie sur le plan d'accès (et ça ne me passionne pas des masses) et j'aimerai quand même bien avoir fini le tutoriel pour la première semaine de février (et pas seulement à la va-vite en faisant du simple copier / coller)
  • Puis une fois ça terminé, je voudrai essayer de lire le reste du livre en entier et si possible avant la sortie de ASP.NET MVC 2 (quitte à lire un peu en diagonale pour les trucs qui me semblent les moins intéressants / indispensables)

Donc, pour ce qui est de la traduction du tutoriel NerdDinner, je me donne seulement comme objectif d'en venir à bout avant la sortie de ASP.NET MVC 2. Ce qui devrait donc me laisser presque un mois pour chaque partie (je peux le faire).

Pour en revenir à la sortie de ASP.NET MVC 2, j'essaie de m'y préparer du mieux possible. Et pour ça, je compte sur la série de billets sur ASP.NET MVC 2 de Scott Gu qui semble être pleine de trucs très intéressants à lire.

Et pour m'encourager à vraiment lire (et surtout réussir à comprendre) ces différents billets, j'essaie de les traduire au fur et à mesure de ma lecture. Mais je ne voudrais pas faire ça de façon aussi approfondie méticuleuse que pour le tutoriel NerdDinner. En fait, j'aimerai réussir à produire une traduction vite fait / bien fait (du "quick and dirty" quoi) sans trop me creuser la tête à trouver la bonne expression ou la tournure qui colle le mieux au texte d'origine (quitte à laisser quelques passages en VO s'il le faut).

Pour l'instant, j'ai déjà réussi à boucler la traduction de la première partie consacrée aux helpers HTML fortement typés en un peu moins d'une heure (mais elle était vraiment très très courte). Malgré tout, c'est un temps tout à fait convenable comparé aux presque quatre heures que j'ai passées sur le billet de Rob Conery pour inciter tout un chacun à apprendre ASP.NET MVC.

Pour la suite, je croise les doigts pour que j'arrive à tenir le rythme, mais la seconde partie m'a posé plus de soucis. Il faut dire qu'elle est beaucoup plus longue que la première (sans compter qu'elle semble susciter pas mal de réactions). Et puis comme je ne maitrise pas encore suffisamment ASP.NET MVC, c'est pas ça qui me simplifie la tâche pour assimiler les nouveautés en ASP.NET MVC 2.

Et vu le nombre d'articles que Scott prévoit d'écrire (et la quantité de travail qu'il abat), on peut pas savoir ce que ca va donner...

jeudi 17 décembre 2009

Les formulaires en ASP.NET MVC

La mise en ligne de la traduction de la 6° partie du tutoriel NerdDinner est enfin terminée. Cette étape est consacrée à la programmation des différents formulaires MVC nécessaires pour pouvoir effectuer la mise à jour de la table Dinners dans la base de données : création, modification et suppression.

Dans cette plutôt longue partie, le tutoriel présente entre autre :

  • certains des helpers disponibles pour faciliter le développement de formulaires
  • l'avantage de ces helpers pour prendre en compte les erreurs de validation du ModelState
  • la façon d'organiser chaque action avec une première requête HTTP GET pour afficher le formulaire de saisie et une seconde requête HTTP POST pour effectuer la mise à jour correspondant à l'action
  • les principales méthodes offertes par ASP.NET MVC pour récupérer les données du formulaire via le "Model Binding".

Le binding consiste à utiliser les données en provenance du formulaire de saisie (mais aussi de l'URL) pour initialiser ou mettre à jour des objets issus du modèle de l'application.

Etant donné que cette correspondance se fait plus ou moins magiquement (ou en tout cas de façon automatique), c'est maintenant de notre responsabilité de bien penser à sécuriser tout ça pour éviter les effets de bords. Pour cela, il est entre autre possible d'utiliser un attribut [Bind] pour définir la liste des propriétés à inclure ou à exclure du binding. Cet attribut peut s'employer au niveau d'une action donnée ou plus généralement de la classe de l'objet.

La prochaine partie du tutoriel NerdDinner va aborder les solutions pour faire passer des données du contrôleur vers les vues et présentera l'utilisation du dictionnaire ViewData ou des objets ViewModel. L'intérêt de ces derniers c'est qu'ils peuvent aussi servir pour filtrer plus précisément quelles propriétés des objets sont exposées via les formulaires.

Et pour commencer à aller encore un plus loin sur tout ce qui concerne l'amélioration et la sécurisation du binding, il est possible de consulter les deux billets suivants :

lundi 7 décembre 2009

Les formulaires de mise à jour en ASP.NET MVC

J'avais prévu de finir la sixième partie du tutoriel NerdDinner la semaine dernière et ça n'a pas marché. Par rapport aux autres parties, je ne peux pas dire que ça soit beaucoup plus compliqué à suivre, c'est juste que c'est un peu plus long. Sans compter que les conditions de travail étaient particulièrement bruyantes cette semaine (quoique ?).

Ce qui fait que j'ai préféré me disperser plutôt que de rester des heures sans pouvoir réellement avancer :

Ces petits écarts ont quand même réussi à me maintenir à peu près d'attaque pour venir à bout du tutoriel. Mais par contre, ça n'a pas été suffisant pour progresser autant sur la traduction.

Le problème, c'est que si dans la journée je patauge pour suivre le tutoriel, ça fait qu'en soirée j'ai beaucoup de mal à avancer sur la traduction puisque c'est des trucs que je n'ai pas encore vu ou suffisamment assimilés... C'est pour cela que malgré mes prévisions pourtant très mesurées, je n'ai pour l'instant réussi à traduire que les 12 premières pages du tutoriel, consacrées majoritairement à la partie Edit de l'application NerdDinner.

Et pour la suite, je n'ai qu'un tout petit plus de la moitié des pages restantes prêtes pour la relecture (et malheureusement, comme j'ai fait ça dans le désordre, je ne suis pas sûr de pouvoir mettre en ligne les pages consacrées au ModelState avant mercredi).

Alors pour me reposer de ma semaine, je me suis laissé aller à programmer pour de vrai. Ca m'a permis de pas mal améliorer le système de rechercher / remplacer qui fait passer le fichier de dans lequel j'enregistre la traduction du format Word au format Wiki de Dotclear. Pour cela, je suis parti de l'application CleanWordHtml de Jeff Atwood. Maintenant, je n'ai plus qu'à exporter la traduction au format HTML filtré et à lancer ma petite application et le résultat obtenu est quasiment prêt à coller sous Dotclear.

Et grâce à ça, j'ai découvert l'existence de deux fonctions trop pratiques : File.ReadAllText() et File.WriteAllText(). C'est des petits trucs comme ça qui devraient suffire à tout le monde pour admettre qu'il y a encore beaucoup de travail avant de pouvoir réaliser proprement une vraie application en ASP.NET 3.5 (MVC ou non). Ou alors, on se contente simplement de refaire plus ou moins bien en .NET 3.5 ou 4.0 ce qu'on a l'habitude de faire en .NET 1.1...

vendredi 27 novembre 2009

Utilisation du contrôleur NerdDinner

J'ai fini de refaire les premières étapes du tutoriel NerdDinner et j'ai réussi à revenir assez vite là où j'en étais au moment de mes premiers essais. Le fait d'avoir bifurqué entre temps par le tutoriel Contact Manager m'a quand même bien aidé ce coup-ci dans la 4° étape qui présente le pattern Repository pour construire le modèle de l'application NerdDinner.

Suite à ça, j'ai aussi réalisé la cinquième étape du tutoriel consacrée aux contrôleurs et aux vues. Dans cette étape, le but est de développer une interface de type liste / détail assez classique pour afficher la liste des dîners et pouvoir consulter un écran avec la fiche complète de chaque dîner.

Je n'ai rencontré qu'un petit souci au moment de générer la vue Details.aspx. Je n'ai pas compris pourquoi, mais Visual Studio ajoutait une ligne pour afficher la propriété IsValid de l'objet Dinner :

        <p>
            IsValid:
            <%= Html.Encode(Model.IsValid) %>
        </p>

Mais apparemment je ne suis pas le seul à qui ça fait ça (et pas le seul à traduire NerdDinner non plus).

Et justement, pour en revenir sur le front de la traduction du tutoriel, j'ai quasiment terminé de traduire cette 5° étape pour laquelle j'ai d'ores et déjà publié 11 pages sur 25. Je relis le reste ce weekend et la totalité de cette partie devrait être disponible en tout début de semaine prochaine.

Pour la suite, je vais attaquer la réalisation de la sixième partie où il sera question des formulaires pour effectuer la mise à jour du contenu de la table des dîners : ajout, modification et suppression d'un dîner. Ca risque d'être un assez gros morceau, notamment côté traduction, parce qu'il y a près de 35 pages. Mais heureusement, cela comprend pas mal de copies d'écran et de codes sources.

Et surtout, une fois cette partie achevée, j'en serai arrivé à la moitié du tutoriel !

jeudi 26 novembre 2009

Création du contrôleur DinnersController.cs

J'ai un peu complété la traduction de la 5° étape consacré à la partie contrôleurs et vues dans ASP.NET MVC. Celle-ci comprend maintenant les explications nécessaires pour ajouter un contrôleur DinnersController.cs qui permettra de gérer une interface de navigation de type liste / détails.

Et pour me simplifier un petit peu la mise à jour des traductions, je suis passé par TinyURL! pour remplacer l'url permanente vers la table des matières du tutoriel NerdDinner en français. Maintenant, il suffit d'aller directement à l'adresse suivante : http://tinyurl.com/NerdDinnerFR.

mercredi 25 novembre 2009

Réalisation du modèle pour NerdDinner

La traduction de la quatrième étape du tutoriel ASP.NET MVC NerdDinner est en ligne. Cette partie un peu longue explique comment créer un modèle LINQ to SQL pour la base de données NerdDinner.mdf, comment y intégrer quelques règles métiers pour la validation et présente le pattern Repository pour séparer les traitements liés à la base de données du reste du code.

Et pour rester motivé pour les jours à venir, voici les trois premiers paragraphes de l'étape 5 sur les contrôleurs et les vues. Plus que 25 pages à traduire ou à finaliser et j'aurai au moins réussi à assurer la traduction d'un tiers du tutoriel NerdDinner...

lundi 23 novembre 2009

Suite de la traduction du NerdDinner

Je n'ai pas aussi bien avancé que ce que j'aurai souhaité dans la traduction du tutoriel NerdDinner, mais j'ai malgré tout réussi à terminer la troisième étape qui concerne la création de la base de données NerdDinner.mdf sous SQL Server. Et j'ai quasiment fini de corriger la traduction de la partie suivante consacrés à la réalisation du modèle (le "M" de MVC) et à la mise en oeuvre des règles de validation. Je devrai donc pouvoir très vite publier les 16 pages de cette nouvelle étape dans sa totalité.

D'autre part, j'ai ajouté une page qui sert à la fois de table des matières et de mini introduction au tutoriel NerdDinner pour lister les différentes parties déjà traduites du tutoriel. Cette nouvelle page index est désormais accessible à partir des différentes pages pour faciliter la navigation entre les différentes étapes du tutoriel. Par la suite, j'ajouterai des liens étape précédente / étape suivante pour passer encore plus simplement d'une étape à l'autre.

vendredi 20 novembre 2009

Traduction du tutoriel NerdDinner

Pour me donner du courage et me remettre à fond sur la réalisation et la compréhension du tutoriel NerdDinner, j'ai profité de l'arrivée de mon nouveau portable pour travailler à la traduction du tutoriel NerdDinner en français.

Comme j'ai réussi à pas mal avancer (j'en ai presque fait un tiers), j'ai décidé de publier ici les parties que j'ai d'ores et déjà terminées :

Si tout va bien, d'ici la semaine prochaine je devrais avoir fini de traduire et de relire les 2 ou 3 étapes suivantes qui concernent la création de la base de données, la réalisation du modèle et peut-être même la présentation du fonctionnement des contrôleurs et des vues.

Je devrais ainsi réussir à rattraper le fil de ma formation où je l'avais laissé et être fin prêt pour attaquer la suite.

vendredi 13 novembre 2009

Portage du tutoriel Contact Manager sous LINQ to SQL

Encouragé par un premier succès pour faire passer le tutoriel ContactManager en tout jQuery, j'ai décidé de continuer ma formation ASP.NET MVC en modifiant l'application ContactManager pour remplacer Entity Framework par LINQ to SQL.

Création du modèle LINQ to SQL

Après un ou deux essais, j'ai vu que le plus pratique était de commencer par exclure ou supprimer du projet tout ce qui touchait à Entity Framework, à savoir ContactManagerModel.edmx et EntityContactManagerRepository.cs. Il devient alors possible d'ajouter un modèle LINQ to SQL au projet pour représenter la base de données, sans que cela provoque des définitions en double avec les objets générés par Entity Framework.

L'ajout d'un modèle LINQ to SQL est plutôt simple :

  • se placer dans le dossier Models de l'explorateur de solution
  • faire un clic droit pour sélectionner la commande "New items..."
  • dans la catégorie "Data", choisir le modèle "LINQ to SQL Classes"
  • le nommer "ContactManagerModel.dbml" et cliquer sur le bouton "Add" pour le créer

Le concepteur LINQ to SQL est alors automatiquement ouvert et il est possible d'y faire glisser les deux tables "Contacts" et "Groups" depuis l'explorateur de base de données, ce qui a pour effet de :

  • créer les classes "Contact" et "Group" basées sur ces deux tables
  • ajouter une propriété "Group" à la classe "Contact" pour refléter le côté "1" de la relation entre les deux tables
  • ajouter une propriété "Contacts" à la classe "Group" pour refléter le côté "N" de la relation entre les deux tables

Cela génère aussi une classe ContactManagerModelDataContext.cs qui fourni deux propriétés "Contacts" et "Groups" permettant de gérer des objets "Contact" et "Group" issus de la base de données.

Concrètement, cette première étape a eu pour résultat de substituer ContactManagerModel.dbml à ContactManagerModel.edmx.

Création d'un Repository basé sur LINQ to SQL

Cette seconde étape va consister à remplacer EntityContactManagerRepository.cs par une classe L2SContactManagerRepository.cs qui implémente elle aussi l'interface IContactManagerRepository. Et au lieu d'utiliser un objet ContactManagerDBEntities, cette nouvelle classe va utiliser un objet ContactManagerModelDataContext.

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

namespace ContactManager.Models
{
    public class L2SContactManagerRepository : IContactManagerRepository
    {
        private ContactManagerModelDataContext _context = new ContactManagerModelDataContext();

        ...
    }
}

Il reste alors à implémenter les différentes méthodes déclarées dans l'interface IContactManagerRepository, mais à la sauce LINQ to SQL. Par rapport au repository basé sur Entity Framework, les différences sont finalement assez mineures.

  • Les objets _entities.ContactSet et _entities.GroupSet deviennent des objets _context.Contacts et _context.Groups :
// ---------- Entity Framework
public IEnumerable<Group> ListGroups()
{
    return _entities.GroupSet.OrderBy(o => o.Name).ToList();
}

// ---------- LINQ to SQL
public IEnumerable<Group> ListGroups()
{
    return _context.Groups.OrderBy(o => o.Name).ToList();
}
  • Les méthodes pour la mise à jour des données ont des noms différents : AddToXxxxxxSet() devient InsertOnSubmit(), DeleteObject() devient DeleteOnSubmit() et SaveChanges() devient SubmitChanges() :
// ---------- Entity Framework
public Group CreateGroup(Group groupToCreate)
{
    _entities.AddToGroupSet(groupToCreate);
    _entities.SaveChanges();
    return groupToCreate;
}

// ---------- LINQ to SQL
public Group CreateGroup(Group groupToCreate)
{
    _context.Groups.InsertOnSubmit(groupToCreate);
    _context.SubmitChanges();
    return groupToCreate;
}
  • La méthode ApplyPropertyChanges() de Entity Framework n'ayant apparemment pas d'équivalent en LINQ to SQL, et le helper UpdateModel() étant lié à la classe Controller, je me suis pour l'instant contenté d'une mise à jour des propriétés à la main pour retrouver la même fonctionnalité :
// ---------- Entity Framework
public Contact EditContact(int groupId, Contact contactToEdit)
{
    // Get original contact
    var originalContact = GetContact(contactToEdit.Id);
    
    // Update with new group
    originalContact.Group = GetGroup(groupId);
    
    // Save changes
    _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
    _entities.SaveChanges();
    return contactToEdit;
}

// ---------- LINQ to SQL
public Contact EditContact(int groupId, Contact contactToEdit)
{
    // Get original contact
    var originalContact = GetContact(contactToEdit.Id);

    // Update with new group
    originalContact.Group = GetGroup(groupId);

    // Update properties
    originalContact.FirstName = contactToEdit.FirstName;
    originalContact.LastName = contactToEdit.LastName;
    originalContact.Phone = contactToEdit.Phone;
    originalContact.Email = contactToEdit.Email;

    // Save changes
    _context.SubmitChanges();
    return contactToEdit;
}

Malgré tout, il y a une autre "grosse" différence dans la mesure où avec Entity Framework on utilise la méthode Include(relation) dans la requête LINQ pour retrouver les objets associés à l'élément sur lequel on effectue la requête.

Concrètement, cela a pour effet de générer une jointure lors de l'exécution de la requête LINQ.

  • Sans le Include(relation) :
return (from c in _entities.ContactSet
        where c.Id == id
        select c).FirstOrDefault();

=>  SELECT 1 AS [C1], 
                [Extent1].[Id] AS [Id], 
                [Extent1].[FirstName] AS [FirstName], 
                [Extent1].[LastName] AS [LastName], 
                [Extent1].[Phone] AS [Phone], 
                [Extent1].[Email] AS [Email], 
                [Extent1].[groupId] AS [groupId]
        FROM [dbo].[Contacts] AS [Extent1]
        WHERE [Extent1].[Id] = @p__linq__1
  • Avec le Include(relation) :
return (from c in _entities.ContactSet.Include("Group")
        where c.Id == id
        select c).FirstOrDefault();

=> SELECT 1 AS [C1], 
                [Extent1].[Id] AS [Id], 
                [Extent1].[FirstName] AS [FirstName], 
                [Extent1].[LastName] AS [LastName], 
                [Extent1].[Phone] AS [Phone], 
                [Extent1].[Email] AS [Email], 
                [Extent2].[Id] AS [Id1], 
                [Extent2].[Name] AS [Name]
        FROM  [dbo].[Contacts] AS [Extent1]
        LEFT OUTER JOIN [dbo].[Groups] AS [Extent2] ON [Extent1].[groupId] = [Extent2].[Id]
        WHERE [Extent1].[Id] = @p__linq__1

Là aussi, je n'ai pas réussi à trouver d'équivalent pour LINQ to SQL, d'autant que dans ce cas je n'ai pas cherché avec beaucoup d'acharnement puisque l'application fonctionne parfaitement même si on s'en passe...

// ---------- Entity Framework
public Contact GetContact(int id)
{
    return (from c in _entities.ContactSet.Include("Group")
            where c.Id == id
            select c).FirstOrDefault();
}

// ---------- LINQ to SQL
public Contact GetContact(int id)
{
    return (from c in _context.Contacts
            where c.Id == id
            select c).FirstOrDefault();
}

Isoler les tests unitaires de l'ORM

Une fois la classe Repository complètement ré-écrite pour s'appuyer sur LINQ to SQL et plus Entity Framework, le projet Contact manager compile sans problème. Encore faut-il penser à modifier la couche de service pour instancier un objet IContactManagerRepository à partir de L2SContactManagerRepository :

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

Par contre, la compilation de la solution provoque une erreur parce que les tests unitaires font appel à la méthode CreateContact() de la classe Contact. Apparemment, Entity Framework génère automatiquement une méthode CreateContact() pour ses objets, alors que LINQ to SQL n'en fait rien !

Cela met en évidence deux problèmes :

  • chez Microsoft, ils sont si peu opiniated qu'ils partent dans tous les sens
  • les tests unitaires du Contact Manager sont fortement liés à l'ORM Entity Framework et ça, c'est pas bon du tout

Dans un premier temps, j'ai tout simplement piraté la méthode CreateContact() générée par Entity Framework et ajouté une classe partielle Contact :

namespace ContactManager.Models
{
    public partial class Contact
    {
        /// <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;
        }
    }
}

Mais j'ai trouvé ça un peu trop spécifique pour LINQ to SQL (puisque c'est redondant avec le code généré par Entity Framework) et j'ai finalement préféré ajouter une méthode CreateContact() utilisant la syntaxe "Object initializers" à la classe ContactManagerServiceTest des tests unitaires :

private Contact CreateContact(int id, string firstName, string lastName, string phone, string email)
{
    Contact contact = new Contact() { Id = id, FirstName = firstName, LastName = lastName, Phone = phone, Email = email };
    return contact;
}

Puis remplacer les appels à Contact.CreateContact() dans ContactManagerServiceTest.cs par de simples appels à CreateContact().

Une fois cette dernière modification réalisée, la solution compile sans erreur, les tests unitaires continuent à être OK et l'application fonctionne sans problème. Je peux :

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

Conclusion

Suite à mes différents essais avec les tutoriels ContactManager et NerdDinner, j'avais eu le pressentiment que cette notion de repository et de service pouvait rendre une application indépendante de la façon d'implémenter l'accès à la base de données.

La réalisation de ce petit exercice m'a permis de vérifier cette impression puisque hormis une ligne de la couche service, seule la classe Repository a été mise à jour. Par conséquent, si on fait les choses comme il faut, on ne se retrouve pas lié corps et âme à un ORM mais au contraire complètement libre d'en changer dans le cas où il ne serait pas satisfaisant.

Dans la pratique, cela veut dire qu'il ne faut pas se focaliser sur des détails techniques mais sur des problèmes réels. Et notamment décider si on conserve une base de données pourrie pléthorique et mal conçue ou si on fait table rase des errements du passé pour construire une vrai base de données et de vrais objets adaptés uniquement à nos besoins actuels.

mercredi 14 octobre 2009

Les types implicites en c#

Le compilateur C# 3.0 permet de déclarer les variables de façon implicite grâce au nouveau mot clé "var". Au lieu de nous forcer à taper le code suivant :

MonTypeSpecial toto = new MonTypeSpecial();

Il est possible d'économiser quelques touches en tapant seulement :

var toto = new MonTypeSpecial();

Dans les deux cas, le résultat est strictement identique et la variable toto est bien toujours une variable de type MonTypeSpecial, qu'elle soit déclarée explicitement ou implicitement. Dans la pratique, le code compilé est également rigoureusement le même.

Lors de la compilation, le compilateur comprend que new MonTypeSpecial() est une expression pour initialiser une variable de type "MonTypeSpecial". Et comme le type de la variable à laquelle on affecte cette expression n'est pas défini, il lui attribue implicitement le même type.

Par conséquent, ce n'est en aucune manière une incursion du C# dans le monde des langages dynamiques ou faiblement typés et le mot clé "var" n'a rien à voir avec la déclaration de variables de type Variant qui existait en Visual Basic.

La mot clé "var" peut être utilisé dans tous les cas où la variable déclarée est initialisée, quelque soit le type de l'expression qui sert à l'initialisation.

var i = 0;                           // i est un int
var j = i;                           // j est un int
var k = i + 10;                      // k est un int
var d = 12.34;                       // d est un double
var q = d / 2;                       // q est un double
var a = k / 3;                       // a est un int (int /int => int)
var f = (float)i;                    // f est un float
var t = "toto";                      // t est un string
var l = t.Length();                  // l est un int
var x = new DateTime(1980, 1, 1);    // x est une DateTime

Par contre, le code suivant ne compilera même pas :

// Impossible d'assigner <null> à une variable locale implicitement typée
var n = null;
// Les variables locales implicitement typées doivent être initialisées
var i;
i = 0;

Par ailleurs, on ne peut employer "var" qu’avec des variables locales et en aucun cas :

  • pour des variables publiques
  • en tant que paramètre de fonction
  • comme valeur de retour d'une méthode
  • comme type d'une propriété

=> Le mot clé contextuel 'var' ne peut apparaître que dans une déclaration de variable locale

Sinon, il (me) semble préférable de réserver l'utilisation des déclarations implicites au cas où l'expression d'initialisation indique en toute lettre le type de l'expression et de les éviter quand cela demande de démarrer le cerveau :

// Ok
var toto = new MonTypeSpecial();
var x = new DateTime(1980, 1, 1);
// Bof
var i = 0;
var j = i;
var t = "toto";
var l = t.Length();

jeudi 23 juillet 2009

Les types nullables en c#

Avec le compilateur C# 1.1, une variable de type valeur (comme un bool, un int, un double... mais aussi une DateTime) ne pouvait pas prendre la valeur null.

  • Quand on déclare une variable de type valeur, il faut obligatoirement lui affecter une valeur avant de l'utiliser, sans quoi on obtient une erreur de compilation : Utilisation d'une variable non assignée.
int compteur;
if (compteur == 0) {
        Console.WriteLine("zéro");
}
  • Il est impossible de lui attribuer la valeur null, car dans ce cas on obtient l'erreur Impossible de convertir une valeur Null en 'int', car il s'agit d'un type valeur.
int compteur = null;
if (compteur == 0) {
        Console.WriteLine("zéro");
}
  • Et bien sûr, on ne peut pas la comparer à la valeur null : L'opérateur '==' ne peut pas être appliqué aux opérandes de type 'int' et '<null>'
int compteur = 0;
if (compteur == null) {
        Console.WriteLine("null");
}

Avec le compilateur C# 2.0, il est désormais possible de faire en sorte que des variables de type valeur acceptent aussi une valeur nulle en plus des valeurs qu'elles peuvent prendre habituellement. Par exemple, une variable nullable de booléen pourra stocker true, false et null. Outre le fait que c'est une aubaine pour les indécis, c'est aussi censé être très pratique pour stocker les valeurs indéfinies en provenance d'une base de données.

Pour qu'un type de valeur devienne nullable, il faut suffixer le type valeur habituel avec un "?" :

int? compteur = null;
bool? ca_marche = null;
DateTime? pour_quand = null;

En fait, les types nullables sont des instances du struct System.Nullable. Par conséquent, les trois déclarations précédentes sont rigoureusement équivalentes aux trois lignes ci-dessous :

Nullable<int> compteur = null;
Nullable<bool> ca_marche = null;
Nullable<DateTime> pour_quand = null;

Les variables nullables ont une propriété HasValue qui renvoie true lorsque la variable contient une valeur et false quand la variable est nulle.

bool? ca_marche = null;
...
if (ca_marche.HasValue == false) {
        Console.Write("Pas de réponse");
} else if (ca_marche == true) {
        Console.Write("Oui");
} else {
        Console.Write("Non");
}

Elles ont aussi une méthode GetValueOrDefault() qui renvoie :

  • la valeur de la variable quand celle-ci n'est pas nulle (soit GetValueOrDefault())
  • la valeur par défaut pour le type de valeur correspondant lorsque la variable est nulle (soit GetValueOrDefault())

C'est ce qui permet par exemple d'affecter une variable de type nullable à une variable de type valeur correspondant :

int? toto = null;

int tutu;
tutu = toto.GetValueOrDefault();
Console.Write(tutu); // affiche 0

toto = 12;

int titi;
titi = toto.GetValueOrDefault();
Console.Write(titi); // affiche 12

On peut aussi utiliser le nouvel opérateur ?? pour définir la valeur par défaut qui sera utilisée dans le cas où on assignerait une variable de type nullable ayant la valeur nulle à une variable de type valeur non nullable :

int? toto = null;

int tutu;
tutu = toto ?? 0;
Console.Write(tutu); // affiche 0
int titi = toto ?? 12;
Console.Write(titi); // affiche 12

Par contre, il y aura une erreur de compilation si on essaie d'affecter directement une variable de type nullable à une variable de type valeur :

int? toto = null;
int tutu = toto; // Erreur de compilation: Cannot implicitly convert type 'int?' to 'int.
int? titi = 12;
tutu = titi; // Erreur de compilation: Cannot implicitly convert type 'int?' to 'int.

A la rigueur, il est possible de caster la variable de type nullable à une variable de type valeur. Cela ne provoquera pas d'erreur de compilation mais attendra l'exécution pour planter :

int? toto = null;
int tutu = (int)toto; // Compile mais provoque une exception de type InvalidCastException à l'exécution

Documentations sur les types nullables :

- page 2 de 3 -