cogimator.net

Une ligne à la fois...

NHibernate entity generator

Avec un peu de retard, j'ai également publié un petit générateur de code pour NHibernate. A partir d'un fichier de mapping, celui-ci génère un peu tout ce qu'on veut à partir de templates modifiables.

Le moteur de template utilisé est SharpTemplate (http://www.codeplex.com/SharpTemplate).

Le projet se télécharge ici : http://www.codeplex.com/EntityGenerator.

SimpleValidation.NET pour ASP.NET MVC

Toujours en beta, voir même proof of concept, j'ai publié une nouvelle version de SimpleValidation.NET, supportant ASP.NET MVC (Preview 4). La validation côté client se fait avec jquery.validation (http://docs.jquery.com/Plugins/Validation).

Pour le moment, seul les validators "required" et "email" sont implémentés coté client. Les autres viendront par la suite !

Le tout se télécharge ici : http://www.codeplex.com/SimpleValidation

Simple à utiliser, tout ce que vous avez à faire :

Créez votre entité

public class User
{
    private string name;
    private int age;
    private string email;
    private string password;
    private decimal size;

    [Required("Name is required")]
    public string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }

    [Required("Age is required")]
    [Range(0, 99, "Age must be in range {0} {1}")]
    [ValidateType("Age format is not valid")]
    public int Age
    {
        get { return this.age; }
        set { this.age = value; }
    }

    [ValidateType("Size format is not valid")]
    public decimal Size
    {
        get { return this.size; }
        set { this.size = value; }
    }

    [Email("Email is invalid")]
    [Confirm("Please confirm your email")]
    [Required("Email is required")]
    public string Email
    {
        get { return this.email; }
        set { this.email = value; }
    }

    [Confirm("Please confirm your password")]
    [Required("Password is required")]
    public string Password
    {
        get { return this.password; }
        set { this.password = value; }
    }
}

Plutot explicite :)

Créez une vue

<%=Html.CreateValidators<Samples.Entities.User>("signupForm")%>

<%using( Html.Form("User", "New", FormMethod.Post, new Dictionary<string, object>() { { "id", "signupForm"} } ) ) { %>
    <div class="inputform">
        <fieldset>
            <legend>
                User
            </legend>
            <label>Name:</label>
            <div class="droite">
                <%= Html.TextBox("Name", ViewData["Name"] as string) %>
                <%= Html.ValidationErrors("Name") %>
            </div>
            
            <label>Password:</label>
            <div class="droite">
                <%= Html.TextBox("Password")%>
                <%= Html.ValidationErrors("Password")%>
            </div>
            
            <label>Confirm Password:</label>
            <div class="droite">
                <%= Html.TextBox("PasswordConfirm")%>
                <%= Html.ValidationErrors("PasswordConfirm")%>
            </div>

            <label>Age:</label>
            <div class="droite">
                <%= Html.TextBox("Age", ViewData["Age"] as string)%>
                <%= Html.ValidationErrors("Age")%>
            </div>
            
            <label>Size:</label>
            <div class="droite">
                <%= Html.TextBox("Size", ViewData["Size"] as string)%>
                <%= Html.ValidationErrors("Size")%>
            </div>

            <label>Email:</label>
            <div class="droite">
                <%= Html.TextBox("Email", ViewData["Email"] as string)%>
                <%= Html.ValidationErrors("Email")%>
            </div>

            <label>Confirm Email:</label>
            <div class="droite">
                <%= Html.TextBox("EmailConfirm", ViewData["EmailConfirm"] as string)%>
                <%= Html.ValidationErrors("EmailConfirm")%>
            </div>
            
            <div class="droite boutons">
                <input type="submit" value="Register" />
            </div>
        </fieldset>

    </div>
<%} %>

 

N'oubliez pas de referencer les js de jquery (http://jquery.com/) et jquery.validation (http://docs.jquery.com/Plugins/Validation) dans votre vue.

Créez votre controller

public class UserController : Controller
{
    public ActionResult Index()
    {
        return View("New");
    }

    public ActionResult New()
    {
        User u = new User();

        if (!MvcValidationHelper.UpdateFrom(u, Request.Form, ViewData))
        {
            return View();
        }

        return View("UserCreated", u);
    }
}

 

Et voila!

Integration continue : Partie 5, Tests unitaires

Tout d'abord, un test unitaire, quesako ? Un test unitaire est un bout de code qui va tester un bout de code de votre application. Unitairement. La partie "unitairement" signifie qu'on va tester une "unité" de notre code, par exemple, une classe et ses méthodes. Si on commence à tester plusieurs classes ensemble, on parlera plutôt de tests d'intégration. Même si dans la pratique, l'outil (NUnit) restera le même.

Prenons une méthode "simple" :

public class StringUtil 
{ 
    public static string GetLength( string input ) 
    { 
        return input.Length; 
    }
}

Son test unitaire ressemblera à ceci (on ajoutera une référence à nunit.framework.dll, situé dans le répertoire d'installation de NUnit):

[TestFixture] 
public class StringUtilTest 
{ 
    [Test] 
    public void GetLenghTest() 
    { 
        Assert.That( StringUtil.GetLength( "ab" ) == 2 ); 
        Assert.That( StringUtil.GetLength( "abc" ) == 3 ); 
        Assert.That( StringUtil.GetLength( "abcd" ) == 4 ); 
    } 
} 

Jusqu'ici tout va bien, on vient de vérifier que le fonctionnement interne de notre méthode est correct, et nous avons 100% de couverture de code. Toutefois, 100% de couverture de code ne signifient pas 0% de bugs. En effet, les tests unitaires ne peuvent pas tester du code inexistant, comme un contrôle d'erreurs par exemple... Nous parlerons dans ce cas de couverture d'états.

Justement, un utilisateur nous soumet un bug : "lorsque je passe null à la méthode, elle plante avec un NullReferenceException !". Ici, deux solutions, soit nous décidons de renvoyer 0 si on passe null à notre fonction GetLength, soit on remonte une exception de type "ArgumentNullException". Partons sur la 2ème solution. Nous pourrions immédiatement modifier le code de notre méthode, mais tout d'abord, modifions notre test unitaire :

[code:c#]

[TestFixture] 
public class StringUtilTest 
{ 
    [Test] 
    public void GetLenghtTest() 
    { 
        Assert.That( StringUtil.GetLength( "ab" ) == 2 ); 
        Assert.That( StringUtil.GetLength( "abc" ) == 3 ); 
        Assert.That( StringUtil.GetLength( "abcd" ) == 4 ); 
    } 

    [Test] 
    [ExpectedException(typeof(ArgumentNullException))] 
    public void GetLenghtArgNulTest 
    { 
        StringUtil.GetLength( null ); 
    } 
} 

Au lancement du jeu de test, GetLenghtArgNulTest devra logiquement échouer : on attend un ArgumentNullException, on reçoit un NullReferenceException. C'est évidemment voulu : nous utilisons les tests unitaires pour reproduire un "bug" avant de corriger celui-ci. Cela permet la non régression : une fois un bug soumis, un test est écrit, et si ce test échoue après la correction du bug, on a régressé. Nous n'augmentons pas encore notre couverture de code : le bug n'est pas corrigé. Par contre, nous améliorons notre couvertures d'états.

Corrigeons maintenant notre code :

public class StringUtil 
{ 
    public static string GetLength( string input ) 
    { 
        if( input == null ) throw new ArgumentNullException( "Merci de ne pas passer de null!"); 
        return input.Length; 
    } 
} 

Tout est maintenant au vert!

Pourquoi faire tout cela, écrire plein de lignes de code en plus, maintenir des tests unitaires ? Pour la fiabilité, et la maintenance future de l'application : une modification dans une partie de l'application non couverte pas les tests unitaires est une modification "sans filet". Il n'est pas possible, avant de tester manuellement, de savoir si la modification a introduit un bug. Si je modifie le fonctionnement interne de GetLength, j'aurai les tests unitaires qui m'indiqueront que, dans notre test, dans le cas des paramètres passés en entrée, les valeurs de retour sont bonnes.

On peut également utiliser NUnit pour "tester" une application avant même d'avoir une interface graphique. Le test d'une couche d'accès au données s'en trouve simplifié.

Enfin, un code qui est écrit à l'avance pour être testable est souvent modularisé et architecturé plus clairement : séparation des rôles, utilisation de couches et d'interfaces.

Même si ce billet peut paraître simple, voire simpliste dans son approche des tests unitaires, j'espère qu'il vous aura donné envie d'essayer.

Integration continue : Partie 4, Microsoft Source Analysis Tool for C#

Initialement, la partie 4 devait parler de tests unitaires, mais la sortie récente de cet outil mérite qu'on fasse un petit détour. Concrètement, MSAT va effectuer une analyse statique de votre code. Il va donc vous indiquer les parties de votre code qui ne se conforment pas à un standard de codage. Il est possible de le configurer selon ses besoins (par exemple, la règle qui dit que les tabulations c'est mal à l'air de faire couler beaucoup d'encre...). Donc attention, le but d'un tel outil n'est pas de définir une norme de codage pour tous les développeurs C# du monde (rien que ça), mais plutôt de maintenir une cohérence au sein des équipes travaillant sur un même projet.

Installation de l'outil

Microsoft Source Analysis Tool for C# peut être téléchargé ici : http://code.msdn.microsoft.com/sourceanalysis/Release/ProjectReleases.aspx?ReleaseId=1047. L'installation est très simple, il faut juste s'assurer que la partie MSBuild est bien installée, sinon, pas d'intégration dans notre processus de build.

Intégration dans vos projets

Une fois l'outil installé, une nouvelle entrée apparait dans le menu "Outils" de Visual Studio : "Run Source Analysis". Elle vous permet d'exécuter l'analyse du code directement dans Visual Studio, et affichera les résultats dans l'onglet "Source Analysis". Un double clc sur un message vous emmene sur la ligne concerné. Rien d'inhabituel donc.

Il est également possible d'intégrer l'analyse de votre code source directement dans le procéssus de build, et là, celà devient vraiment intéressant : lorsqu'un développeur lancera une compilation, toutes les erreurs liées à l'analyse du code source apparaitront en tant que warning dans l'onglet "Liste d'erreurs".

Pour permettre cela, deux solutions :

Si MSAT est installé sur tous les postes de développeur

Ouvrez chaque fichier .csproj pour lequel vous souhaitez analyser le code, et modifiez le de la façon suivante :

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 
    [...] 
    <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> 
    <Import Project="$(ProgramFiles)\MSBuild\Microsoft\SourceAnalysis\v4.2\Microsoft.SourceAnalysis.targets" /> 
    [...] 
</Project> 

Si MSAT n'est pas installé sur tous les postes de développeur

Pour faire suite au billet sur l'arborescence projet, vous pouvez maintenant rajouter un autre répertoire à la racine de la solution : "External" (par exemple... vous pouvez aussi l'appeler Tools, ou Trucs, peu importe!). Créez un répertoire "SourceAnalysis", et copiez y le contenu du répertoire "C:\Program Files\Microsoft Source Analysis Tool for C#". Maintenant, ouvrez chaque fichier .csproj pour lequel vous souhaitez analyser le code, et modifiez le de la façon suivante :

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 
    [...] 
    <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> 
    <Import Project="..\..\External\SourceAnalysis\Microsoft.SourceAnalysis.targets" /> 
    [...] 
</Project> 

Ensuite, intégrez ce répertoire dans le contrôle de code source. Les développeurs bénéficieront de l'analyse de code source dès qu'ils mettront leur copie locale à jour. Lors du premier rechargement du projet, vous obtiendrez un message d'avertissement :

avertissement_securite  

Choisissez de charger normalement le projet.

Et voilà, l'analyse statique intégrée dans Visual Studio, et dans le processus de compilation (et dans CruiseControl, de fait!).

Integration continue : Partie 3, Arborescence Projet

Arborescence projetAvant d'aller plus loin, je vais rapidement décrire l'arborescence projet que je vais utiliser pour les billets à venir (cf image):

  • Apps : applications Winforms, Console, WebForms, tout ce qui est exécutable
  • Libs : toutes les bibliothèques de classes, ou tout ce qui n'est pas exécutable
  • References : toutes les librairies externes référencéés dans votre solution (NHibernate, Castle Project, etc)(non présent dans la solution visual studio, uniquement dans l'arborescence "physique")
  • Scripts : scripts de compilation, de création de base de données
  • Tests : tests unitaires

Important : si vous créez des dossiers de solution dans Visual Studio, attention lors de la création d'un nouveau projet : par défaut, celui-ci est placé à la racine de votre dossier de solution. Il en est de même pour les éléments de solution (par ex : Demo.config et Demo.nunit), il est préférable de les créer physiquement dans le dossier voulu, et de les ajouter à la solution dans un deuxième temps (via "Ajouter un élément existant").

Cette arborescence n'est évidemment pas figée dans ses termes, il s'agit juste de "classer" un peu notre solution. Il est également possible de créer des sous dossiers, comme par exemple "Console" ou "WebApps" dans "Apps".

Concernant les librairies externes, j'ai pris le parti de les lier à chaque solution : cela permet d'éviter les problèmes lors d'une mise à niveau de librairie. Si tous vos projets référencent la meme librairie (NHibernate par exemple), qu'une mise à jour avec un changement d'API de leur part est effectuée, tous vos projets sont impactés. En liant les librairies par projet, vous pouvez effectuer les mises à jour dans chaque projet, si le besoin s'en fait ressentir.

Integration continue : Partie 2, Controle de code source

Pour cette partie sur le contrôle de code source, nous allons mettre en place et utiliser Subversion. Il vous faudra télécharger VisualSVN Server (http://www.visualsvn.com/server/), ainsi que TortoiseSVN (http://tortoisesvn.tigris.org/). Vous pouvez également installer AnkhSVN (http://ankhsvn.open.collab.net/) si vous souhaitez bénéficier de l'intégration dans Visual Studio (attention, pour Visual Studio 2008, prenez bien la version 1.0.3 ou supérieure).

Une fois VisualSVN Server installé sur le serveur, TortoiseSVN et AnkhSVN installés sur les clients, nous allons pouvoir commencer.

Sur le serveur

En premier lieu, il va falloir créer un repository. Un repository est un espace où sera stocké le code source. Toute modification de fichier (commit) dans le repository entraînera une incrémentation du numéro de version (revision) global au repository. Le repository sera physiquement stocké à l'endroit spécifié lors de l'installation de VisualSVN Server. C'est ce répertoire que vous devrez sauvegarder régulièrement. Pour aller plus loin : Subversion in action : revisions.

Donnez un nom explicite à votre repository (le nom de la solution Visual Studio, le codename du projet...). Attention à bien laisser cocher la case "Create default structure". Cette structure de répertoire est une convention :

  • trunk : code source "actuel" du projet
  • tags : versions spécifiques du projet, comme par exemple les différentes livraisons ou versions installées en production
  • branches : version en développement : ajout de nouvelles fonctionnalités.

Tous les détails sont expliqués dans ce post de Bill Simser.

Le repository étant crée, nous allons en autoriser l'accès à nos développeurs. Créez un (ou plusieurs) utilisateurs, via "Create new user" de la page d'accueil.

Il ne reste plus qu'à créer un groupe "developpers", via "Create new group" de la page d'accueil et enfin d'associer notre utilisateur au groupe.

La dernière étape consiste à accorder les droits d'accès à notre groupe sur le repository (ou sur tous les repositories, selon votre goût). Pour cela, clic-droit sur un objet dans l'arborescence, et "Security".

Avant de passer sur le poste de développeur, pensez à note l'url du repository (normalement de la forme http://serveur:8080/svn/NomDuRepository).

Sur le poste de développeur

Nous allons créer une copie locale du repository. Toute modification dans cette copie locale est répercutée via un "Commit", et toute modification du repository est repercutée sur la copie locale via un "Update". Pour gérer cette copie locale, nous allons utiliser TortoiseSVN (AnkhSVN reprend les mêmes principes, mais directement dans Visual Studio).

Après avoir installé TortoiseSVN, créez un nouveau dossier (par ex: d:\IntegrationContinue), faites un clic droit sur ce nouveau dossier, et choisissez "SVN Checkout". Renseignez l'url du repository crée plus tot (http://serveur:8080/svn/IntegrationContinue/trunk), et validez. Attention, l'url doit être suivie de "trunk". Votre copie locale est maintenant créée !

Ensuite, toujours dans ce dossier : faites un clic droit -> TortoiseSVN -> Settings. Dans l'entrée "General", "Subversion", "Global ignore patterns", vous pouvez saisir "bin obj *.suo *.user". Ceci indiquera a TortoiseSVN de toujours ignorer ces répertoires et ces fichiers, et évitera de placer les exécutables ainsi que les fichiers temporaires de compilation sous controle de code source. Les paramètres de solution/projet par utilisateur ne seront également pas placés sous contrôle de code source.

ignore_pattern

Il est également possible de définir quels fichiers/répertoires seront ignorés, et ce par projet, dans la fenêtre de commit :

ignore_property

Enfin, tant que vous êtes dans les paramètres de TortoiseSVN, vous pouvez également ajouter les valeurs suivantes dans la partie "Icon overlays" :

exclude

Cela vous évitera des petits soucis de fichiers vérouillés aléatoirement.

Ce qui nous amène à une autre convention : tout ce qui n'est pas nécessaire à la compilation n'est pas nécessaire dans le contrôle de code source. En effet, à quoi bon stocker N versions d'un code source, ainsi que N versions de l'éxecutable produit par ce code source ?

Processus de mise à jour des sources

Pour le moment, nous avons :

  • un serveur SVN
  • un client SVN

Il ne nous reste plus qu'à créer une nouvelle solution Visual Studio : si vous avez crée le dossier "IntegrationContinue" dans d:\, créez la solution également dans d:\, de manière à ce que le fichier IntegrationContinue.sln se trouve dans d:\IntegrationContinue\IntegrationContinue.sln. Ajoutez vos projets à cette solution.

Une fois ceci fait, retournez dans le dossier d:\IntegrationContinue, pour faire un "commit" avec un clic droit -> TortoiseSVN -> Commit, sélectionnez tous les fichiers, et cliquez sur "Commit". Tous vos fichiers se trouvent maintenant sous contrôle de code source!

Ce qui nous amène à une première version du processus de mise à jour du code source sous SVN.

1. Bob (notre développeur) fait un update de sa copie locale pour obtenir la dernière version du code source
2. Bob fait une modification dans le code
     2.1 Si le code ne compile pas, retour en 2.
     2.2 Si le code compile, passer en 3.
3. Bob fait un update de sa copie locale pour obtenir les modifications qui auraient été effectuées depuis 1.
     3.1 Si le code ne compile pas, retour en 2.
     3.2 Si le code compile, passer en 4.
4. Bob fait un commit pour mettre à jour le code source sur SVN.

Le point 3 est important, il permet de s'assurer que les modifications effectuées en 2 ne sont pas incompatibles avec une éventuelle modification ayant eu lieu entre 1 et 3.

Enfin, le point le plus important : le code source situé dans le contrôleur de code source (plus précisément dans la partie trunk), doit OBLIGATOIREMENT compiler. Si le code ne compile pas, on ne fait pas de commit de ce code.

Gestion de favoris avec del.icio.us

Il fut un temps, où mes favoris étaient tous stockés en local, et je considérais de les stocker chez un "fournisseur", un peu comme une hérésie... Paranoïa peut-être, résistance au changement sans doute.

Maintenant, deux problèmes sont posés par le modèle "traditionnel" du favori :

  • classement arborescent
  • réplication difficile entre plusieurs postes de travail

Le classement arborescent pose vite ses limites à partir de 100 favoris : à la fois pour la sauvegarde, et pour la lecture. Si on duplique un favoris dans deux sous branches de l'arborescence, on complexifie le processus de sauvegarde. Si on ne le place que dans une seule sous branche, on risque de ne plus retrouver son favori si on ne cherche pas exactement au bon endroit.

Concernant la réplication, il suffit de vouloir synchroniser ses favoris sur deux postes ou plus pour se rendre compte de la lourdeur de la chose.

Donc, la solution (avec un peu de retard, ce service n'est plus tout jeune!): del.icio.us. Un service de "stockage" de favoris en ligne, avec un classement par "tags". Au lieu de classer un favori dans une arborescence, on lui affecte des tags : des mots-clés. Et le plus beau dans tout ca, c'est qu'on peut retrouver une arborescence : si un favori est taggé "blog it sharepoint", il sera dans l'arborescence "blog / it / sharepoint", "sharepoint / blog / it", "it / sharepoint / blog", etc. Ceci permet de retrouver un favori bien plus facilement : on part d'un tag, et on voit immediatement les autres tags "voisins".

Le plus dur étant d'être rigoureux dans la taxinomie afin d'éviter les doublons du style graphic/graphics/graphism/images. Quand cette habitude est prise (avec l'aide l'extension Firefox fournie), le processus devient beaucoup plus naturel que le classement arborescent.

Une fois le compte crée, l'extension Firefox installée, et les anciens favoris importés (et reclassés), on ne peut plus s'en passer, rien que pour l'aspect synchronisation, et pour la barre d'outils, qui permet de placer des tags judicieusement choisis comme menus déroulants. Comme par exemple :

  • frequent : sites que je fréquente très ... fréquemment
  • reference : sites de reference (msdn, nhibernate doc, nant help, etc. )
  • toread : articles que j'estime interessants à lire tranquillement plus tard
  • tostudy : idem que toread, mais pour des outils

Avantages : classement par tags, synchronisation

Inconvenients : impossible de mettre des mots composés ("intégration continue" par exemple) comme tags


Integration continue : Partie 1, Introduction

Je vais tacher de vous expliquer comment mettre en place un environnement d'intégration continue au travers de cette petite série de billets.

Tout d'abord, il faut un environnement de développement : Visual Studio 2005 (ou 2008).

Les articles suivants viendront, dans l'ordre :

Formulaires (x)html : comment faire ?

Si comme moi vous aimez les standards et l'accessibilité vous avez déjà du vous arracher des cheveux sur les formulaires web. En plus d'entretenir une future calvitie, ca me chagrinais (oui je suis comme ça...) de ne pas avoir de "modèle". En voilà donc un, qui permet de gérer le traditionnel formulaire sur deux colonnes, avec à gauche les libellés, et à droite les champs de saisie.

Rien de bien revolutionnaire dans tout cela, on pourra juste noter que ce modèle

  • se base sur des position:absolute au lieu de float:left pour aligner les label, suite à la lecture de cet article.
  • permet d'avoir une zone de "droite" plus haute que celle de gauche (cf l'exemple).
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
 <head>
  <title>Test formulaires</title>
  <style type="text/css">
  .left {
    position: absolute;
    width: 250px;
    border: 1px solid black;
    padding: 5px;
    margin-top: 5px;
  }
  form {
   position: absolute;
   width: 500px;
   margin-left: 270px;
  }
  form label {
   position: absolute;
   width: 180px;
  }

  form label strong {
   color: red;
  }

  form .droite {
   padding-left: 190px;
   width: 350px;
   padding-bottom: 20px;
  }  

  form .droite .error {
   color: red;
   margin: 0;
  }

  form .droite .info {
   font-decoration: none;
   margin: 0;
  }

  form .boutons {
   margin-left: 190px;
  }
  </style>
 </head>
 <body>
  <div class="left">
   <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
  </div>
  <form action="#">
   <fieldset>
    <legend>test</legend>
    <label for="nom">Nom <strong>*</strong></label>
    <div class="droite">
     <input type="text" id="nom"/>
     <p class="error">Le nom est obligatoire</p>
    </div>
    
    <label for="prenom">Prenom <strong>*</strong></label>
    <div class="droite">
     <input type="text" id="prenom"/>
     <p class="error">Le prenom est obligatoire</p>
    </div>
   
    <label for="age">Age</label>
    <div class="droite">
     <input type="text" id="age"/>
     <p class="info">Purement indicatif</p>
    </div>
   
    <label for="couleur">Couleur préférée</label>
    <div class="droite">
     <select id="couleur">
      <option>rouge</option>
      <option>vert</option>
      <option>bleu</option>
     </select>
    </div>
   
    <label>Couleurs detestées</label>
    <div class="droite">
     <input type="checkbox" id="noir"/><label for="noir">Noir</label><br/>
     <input type="checkbox" id="gris"/><label for="gris">Gris</label>
    </div>
   
    <div class="boutons">
     <input type="submit" value="Valider"/>
     <input type="reset" value="Annuler"/>
    </div>
   </fieldset>
  </form>
 </body>
</html>

Vous pouvez voir le rendu ici formulaire_xhtml.html (2,47 kb)