blog.pagesd.info

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

mardi 11 mars 2014

Upload de gros fichiers avec ASP.NET MVC

J'avais complètement oublié ça, mais on tombe sur une erreur "Internet Explorer ne peut pas afficher cette page Web" ou "Page Web inaccessible" quand on, essaie d'uploader un fichier trop gros avec ASP.NET.

Le truc, c'est que les "gros" fichiers sont bloqués pour éviter que le serveur soit victime d'une attaque par déni de service.

Dans un premier temps, j'ai résolu ça par un <httpRuntime maxRequestLength="10240" /> dans la partie <system.web> de mon Web.config.

Puis j'ai cherché s'il était possible de limiter cette liberté à certaine parties du site.

Utiliser un tag <location>

ASP.NET MVC and httpRuntime executionTimeout

You can include the whole MVC path (controller and action) in the <location> tag's path attribute. Something like this should work:

<location path="Images/Upload">
    <system.web>
        <httpRuntime executionTimeout="600" />
    </system.web>
</location>

Il y a aussi un maxAllowedContentLength ?

ASP.NET MVC are maxRequestLength and maxAllowedContentLength ignored in a subfolder web.config?

In your webconfig file, you need to declare the <location> tag with the path that way:

<location path="controller/action">
  <system.web>
    <!-- maxRequestLength is in kilobytes (KB)  -->
    <httpRuntime maxRequestLength="5120" /> <!-- 5MB -->
  </system.web>
  <system.webServer>
    <security>
      <requestFiltering>
        <!-- maxAllowedContentLength is in bytes (B)  -->
        <requestLimits maxAllowedContentLength="5242880"/> <!-- 5MB -->
      </requestFiltering>
    </security>
  </system.webServer>
</location>

Alors quoi ?

Which gets priority? maxRequestLength or maxAllowedContentLength?

maxRequestLength indicates the maximum request size supported by ASP.NET, whereas maxAllowedContentLength specifies the maximum length of content in a request supported by IIS. So you need to set both in order to upload large files: the smaller one "takes priority".

En fait, ça dépend su serveur

J'ai tout compris grâce au billet From IIS6 maxRequestLength to IIS7 – specifying maximum file upload size :

Avec IIS6, si on veut des fichiers supérieurs à 4 MB :

<system.web>
  <!-- maxRequestLength is in kilobytes (KB)  -->
  <httpRuntime maxRequestLength="5120" /> <!-- 5MB -->
</system.web>

Avec IIS7, si on veut des fichiers supérieurs à 30 MB

<system.web>
  <!-- maxRequestLength is in kilobytes (KB)  -->
  <httpRuntime maxRequestLength="51200" /> <!-- 50MB -->
</system.web>
<system.webServer>
  <security>
    <requestFiltering>
      <!-- maxAllowedContentLength is in bytes (B)  -->
      <requestLimits maxAllowedContentLength="52428800"/> <!-- 50MB -->
    </requestFiltering>
  </security>
</system.webServer>

Et comme dans mon cas j'ai besoin de 10 Mo au maximum, le maxRequestLength est suffisant.

mercredi 11 septembre 2013

Ecrire des nombres en français

Mehdi Khalili a développé Humanizer, une librairie C# pour humaniser les dates et les nombres et il souhaite désormais essayer d'internationaliser. J'ai regardé vite fait ce que cela pourrait donner en français et ça n'a pas été franchement concluant en ce qui concerne les nombres. On pourrait presque croire que le français est plus compliqué que l'anglais...

Un peu d'archéologie

Heureusement, il y a très longtemps j'avais déjà fait un petit programme pour écrire les nombres en lettres parce qu'en ces temps reculés c'était encore nécessaire pour pouvoir imprimer des chèques (sur une imprimante matricielle). A l'époque l'Internet n'existait pas trop (Compuserve peut être ?) et j'avais adapté une macro Lotus 123 (sans doute trouvée sur l'Ordinateur Individuel) en Quick Basic.

Et donc, voici en 106 lignes de QuickBasic comment écrire à peu près correctement des nombres en lettres :

DECLARE FUNCTION CDU$ (NOMBRE#)
DECLARE FUNCTION STRNUM$ (NUM AS STRING)
DIM SHARED TABLE1$(9), TABLE2$(9), TABLE3$(9)

WIDTH 80, 25

DATA "","un","deux","trois","quatre","cinq","six","sept","huit","neuf"
DATA "","dix","vingt","trente","quarante","cinquante","soixante","soixante","quatre-vingt","quatre-vingt"
DATA "dix","onze","douze","treize","quatorze","quinze","seize","dix-sept","dix-huit","dix-neuf"

FOR DUM% = 0 TO 9
     READ TABLE1$(DUM%)
NEXT

FOR DUM% = 0 TO 9
     READ TABLE2$(DUM%)
NEXT

FOR DUM% = 0 TO 9
     READ TABLE3$(DUM%)
NEXT

GOTO toto
N% = -100
DO
     N% = N% + 1
     PRINT N%, STRNUM$(STR$(N%))
     IF CSRLIN > 40 THEN T$ = INPUT$(1): CLS
LOOP WHILE T$ <> CHR$(27)
END
toto:
DO
     INPUT N$
     PRINT STRNUM$(N$)
LOOP WHILE N$ <> ""

FUNCTION CDU$ (NOMBRE#)
NBR# = NOMBRE#
IF NBR# > 99 THEN
     FOCUS% = INT(NBR# / 100)
     NBR# = NBR# - (CDBL(FOCUS% * 100#))
     IF FOCUS% > 1 THEN N$ = " " + TABLE1$(FOCUS%)
     N$ = N$ + " cent"
     IF FOCUS% > 1 AND INT(NBR#) = 0 THEN N$ = N$ + "s"
     N$ = N$ + CDU$(NBR#)
ELSEIF NBR# > 19 THEN
     FOCUS% = INT(NBR# / 10)
     NBR# = NBR# - (CDBL(FOCUS% * 10#))
     IF FOCUS% > 0 THEN N$ = N$ + " " + TABLE2$(FOCUS%)
     IF FOCUS% = 7 THEN
          IF INT(NBR#) = 1 THEN N$ = N$ + " et"
          NBR# = NBR# + 10
     ELSEIF FOCUS% = 8 THEN
          IF INT(NBR#) = 0 THEN N$ = N$ + "s"
     ELSEIF FOCUS% = 9 THEN
          NBR# = NBR# + 10
     ELSEIF FOCUS% <> 0 THEN
          IF INT(NBR#) = 1 THEN N$ = N$ + " et"
     END IF
     N$ = N$ + CDU$(NBR#)
ELSEIF NBR# > 9 THEN
     FOCUS% = INT(NBR#) - 10
     NBR# = NBR# - FOCUS%
     N$ = N$ + " " + TABLE3$(FOCUS%)
ELSEIF NBR# > 0 THEN
     FOCUS% = INT(NBR#)
     NBR# = NBR# - FOCUS%
     IF FOCUS% > 0 THEN N$ = N$ + " " + TABLE1$(FOCUS%)
END IF
CDU$ = N$
END FUNCTION

FUNCTION STRNUM$ (NUM AS STRING)
NOMBRE$ = ""
NOMBRE# = CDBL(VAL(NUM$))
IF NOMBRE# < 0# THEN
     NOMBRE$ = "moins" + STRNUM$(STR$(-NOMBRE#))
ELSEIF NOMBRE# > 0# THEN
     RESTE# = NOMBRE#
     NOMBRE# = CDBL(INT(RESTE# / 1000000000#))
     IF NOMBRE# > 0# THEN
          RESTE# = RESTE# - (NOMBRE# * 1000000000#)
          NOMBRE$ = NOMBRE$ + CDU$(NOMBRE#)
          NOMBRE$ = NOMBRE$ + " milliard"
          IF NOMBRE# > 1 THEN NOMBRE$ = NOMBRE$ + "s"
     END IF
     NOMBRE# = CDBL(INT(RESTE# / 1000000#))
     IF NOMBRE# > 0# THEN
          RESTE# = RESTE# - (NOMBRE# * 1000000#)
          NOMBRE$ = NOMBRE$ + CDU$(NOMBRE#)
          NOMBRE$ = NOMBRE$ + " million"
          IF NOMBRE# > 1 THEN NOMBRE$ = NOMBRE$ + "s"
     END IF
     NOMBRE# = CDBL(INT(RESTE# / 1000#))
     IF NOMBRE# > 0# THEN
          RESTE# = RESTE# - (NOMBRE# * 1000#)
          IF NOMBRE# > 1 THEN NOMBRE$ = NOMBRE$ + CDU$(NOMBRE#)
          NOMBRE$ = NOMBRE$ + " mille"
          IF NOMBRE# > 1 THEN NOMBRE$ = NOMBRE$ + ""
     END IF
     NOMBRE# = CDBL(INT(RESTE#))
     IF NOMBRE# > 0# THEN NOMBRE$ = NOMBRE$ + CDU$(NOMBRE#)
     NOMBRE# = CDBL(VAL(MID$(NUM$, INSTR(NUM$ + ".", ".") + 1)))
     IF NOMBRE# > 0# THEN NOMBRE$ = NOMBRE$ + " virgule" + STRNUM$(STR$(NOMBRE#))
END IF
STRNUM$ = LTRIM$(NOMBRE$)
END FUNCTION

C'est donc pas si compliqué. Il suffit juste de se replonger dans ce code pour trouver comment ça marche pour écrire des nombres en bon français puis d'essayer de l'adapter en C#.

Un peu de spécifications

Mais pour mettre toutes les chances de mon côté, je vais quand même m'aider d'Internet et plus particulièrement de l'extraordinaire documentation suivante :

Écriture des nombres en français par Olivier Miakinen.

GOTO "http://www.miakinen.net/vrac/nombres"

C'est un peu long, mais c'est vraiment super intéressant à lire. Toutes les explications données ici sont reprises de ce site (et donc à consulter quand j'ai un peu trop simplifié).

Le principe pour écrire un nombre en toutes lettres, c'est de découper ce nombre en paquets de 3 chiffres en partant de la droite et d'écrire chaque bloc de chiffres sous forme de lettres puis d'ajouter l'unité :

  • 1 => 1 => "un"
  • 1234 => 1 234 => "un mille" "deux-cent-trente-quatre"
  • 1234567 => 1 234 567 => "un million" "deux-cent-trente-quatre mille" "cinq-cent-soixante-sept"

Donc pour commencer, je m'occupe uniquement de traiter les nombres de 0 à 999. Grosso-modo, c'est l'équivalant de la fonction CDU$ (NOMBRE#) dans mon vieux code ("CDU" pour CentaineDizaineUnite).

Pour écrire les nombres, on s'appuie sur un certain nombre de mots (et plus précisément des adjectifs cardinaux) pour pouvoir "nommer" les différents nombres. Entre autre :

  • "zéro, un, deux, trois, quatre, cinq, six, sept, huit et neuf" pour écrire les nombres de 0 à 9.
  • "dix, onze, douze, treize, quatorze, quinze et seize" pour écrire les nombres de 10 à 19 (il existe un mot spécifique pour écrire de 10 à 16 et pour les nombres 17, 18 et 19 on combine 2 mots : "dix-sept", "dix-huit" et "dix-neuf".
  • "vingt, trente, quarante, cinquante et soixante" pour pouvoir écrire les nombres de 20 à 99 en combinant éventuellement avec les mots déjà vu et dans quelque cas en utilisant le mot "et" pour lier les mots.
  • "cent" pour écrire les nombres de 100 (cent) à 999 (neuf-cent-quatre-vingt-dix-neuf).
  • puis "mille, million et milliard" pour les nombres plus grands.

Pour information, je met des traits d'union partout, conformément aux recommandations orthographiques de 1990 :

  • Avant : on utilisait les traits d'union uniquement pour écrire les nombres composés inférieurs à cent, sauf autour du mot "et" (qui servait donc à remplacer le trait d'union). Et pour les nombres plus grand, on utilisait des espaces. Ce qui donnait "dix-sept", "vingt et un", "trente-deux mille cinq cent soixante et onze".
  • Après : on met des traits d'union partout : "vingt-et-un", "trente-deux-mille-cinq-cent-soixante-et-onze". Seuls les noms tels que "million" ou "milliard" en sont exemptés.

1° étape : Le zéro

Le trucs le plus simple à faire pour commencer, c'est de gérer le cas du 0 qui s'écrit "zéro" tout simplement :

namespace Amstramgram
{
  public static class ToWordsExtension
  {
    public static string ToWords(this int number)
    {
      if (number == 0) return "zéro";

      return number.ToString();
    }
  }
}

Et pour tester que c'est OK :

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Amstramgram.Tests
{
  [TestClass]
  public class Tests
  {
    [TestMethod]
    public void Le_0_renvoie_zero()
    {
      Assert.AreEqual("zéro", 0.ToWords());
    }
  }
}

Et ça marche. Je sais donc écrire 0 sous forme de lettres :)

2° étape : Les nombres de 1 à 19

Les nombres de 1 à 16 correspondent à des mots spécifiques. Leur écriture est donc à coder "en dur". Pour simplifier la suite (quand il faudra écrire les nombres en 70 et les nombres en 90), il est plus pratique de définir en dur tous les nombres de 1 à 19.

Le test unitaire :

[TestMethod]
public void Les_nombres_de_1_a_19_sont_corrects()
{
  var basics = new[] { "", "un", "deux", "trois", "quatre", "cinq", "six", "sept",
                       "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze",
                       "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" };
  for (int i = 1; i < 20; i++)
  {
    Assert.AreEqual(basics[i], i.ToWords());
  }
}

Et le code :

public static string ToWords(this int number)
{
  // Le zéro est un cas un peu spécial
  if (number == 0) return "zéro";

  // Les nombres basiques qui serviront à former des combinaisons
  var basics = new[] { "", "un", "deux", "trois", "quatre", "cinq", "six", "sept",
                       "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze",
                       "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" };

  if (number < 20) return basics[number];

  return number.ToString();
}

Résultat : les 2 tests unitaires sont OK.

3° étape : Les nombres de 20 à 99

Là ça commence à se compliquer un peu. Par conséquent, je vais faire le plus facile d'abord puis je corrigerai les cas tordus après.

Pour écrire les dizaines, on décompose le nombre en dizaine et en unité puis on prend le mot qui va bien pour exprimer la dizaine que l'on fait suivre du mot qui correspond à l'unité :

  • 17 = (1 x 10) + 7 => dix sept
  • 23 = (2 x 10) + 3 => vingt trois
  • 34 = (3 x 10) + 4 => trente quatre

Déjà là, on a 3 petites exceptions :

  • pour les nombres de 11 à 16, on a des mots spéciaux (et pas dix-un, dix-deux, dix-trois...)
  • pour les nombres de 71 à 79, on n'a pas de mot pour exprimer la soixantedizaine (les belges ont septante, mais c'est les belges)
  • pour les nombres de 91 à 99, on n'a pas nom plus de pour exprimer la quatrevingtdizaine (là encore, les belges ont nonante).

Pour les nombres de 11 à 16, pas de souci, je gère (déjà).

Pour les nombres en 70, on prend le mot qui sert normalement pour le 60 (soixante) et au lieu de le faire suivre du mot pour l'unité, on utilise le mot qui correspond à 10 + l'unité :

  • 70 = 70 + 0 = 60 + 10 + 0 = 60 + 10 => soixante dix
  • 71 = 70 + 1 = 60 + 10 + 1 = 60 + 11 => soixante onze
  • 72 = 70 + 2 = 60 + 10 + 2 = 60 + 12 => soixante douze
  • etc...

Et pour les nombres en 90, on fait pareil, mais en partant du mot qui sert pour le 80 (quatre-vingt) :

  • 90 = 90 + 0 = 80 + 10 + 0 = 80 + 10 => quatre-vingt dix
  • 91 = 90 + 1 = 80 + 10 + 1 = 80 + 11 => quatre-vingt onze
  • 92 = 90 + 2 = 80 + 10 + 2 = 80 + 12 => quatre-vingt douze
  • etc...

Pour l'instant, ça devrait m'occuper pour un petit moment. Je traiterai donc des raffinements tels que le "s" à "quatre-vingt" ou le "et" pour les dizaines-et-un à l'étape suivante.

Premier test : vérifier que les dizaines piles sont écrites correctement (étant donné que les mots correspondants sont codés quasiment "en dur") :

[TestMethod]
public void Les_dizaines_exactes_sont_correctes()
{
  // En vrai, 80 s'écrit quatre-vingts et pas quatre-vingt, mais ce sera pour plus tard
  var oks = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                    "soixante", "soixante-dix", "quatre-vingt", "quatre-vingt-dix" };
  for (int i = 1; i < 10; i += 1)
  {
    Assert.AreEqual(oks[i], (i * 10).ToWords());
  }
}

Second test : vérifier que les autres dizaines sont bien le résultat d'une combinaison de la dizaine et de l'unité :

[TestMethod]
public void Les_dizaines_avec_unites_sont_correctes()
{
  var oks = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                    "soixante", "soixante-dix", "quatre-vingt", "quatre-vingt-dix" };
  for (int i = 1; i < 10; i += 1)
  {
    Assert.AreEqual(oks[i] + "-sept", (i * 10 + 7).ToWords());
  }
}

Et le code mis à jour pour que ces 2 nouveaux tests unitaires passent :

public static string ToWords(this int number)
{
  // Le zéro est un cas un peu spécial
  if (number == 0) return "zéro";

  // Les nombres basiques qui serviront à former des combinaisons
  var basics = new[] { "", "un", "deux", "trois", "quatre", "cinq", "six", "sept",
                       "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze",
                       "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" };

  if (number < 20) return basics[number];

  // Gère les dizaines et les unités
  var tens = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                     "soixante", "soixante", "quatre-vingt", "quatre-vingt" };

  int result = number / 10;
  int remainder = number % 10;

  var text = tens[result];
  if (result == 7)
  {
    remainder += 10;
  }
  if (result == 9)
  {
    remainder += 10;
  }
  if (remainder > 0)
  {
    text += "-";
    text += basics[remainder];
  }
  return text;
}

4° étape : Cas des 21, 31, 41, 51, 61 et 71

Normalement, dans le cas des dizaines avec des unités différentes de zéro, le mot pour l'unité est simplement accolé au mot pour la dizaine via un trait d'union. Dans le cas où l'unité a pour valeur 1, les deux mots sont regroupés en utilisant le mot "et" (ou plus précisément "-et-") :

  • 21, 31, 41 ... => vingt-et-un, trente-et-un, quarante-et-un ...
  • 22, 32, 42 ... => vingt-deux, trente-deux, quarante-deux ...

Déjà, cette règle ne concerne pas le nombre 11 (puisque c'est onze et pas dix-un). Mais elle ne s'applique pas non plus pour les nombres 81 et 91 :

  • 81 => quatre-vingt-un (et pas quatre-vingt-et-un)
  • 91 => quatre-vingt-onze (et pas quatre-vingt-et-onze)

Les 2 tests unitaires pour gérer cette particularité :


[TestMethod]
public void Les_nombres_en_dizaine_et_un_de_21_a_71_contiennent_un_et_devant_l_unite()
{
  for (int i = 21; i < 81; i += 10)
  {
    Assert.IsTrue(i.ToWords().Contains("-et-"));
  }
}

[TestMethod]
public void Les_nombres_81_et_91_ne_contiennent_pas_un_et_devant_l_unite()
{
  Assert.IsFalse(81.ToWords().Contains("-et-"));
  Assert.IsFalse(91.ToWords().Contains("-et-"));
}

Et le code devient alors :

public static string ToWords(this int number)
{
  // Le zéro est un cas un peu spécial
  if (number == 0) return "zéro";

  // Les nombres basiques qui serviront à former des combinaisons
  var basics = new[] { "", "un", "deux", "trois", "quatre", "cinq", "six", "sept",
                       "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze",
                       "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" };

  if (number < 20) return basics[number];

  // Gère les dizaines et les unités
  var tens = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                     "soixante", "soixante", "quatre-vingt", "quatre-vingt" };

  int result = number / 10;
  int remainder = number % 10;

  // - la dizaine
  var text = tens[result];

  // - cas où l'unité vaut 1
  if (remainder == 1)
  {
    // la dizaine est séparée de l'unité par "et"
    // pour 21, 31, 41, 51, 61, 71 mais pas 81 et 91
    if (result < 8) text += "-et";
  }

  // l'unité
  if (result == 7)
  {
    remainder += 10;
  }
  else if (result == 9)
  {
    remainder += 10;
  }
  if (remainder > 0)
  {
    text += "-";
    text += basics[remainder];
  }

  return text;
}

5° étape : Le pluriel de vingt

C'est compliqué. Déjà, "vingt" peut se mettre au pluriel parce que c'est un mot un peu particulier. Avant, on comptait aussi en vingtaines, c'est à dire en base 20, ce qui explique le quatre-vingts = 4 vingtaines.

Et donc la règle c'est que on met un "s" à vingt :

  • quand il est précédé d'un nombre qui le multiplie
  • et qu'il n'est pas suivi par un autre nombre ou par "mille" (parce que "mille" est un nombre cardinal)

Ce qui donne donc :

  • quatre-vingts : 20 est précédé par 4 ET suivi par rien d'autre
  • cent-vingt : 20 est précédé par 100 MAIS pas multiplié par 100
  • quatre-vingt-un : 20 est précédé par 4 MAIS suivi par 1
  • quatre-vingt-mille car vingt est suivi de "mille" (sans "s" à mille, mais ça c'est une autre histoire...)
  • quatre-vingts millions ou quatre-vingts milliards car "million" et "milliard" sont des noms et pas des nombres cardinaux.

Pour contrôler tout ça, je doit revenir sur le test qui concernait les dizaines exactes pour utiliser la véritable orthographe de 80. Et je dois également ajouter un test pour le cas du 80000. Par contre, l'absence de "s" lorsque l'unité est différente de zéro est déjà correctement prise en compte par le test "Les_dizaines_avec_unites_sont_correctes".

[TestMethod]
public void Les_dizaines_exactes_sont_correctes()
{
  var oks = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                    "soixante", "soixante-dix", "quatre-vingts", "quatre-vingt-dix" };
  for (int i = 1; i < 10; i += 1)
  {
    Assert.AreEqual(oks[i], (i * 10).ToWords());
  }
}

[TestMethod]
public void Pas_de_s_a_80_quand_suivi_de_mille()
{
  Assert.AreEqual("quatre-vingt-mille", 80000.ToWords());
}

Il ne reste plus qu'à corriger mon code pour respecter ces tests unitaires :

...
// Gère les dizaines et les unités
var tens = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                   "soixante", "soixante", "quatre-vingt", "quatre-vingt" };

int result = number / 10;
int remainder = number % 10;

// - la dizaine
var text = tens[result];

// - cas où l'unité vaut 1
if (remainder == 1)
{
  // la dizaine est séparée de l'unité par "et"
  // pour 21, 31, 41, 51, 61, 71 mais pas 81 et 91
  if (result < 8) text += "-et";
}

// - pluriel de vingt
if (result == 8)
{
  // quatre-vingts prend un "s" quand pas suivi d'un autre nombre
  if (remainder == 0) text += "s";
}
...

Pour l'instant, je ne gère que les nombres entre 0 et 999. Donc je n'ai pas trop les moyens de coder les cas du 80000. Je vais donc devoir supporter un test qui échoue pendant un petit moment...

Attention :

  • Le nombre 80 s'écrit donc "quatre-vingts" avec un "s" lorsque que c'est un nombre cardinal qui sert à exprimer une quantité : "il y a quatre-vingts pages dans ce livre".
  • Par contre, un nombre ordinal est toujours invariable. Un nombre ordinal sert à exprimer un numéro d'ordre : "la quatre-vingtième page", mais aussi "la page quatre-vingt" (sans "s" !).
  • Heureusement pour moi, je ne m'intéresse qu'aux nombres cardinaux :)

6° étape : Les nombres de 100 à 999

Là aussi on procède par découpage un peu comme pour les dizaines. Il faut commencer par écrire le chiffre qui correspond à la centaine que l'on fait suivre du mot "cent" puis on continue en écrivant le reste du nombre, c'est à dire la dizaine et l'unité comme on l'a fait aux étapes précédentes :

  • 101 = 100 + 1 => cent un
  • 123 = 100 + 23 => cent vingt-trois
  • 235 = 200 + 35 => deux-cent trente-cinq
  • 551 = 500 + 51 => cinq-cent cinquante-et-un

Notes :

  • Le nombre 100 s'écrit "cent" et pas "cent zéro" pour 100 + 0
  • Le pluriel de "cent" suit les mêmes règles que le pluriel de "vingt", mais j'attendrai l'étape suivante pour gérer ça.

Je rajoute deux tests unitaires pour vérifier que mon futur code sera bien OK :

[TestMethod]
public void Le_100_renvoie_cent()
{
    Assert.AreEqual("cent", 100.ToWords());
}

[TestMethod]
public void Les_centaines_avec_dizaines_ou_unites_sont_correctes()
{
  Assert.AreEqual("cent-un", 101.ToWords());
  Assert.AreEqual("cent-vingt-trois", 123.ToWords());
  Assert.AreEqual("deux-cent-trente-quatre", 234.ToWords());
  Assert.AreEqual("cinq-cent-cinquante-et-un", 551.ToWords()); // -et-un
  Assert.AreEqual("huit-cent-quatre-vingts", 880.ToWords());   // s à 80
  Assert.AreEqual("huit-cent-quatre-vingt-un", 881.ToWords()); // ni s, ni et à 81
}

Puis je modifie la fonction ToWords() pour gérer les centaines. Cela provoque pas mal de changements, mais normalement je suis tranquille puisque j'ai des tests unitaires pour vérifier que tout continue de fonctionner comme prévu.

public static string ToWords(this int number)
{
  // Le zéro est un cas un peu spécial
  if (number == 0) return "zéro";

  // Les nombres basiques qui serviront à former des combinaisons
  var basics = new[] { "", "un", "deux", "trois", "quatre", "cinq", "six", "sept",
                       "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze",
                       "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" };

  // Les mots qui serviront à former les dizaines
  var tens = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                     "soixante", "soixante", "quatre-vingt", "quatre-vingt" };

  var text = "";

  // Gère les centaines
  if (number > 99)
  {
    // Ecrit le chiffre des centaines
    int result = number / 100;
    int remainder = number % 100;

    if (result == 1)
    {
      // cent... et pas un-cent...
      text = "cent";
    }
    else
    {
      // deux-cent..., trois-cent...
      text = basics[result] + "-cent";
    }

    // Reste à écrire les dizaines et les unités
    number = remainder;
  }

  // Gère les petits nombres (codés en dur)
  if (number < 20)
  {
    if (number > 0)
    {
      if (text != "") text += "-";
      text += basics[number];
    }
  }

  // Gère les dizaines et les unités
  if (number > 19)
  {
    int result = number / 10;
    int remainder = number % 10;

    // - la dizaine
    if (text != "") text += "-";
    text += tens[result];

    // - cas où l'unité vaut 1
    if (remainder == 1)
    {
      // la dizaine est séparée de l'unité par "et"
      // pour 21, 31, 41, 51, 61, 71 mais pas 81 et 91
      if (result < 8) text += "-et";
    }

    // - pluriel de vingt
    if (result == 8)
    {
      // quatre-vingts prend un "s" quand pas suivi d'un autre nombre
      if (remainder == 0) text += "s";
    }

    // l'unité
    if (result == 7)
    {
      remainder += 10;
    }
    else if (result == 9)
    {
      remainder += 10;
    }
    if (remainder > 0)
    {
      if (text != "") text += "-";
      text += basics[remainder];
    }
  }

  // Renvoie le texte pour 0 à 999
  return text;
}

Ca a été un peu compliqué, mais c'est passé !

7° étape : Le pluriel de cent

C'est facile. C'est la même règle que pour le pluriel de vingt et on met donc un "s" à cent :

  • quand il est précédé d'un nombre qui le multiplie
  • et qu'il n'est pas suivi par un autre nombre ou par "mille" (parce que "mille" est un nombre cardinal)

Ce qui donne donc :

  • deux-cents : 100 est précédé par 2 ET suivi par rien d'autre
  • mille-cent : 100 est précédé par 1000 MAIS pas multiplié par 1000
  • deux-cent-un : 100 est précédé par 2 MAIS suivi par 1
  • deux-cent-mille (sans "s") car cent est suivi de "mille"
  • deux-cents millions ou deux-cents milliards car "million" et "milliard" sont des noms et pas des nombres cardinaux.
[TestMethod]
public void Cent_est_au_pluriel_pour_les_multiples_de_100()
{
  Assert.IsTrue(200.ToWords().Contains("cents"));
}

[TestMethod]
public void Cent_est_au_singulier_si_pas_un_multiple_de_100()
{
  Assert.IsFalse(201.ToWords().Contains("cents"));
}

[TestMethod]
public void Cent_ne_prend_pas_de_s_quand_suivi_de_mille()
{
    Assert.AreEqual("deux-cent-mille", 200000.ToWords());
}

Puis je modifie le code pour ajouter un "s" lorsque c'est nécessaire, en laissant pour l'instant de côté le cas du 200000, ce qui me fera un deuxième test unitaire KO.

  // Gère les centaines
  if (number > 99)
  {
    // Ecrit le chiffre des centaines
    int result = number / 100;
    int remainder = number % 100;

    if (result == 1)
    {
      // cent... et pas un-cent...
      text = "cent";
    }
    else
    {
      // deux-cent..., trois-cent...
      text = basics[result] + "-cent";
      if (remainder == 0) text += "s";    // <- code ajouté
    }

    // Reste à écrire les dizaines et les unités
    number = remainder;
  }

8° étape : Les nombres de 1000 à 999999

Maintenant qu'on sait gérer les blocs de 3 chiffres, c'est super facile. Il suffit simplement de gérer 2 paquets au lieu d'un seul comme on l'a fait jusqu'à présent :

  • écrire en toute lettre le bloc qui correspond aux milliers,
  • ajouter "mille"
  • écrire en toute lettre le bloc qui correspond centaines-dizaines-unités

Exemple :

  • 1234 = 1 234 => mille deux-cent-trente-quatre
  • 12345 = 12 345 => douze-mille trois-cent-quarante-cinq
  • 123456 = 123 456 => cent-vingt-trois-mille quatre-cent-cinquante-six

Et évidemment, on n'écrit pas "un mille", mais "mille".

Normalement, il devrait suffire des 2 tests unitaires suivants pour vérifier que les milliers sont correctement gérés :

[TestMethod]
public void Le_1000_renvoie_mille()
{
  Assert.AreEqual("mille", 1000.ToWords());
}

[TestMethod]
public void Decoupe_par_blocs_de_3_chiffres()
{
  Assert.AreEqual("mille-deux-cent-trente-quatre", 1234.ToWords());
  Assert.AreEqual("douze-mille-trois-cent-quarante-cinq", 12345.ToWords());
  Assert.AreEqual("cent-vingt-trois-mille-quatre-cent-cinquante-six", 12345.ToWords());
}

Pour ce qui concerne le code, ça va demander pas mal de réorganisation pour réussir à faire ça. Il faut extraire la partie qui gère le bloc de 3 chiffres et l'appeler 2 fois. Mais même si c'est un "gros" chantier, les tests unitaires sont là pour être certain qu'il n'y a pas eu de casse pendant le déménagement.

Au final, j'ai reporté la quasi-totalité du code de la fonction ToWords() dans une fonction privée Textify(int) et maintenant la méthode d'extension ToWords() ne sert plus qu'à appeler cette nouvelle fonction pour chaque bloc de 3 chiffres et à concaténer les 2 chaines obtenues.

public static string ToWords(this int number)
{
  if (number >= 1000000) return number.ToString();

  if (number == 0) return "zéro";

  var text = "";

  int millier = number / 1000;
  if (millier > 0)
  {
    text += millier == 1 ? "mille" : Textify(millier) + "-mille";
    number = number % 1000;
    if (number > 0) text += "-";
  }

  text += Textify(number);

  return text;
}

private static string Textify(int number)
{
  // Les nombres basiques qui serviront à former des combinaisons
  var basics = new[] { "", "un", "deux", "trois", "quatre", "cinq", "six", "sept",
                       "huit", "neuf", "dix", "onze", "douze", "treize", "quatorze",
                       "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" };

  // Les mots qui serviront à former les dizaines
  var tens = new[] { "", "dix", "vingt", "trente", "quarante", "cinquante",
                     "soixante", "soixante", "quatre-vingt", "quatre-vingt" };

  var text = "";

  // Gère les centaines
  if (number > 99) ...

  // Gère les petits nombres (codés en dur)
  if (number < 20) ...

  // Gère les dizaines et les unités
  if (number > 19) ...

  // Renvoie le texte pour 0 à 999
  return text;
}

9° étape : Le pluriel de mille

C'est super facile. Mille est invariable, parce que dans le temps "mille" était la forme plurielle de "mil".

Par conséquent, je vais juste tester que mille ne prend pas de "s" quand il y a plusieurs milliers :

[TestMethod]
public void Mille_ne_prend_jamais_de_s()
{
    Assert.IsFalse(2000.ToWords().Contains("milles"));
}

Et je n'ai rien à changer au code pour que ce test passe.

En contrepartie, je vais en profiter pour gérer le pluriel de "vingt" et de "cent" devant "mille" et faire en sorte de réussir les 2 tests KO que je traine depuis un certain temps.

public static string ToWords(this int number)
{
  // Ne gère pas les millions
  if (number >= 1000000) return number.ToString();

  // Le zéro est un cas un peu spécial
  if (number == 0) return "zéro";

  var text = "";

  int millier = number / 1000;
  if (millier > 0)
  {
    text += millier == 1 ? "mille" : Textify(millier) + "-mille";
    number = number % 1000;
    if (number > 0) text += "-";

    // Vingt ou cent suivis de mille sont toujours au singulier
    text = text.Replace("cents-mille", "cent-mille");
    text = text.Replace("vingts-mille", "vingt-mille");
  }

  text += Textify(number);

  return text;
}

Et avec cette petite modification j'ai maintenant 15 tests unitaires sur 15 qui passent.

10° étape : Les millions

Une fois que les bases sont posées, c'est assez simple de faire évoluer le truc. Dans le cas des millions, il suffit de reproduire ce qui a été fait pour les milliers, sauf qu'on gère un bloc supplémentaire :

  • écrire en toute lettre le bloc qui correspond aux millions,
  • ajouter "millions"
  • écrire en toute lettre le bloc qui correspond aux milliers,
  • ajouter "mille"
  • écrire en toute lettre le bloc qui correspond aux unités

Exemple :

  • 1234567 = 1 234 567 => un million deux-cent-trente-quatre-mille cinq-cent-soixante-sept

Comme le montre cet exemple, on écrit bien "un million" et pas juste "million", parce que c'est un nom et pas un nombre cardinal. Et alors que le trait d'union sert habituellement de séparateur entre les mots, ce n'est pas le cas avec "million" pour lequel on utilise plutôt un espace.

Enfin, comme "million" est un nom, c'est beaucoup plus simple de gérer le pluriel. On met un "s" à million dès lors qu'il est précédé d'un nombre supérieur à 1 (ce qui signifie que le pluriel commence à 2 millions).

Par conséquent, il suffit de 3 tests unitaires supplémentaires pour s'assurer que tout se déroule comme prévu :

[TestMethod]
public void Le_1_000_000_renvoie_un_million()
{
  Assert.AreEqual("un million", 1000000.ToWords());
}

[TestMethod]
public void Decoupe_en_3_blocs_de_3_chiffres()
{
  Assert.AreEqual("un million deux-cent-trente-quatre-mille-cinq-cent-soixante-sept", 1234567.ToWords());
}

[TestMethod]
public void Million_prend_un_s_au_pluriel()
{
  Assert.IsTrue(2000000.Contains("millions"));
}

Côté code, cela n'implique pas une trop grosse modification :

public static string ToWords(this int number)
{
  // Ne gère pas les milliards
  if (number >= 1000000000) return number.ToString();

  // Le zéro est un cas un peu spécial
  if (number == 0) return "zéro";

  var text = "";

  // Gère les millions
  int million = number / 1000000;
  if (million > 0)
  {
      text += Textify(million) + " million";
      if (million > 1) text += "s";
      number = number % 1000000;
      if (number > 0) text += " ";
  }

  // Gère les milliers
  int millier = number / 1000;
  if (millier > 0)
  {
    text += millier == 1 ? "mille" : Textify(millier) + "-mille";
    number = number % 1000;
    if (number > 0) text += "-";

    // Vingt ou cent suivis de mille sont toujours au singulier
    text = text.Replace("cents-mille", "cent-mille");
    text = text.Replace("vingts-mille", "vingt-mille");
  }

  // Gère les centaines, dizaines et unités
  text += Textify(number);

  return text;
}

11° étape : Les milliards

C'est quasiment la même chose que pour gérer les millions. On découpe le nombre en 4 blocs de 3 chiffres et le 1° bloc correspond au(x) milliard(s). Comme "milliard" est aussi un nom, on dit également "un milliard" et il suit exactement les mêmes règles que "million" en matière d'espaces et de pluriel.

Mais comme j'ai décidé de ne pas gérer les milliards, je vais plutôt écrire un test unitaire pour contrôler qu'ils sont bien hors périmètre :

[TestMethod]
public void Les_milliards_ne_sont_pas_geres()
{
  Assert.AreEqual(1000000000.ToString(), 1000000000.ToWords());
  Assert.AreNotEqual(999999999.ToString(), 999999999.ToWords());
}

Et il n'y a rien à modifier dans le code puisque il tenait déjà compte du cas où le nombre à écrire en lettre est supérieur ou égal à 1 milliard.

  // Ne gère pas les milliards
  if (number >= 1000000000) return number.ToString();

12° étape : Les nombres négatifs

Pour mettre la touche finale à la fonction ToWords(), j'ajoute une dernière fonctionnalité pour qu'elle gère le cas des nombres négatifs.

Pour cela, j'ai besoin de vérifier qu'avec un nombre négatif le texte qu'elle renvoie commence par "moins". Et pour compléter le test unitaire précédent, j'ai également besoin de tester que les milliards négatifs ne sont pas pris en compte, ce qui m'assure que seuls les nombres entiers de -999999999 à +999999999 sont gérés.

[TestMethod]
public void Un_nombre_negatif_commence_par_moins()
{
  Assert.IsTrue((-123).ToWords().StartsWith("moins "));
}

[TestMethod]
public void Les_milliards_negatifs_ne_sont_pas_geres()
{
  Assert.AreEqual((-1000000000).ToString(), (-1000000000).ToWords());
  Assert.AreNotEqual((-999999999).ToString(), (-999999999).ToWords());
}

Côté code, cela se traduit par quelques lignes en plus :

public static string ToWords(this int number)
{
  // Ne gère pas les milliards
  if (Math.Abs(number) >= 1000000000) return number.ToString();

  // Le zéro est un cas un peu spécial
  if (number == 0) return "zéro";

  var text = "";

  // Gère les nombres négatifs
  if (number < 0)
  {
    text = "moins ";
    number = 0 - number;
  }

  // Gère les millions
  ...

Conclusion

Je ne suis pas vraiment certain que cette fonction me servira un jour, mais c'était un chouette exercice. Et je pense m'en être pas trop mal tiré pour tout ce qui concerne les tests unitaires. Par rapport à ma vieille fonction STRNUM$() je ne gère pas les nombres décimaux, mais ça pourra faire l'objet d'une V2.

PS: le projet Amstramgram sur GitHub.

mardi 3 septembre 2013

Clé déjà existante dans EF.ObjectStateManager

En ce moment, je testunite beaucoup et comme je persiste à utiliser une base de données SQL CE pour ce qui touche à Entity Framework, je cherche tous les moyens possibles pour améliorer la vitesse de mes tests.

Suite à ma dernière optimisation, j'avait 2 tests unitaires qui ne passaient plus.

Un objet ayant la même clé existe déjà dans ObjectStateManager. ObjectStateManager ne peut pas assurer le suivi de plusieurs objets ayant la même clé.

Ou en anglais :

An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.

Dans les 2 tests, il s'agissait de vérifier que le POST sur l'action "Edit" d'un contrôleur redirigeait bien vers l'action "Details" en cas de succès.

//
// POST: /Theaters/Edit/5

[HttpPost, ValidateAntiForgeryToken]
public ActionResult Edit(Theater theater)
{
    if (ModelState.IsValid)
    {
        // Enregistre les modifications
        var place = db.Places.Find(theater.Place_ID);
        theater.KeyTheater = StringHelper.Slugify(place.Caption + " " + theater.ShortName);
        db.Entry(theater).State = EntityState.Modified;
        db.SaveChanges();

        return RedirectToAction("Details", new { id = theater.Theater_ID });
    }

    ViewBag.Place_ID = db.SelectListPlaces(this.Department_ID, theater.Place_ID);
    return View(theater);
}

L'erreur apparaissait pile sur la ligne db.Entry(theater).State = EntityState.Modified;.

Dans tous les autres contrôleurs, le problème ne se présentait pas, mais uniquement dans ces 2 cas là. Après quelques recherches, il semblerait que le fait d'utiliser l'objet theater dans la ligne var place = db.Places.Find(theater.Place_ID); soit suffisant pour que l'entité soit référencée dans l'ObjectStateManager de Entity Framework.

Et ensuite, la commande db.Entry(theater).State = EntityState.Modified; provoquait à nouveau son référencement alors qu'il l'avait déjà été deux lignes plus tôt (mais ça EF ne semblait pas capable de s'en rendre compte).

J'ai trouvé plusieurs trucs sur internet pour éviter le problème, mais quant à moi, j'ai préféré tout simplement marquer l'objet à l'état "Modified" avant de l'utiliser pour retrouver la commune correspondante :

        // Enregistre les modifications
        db.Entry(theater).State = EntityState.Modified;
        var place = db.Places.Find(theater.Place_ID);
        theater.KeyTheater = StringHelper.Slugify(place.Caption + " " + theater.ShortName);
        db.SaveChanges();

mercredi 27 mars 2013

Convertir un projet ASP.NET MVC 3 en MVC 4

J'ai migré mon application Répertoir vers ASP.NET MC 4. Comme le dit la doc :

The simplest way to upgrade is to create a new ASP.NET MVC 4 project and copy all the views, controllers, code, and content files from the existing MVC 3 project to the new project. Upgrading an ASP.NET MVC 3 Project to ASP.NET MVC 4

Créer un projet ASP.NET MVC 4 réellement vide

Note : le projet vide est déjà pas mal vide, mais il intègre Web API par défaut et je n'en ai pas l'utilité.

Créer un projet "Empty"

  • Renommer le dossier C:\MVC\Repertoir en C:\MVC\Repertoir3
  • Sous Visual Studio 2010, créer un nouveau projet "Repertoir" de type "ASP.NET MVC 4 Web Application" dans le dossier "C:\MVC"
  • Sélectionner le template "Empty" avec bien évidemment :
    • Razor comme View Engine
    • Ok pour créer le projet de test unitaire

Supprimer Web API du projet

  • Pour le projet Repertoir : Références, clic-droit et "Gérer les packages NuGet..."
  • Désinstaller "Microsoft ASP.NET Web API" => signale que va aussi désinstaller :
    • Microsoft.AspNet.WebApi.WebHost
    • Microsoft.AspNet.WebApi.Core
    • Microsoft.AspNet.WebApi.Client
    • Newtonsoft.Json
    • Microsoft.Net.Http
  • Confirmer que c'est OK => il ne reste plus que :
    • Microsoft.AspNet.Mvc
    • Microsoft.AspNet.Razor
    • Microsoft.AspNet.WebPages
    • Microsoft.Web.Infrastructure

Supprimer Web API du projet de test unitaire

  • Pour le projet Repertoir.Tests : Références, clic-droit et "Gérer les packages NuGet..."
  • Désinstaller "Microsoft.AspNet.WebApi.WebHost" => signale que va aussi désinstaller :
    • Microsoft.AspNet.WebApi.Core
    • Microsoft.AspNet.WebApi.Client
    • Newtonsoft.Json
    • Microsoft.Net.Http
  • Confirmer que c'est OK => il ne reste plus que :
    • Microsoft.AspNet.Mvc
    • Microsoft.AspNet.Razor
    • Microsoft.AspNet.WebPages
    • Microsoft.Web.Infrastructure

Finaliser le projet vide

  • Dans le projet Repertoir / App_Start, supprimer le fichier WebApiConfig.cs
  • Dans Repertoir / Global.asax.cs, supprimer la ligne WebApiConfig.Register(GlobalConfiguration.Configuration); dans Application_Start().
  • Vérifier que tout est OK :
    • Fichier / Enregistrer tout
    • Générer / Regénérer la solution
    • => La regénération globale a réussi

Ajouter quelques packages

Avant de commencer, faire un clic-droit sur la solution "Repertoir" et sélectionner "Activer la restauration du package NuGet".

Ensuite, faire clic-droit sur Repertoir /Références et "Gérer les packages NuGet..." pour installer les packages dont j'ai besoin :

  • AutoMapper
  • EntityFramework
  • jQuery
  • jQuery.Validation
  • LowercaseRoutesMVC (pas LowercaseRoutesMVC4 puisque Web API a été éjecté)
  • Microsoft.jQuery.Unobtrusive.Validation
  • MiniProfiler
  • MiniProfiler.EF
  • Modernizr

Puis clic-droit sur Repertoir.Tests /Références et "Gérer les packages NuGet..." pour installer les packages suivants :

  • EntityFramework
  • Moq
  • MvcRouteUnitTester

Vérifier que tout est OK :

  • Fichier / Enregistrer tout
  • Générer / Regénérer la solution
  • => La regénération globale a réussi

Copier les sources du projet MVC 3 vers MVC 4

Dans le cas du projet Repertoir, cela consiste à copier :

  • le contenu de /App_Data
  • le répertoire /Contents
  • le contenu de /Controllers
  • le répertoire /Helpers
  • le contenu de /Models
  • le fichier /Scripts/chosen.jquery.fr.js
  • le fichier /Scripts/gmaps.js
  • le contenu de /Views (à l'exception du Web.Config)

Et ne pas oublier d'inclure ces différents fichiers dans le projet (à l'exception des fichiers contenus dans /App_Data)

Pour le projet Repertoir.Tests, il faut copier :

  • le répertoire /Controllers
  • le répertoire /Helpers
  • le fichier /RoutesTest.cs

Là aussi, penser à inclure ces nouveaux fichiers dans le projet.

Mettre à jour les fichiers de configuration

  • Repertoir/Web.Config :
    • recopier la section <connectionStrings>
    • <customErrors mode="RemoteOnly" defaultRedirect="~/Error" />
    • Les sections <authentication>, <membership>, <profile> et <roleManager> sont absentes (sans doute parce que je suis parti d'un template vide) mais je n'en ai pas besoin.
  • Repertoir/Web.Release.Config : recopier celui de Repertoir3 (pour la transformation lors de la mise en production sur Appharbor)
  • Repertoir/Views/Web.Config : ajouter <add namespace="Repertoir.Helpers" /> à <system.web.webPages.razor>
  • Repertoir.Tests/App.Config : recopier la section <connectionStrings>

Adapter le Global.asax

using System;
using System.Web.Mvc;
using System.Web.Routing;
using Repertoir.Helpers;
using Repertoir.Models;
using StackExchange.Profiling;

namespace Repertoir
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            ModelBinders.Binders.Add(typeof(string), new StringModelBinder());

            ViewEngines.Engines.Clear();
            ViewEngines.Engines.Add(new RazorViewEngine());

            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            AutoMapperConfiguration.Configure();

            MiniProfilerEF.Initialize(true);
        }

        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            if (Request.IsLocal) MiniProfiler.Start();
        }

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            MiniProfiler.Stop();
        }
    }
}

Mise au point des routes

Il faut également adapter /App_Start/RouteConfig.cs pour tenir compte de la route "Id_Slug" et du fait que j'utilise LowercaseRoutesMVC.

using System.Web.Mvc;
using System.Web.Routing;
using LowercaseRoutesMVC;

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

            routes.MapRouteLowercase(
                name: "Id_Slug",
                url: "{controller}/{action}/{id}/{slug}",
                defaults: new { controller = "Contacts", action = "Index" }
            );

            routes.MapRouteLowercase(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Contacts", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

Vérifier que tout est OK

  • Fichier / Enregistrer tout
  • Générer / Regénérer la solution

=> 5 erreurs "Repertoir.MvcApplication ne contient pas de définition pour RegisterRoutes" dans le projet Repertoir.Tests

Remplacer 5 fois la ligne :

MvcApplication.RegisterRoutes(routes);

par :

RouteConfig.RegisterRoutes(routes);

Revérifier que tout est OK

  • Fichier / Enregistrer tout
  • Générer / Regénérer la solution

=> La regénération globale a réussi

Lancer les tests unitaires

Test / Exécuter / Tous les tests de la solution

=> 2 erreurs sur 151 tests

Les deux tests TestIncomingRoutes et TestOutgoingRoutes lèvent une exception System.InvalidOperationException parce que la classe Repertoir.MvcApplication n'a pas de méthode RegisterRoutes. Problème un peu plus compliqué, mais pas insoluble. Là encore, il faut remplacer la ligne :

var tester = new RouteTester<MvcApplication>();

par :

var routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes);
var tester = new RouteTester(routes);

Test / Exécuter / Tous les tests de la solution

=> 151/151 réussi(s)

Lancer l'application

Boum !

L'exception SqlCeException n'a pas été gérée par le code utilisateur

The column name is not valid. [ Node name (if any) = c,Column name = CreatedOn ]

Zut ! J'avais complètement oublié ça : SqlException on EF 5 w/ .NET 4.5 => Entity Framework 5 expects CreatedOn column from MigrationHistory table.

Mais moi j'utilise SQL Server CE, alors ça le fait pas avec le truc pour "System.Data.SqlClient.SqlException". Sous Visual Studio, il faut donc faire Déboguer / Exceptions... puis [ Ajouter... ] et :

  • Type : Common Language Runtime Exceptions
  • Nom : System.Data.SqlServerCe.SqlCeException
  • Puis décocher les 2 cases ("Levé" et "Non géré par l'utilisateur")

Et enfin tout marche !!!!

Rebrancher Git

C'est là que la magie opèpe : copier le dossier ".git" et les fichiers ".gitattributes", ".gitignore" et "readme.md" de l'ancien projet C:\MVC\Repertoir3 vers le nouveau projet dans C:\MVC\Repertoir. C'est pas avec SourceSafe qu'on pourrait jouer à des trucs pareils...

Puis lancer Github for Windows et commiter "Migration ASP.NET MVC 4".

lundi 11 février 2013

Erreur génération template contrôleur avec SQL CE

J'ai encore eu un problème avec SQL Server :)

Ce coup-ci, c'est SQL CE qui me donne du soucis. J'ai commencé à faire des tests pour utiliser Twitter Bootstrap avec ASP.NET MVC. Après quelques essais de base, j'ai lancé la génération automatique d'un contrôleur et des vues afférentes pour avoir un peu plus de matière à travailler.

Je procède normalement :

  • Clic-droit sur le dossier Controllers dans l'explorateur de solution,
  • Ajouter,
  • Controller...

controller-sql-ce-add.jpg

Je clique sur le bouton [Add] et boum !

controller-sql-ce-error.jpg

Unable to retrieve metadata for 'Bootstrap3.Models.Events'. Access to the database file is not allowed. [ 1884,File name = c:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\Department.sdf,SeCreateFile ]

J'avais déjà eu ce problème sur un autre projet mais je pensais que c'était "normal" et que j'avais dû y casser quelque chose. Mais là, un projet tout neuf !

En fait, le problème vient du fait que j'utilise une base de données SQL Server CE que j'ai donc configurée dans le fichier Web.config :

<connectionStrings>
  <add name="DepartmentContext"
       connectionString="Data Source=|DataDirectory|Department.sdf"
       providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>

Et apparemment, le système de template de VS 2010 ou ASP.NET MVC 3 n'aime pas trop le |DataDirectory| pour figurer le sous-répertoire "App_Data".

Le problème disparaît en indiquant le vrai chemin pour ce dossier :

<connectionStrings>
  <add name="DepartmentContext"
       connectionString="Data Source=C:\MVC\Bootstrap3\Bootstrap3\App_Data\Department.sdf"
       providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>

mercredi 6 février 2013

La boucle "times" de Ruby en C#

Pour mon billet sur les boucles en Ruby, j'étais tombé sur une version de l'itérateur times de Ruby porté sur C# :

public static class Extensions
{
    public static void Times(this int count, Action action)
    {
        for (int i=0; i < count; i++)
        {
             action();
        }
    }

    public static void Times(this int count, Action<int> action)
    {
        for (int i=0; i < count; i++)
        {
             action(i);
        }
    }
}

Ca s'utilise aussi simplement que ça :

5.Times(() => Console.WriteLine("Hi"));
5.Times(i => Console.WriteLine("Index: {0}", i));

Source : Any chances to imitate times() Ruby method in C#? et c'est une réponse de Jon Skeet. Le Jon Skeet !

Et maintenant, grâce à cette extension, je peux remplacer ce "vieux" code :

for (int i = 0; i < 5; i++)
{
  var fois2 = i + i;
  Console.WriteLine(fois2);
}

Par ça :

5.Times(i => {
  var fois2 = i + i;
  Console.WriteLine(fois2);
});

Pour qu'il ait un meilleur goût de Ruby :

5.times do |i|
  fois2 = i + i
  puts fois2
end

mardi 29 janvier 2013

VS 2010 très très lent voire bloqué

Au boulot, certains de mes collègues voyaient leur Visual Studio 2010 se mettre à ramer comme pas possible et ils finissaient même par se retrouver bloqués à devoir attendre que VS 2010 finisse de "freezer" pour pouvoir espérer taper deux ou trois caractères ou esquisser quelques clics de souris.

Après avoir passé des éternités à tout essayer :

  • installer tous les derniers services packs disponibles,
  • tester toutes les combinaisons de paramétrages possibles,
  • supprimer les très rares extensions utilisées,
  • changer de PC,
  • ajouter des tonnes de mémoire
  • etc...

Ca ne donnait jamais rien et la seule vrai solution c'était de quitter tant bien que mal Visual Studio, éteindre le PC, redémarrer et partir en pause café avant de relancer VS 2010 et d'attendre le prochain ralentissement...

Puis un jour, un plus obstiné qui refusait de se plier à cette situation a réussi à isoler le problème au milieu de la solution de 19 projets et 7161 fichiers. D'abord le projet qui enclenchait la lenteur de VS 2010 dès qu'il était chargé, puis de fil en aiguille le fichier de ce projet directement responsable de ce ralentissement et enfin il a débusqué le bout de code "fautif" :

public IQueryOver<Contrat, Contrat> ContratPourImpression()
{
  var contrat = ContratSrv.QueryOver()
                          .Fetch(x => x.Rubriques).Eager
                          .Fetch(x => x.Destinataire).Eager
                          .Fetch(x => x.Destinataire.Centre).Eager
                          .Fetch(x => x.Destinataire.Specifiques).Eager
                          .Fetch(x => x.Employe).Eager
                          .Fetch(x => x.Employe.Paiement).Eager
                          .Fetch(x => x.Employe.Specifique).Eager
                          .Fetch(x => x.Employe.Papier).Eager
                          .Fetch(x => x.Employe.Papier.Type).Eager
                          .Fetch(x => x.Employe.Papier.Source).Eager
                          .Fetch(x => x.Employe.Nationalite).Eager
                          .Fetch(x => x.Agence).Eager
                          .Fetch(x => x.Agence.Centre).Eager
                          .Fetch(x => x.Agence.Societe).Eager
                          .Fetch(x => x.Agence.Societe.Siege).Eager
                          .Fetch(x => x.Bureau).Eager
                          .Fetch(x => x.Bureau.Implantation).Eager
                          .Fetch(x => x.Metier).Eager
                          .Fetch(x => x.Type).Eager
                          .Fetch(x => x.Categorie1).Eager
                          .Fetch(x => x.Categorie2).Eager
                          .Fetch(x => x.Cycle).Eager
                          .Fetch(av => av.Mission).Eager
                          .Fetch(av => av.Mission.Qualification).Eager
                          .Fetch(av => av.Mission.Specifique).Eager
                          .TransformUsing(Transformers.DistinctRootEntity);

  return contrat;
}

En supprimant ce code, Visual Studio redevenait rapide et le restait pendant toute la journée. Comme c'était malgré tout du code nécessaire, il a bien fallu trouver un plan B pour le conserver tout en évitant l'asphixie.

En cherchant bien, il est apparu que l'accumulation des "fetch" et des expressions lambdas avait pour résultat de bloquer Visual Studio quand il essayait d'analyser ces quelques lignes de code. Oui, parce que VS n'est pas qu'un simple éditeur de texte, il cherche aussi à comprendre ce qu'on lui tape...

La solution, ça a été de lui faire avaler cette expression par petites bouchées :

public IQueryOver<Contrat, Contrat> ContratPourImpression()
{
  var contrat = ContratSrv.QueryOver()
                          .Fetch(x => x.Rubriques).Eager
                          .Fetch(x => x.Destinataire).Eager
                          .Fetch(x => x.Destinataire.Centre).Eager
                          .Fetch(x => x.Destinataire.Specifiques).Eager;
  contrat = contrat.Fetch(x => x.Employe).Eager
                   .Fetch(x => x.Employe.Paiement).Eager
                   .Fetch(x => x.Employe.Specifique).Eager
                   .Fetch(x => x.Employe.Papier).Eager
                   .Fetch(x => x.Employe.Papier.Type).Eager;
  contrat = contrat.Fetch(x => x.Employe.Papier.Source).Eager
                   .Fetch(x => x.Employe.Nationalite).Eager
                   .Fetch(x => x.Agence).Eager
                   .Fetch(x => x.Agence.Centre).Eager
                   .Fetch(x => x.Agence.Societe).Eager;
  contrat = contrat.Fetch(x => x.Agence.Societe.Siege).Eager
                   .Fetch(x => x.Bureau).Eager
                   .Fetch(x => x.Bureau.Implantation).Eager
                   .Fetch(x => x.Metier).Eager
                   .Fetch(x => x.Type).Eager;
  contrat = contrat.Fetch(x => x.Categorie1).Eager
                   .Fetch(x => x.Categorie2).Eager
                   .Fetch(x => x.Cycle).Eager
                   .Fetch(av => av.Mission).Eager
                   .Fetch(av => av.Mission.Qualification).Eager
                   .Fetch(av => av.Mission.Specifique).Eager
                   .TransformUsing(Transformers.DistinctRootEntity);

  return contrat;
}

Slurp. Tout de suite ça devient bien plus appétissant !

lundi 21 janvier 2013

Créer une vue sous Access

Mon problème

Pour simplifier un vieux traitement, j'ai eu besoin de créer une vue légèrement compliquée sous Access.

Après avoir mis au point cette vue directement dans Access, j'ai voulu faire un script SQL pour la créer sur la base de données de production. Ce qui a donné un code tout simple :

CREATE VIEW dir_TagsByPlaces
AS
SELECT T5.Place_ID, T5.KeyPlace, T3.Tag_ID, T3.Caption
     , COUNT(T1.Link_ID) AS LinkCount
FROM   dir_Links      AS T1
     , dir_TagLinks   AS T2
     , dir_Tags       AS T3
     , dir_PlaceLinks AS T4
     , dir_Places     AS T5
WHERE  (T2.Link_ID  = T1.Link_ID)
AND    (T3.Tag_ID   = T2.Tag_ID)
AND    (T4.Link_ID  = T1.Link_ID)
AND    (T5.Place_ID = T4.Place_ID)
GROUP BY T5.Place_ID, T5.KeyPlace, T3.Tag_ID, T3.Caption
ORDER BY 2, 5 DESC

Mais quand j'ai voulu vérifier que ce script fonctionnait correctement, j'ai obtenu l'erreur Seules les requêtes SELECT simples sont autorisées dans VIEWS. (3766) !

Et même en enlevant le COUNT() et le GROUP BY ça restait trop compliqué...

Finalement, j'ai réussi à trouver le truc pour que ça marche. Je ne sais pas trop à quoi servent les vues sous Access, mais c'est pas ça qu'il faut utiliser quand on veut l'équivalent d'une vue dans les autres bases de données.

La solution, c'est de créer une procédure :

CREATE PROCEDURE dir_TagsByPlaces
AS
SELECT T5.Place_ID, T5.KeyPlace, T3.Tag_ID, T3.Caption
     , COUNT(T1.Link_ID) AS LinkCount
FROM   dir_Links      AS T1
     , dir_TagLinks   AS T2
     , dir_Tags       AS T3
     , dir_PlaceLinks AS T4
     , dir_Places     AS T5
WHERE  (T2.Link_ID  = T1.Link_ID)
AND    (T3.Tag_ID   = T2.Tag_ID)
AND    (T4.Link_ID  = T1.Link_ID)
AND    (T5.Place_ID = T4.Place_ID)
GROUP BY T5.Place_ID, T5.KeyPlace, T3.Tag_ID, T3.Caption
ORDER BY 2, 5 DESC

Et là, ça passe comme une lettre à la poste et si je regarde la liste des requêtes dans Access, j'y trouve bien ma "vue" dir_TagsByPlaces.

La documentation Access

CREATE PROCEDURE, instruction

Crée une procédure stockée.

Syntaxe

CREATE PROCEDURE procédure (typedonnées param1[, typedonnées param2][, ...]) AS instructionsql

L'instruction CREATE PROCEDURE se compose des éléments suivants :

  • procédure : Nom donné à la procédure.
  • param1, param2 : De un à 255 noms de champ ou paramètres (paramètre : valeur qui est attribuée à une variable au début d'une opération ou avant qu'une expression soit évaluée par un programme. Un paramètre peut être du texte, un nombre ou un nom d'argument affecté à une autre valeur.).
  • typedonnées : Un des types de données SQL Microsoft Access primaires ou un de leurs synonymes.
  • instructionsql : Instruction SQL telle que SELECT, UPDATE, DELETE, INSERT, CREATE TABLE, DROP TABLE, etc.

Exemple

CREATE PROCEDURE Sales_By_CountryRegion ([Beginning Date] DateTime, [Ending Date] DateTime)
AS
SELECT Customer, [Ship Address] 
WHERE [Shipped Date] Between [Beginning Date] And [Ending Date]

Notes

Une procédure SQL se compose d'une clause PROCEDURE (qui spécifie le nom de la procédure), d'une liste facultative de définitions de paramètres et d'une unique instruction SQL (instruction/chaîne SQL : expression qui définit une commande SQL, telles que SELECT, UPDATE ou DELETE, et qui inclut des clauses telles que WHERE et ORDER BY. Les instructions/chaînes SQL sont généralement utilisées dans des requêtes et dans des fonctions de regroupement.).

Le nom d'une procédure ne peut être identique à celui d'une table existante

lundi 14 janvier 2013

Supprimer les espaces lors du Model Binding

Il arrive quelquefois que les utilisateurs saisissent des données en "insérant" des espaces au début ou à la fin de leur saisie. Et c'est même très souvent le cas quand ils font du copier / coller. Par exemple, si pour saisir une adresse client, on fait un copier / coller depuis un site internet ou un fichier Excel, il y a un gros risque de se retrouver avec des espaces en début ou en fin de ligne.

Du temps de mes applications en WebForm, j'avais résolu ça en appliquant systématiquement un Trim() à tous les contrôles TextBox :

protected void btnUpdate_Click(object sender, System.EventArgs e)
{
  if (Page.IsValid == true)
  {
    Book obj = new Book();
    obj.title = txtTitle.Text.Trim();
    ...
    obj.Save();

De cette façon, je suis sûr que les colonnes textes de ma base de données ne sont pas polluées par des espaces oubliés en début ou en fin de texte. Sans cela, on se retrouve ensuite avec des trucs bizarres quand on fait des tris ou pire si on doit faire une recherche sur un libellé exact...

Depuis que j'utilise MVC, j'avais un peu laissé ça de côté.

//
// POST: /Books/Create
[HttpPost, ValidateAntiForgeryToken]

public ActionResult Create(Book book)
{
  if (ModelState.IsValid)
  {
    db.Entry(book).State = EntityState.Modified;
    db.SaveChanges();

Je vais quand même pas rajouter des lignes book.title = book.title.Trim() ... dans le code de mes actions ? Je pourrais aussi faire ça au niveau du modèle ou avoir une méthode d'extension suffisamment générique pour faire un Trim() sur toutes les propriétés de type string.

Mais, inspiré par un "vieux" billet de Phil Haack qui expliquait comment faire un model binder personnalisé (Model Binding Decimal Values), j'ai décidé de m'y essayer.

La technique consiste à créer un Model Binder spécifique qui remplacera le ModelBinder par défaut pour les chaînes de caractères de façon à effectuer un Trim() de la valeur postée avant qu'elle ne soit transmise à l'action du contrôleur.

public class StringModelBinder : IModelBinder
{
  /// <summary>
  /// Supprime les espaces en début et en fin des chaines de caractères saisies
  /// </summary>
  /// <param name="controllerContext"></param>
  /// <param name="bindingContext"></param>
  /// <returns></returns>
  public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
  {
    var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
    object actualValue = valueResult == null ? null : valueResult.AttemptedValue;

    if (actualValue != null)
    {
      actualValue = ((string)actualValue).Trim();
      if ((string)actualValue == string.Empty)
      {
        actualValue = null;
      }
    }

    return actualValue;
  }

Les 2 premières lignes commencent par récupérer la valeur. Si celle-ci n'est pas nulle, on lui applique un Trim(). Et dans le cas où la valeur alors obtenue est vide, on renvoie null, sinon on renvoie la valeur trimée.

Il faut ensuite déclarer ce ModelBinder. Cela se passe dans la méthode Application_Start() du fichier Global.asax.cs :

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();

  ModelBinders.Binders.Add(typeof(string), new StringModelBinder());

Et maintenant, même si un utilisateur laisse passer quelques espaces en début ou en fin de saisie, ceux-ci seront automatiquement supprimés avant même que les données soient transmises à l'action.

L'avantage par rapport à coder des Trim() explicites, c'est qu'on n'a pas à y penser. Il n'y a "rien" à faire puisque ça se fait tout seul...

mercredi 19 décembre 2012

Les boucles en Ruby

La boucle "for"

Avec mon passé de développeur Basic, je commence par la boucle "for".

10 FOR I = 0 TO 5
20   PRINT I
30 NEXT

En Ruby, le for sert à parcourir les différentes valeurs des objets qui sont capables de répondre à la méthode each (comme les Array et les Range).

C'est donc plus une instruction "for ... in" (un peu comme le foreach du C#). Utilisé avec un tableau, cela donne le code suivant :

for i in [0, 1, 2, 3, 4, 5] do
  puts "- nombre = #{i}"
end

=>

- nombre = 0
- nombre = 1
- nombre = 2
- nombre = 3
- nombre = 4
- nombre = 5

Apparemment, Ruby traite ça comme si c'était :

[0, 1, 2, 3, 4, 5].each do |i|
  puts "- nombre = #{i}"
end

Et pour les Range, cela donne :

for i in (0..5) do
  puts "- nombre = #{i}"
end

=>

- nombre = 0
- nombre = 1
- nombre = 2
- nombre = 3
- nombre = 4
- nombre = 5

Que Ruby traite comme :

(0..5).each do |i|
  puts "- nombre = #{i}"
end

Par conséquent, il est beaucoup plus naturel d'employer les each en Ruby que le for.

Les itérations

each est donc un "itérateur" : il permet de parcourir tous les éléments d'un ensemble (un tableau, un range...) un par un.

times est un autre itérateur qui est utilisé assez couramment en Ruby. Par exemple, pour refaire la boucle des exemples précédents qui tourne un nombre précis de fois, on aura plus tendance à utiliser cette méthode times :

6.times do |i|
  puts "- nombre = #{i}"    # => 0, 1, 2, 3, 4, 5
end

Note : si le traitement à l'intérieur de la boucle n'a pas besoin de la valeur de l'indice, il n'est pas nécessaire de le faire apparaître :

3.times do
  puts "Penny"      # => Penny, Penny, Penny
end

Il existe 3 autres itérateurs pour boucler d'une valeur à l'autre et qui se rapprocherait peut être plus du fonctionnement du "FOR ... NEXT" de Basic :

# FOR I = O TO 5 : ... : NEXT
0.upto(5) do |i|
  puts "- nombre = #{i}"      # => 0, 1, 2, 3, 4, 5
end

# FOR I = 5 TO 0 STEP -1 : ... : NEXT
5.downto(0) do |i|
  puts "- nombre = #{i}"      # => 5, 4, 3, 2, 1
end

# FOR I = 0 TO 5 STEP 2 : ... : NEXT
0.step(5, 2) do |i|
  puts "- nombre = #{i}"      # => 0, 2, 4
end

Les boucles "while" et "until"

Là il s'agit d'instructions qui font vraiment parti du langage Ruby (pas comme le for qui est plus une "surcouche" pour le each).

La boucle "while" permet de répéter le traitement d'un bloc de code tant qu'une condition est vraie :

i = 0
while i <= 5 do
  puts "- nombre = #{i}"    # => 0, 1, 2, 3, 4, 5
  i += 1
end

Et la boucle "until" sert pour répéter le traitement jusqu'à ce que la condition soit vraie :

i = 0
until i > 5 do
  puts "- nombre = #{i}"    # => 0, 1, 2, 3, 4, 5
  i += 1
end

On peut même arriver à faire des trucs comme :

i = 0
puts "- nombre = #{i += 1}" while i <= 5    # => 1, 2, 3, 4, 5, 6

La boucle "loop"

Et finalement, Ruby dispose aussi d'une autre instruction native pour effectuer des boucles.

loop do
   ...
end

Le seul truc, c'est qu'il faut prévoir quelque chose dans le corps de la boucle pour en sortir, sinon ça bouclera éternellement.

C'est pour cela qu'il existe l'instruction break :

i = 0
loop do
  break if i > 5
  puts "- nombre = #{i}"    # => 0, 1, 2, 3, 4, 5
  i += 1
end

Et comme on fait le "break" sur une condition, on a droit à toutes les variantes de notre choix :

  • break if i > 5
  • if i > 5 break
  • break unless i <= 5
  • unless i <= 5 break

On peut même placer l'instruction break où on veut dans la boucle :

i = 0
loop do
  puts "- nombre = #{i}"    # => 0, 1, 2, 3, 4, 5
  i += 1
  break if i > 5
end

i = 0
loop do
  puts "- nombre = #{i}"    # => 0, 1, 2, 3, 4, 5, 6
  break if i > 5
  i += 1
end

Dans le même style que break, il existe next qui permet en quelque sorte de "passer un tour" :

i = 0
loop do
  i += 1
  break if i > 5
  next if i < 3
  puts "- nombre = #{i}"    # => 3, 4, 5
end

Portée des variables

En C#, une variable déclarée dans la boucle est locale et ne peut pas être utilisé en dehors de la boucle :

for (int i = 0; i < 6; i++)
{
  var fois2 = i + i;
  Console.WriteLine(fois2);
}
Console.WriteLine(fois2);

=> Le nom 'fois2' n'existe pas dans le contexte actuel

C'est pareil en Ruby où une variable qui apparaît dans la boucle est locale :

6.times do |i|
  fois2 = i + i
  puts fois2
end
puts fois2

=> c:/Ruby/Rubyq.rb:5:in `<main>': undefined local variable or method `fois2' for main:Object (NameError)

Attention toutefois, car la variable ne sera pas locale du moment qu'elle est apparue avant dans le source, y compris dans le cas où le code où elle apparaissait n'a pas été exécuté :

if (1 == 2)
  fois2 = -3
end
6.times do |i|
  fois2 = i + i
  puts fois2
end
puts fois2

=> Le dernier puts fois2 renverra 10.

D'autre part, la variable utilisée pour indicer la boucle est toujours locale à la boucle, même dans le cas où une variable de même nom existe en dehors de la boucle.

i = 99
6.times do |i|
  puts i
end
puts i

=> Le dernier puts i affichera 99 (et pas 10).

mardi 18 décembre 2012

Windows, PowerShell, Ruby et les accents

Depuis que j'essaie de faire du Ruby, j'ai toujours eu des problèmes avec les caractères accentués du français.

Pour Sinatra, il y a eu le jour où je ne sais plus quelle version a résolu le problème grâce à la célèbre ligne "# encoding: UTF-8" en début de fichier :)

Mais pour les applications "console" ou les tests sous IRB, rien à faire :

Windows PowerShell
Copyright (C) 2012 Microsoft Corporation. Tous droits réservés.

C:\Users\michel> cd C:\Ruby

C:\Ruby> irb
irb(main):001:0> str = "Réel"
=> "R\x82el"
irb(main):002:0>

Et ce soir je suivais les exemples de String in Ruby pour les développeurs C# et j'en ai eu marre de devoir me priver des accents.

Alors j'ai cherché "Windows Ruby caractères français"... Mais bon, il faut se rendre à l'évidence. Tout le monde s'accorde pour dire que la version 1.9.3 (1.9.x ?) de Ruby gère très bien les accents.

Bon. Ok. Le problème vient de Windows. "PowerShell caractère français" ? De liens en liens j'arrive sur la page $OutputEncoding to the rescue (j'ai un peu perdu le fil des liens qui m'y a conduit, mais merci à eux).

Et il est là, dans le dernier commentaire, comme une dernière chance :

chcp 1250

Et ce 1250 me rappelle mes années Windows-1252 (sous ASP3 je crois).

Je lance une énième tentative :

C:\Ruby> chcp 1252
Page de codes active : 1252

C:\Ruby> irb
irb(main):001:0> str = "Réel"
=> "Réel"

Ca marche !!!

Je vérifie :

irb(main):002:0> str.each_char.map { |c| c }
=> ["R", "é", "e", "l"]

Et pour le plaisir :

irb(main):003:0> __ENCODING__
=> #<Encoding:Windows-1252>

Ruby sous Windows c'est génial !

vendredi 14 décembre 2012

Mise à jour Ruby 1.9.3 sous Windows 7

Je viens enfin de mettre à jour mon installation Ruby 1.9.2 alors je note pour le jour où Ruby 2.0 sortira.

Etape 1 : désinstaller Ruby 1.9.2

Je procède à quelques sauvegardes personnelles avant de lancer la désinstallation.

  • C:\Ruby\root (pour archivage parce que j'y ai quelques fichiers de notes)
  • C:\Ruby\_projets (mes projets sous Sinatra)
  • C:\Ruby\Rubyq (pour faire quelques essais)

Puis désinstaller Ruby 1.9.2 et supprimer le répertoire C:\Ruby.

Etape 2 : Installer Ruby 1.9.3

Installer Ruby 1.9.3 (depuis RubyInstaller pour Windows)

  • dans C:\Ruby
  • cocher "Add Ruby executables to your PATH"
  • cocher "Associate .rb and .rbw files with this Ruby installation"

Installer DevKit 4.5.2 (re-depuis RubyInstaller pour Windows)

  • le décompacter dans C:\Ruby\DevKit
  • CD C:\Ruby\DevKit
  • ruby dk.rb init
  • ruby dk.rb install

Ce coup-ci, je pense à créer un fichier .gemrc (avec SublimeText parce que l'explorateur de fichiers n'aime pas un nom qui commence par un point) dans mon répertoire utilisateur et j'y enregistre les deux lignes suivantes :

install: --no-rdoc --no-ri
update:  --no-rdoc --no-ri

Source : How to make --no-ri --no-rdoc the default for gem install?

Mettre à jour les quelques gems installées par défaut

  • gem update --system
  • gem update

Etape 3 : Installer Sinatra

Sinatra lui-même

  • gem install sinatra
  • gem install sinatra-reloader (pour recharger automatiquement les sources modifiés)

SQLite et DataMapper

  • Récupérer la dernière version de sqlite.dll sur sqlite.org et la copier dans C:\Ruby\bin
  • gem install sqlite3 (c'est le nouveau nom de sqlite3-ruby)
  • gem install data_mapper
  • gem install dm-sqlite-adapter

Les 2 gems dont je vais avoir besoin dans l'immédiat

  • gem install haml
  • gem install heroku (préconise toolbet qui est en fait heroku + foreman + git)

Pour les autres gems (pony, rack-flash...), je les installerai au fur et à mesure des besoins.

jeudi 13 décembre 2012

Tester Entity Framework avec SQL CE

Un petit rappel

Au cours de mes recherches pour faire des tests unitaires avec Entity Framework, j'étais tombé sur un article de Code Project qui présentait deux méthodes plus ou moins complémentaires pour cela, dont l'utilisation d'une base de données SQL Server CE créée "au vol" spécialement pour faire tourner les tests unitaires : Two strategies for testing Entity Framework - Effort and SQL CE.

Etant donné qu'en développement j'ai déjà tendance à privilégier SQL Server CE plutôt que SQL Server Express, ça ne pouvait pas mieux tomber...

En plus de ça, j'avais dans mes favoris un autre article de chez Arrange Act Assert qui me sert souvent d'inspiration pour mes tests unitaires et qui proposait le même genre d'approche dans son billet Code First Entity Framework Unit Test Examples.

Alea jacta est. Vu que tout le reste n'était pas ultra convainquant, autant essayer au plus simple !

Un peu de code

Ce qu'il me faut donc, c'est avoir une base de données SQL CE dédiée pour les tests unitaires. Pour cela, je commence par ajouter une chaîne de connexion dans le fichier App.config de mon projet de tests :

<connectionStrings>
  <add name="RepertoirContext"
       connectionString="Data Source=|DataDirectory|\Repertoir_UnitTest.sdf"
       providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>

Ensuite, il faut que le projet de tests unitaires fasse référence à cette base de données via un objet RepertoirContext hérité de DbContext. Dans mes classes contrôleurs, je me contente normalement d'une ligne :

private RepertoirContext db = new RepertoirContext();

Mais là, je vais devoir faire un tout petit plus compliqué :

private RepertoirContext db { get; set; }

public ContactsControllerTest()
{
    Database.SetInitializer<RepertoirContext>(new DropCreateDatabaseAlways<RepertoirContext>());
    db = new RepertoirContext();
}

La ligne Database.SetInitializer ... permet de s'assurer que la base de données est re-créée à chaque fois que la classe ContactsControllerTest est instanciée (et donc à chaque fois que les tests sont lancés).

Et là, il ne me reste plus qu'à faire passer le DbContext de ma classe de tests au contrôleur à tester, ce qui peut se faire tout simplement au moment où je l'instancie :

var controller = new PeopleController(db);

Puis au niveau du contrôleur j'ajoute un constructeur qui attend un paramètre de type DbContext (ou plus précisément RepertoirContext) pour initialiser le contexte utilisé par le contrôleur :

private RepertoirContext db;

public ContactsController() { db = new RepertoirContext(); }
public ContactsController(RepertoirContext context) { db = context; }

La ligne public ContactsController() { db = new RepertoirContext(); } correspond au constructeur par défaut du contrôleur qui initialise un DbContext correspondant à la "vraie" base de données.

En pratique, j'aurai pu me passer de cette ligne de code et mettre en place un système d'injection de dépendance. Mais pour 1 seule ligne de code...

Un premier test

Je peux maintenant tenter un premier vrai test sur ContactsController pour vérifier que son action Index() renvoie bien la vue par défaut :

//
// GET: /Contacts/

public ViewResult Index()
{
  var contacts = db.Contacts.To_ContactList();
  return View(contacts);
}

Et le test unitaire :

[TestMethod]
public void ContactsIndex_doit_renvoyer_la_vue_par_defaut()
{
  // Arrange
  var controller = new ContactsController(db);

  // Act
  var result = controller.Index();

  // Assert
  Assert.IsNotNull(result);
  Assert.IsTrue(string.IsNullOrEmpty(result.ViewName));
}

Jusqu'ici tout va bien... Ca fonctionne en local. Mais qu'est-ce que ça va donner quand je vais déployer vers AppHarbor ? Est-ce qu'il va me permettre de créer une base SQL CE temporaire pendant et pour mes tests ?

  • Je commite
  • Je pousse vers GitHub
  • Je compte jusqu'à 3
  • je vais voir ce que ça donne sur AppHarbor
  • Ca a marché !!!

Une conclusion

C'est bon, je vais pouvoir faire tout un tas de tests unitaires (71 pour l'instant) pour vérifier que les contrôleurs font correctement ce que j'attends d'eux.

L'avantage, c'est que même si ce n'est "que" du SQL Server CE, c'est du SQL Server quand même et donc suffisamment proche d'un mode "production". En tout cas, c'est bien suffisant pour moi et avec ça j'ai réglé mes problèmes de tests unitaires liés à Entity Framework.

lundi 19 novembre 2012

Mémo de plus de 4000 caractères sous SQL CE

Je fais une petite application pour migrer une "vieille" base de données Access vers SQL Server CE et je suis tombé sur une erreur d'un autre âge lorsque j'essaie de transférer le contenu d'une colonne "mémo" :

> The field Content must be a string or array type with a maximum length of '4000'.

Pourtant, j'avais bien expliqué à Entity Framework que ma colonne devait être de type "ntext" :

[Column(TypeName = "ntext")]
[DataType(DataType.MultilineText)]
public string Content { get; set; }

Ce qui devrait m'affranchir de cette barrière des 4000 octets : SQL Server Compact Data Types

Une première recherche de "The field * must be a string or array type with a maximum length of '4000'." m'envoie sur une solution à première vue un peu brutale : Error storing Image in SQL CE 4.0 with ASP.NET MVC 3 and Entity Framework 4.1 Code First.

Le truc, c'est donc de carrément désactiver la validation (?!?!) de l'entité au moment de la persister dans la base de données : DbContext.Configuration.ValidateOnSaveEnabled = false. Je ne suis pas très certain de trouver ça bien...

Mais ça a le mérite de fonctionner.

Heureusement, en y regardant de plus près, cette solution propose aussi un lien vers le billet Saving images and long strings to SQL Server Compact with Entity Framework 4.1 Code First qui m'a permi de découvrir une bien meilleure solution.

Dans le cas des colonnes "mémo", il suffit d'ajouter un attribut [MaxLength] à la colonne pour forcer Entity Framework :

  1. à créer une colonne de type "ntext" (l'équivalent de l'attribut [Column(TypeName = "ntext")]) que j'utilisais jusqu'ici)
  2. et surtout à bien comprendre qu'il ne s'agit pas d'une simple colonne texte limitée à 4000 caractères.

Et donc, avec :

[MaxLength]
[DataType(DataType.MultilineText)]
public string Content { get; set; }

Ca marche !

lundi 12 novembre 2012

Héritage en génération des routes ASP.NET MVC

Quand ça marche

J'ai développé une petite application ASP.NET MVC pour gérer des "brochures" de voyages. Chaque brochure est constitué d'un ensemble de voyages et chaque voyage se décompose en plusieurs sections (en quelque sorte des étapes).

Habituellement, dans les exemples basiques pour ASP.NET MVC, les routes sont sous la forme {controller}/{action}/{id} où le paramètre "id" correspond à l'élément à traiter. On a donc :

  • /brochures/edit/12 => action pour modifier la brochure n° 12
  • /voyages/delete/34 => action pour suppression du voyage n° 34
  • /sections/edit/56 => action pour modifier la section n° 56

Dans mon cas, j'avais besoin d'avoir en permanence l'identifiant de la brochure dans l'URL pour pouvoir afficher facilement son titre dans l'en-tête des pages. Ca aurait pu donner quelque chose dans ce genre :

  • /brochures/edit/12
  • /voyages/delete/34?brochure_id=12
  • /sections/edit/56?brochure_id=12

Plutôt que de me trimbaler et de gérer cet identifiant en QueryString, j'avais eu la super idée d'en faire un élément de la route en re-définissant la route par défaut :

routes.MapRouteLowercase(
  "Default",
  "{root_id}/{controller}/{action}/{id}",
  new {
    root_id = "0",
    controller = "Home",
    action = "Index",
    id = UrlParameter.Optional
  }
);

De cette façon, une fois que j'avais sélectionné une brochure, mes routes étaient de la forme :

  • /12/brochures/edit/12
  • /12/voyages/delete/34
  • /12/sections/edit/56

Ce qui est vraiment pratique, c'est que ça n'a quasiment rien changé à mon code. J'ai pu continuer à générer mes liens à coups de @Html.ActionLink() sans avoir à y indiquer à chaque fois la valeur pour "root_id". En effet, le système de génération des URLs de ASP.NET MVC est suffisamment bien foutu pour gérer le cas où un élément de la route n'est pas défini.

Dans ce cas, il regarde si cet élément existe dans l'URL de la page en cours et si c'est le cas, @Html.ActionLink() ré-utilise cette valeur pour générer la nouvelle route.

Et en fait, je viens de m'apercevoir tout récemment que c'est encore plus sophistiqué que ça !

Quand ça ne marche pas

Dans un autre projet, j'ai encore eu besoin de faire suivre une valeur tout au long des différentes actions liées à une série de traitements. Dans ce cas, il s'agissait de mémoriser d'où provenait l'utilisateur pour pouvoir l'y reconduire à la fin de la série de traitements. Comme c'était une valeur plus "accessoire" que le numéro de brochure, j'ai préféré la faire apparaître à la fin de la route :

routes.MapRoute(
  "Sirens",
  "Sirens/{action}/{source_id}/{id}",
  new { controller = "Sirens", action = "Index", source_id = 0, id = "" }
);

Et à ma grande déception, mon paramètre "source_id" n'était pas repris d'URL en URL mais initialisé à chaque fois à zéro ! Au début, j'ai opportunément accusé l'utilisation de helpers @Html.ActionLink() fortement typés au lieu de la version standard à base de "chaînes magiques". Bien tenté, mais c'était pas ça...

Puis j'ai constaté un truc vraiment paradoxal : ça marchait dans certains cas mais pas tous les cas (la définition du bug en quelque sorte).

Depuis la vue Details, les URLs qui pointaient vers l'action Details pour un autre numéro de Siren fonctionnaient, mais pas celles qui pointaient vers l'action Edit pour le numéro Siren en cours...

Par exemple, depuis la fiche du Siren 732829320 (URL /Sirens/Details/99/732829320), j'obtenais les URLs :

  • /Sirens/Details/99/123456789 : ok
  • /Sirens/Details/99/111111111 : ok
  • /Sirens/Edit/0/732829320 : ko

En creusant, j'ai compris d'où venait le problème. En fait, le système de génération d'URLs ne se contente pas de recopier bêtement un élément manquant depuis l'URL courante. Il faut que cet élément fasse en quelque sorte parti de la "route" en cours.

Depuis la route "/ Sirens / Details / 99 / 732829320", quand on veut afficher un autre Siren, on reste en quelque sorte dans le même chemin : "/ Sirens / Details / 99 / 111111111" => le paramètre "source_id" est hérité depuis l'URL courante.

Par contre, pour modifier la fiche du Siren en cours, on bifurque dès la sortie du contrôleur : "/ Sirens / Edit ...". Comme on a changé d'itinéraire, le paramètre "source_id" est ré-initialisé à zéro.

Mise à jour : j'ai trouvé l'explication de ce comportement dans le chapitre 9 de Professional ASP.NET MVC consacré au routing :

The ambient values are the current values for those parameters within the RouteData for the current request.

Par conséquent, la solution est toute simple. Il suffit de placer le paramètre suffisamment tôt dans la route pour qu'il n'y ait pas besoin de sortir de la route en cours tout au long des traitements :

routes.MapRoute(
  "Sirens",
  "Sirens/{source_id}/{action}/{id}",
  new { controller = "Sirens", action = "Index", source_id = 0, id = "" }
);

lundi 5 novembre 2012

Tests unitaires pour Entity Framework

J'avais longtemps repoussé l'écriture de tests unitaires pour les contrôleurs de mon application Répertoir, parce qu'ils accèdent à la base de données via Entity Framework et que je ne savais pas trop comment m'y prendre pour tester ça.

J'ai pensé, réfléchi, exploré plus ou moins 3 pistes différentes :

  • Ajouter une couche "service" entre les contrôleurs et Entity Framework
  • Mocker le DbContext d'Entity Framework
  • Utiliser une base de données SQLite en mémoire

Méthode 1 : Ajouter une couche

Comme j'essaie de faire au plus simple, j'ai conservé le mode "standard" des contrôleurs générés par VS 2010 et j'instancie un objet DbContext :

private RepertoirContext db = new RepertoirContext();

Les contrôleurs utilisent ensuite directement cet objet DbContext pour communiquer avec la base de données, que ce soit pour faire des requêtes ou pour la mettre à jour :

var contact = db.Contacts.Find(person.Contact_ID);
contact.Update_With_ViewPerson(person);
db.Entry(contact).State = EntityState.Modified;
db.SaveChanges();

Les "bonnes pratiques" de codage auraient tendance à considérer que cette proximité entre le contrôleur et la base de données (la "dépendance" comme ils disent) n'est pas très convenable.

Il serait plus de plus bon goût d'ajouter une couche "service" et de s'en servir au niveau des contrôleurs pour éviter d'accéder directement à Entity Framework :

private RepertoirContactService srv = new RepertoirContactService();
...
var contact = srv.Find(person.Contact_ID);
srv.Update(person);

Hourra ! Le contrôleur est devenu tellement fin et tellement élégant ("Look how thin I am. Thin and dainty.") et surtout il ne fait plus rien par lui même ("Moisturize me! Moisturize me!").

Maintenant, c'est une vraie partie de plaisir de faire des tests unitaires pour les contrôleurs :

  • Il faut un peu "mocker" le service.
  • Ajouter une pincé d'injection de dépendance au contrôleur.
  • Et roule ma poule !
[TestMethod]
public void PeopleDetails_doit_renvoyer_le_contact_demande_a_la_vue()
{
  // Arrange
  IRepertoirContactService srv = new MockedRepertoirContactService();
  var controller = new PeopleController(srv);
  srv.FakeSource.Add(new Contact
                      {
                          Contact_ID = 1,
                          DisplayName = "test",
                          Phone1 = "0"
                      };)

  // Act
  var result = controller.Details(1);

  // Assert
  var model = result.ViewData.Model as ViewPerson;
  Assert.AreEqual("test", model.DisplayName, "Model aurait dû correspondre au contact demandé");
  Assert.AreEqual("0", model.Phone1, "Model aurait dû correspondre au contact demandé");
}

En "vrai", je vais devoir :

  • Définir une interface.
  • Implémenter la classe RepertoirContactService en faisant appel à DbContext ou encore mieux passer par une interface IRepertoirContactRepository qui fera appel au DbContext.
  • Implémenter MockedRepertoirContactService.
  • Ajouter de l'injection de dépendance au niveau du contrôleur.
  • Etc...

Conclusion : C'est beaucoup de boulot pas très zintéressant pour un projet censé m'amuzer...

Méthode 2 : Mocker le DbContext

Certains ont pris le parti de mocker directement le DbContext, ce qui évite d'avoir à ajouter une couche et ce qui me plait beaucoup plus.

J'ai étudié et essayé de faire ça à partir des 2 ou 3 billets suivants :

Je trouve cette méthode bien "mieux", même si elle impose de créer quelques interfaces. Et idéalement, ce serait encore mieux si les gens de Entity Framework avaient un peu plus préparé le terrain.

Dans le même genre de truc, je suis tombé sur un article sur Code Project qui présente Two strategies for testing Entity Framework, dont Effort, un provider ADO.NET pour Entity Framework qui travaille entièrement en mémoire. Ca commence à devenir intéressant, mais comme le dit l'article, on ne travaille pas réellement avec une "vraie" base de données et surtout tout n'est pas géré...

Conclusion : c'est pas mal du tout et ça pourrait le faire mais ça demande un peu plus de temps que ce que je peux veux y consacrer.

Méthode 3 : SQLite en mémoire

Le problème (entre autres) des tests unitaires qui attaquent la base de données, c'est que c'est lent. Il faudrait donc avoir une base de données capable de travailler totalement en mémoire pour que ça aille suffisament vite et que cela ne soit pas pénalisant.

Une base de données en mémoire, je connais... C'est SQLite avec une chaîne de connexion du style :

Data Source=:memory:;Version=3;New=True;

Malheureusement, la version actuelle du provider de SQLite (System.Data.SQLite.dll) ne gère pas la partie "Code First" de Entity Framework. (lien vers FAQ qui dit ça).

Deux solutions possibles pour pallier à ces difficultés :

  • Passer par le provider dotConnect for SQLite de Devart qui gère complètement ça. Mais c'est payant...
  • Générer "à la main" les scripts de création et d'initialisation de la base de données. Mais c'est lourd...

Alors bien entendu, j'aurai pu utiliser une version d'évaluation de dotConnect for SQLite ou bien me résoudre à scripter l'unique table de ma base de données... Mais je n'ai pas eu trop envie de "bidouiller" pour arriver à faire un truc qui devrait être naturel.

Conclusion : c'est vraiment dommage, mais ça ne vas pas être possible.

La méthode retenue

J'avais donc le choix entre laisser tomber les tests unitaires de mes contrôleurs ou tester coûte que coûte.

Et j'ai donc décidé qu'en attendant que Microsoft me facilite la vie en proposant un SQL Server Memory Edition ou sorte un Provider For SQLite Entity Framework Code First Ready, un tout bête SQL Server CE devrait pouvoir faire l'affaire...

Je vais maintenant essayer de ne pas trop tarder pour faire un billet expliquant comment je m'y suis pris. C'est enfin fait : Tester Entity Framework avec SQL CE.

mercredi 17 octobre 2012

Tests unitaires de PeopleController

Ce billet vient conclure ma série de billets sur les tests unitaires consacrés au contrôleur PeopleController.

Il sert de récapitulatif sur les tests unitaires présentés dans les billets précédents et présente succintement les tests unitaires réalisés pour les actions Edit() et Delete().

Tests unitaires action Detail()

Le code testé :

// GET: /People/Details/5
public ViewResult Details(int id)
{
  var contact = db.Contacts.Find(id);
  var person = contact.To_ViewPerson();

  return View(person);
}

Les 3 tests unitaires :

  • PeopleDetails doit renvoyer la vue par defaut
  • PeopleDetails doit renvoyer un objet ViewPerson a la vue
  • PeopleDetails doit renvoyer le contact demande a la vue

Référence : Tester l'action Detail()

Tests unitaires action Create()

Le code GET testé :

// GET: /People/Create
public ViewResult Create(int ParentID = 0)
{
  var contact = new Contact();
  if (ParentID != 0)
  {
    contact.Company_ID = ParentID;
    contact.Company = db.Contacts.Find(ParentID);
  }
  var person = contact.To_ViewPerson();

  person.Companies = ListCompanies(person.Company_ID);
  return View(person);
}

Les 5 tests unitaires :

  • PeopleCreate get doit renvoyer la vue par defaut
  • PeopleCreate get doit renvoyer un objet ViewPerson a la vue
  • PeopleCreate get doit initialiser la liste des societes
  • PeopleCreate get doit initialiser la societe parente si elle est renseignee
  • PeopleCreate get doit initialiser la societe parente si elle est renseignee

Référence : Tester la partie GET d'une action Create()

Le code POST testé :

// POST: /People/Create
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Create(ViewPerson person)
{
  if (ModelState.IsValid)
  {
    var contact = new Contact();
    contact.Update_With_ViewPerson(person);
    db.Contacts.Add(contact);
    db.SaveChanges();

    this.Flash(string.Format("La fiche de {0} a été insérée", contact.DisplayName));
    return RedirectToAction("Details", new { id = contact.Contact_ID, slug = contact.Slug });
  }

  person.Companies = ListCompanies(person.Company_ID);
  return View(person);
}

Les 6 tests unitaires :

  • PeopleCreate post doit renvoyer la vue par defaut quand saisie incorrecte
  • PeopleCreate post doit initialiser la liste des societes quand saisie incorrecte
  • PeopleCreate post doit renvoyer le meme objet ViewPerson quand saisie incorrecte
  • PeopleCreate post doit enregistrer contact quand saisie correcte
  • PeopleCreate post doit definir message de succes quand saisie correcte
  • PeopleCreate post doit rediriger vers details quand saisie correcte

Références :

Tests unitaires de l'action Edit()

Le code source à tester :

// GET: /People/Edit/5
public ViewResult Edit(int id)
{
  var contact = db.Contacts.Find(id);
  var person = contact.To_ViewPerson();

  person.Companies = ListCompanies(person.Company_ID);
  return View(person);
}

// POST: /People/Edit/5
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Edit(ViewPerson person)
{
  if (ModelState.IsValid)
  {
    var contact = db.Contacts.Find(person.Contact_ID);
    contact.Update_With_ViewPerson(person);
    db.Entry(contact).State = EntityState.Modified;
    db.SaveChanges();

    this.Flash(string.Format("La fiche de {0} a été mise à jour", contact.DisplayName));
    return RedirectToAction("Details", new { id = contact.Contact_ID, slug = contact.Slug });
  }

  person.Companies = ListCompanies(person.Company_ID);
  return View(person);
}

Les 10 tests unitaires :

  • PeopleEdit get doit renvoyer la vue par defaut
  • PeopleEdit get doit renvoyer un objet ViewPerson a la vue
  • PeopleEdit get doit renvoyer le contact demande a la vue
  • PeopleEdit get doit initialiser la liste des societes
  • PeopleEdit post doit renvoyer la vue par defaut quand saisie incorrecte
  • PeopleEdit post doit initialiser la liste des societes quand saisie incorrecte
  • PeopleEdit post doit renvoyer le meme objet ViewPerson quand saisie incorrecte
  • PeopleEdit post doit enregistrer modification quand saisie correcte
  • PeopleEdit post doit definir message de succes quand saisie correcte
  • PeopleEdit post doit rediriger vers details quand saisie correcte

En général, ces tests unitaires sont presque du copié / collé de ceux écrits pour l'action Create(). Le test unitaire "PeopleEdit get doit renvoyer le contact demande a la vue" est quant à lui réalisé dans le même style que "PeopleDetails doit renvoyer le contact demande a la vue".

La seule "particularité" concerne le test unitaire qui sert à vérifier que l'action a bien enregistré la modification apportée au contact :

[TestMethod]
public void PeopleEdit_post_doit_enregistrer_modification_quand_saisie_correcte()
{
  // Arrange
  var controller = new PeopleController(db);
  var contact = InsertPerson("test", "0");
  contact.LastName = "maj";

  // Act
  var result = controller.Edit(contact.To_ViewPerson());

  // Assert
  var updated_contact = db.Contacts.Where(x => x.LastName == contact.LastName).FirstOrDefault();
  Assert.IsNotNull(updated_contact, "People.Edit() aurait dû mettre à jour le contact");
}

Tests unitaires de l'action Delete()

Le code source à tester :

// GET: /People/Delete/5
public ViewResult Delete(int id)
{
  var contact = db.Contacts.Find(id);
  var person = contact.To_ViewPerson();

  return View(person);
}

// POST: /People/Delete/5
[HttpPost, ValidateAntiForgeryToken, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
  var contact = db.Contacts.Find(id);
  db.Contacts.Remove(contact);
  db.SaveChanges();

  this.Flash(string.Format("La fiche de {0} a été supprimée", contact.DisplayName));
  return RedirectToAction("Index", "Contacts");
}

Les 6 tests unitaires :

  • PeopleDelete doit renvoyer la vue par defaut
  • PeopleDelete doit renvoyer un objet ViewPerson a la vue
  • PeopleDelete doit renvoyer le contact demande a la vue
  • PeopleDeleteConfirmed doit supprimer le contact
  • PeopleDeleteConfirmed doit definir message de succes
  • PeopleDeleteConfirmed doit rediriger vers liste des contacts

Les 3 premiers tests unitaires sont des "standards".

"PeopleDeleteConfirmed doit supprimer le contact" : dans le style de "PeopleCreate post doit enregistrer contact quand saisie correcte" et de "PeopleEdit post doit enregistrer modification quand saisie correcte".

"PeopleDeleteConfirmed doit rediriger vers liste des contacts" un peu dans le style de "PeopleCreate post doit rediriger vers details quand saisie correcte" et de "PeopleEdit post doit rediriger vers details quand saisie correcte", sauf que dans le cas d'une suppression l'action ne ré-affiche pas l'élément (puisqu'on l'a supprimé) mais renvoie à la liste des contacts (qui se trouve par ailleurs dans un autre contrôleur). Ce qui donne le test unitaire suivant :

[TestMethod]
public void PeopleDeleteConfirmed_doit_rediriger_vers_liste_des_contacts()
{
  // Arrange
  var controller = new PeopleController(db);
  var person = InsertPerson("test", "0");

  // Act
  var result = controller.DeleteConfirmed(person.Contact_ID) as RedirectToRouteResult;

  // Assert
  Assert.IsNotNull(result, "People.DeleteConfirmed() aurait dû renvoyer un RedirectToRouteResult");
  Assert.AreEqual("Contacts", result.RouteValues["controller"], "People.DeleteConfirmed() aurait dû rediriger vers le contrôleur Contacts");
  Assert.AreEqual("Index", result.RouteValues["action"], "People.DeleteConfirmed() aurait dû rediriger vers l'action Index");
}

lundi 8 octobre 2012

Base de données auto-nommée sous SQL Server

En général, je développe en local avec des bases de données SQL Server CE. Mais de temps en temps, je regarde ce que ça donne sous SQL Server Express 2008. Pour cela, j'ai tendance à préférer les bases attachées, parce que je trouve ça plus pratique d'avoir un fichier ".sdf" directement dans le dossier App_Data de mon application.

Et ce week-end j'ai eu une erreur vraiment troublante avec SQL Server Express 2008. Je faisais un essai complet avec Entity Framework et mon application aurait dû créer la base de données (le fichier ".sdf") et les tables qui la compose.

An attempt to attach an auto-named database for file C:\MVC\Bookmaker\Bookmaker\App_Data\Bookmaker_2013.mdf failed. A database with the same name exists, or specified file cannot be opened, or it is located on UNC share.

Il existe apparemment pas mal de raisons possibles à ce message d'erreur, mais dans mon cas c'était "A database with the same name exists".

Pour une fois, j'ai trouvé la solution sur Code Project.

Jusqu'à présent, ma chaine de connexion à la base de données était :

<add name="BookmakerContext"
     connectionString="Data Source=.\SQLEXPRESS;
                       Integrated Security=SSPI;
                       AttachDBFilename=|DataDirectory|\Bookmaker_2013.mdf;
                       User Instance=true"
     providerName="System.Data.SqlClient" />

En fait, il faut la compléter avec le nom de la base de données pour éviter que SQL Server auto-nomme la base de données à créer, ce qui doit poser un problème si une base de données de ce nom existe déjà sous SQL Server (je suppose).

Et donc, tout est rentré dans l'ordre en ajoutant ;Database=Bookmaker_2013 après User Instance=true :

<add name="BookmakerContext"
     connectionString="Data Source=.\SQLEXPRESS;
                       Integrated Security=SSPI;
                       AttachDBFilename=|DataDirectory|\Bookmaker_2013.mdf;
                       User Instance=true;
                       Database=Bookmaker_2013"
     providerName="System.Data.SqlClient" />

jeudi 4 octobre 2012

Tester l'action Details()

Avant d'attaquer le codage des derniers tests pour le contrôleur PeopleController, je remet au propre les 3 tests unitaires que j'ai déjà codés pour valider le fonctionnement de l'action Details() et surtout j'en profite pour décrire ce que j'ai voulu faire.

L'action à tester

//
// GET: /People/Details/5

public ViewResult Details(int id)
{
  var contact = db.Contacts.Find(id);
  var person = contact.To_ViewPerson();

  return View(person);
}

Il s'agit de l'action standard en ASP.NET MVC pour afficher une fiche particulière. Ca correspondrait à l'action "show" dans un contrôleur Ruby on Rails.

Cette action reçoit en paramètre (int id) l'identifiant de la fiche contact à afficher.

Elle commence par faire appel à un objet DbContext d'Entity Framework pour retrouver le contact correspondant à cet identifiant dans la base de données.

L'objet contact récupéré est ensuite transformé en objet ViewModel à l'aide de la méthode d'extension To_ViewPerson().

Puis l'action fait passer cet objet ViewModel à la vue et renvoie le résultat : return View(person);.

Par conséquent, les trucs à tester sont assez simples :

  • un premier test quasi obligatoire pour vérifier que l'action renvoie bien la vue par défaut.
  • un deuxième test pour contrôler que l'action fait bien passer un objet ViewPerson à la vue, puisque c'est ce qu'elle attend.
  • un dernier test pour s'assurer que l'action renvoie bien le contact que l'on souhaite afficher.

1° test : l'action renvoie la vue par défaut

C'est un test un peu répétitif comme test puisqu'on le retrouve à chaque action. Mais l'avantage, c'est que ça permet d'avoir un point de départ et de se lancer dans l'écriture des tests unitaires sans buter sur le syndrome de la page blanche.

[TestMethod]
public void PeopleDetails_doit_renvoyer_la_vue_par_defaut()
{
  // Arrange
  var controller = new PeopleController(db);
  var contact = new Contact
  {
    DisplayName = "test",
    Phone1 = "0"
  };
  db.Contacts.Add(contact);
  db.SaveChanges();

  // Act
  var result = controller.Details(contact.Contact_ID);

  // Assert
  Assert.IsNotNull(result, "People.Details() aurait dû renvoyer un ViewResult");
  Assert.IsTrue(string.IsNullOrEmpty(result.ViewName), "People.Details() aurait dû utiliser la vue par défaut");
}

Je copie/colle plus ou moins les explications des billets précédents...

var controller = new PeopleController(db);

=> instancie un objet contrôleur en lui passant un DbContext pour que l'action puisse utiliser Entity Framework pour rechercher la fiche contact.

var contact = new Contact(); ... db.SaveChanges();

=> enregistre un contact pour en avoir un à rechercher.

var result = controller.Details(contact.Contact_ID);

=> appelle l'action Details() en demandant le contact qui a été créé à l'étape précédente.

Assert.IsNotNull(result, "...");

=> 1° test pour vérifier que l'action répond bien par un return View et pas par un return RedirectToAction.

Assert.IsTrue(string.IsNullOrEmpty(result.ViewName), "...");

=> 2° test pour vérifier que l'action n'a pas défini le nom de la vue à utiliser et qu'elle laisse faire le moteur de vue pour qu'il utilise la vue par défaut.

2° test : l'action transmet à la vue l'objet qu'elle attend

Là aussi, on retrouve ce genre de test dans les autres actions. Au moins, on ne peut pas dire que ça soit compliqué d'écrire des tests unitaires.

[TestMethod]
public void PeopleDetails_doit_renvoyer_un_objet_ViewPerson_a_la_vue()
{
  // Arrange
  var controller = new PeopleController(db);
  var contact = new Contact
  {
    DisplayName = "test",
    Phone1 = "0"
  };
  db.Contacts.Add(contact);
  db.SaveChanges();

  // Act
  var result = controller.Details(contact.Contact_ID);

  // Assert
  var model = result.ViewData.Model as ViewPerson;
  Assert.IsNotNull(model, "Model devrait être du type ViewPerson");
}

var model = result.ViewData.Model as ViewPerson;

=> récupère l'objet transmis à la vue par le contrôleur.

Assert.IsNotNull(model, "Model devrait être du type ViewPerson");

=> vérifie que l'action a bien transmis un objet de type ViewPerson.

3° test : l'action renvoie le contact demandé à la vue

Ce test unitaire est un peu nouveau par rapport au 2 premiers tests, mais rien de bien compliqué quand même.

[TestMethod]
public void PeopleDetails_doit_renvoyer_le_contact_demande_a_la_vue()
{
  // Arrange
  var controller = new PeopleController(db);
  var contact1 = new Contact { DisplayName = "test1", Phone1 = "1" };
  db.Contacts.Add(contact1);
  var contact2 = new Contact { DisplayName = "test2", Phone1 = "2" };
  db.Contacts.Add(contact2);
  db.SaveChanges();

  // Act
  var result = controller.Details(contact1.Contact_ID);

  // Assert
  var model = result.ViewData.Model as ViewPerson;
  Assert.AreEqual(contact1.DisplayName, model.DisplayName, "Model aurait dû correspondre au contact demandé");
  Assert.AreEqual(contact1.Phone1, model.Phone1, "Model aurait dû correspondre au contact demandé");
}

var contact1 = new Contact ...

var contact2 = new Contact ...

=> enregistrement de 2 contacts pour corser un peu.

var result = controller.Details(contact1.Contact_ID);

=> appelle l'action Details() en demandant un des 2 contacts créé à l'étape précédente.

Assert.AreEqual(contact1.DisplayName, model.DisplayName, "...");

Assert.AreEqual(contact1.Phone1, model.Phone1, "...");

=> teste que le contact transmis à la vue correspond bien à celui qui a été demandé et pas à un autre de la table des contacts.

Remarque en passant

Je ne teste pas ce que ferait l'action Details() dans le cas où le contact demandé n'existerait pas. C'est parce que j'ai décidé que (pour l'instant) je ne voulais pas gérer ce genre de truc.

Et donc, comme cela ne fait pas partie de mes "spécifications", cela n'a pas à être testé.

Même si bien sûr c'est une fonctionnalité qu'il est tout à fait raisonnable de gérer.

Conclusion

Ca, c'est fait. Il faut maintenant que je teste les autres actions du contrôleur : Edit() en versions GET et POST, Delete() en mode GET, DeleteConfirmed() en mode POST() et si possible accompagner ça d'un nouveau billet (c'est fait !).

Puis il faudra reporter ces tests au niveau du contrôleur CompaniesController qui gère les contacts de type sociétés.

Et pour finir, essayer de me motiver suffisamment pour faire un billet qui explique comment j'ai décidé de gérer les tests unitaires pour ce qui touche à Entity Framework (c'est presque fait).

mercredi 3 octobre 2012

Tester la partie GET d'une action Create()

Pour compléter mes deux billets sur les tests unitaires concernant la partie POST de l'action Create() dans le contrôleur PeopleController (partie 1 et partie 2), je me suis dit que ça serait une bonne idée de présenter aussi les tests unitaires qui concernent la partie GET de l'action.

Normalement, la version GET venant avant le POST, j'aurais pu réfléchir et présenter les tests dans le "bon" ordre, d'autant plus que certains des tests se retrouvent dans les 2 versions. Mais bon, mieux vaut tard que jamais...

L'action à tester

//
// GET: /People/Create

public ViewResult Create(int ParentID = 0)
{
  var contact = new Contact();
  if (ParentID != 0)
  {
    contact.Company_ID = ParentID;
    contact.Company = db.Contacts.Find(ParentID);
  }
  ViewPerson person = contact.To_ViewPerson();

  person.Companies = ListCompanies(person.Company_ID);
  return View(person);
}

Cette action renvoie la vue par défaut en lui passant un objet ViewModel de type ViewPerson composé :

  • à partir d'un objet Model de type Contact (un contact vide en l’occurrence),
  • d'une liste des sociétés existantes pour générer une liste déroulante dans la vue et permettre de rattacher la personne à créer à une société.

Par ailleurs, lorsque le paramètre optionnel ParentID est défini, l'action l'utilise pour pré-rattacher la personne qui va être créée à une société donnée. Ca me permet de gérer un bouton "Ajouter une personne à cette société" dans la vue société et ainsi faciliter la vie de l'utilisateur quand il veut créer des contacts pour une société.

1° test : vérifier que l'action renvoie la vue par défaut

[TestMethod]
public void PeopleCreate_get_doit_renvoyer_la_vue_par_defaut()
{
  // Arrange
  var controller = new PeopleController(db);

  // Act
  var result = controller.Create();

  // Assert
  Assert.IsNotNull(result, "People.Create() aurait dû renvoyer un ViewResult");
  Assert.IsTrue(string.IsNullOrEmpty(result.ViewName), "People.Create() aurait dû utiliser la vue par défaut");
}

Je n'ai rien inventé là dedans. C'est un truc assez basique et qui reprend quasiment tel quel ce que j'ai vu sur internet ou bien lu sur Professional ASP.NET MVC.

Je ré-explique malgré tout ce qu'il fait pour éviter d'avoir à repasser par les billets sur la partie POST de l'action pour les explications.

var controller = new PeopleController(db);

=> instancie un objet contrôleur en lui passant un DbContext pour que l'action puisse utiliser Entity Framework pour générer la liste des sociétés.

var result = controller.Create();

=> appelle l'action Create() sans argument.

Note : ce test "attaque" la version GET de l'action car il l'appelle sans paramètre et que fort heureusement pour moi, la version GET n'attend pas de paramètre (ou plutôt un paramètre facultatif de type int) alors que la version POST attend un paramètre de type ViewPerson.

Assert.IsNotNull(result, "...");

=> 1° test pour vérifier que l'action répond bien par un return View et pas par un return RedirectToAction.

Assert.IsTrue(string.IsNullOrEmpty(result.ViewName), "...");

=> 2° test pour vérifier que l'action n'a pas défini le nom de la vue à utiliser et qu'elle laisse faire le moteur de vue pour qu'il utilise la vue par défaut.

Ce test unitaire sert à vérifier que dans le code de l'action je n'ai pas malencontreusement défini le nom de la vue à utiliser. Grâce à lui, je suis certain de ne pas avoir codé quelque chose comme :

return View("Create", person);

ou :

return View("Create");

ou même :

return View("New");

Rappel : Je n'ai pas à tester le fait que la vue par défaut sera bien Create.cshtml. Ca, c'est le travail de ceux qui ont développé ASP.NET MVC.

2° test : vérifier que Create renvoie l'objet attendu par la vue

La vue pour créer une personne a besoin d'un objet ViewPerson. Ce test va donc servir à contrôler que l'action Create() transmet bien cet objet à la vue.

[TestMethod]
public void PeopleCreate_get_doit_renvoyer_un_objet_ViewPerson_a_la_vue()
{
  // Arrange
  var controller = new PeopleController(db);

  // Act
  var result = controller.Create();

  // Assert
  var model = result.ViewData.Model as ViewPerson;
  Assert.IsNotNull(model, "Model devrait être du type ViewPerson");
}

var model = result.ViewData.Model as ViewPerson;

=> récupère l'objet transmis à la vue par le contrôleur.

Assert.IsNotNull(model, "Model devrait être du type ViewPerson");

=> vérifie que l'action a bien transmis un objet de type ViewPerson.

Ce test unitaire me permet de contrôler que dans le code de l'action j'ai bien pris les mesures nécessaires pour transmettre à la vue l'objet de type ViewPerson dont elle a besoin.

Grâce à lui, je suis certain de ne pas avoir codé quelque chose comme :

return View();

ou :

return View(42);

3° test : vérifier que Create rempli la liste des sociétés

La vue s'attend à ce que la propriété Companies de l'objet ViewPerson qu'elle reçoit du contrôleur lui permette de générer une liste déroulante (dropdownlist) pour que l'utilisateur puisse sélectionner à quelle société il veut rattacher la personne qu'il est en train de saisir.

[TestMethod]
public void PeopleCreate_get_doit_initialiser_la_liste_des_societes()
{
  // Arrange
  var controller = new PeopleController(db);

  // Act
  var result = controller.Create();

  // Assert
  var model = result.ViewData.Model as ViewPerson;
  Assert.IsNotNull(model.Companies, "Model.Companies devrait être initialisée");
}

Assert.IsNotNull(model.Companies, "...");

=> test pour vérifier que la propriété Companies n'est pas nulle (ce qui est le cas par défaut) et que l'action Create() l'a bien initialisée avec la liste des sociétés existantes.

Grâce à ce test unitaire, je suis certain de ne pas voir oublié la ligne suivante dans le code de l'action Create() :

person.Companies = ListCompanies(person.Company_ID);

Note : en rédigeant ce billet, je me demande si c'est "bon". Parce que j'aurais très bien pu coder :

person.Companies = new SelectList("".ToArray()); // TODO

3° test bis : vérifier que Create rempli la liste des sociétés

[TestMethod]
public void PeopleCreate_get_doit_initialiser_la_liste_des_societes()
{
  // Arrange
  var controller = new PeopleController(db);
  var company = new ViewCompany
  {
    CompanyName = "soc",
    Phone1 = "9"
  };
  var contact = new Contact().Update_With_ViewCompany(company);
  db.Contacts.Add(contact);
  db.SaveChanges();

  // Act
  var result = controller.Create();

  // Assert
  var model = result.ViewData.Model as ViewPerson;
  Assert.IsNotNull(model.Companies, "Model.Companies devrait être initialisée");
  var count = model.Companies.Count();
  Assert.IsTrue(count > 0, "Model.Companies devrait contenir des sociétés");
  var check = model.Companies.Where(x => x.Text == "soc").Count();
  Assert.IsTrue(check > 0, "Model.Companies devrait contenir 'soc'");
}

var company = new ViewCompany ... / db.SaveChanges();

=> création et enregistrement d'une société pour que la base de données en contienne au moins une, de façon à ce que la liste des sociétés ne soit pas vide.

var count = ... / Assert.IsTrue(count > 0, "...");

=> vérifie que la liste n'est pas vide.

var check = ... / Assert.IsTrue(check > 0, "...");

=> vérifie que la liste contient la société qui vient d'être créée.

"A ce coup", je suis certain que j'ai bien rempli ma liste avec les sociétés présentes en base de données. Mais je me demande aussi si ça fait pas un tout petit peu trop ?

4° test : vérifier que Create gère une société parente

Lorsque l'action Create() reçoit un paramètre ParentID, il faut qu'elle le gère pour que côté vue, la société correspondante soit pré-sélectionnée dans la liste déroulante des sociétés.

[TestMethod]
public void PeopleCreate_get_doit_initialiser_la_societe_parente_si_elle_est_renseignee()
{
  // Arrange
  var controller = new PeopleController(db);
  var company = new ViewCompany
  {
      CompanyName = "soc",
      Phone1 = "9"
  };
  var contact = new Contact().Update_With_ViewCompany(company);
  db.Contacts.Add(contact);
  db.SaveChanges();

  // Act
  var result = controller.Create(contact.Contact_ID);

  // Assert
  var model = result.ViewData.Model as ViewPerson;
  Assert.AreEqual(contact.Contact_ID, model.Company_ID, "Model.Company_ID aurait dû correspondre à la société en paramètre");
}

var company = new ViewCompany ... / db.SaveChanges();

=> création et enregistrement d'une société pour que la base de données en contienne au moins une.

var result = controller.Create(societe.Contact_ID);

=> appelle l'action Create() en lui passant l'ID d'une société existant en base de données (c'est un argument de type int => "attaque" la version GET de l'action).

Assert.AreEqual(societe.Contact_ID, model.Company_ID, "...");

=> teste que l'ID de la société transmise à la vue correspond à l'ID qui avait été passée à l'action du contrôleur.

Note : En fait, je ne teste pas que la société passée en paramètre soit bien sélectionnée dans la SelectList model.Companies puisque ce n'est pas l'action Create() qui s'occupe de ça. Par contre, il faut que je prévoie de faire des tests unitaires pour ma fonction ListCompanies() dont c'est le boulot.

Conclusion

Je suis assez satisfait car j'ai l'impression de faire des progrès :

  • J'arrive à faire des tests unitaires quand je m'y mets (il faudrait que je me décide à les faire avant de démarrer le codage).
  • Je commence à bien voir ce qui fait parti de la méthode à tester et ce qui est en dehors de son périmètre.

J'ai encore besoin de m'améliorer pour "doser" le bon niveau de détail. Mais je pense que c'est un défaut de débutant et qu'avec l'habitude je saurai de mieux en mieux aller à l'essentiel.

- page 1 de 36